package ircserver_test import ( "bufio" "log/slog" "net" "os" "strings" "testing" "time" "sneak.berlin/go/neoirc/internal/config" "sneak.berlin/go/neoirc/internal/ircserver" ) // newKillVictim returns a Conn for the nick victim whose // socket is one end of a net.Pipe, and the other end, from // which the test reads what the victim is sent. A write to // the pipe blocks until the other end reads. func newKillVictim(t *testing.T) (*ircserver.Conn, net.Conn) { t.Helper() serverSide, clientSide := net.Pipe() t.Cleanup(func() { _ = clientSide.Close() }) log := slog.New(slog.NewTextHandler( os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}, )) cfg := &config.Config{ServerName: testServerName} return ircserver.NewTestConn( log, cfg, serverSide, "victim", ), clientSide } // TestDisconnectDoesNotBlockOnUnresponsiveVictim checks // that Disconnect returns although the victim never reads, // so a KILL cannot stall the operator who sent it. The // limit is well below one KillWriteWindow, so a Disconnect // that waits on any write to the victim fails. func TestDisconnectDoesNotBlockOnUnresponsiveVictim( t *testing.T, ) { t.Parallel() victim, _ := newKillVictim(t) returned := make(chan struct{}) go func() { victim.Disconnect("killed by oper") close(returned) }() select { case <-returned: case <-time.After(ircserver.KillWriteWindow / 4): t.Fatal("Disconnect waited for the victim to read") } } // TestDisconnectNotifiesAndClosesVictim checks that a // victim that reads is sent KILL and ERROR, and that its // connection is then closed. func TestDisconnectNotifiesAndClosesVictim(t *testing.T) { t.Parallel() victim, clientSide := newKillVictim(t) received := make(chan []string, 1) go func() { var lines []string scanner := bufio.NewScanner(clientSide) for scanner.Scan() { lines = append(lines, scanner.Text()) } received <- lines }() victim.Disconnect("killed by oper") var lines []string select { case lines = <-received: case <-time.After(testTimeout): t.Fatal("the victim's connection was not closed") } joined := strings.Join(lines, "\n") if !strings.Contains(joined, "KILL victim :killed by oper") { t.Errorf("missing KILL line, got: %q", joined) } if !strings.Contains(joined, "ERROR :Closing Link:") { t.Errorf("missing ERROR line, got: %q", joined) } }