check / check (push) Waiting to run
The IRC listener relayed only the first line of a PRIVMSG or NOTICE body; it now sends one line per body line. A relayed MODE change now carries its key, limit or mask, each value its own parameter, also in the echo to the user who made it. CR, LF and NUL are removed from each parameter and from each whole line sent to an IRC listener connection, so no stored value can start a line of its own. The channel name check moved into service.JoinChannel, so the IRC listener's JOIN refuses the same names with 403. Model: opus-5-5 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
838 lines
16 KiB
Go
838 lines
16 KiB
Go
package ircserver_test
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/rand"
|
|
"database/sql"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/neoirc/internal/broker"
|
|
"sneak.berlin/go/neoirc/internal/config"
|
|
"sneak.berlin/go/neoirc/internal/db"
|
|
"sneak.berlin/go/neoirc/internal/ircserver"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
const testTimeout = 5 * time.Second
|
|
|
|
func TestMain(m *testing.M) {
|
|
db.SetBcryptCost(4)
|
|
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// testEnv holds the shared test infrastructure.
|
|
type testEnv struct {
|
|
database *db.Database
|
|
brk *broker.Broker
|
|
cfg *config.Config
|
|
srv *ircserver.Server
|
|
}
|
|
|
|
// testServerName is the SERVER_NAME of newTestEnv.
|
|
const testServerName = "test.irc"
|
|
|
|
func newTestEnv(t *testing.T) *testEnv {
|
|
t.Helper()
|
|
|
|
return newTestEnvWithConfig(t, &config.Config{
|
|
ServerName: testServerName,
|
|
MOTD: "Welcome to test IRC",
|
|
})
|
|
}
|
|
|
|
// newTestEnvWithOper is newTestEnv with the operator name
|
|
// testoper and password testpass.
|
|
func newTestEnvWithOper(t *testing.T) *testEnv {
|
|
t.Helper()
|
|
|
|
return newTestEnvWithConfig(t, &config.Config{
|
|
ServerName: testServerName,
|
|
MOTD: "Welcome to test IRC",
|
|
OperName: "testoper",
|
|
OperPassword: "testpass",
|
|
})
|
|
}
|
|
|
|
// newTestEnvWithConfig starts an IRC server with cfg on a
|
|
// fresh database.
|
|
func newTestEnvWithConfig(
|
|
t *testing.T,
|
|
cfg *config.Config,
|
|
) *testEnv {
|
|
t.Helper()
|
|
|
|
// A random name, so that no other test environment, not
|
|
// even an earlier run of the same test, can share it.
|
|
dsn := fmt.Sprintf(
|
|
"file:%s?mode=memory&cache=shared",
|
|
rand.Text(),
|
|
)
|
|
|
|
conn, err := sql.Open("sqlite", dsn)
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
err := conn.Close()
|
|
if err != nil {
|
|
t.Logf("close db: %v", err)
|
|
}
|
|
})
|
|
|
|
conn.SetMaxOpenConns(1)
|
|
|
|
_, err = conn.ExecContext(
|
|
t.Context(), "PRAGMA foreign_keys = ON",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("pragma: %v", err)
|
|
}
|
|
|
|
database := db.NewTestDatabaseFromConn(conn)
|
|
|
|
err = database.RunMigrations(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
|
|
brk := broker.New()
|
|
|
|
var listenConfig net.ListenConfig
|
|
|
|
listener, err := listenConfig.Listen(t.Context(), "tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
|
|
addr := listener.Addr().String()
|
|
|
|
err = listener.Close()
|
|
if err != nil {
|
|
t.Fatalf("close listener: %v", err)
|
|
}
|
|
|
|
log := slog.New(slog.NewTextHandler(
|
|
os.Stderr,
|
|
&slog.HandlerOptions{Level: slog.LevelError}, //nolint:exhaustruct
|
|
))
|
|
|
|
srv := ircserver.NewTestServer(log, cfg, database, brk)
|
|
|
|
err = srv.Start(addr)
|
|
if err != nil {
|
|
t.Fatalf("start irc server: %v", err)
|
|
}
|
|
|
|
// Cleanups run last registered first, so the server stops
|
|
// before its database is closed.
|
|
t.Cleanup(srv.Stop)
|
|
|
|
return &testEnv{
|
|
database: database,
|
|
brk: brk,
|
|
cfg: cfg,
|
|
srv: srv,
|
|
}
|
|
}
|
|
|
|
// dial connects to the test server.
|
|
func (env *testEnv) dial(t *testing.T) *testClient {
|
|
t.Helper()
|
|
|
|
dialer := net.Dialer{Timeout: testTimeout}
|
|
|
|
conn, err := dialer.DialContext(
|
|
t.Context(),
|
|
"tcp",
|
|
env.srv.Listener().Addr().String(),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
err := conn.Close()
|
|
if err != nil {
|
|
t.Logf("close conn: %v", err)
|
|
}
|
|
})
|
|
|
|
return &testClient{
|
|
t: t,
|
|
conn: conn,
|
|
scanner: bufio.NewScanner(conn),
|
|
}
|
|
}
|
|
|
|
// testClient wraps a raw TCP connection with helpers.
|
|
type testClient struct {
|
|
t *testing.T
|
|
conn net.Conn
|
|
scanner *bufio.Scanner
|
|
}
|
|
|
|
func (tc *testClient) send(line string) {
|
|
tc.t.Helper()
|
|
|
|
_ = tc.conn.SetWriteDeadline(
|
|
time.Now().Add(testTimeout),
|
|
)
|
|
|
|
_, err := fmt.Fprintf(tc.conn, "%s\r\n", line)
|
|
if err != nil {
|
|
tc.t.Fatalf("send: %v", err)
|
|
}
|
|
}
|
|
|
|
func (tc *testClient) readLine() string {
|
|
tc.t.Helper()
|
|
|
|
_ = tc.conn.SetReadDeadline(
|
|
time.Now().Add(testTimeout),
|
|
)
|
|
|
|
if !tc.scanner.Scan() {
|
|
err := tc.scanner.Err()
|
|
if err != nil {
|
|
tc.t.Fatalf("read: %v", err)
|
|
}
|
|
|
|
tc.t.Fatal("connection closed unexpectedly")
|
|
}
|
|
|
|
return tc.scanner.Text()
|
|
}
|
|
|
|
// readUntil reads lines until one matches the predicate.
|
|
func (tc *testClient) readUntil(
|
|
pred func(string) bool,
|
|
) []string {
|
|
tc.t.Helper()
|
|
|
|
var lines []string
|
|
|
|
for {
|
|
line := tc.readLine()
|
|
lines = append(lines, line)
|
|
|
|
if pred(line) {
|
|
return lines
|
|
}
|
|
}
|
|
}
|
|
|
|
// register sends NICK + USER and reads through the welcome
|
|
// burst.
|
|
func (tc *testClient) register(nick string) []string {
|
|
tc.t.Helper()
|
|
|
|
tc.send("NICK " + nick)
|
|
tc.send("USER " + nick + " 0 * :Test User")
|
|
|
|
return tc.readUntil(func(line string) bool {
|
|
return strings.Contains(line, " 376 ") ||
|
|
strings.Contains(line, " 422 ")
|
|
})
|
|
}
|
|
|
|
// readUntilClosed returns the lines received until the
|
|
// server closes the connection, and fails the test if it is
|
|
// still open after testTimeout.
|
|
func (tc *testClient) readUntilClosed() []string {
|
|
tc.t.Helper()
|
|
|
|
_ = tc.conn.SetReadDeadline(
|
|
time.Now().Add(testTimeout),
|
|
)
|
|
|
|
var lines []string
|
|
|
|
for tc.scanner.Scan() {
|
|
lines = append(lines, tc.scanner.Text())
|
|
}
|
|
|
|
err := tc.scanner.Err()
|
|
if err != nil {
|
|
tc.t.Fatalf(
|
|
"connection not closed: %v (lines: %v)",
|
|
err, lines,
|
|
)
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
// assertContains checks that at least one line matches the
|
|
// given substring.
|
|
func assertContains(
|
|
t *testing.T,
|
|
lines []string,
|
|
substr, description string,
|
|
) {
|
|
t.Helper()
|
|
|
|
for _, line := range lines {
|
|
if strings.Contains(line, substr) {
|
|
return
|
|
}
|
|
}
|
|
|
|
t.Errorf("did not find %q in output: %s", substr, description)
|
|
}
|
|
|
|
// assertNotContains checks that no line matches the given
|
|
// substring.
|
|
func assertNotContains(
|
|
t *testing.T,
|
|
lines []string,
|
|
substr, description string,
|
|
) {
|
|
t.Helper()
|
|
|
|
for _, line := range lines {
|
|
if strings.Contains(line, substr) {
|
|
t.Errorf(
|
|
"unexpectedly found %q in output: %s",
|
|
substr, description,
|
|
)
|
|
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// joinAndDrain joins a channel and reads until
|
|
// RPL_ENDOFNAMES.
|
|
func (tc *testClient) joinAndDrain(channel string) {
|
|
tc.t.Helper()
|
|
|
|
tc.send("JOIN " + channel)
|
|
|
|
tc.readUntil(func(line string) bool {
|
|
return strings.Contains(line, " 366 ")
|
|
})
|
|
}
|
|
|
|
// sendAndExpect sends a command and reads until a line
|
|
// containing the expected substring is found.
|
|
func (tc *testClient) sendAndExpect(
|
|
cmd, expect string,
|
|
) []string {
|
|
tc.t.Helper()
|
|
|
|
tc.send(cmd)
|
|
|
|
return tc.readUntil(func(line string) bool {
|
|
return strings.Contains(line, expect)
|
|
})
|
|
}
|
|
|
|
func TestRegistration(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
lines := client.register("alice")
|
|
assertContains(t, lines, " 001 ", "RPL_WELCOME")
|
|
}
|
|
|
|
// TestEachTestEnvHasItsOwnDatabase checks that a test
|
|
// environment starts on an empty database while another one
|
|
// is still open.
|
|
func TestEachTestEnvHasItsOwnDatabase(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
first := newTestEnv(t)
|
|
first.dial(t).register("samenick")
|
|
|
|
second := newTestEnv(t)
|
|
lines := second.dial(t).register("samenick")
|
|
assertContains(t, lines, " 001 ", "RPL_WELCOME")
|
|
}
|
|
|
|
func TestWelcomeContainsNick(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
lines := client.register("bob")
|
|
|
|
for _, line := range lines {
|
|
if strings.Contains(line, " 001 ") &&
|
|
!strings.Contains(line, "bob") {
|
|
t.Errorf("001 should contain nick: %s", line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPingPong(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("pingtest")
|
|
lines := client.sendAndExpect("PING :hello", "PONG")
|
|
assertContains(t, lines, "PONG", "PONG response")
|
|
}
|
|
|
|
// TestIdleCleanupKeepsActiveUser checks that a user who
|
|
// sends commands is kept by the idle cleanup, and a user who
|
|
// sends nothing is not.
|
|
func TestIdleCleanupKeepsActiveUser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const idleTimeout = 500 * time.Millisecond
|
|
|
|
env := newTestEnv(t)
|
|
|
|
active := env.dial(t)
|
|
active.register("active")
|
|
|
|
idle := env.dial(t)
|
|
idle.register("idle")
|
|
|
|
time.Sleep(idleTimeout)
|
|
active.sendAndExpect("PING :still here", "PONG")
|
|
|
|
// The idle cleanup in internal/handlers removes exactly
|
|
// the users this returns.
|
|
stale, err := env.database.GetStaleOrphanSessions(
|
|
t.Context(), time.Now().Add(-idleTimeout),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("get stale sessions: %v", err)
|
|
}
|
|
|
|
if len(stale) != 1 || stale[0].Nick != "idle" {
|
|
t.Errorf("cleanup removes %v, want only idle", stale)
|
|
}
|
|
}
|
|
|
|
func TestJoinChannel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("joiner")
|
|
client.send("JOIN #test")
|
|
|
|
lines := client.readUntil(func(line string) bool {
|
|
return strings.Contains(line, " 366 ")
|
|
})
|
|
|
|
assertContains(t, lines, "JOIN", "JOIN echo")
|
|
assertContains(t, lines, " 366 ", "RPL_ENDOFNAMES")
|
|
}
|
|
|
|
// TestJoinInvalidChannelName checks that JOIN refuses a name
|
|
// that is not # followed by 1 to 63 letters, digits, _ or -
|
|
// with ERR_NOSUCHCHANNEL, and creates no such channel.
|
|
func TestJoinInvalidChannelName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("joiner")
|
|
|
|
for _, channel := range []string{
|
|
"#bad.name",
|
|
"#",
|
|
"#" + strings.Repeat("a", 64),
|
|
} {
|
|
lines := client.sendAndExpect("JOIN "+channel, " 403 ")
|
|
|
|
assertContains(
|
|
t, lines,
|
|
" 403 joiner "+channel+" :No such channel",
|
|
channel,
|
|
)
|
|
|
|
_, err := env.database.GetChannelByName(t.Context(), channel)
|
|
if err == nil {
|
|
t.Errorf("channel %q was created", channel)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPrivmsgBetweenClients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
alice := env.dial(t)
|
|
alice.register("alice_pm")
|
|
|
|
bob := env.dial(t)
|
|
bob.register("bob_pm")
|
|
|
|
alice.joinAndDrain("#chat")
|
|
bob.joinAndDrain("#chat")
|
|
|
|
alice.send("PRIVMSG #chat :hello bob!")
|
|
|
|
lines := bob.sendAndExpect("PING :sync", "hello bob!")
|
|
assertContains(t, lines, "hello bob!", "channel PRIVMSG")
|
|
}
|
|
|
|
func TestNickChange(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("oldnick")
|
|
lines := client.sendAndExpect("NICK newnick", "newnick")
|
|
assertContains(t, lines, "NICK", "NICK change echo")
|
|
}
|
|
|
|
func TestDuplicateNick(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
first := env.dial(t)
|
|
first.register("taken")
|
|
|
|
second := env.dial(t)
|
|
second.send("NICK taken")
|
|
second.send("USER taken 0 * :Test")
|
|
|
|
lines := second.readUntil(func(line string) bool {
|
|
return strings.Contains(line, " 433 ")
|
|
})
|
|
|
|
assertContains(t, lines, " 433 ", "ERR_NICKNAMEINUSE")
|
|
}
|
|
|
|
func TestListChannels(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("lister")
|
|
client.joinAndDrain("#listtest")
|
|
lines := client.sendAndExpect("LIST", " 323 ")
|
|
assertContains(t, lines, " 323 ", "RPL_LISTEND") //nolint:misspell // IRC term
|
|
}
|
|
|
|
func TestWhois(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("whoistest")
|
|
lines := client.sendAndExpect(
|
|
"WHOIS whoistest", " 318 ",
|
|
)
|
|
|
|
assertContains(t, lines, " 311 ", "RPL_WHOISUSER")
|
|
assertContains(t, lines, " 318 ", "RPL_ENDOFWHOIS")
|
|
}
|
|
|
|
func TestQuit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("quitter")
|
|
lines := client.sendAndExpect(
|
|
"QUIT :goodbye", "ERROR",
|
|
)
|
|
|
|
assertContains(t, lines, "goodbye", "QUIT reason")
|
|
}
|
|
|
|
func TestTopicSetAndGet(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("topicuser")
|
|
client.joinAndDrain("#topictest")
|
|
lines := client.sendAndExpect(
|
|
"TOPIC #topictest :New topic here",
|
|
"New topic here",
|
|
)
|
|
|
|
assertContains(
|
|
t, lines, "New topic here", "TOPIC echo",
|
|
)
|
|
}
|
|
|
|
func TestUnknownCommand(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("unknowncmd")
|
|
lines := client.sendAndExpect("FOOBAR", " 421 ")
|
|
assertContains(t, lines, " 421 ", "ERR_UNKNOWNCOMMAND")
|
|
}
|
|
|
|
func TestDirectMessage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
sender := env.dial(t)
|
|
sender.register("dmsender")
|
|
|
|
receiver := env.dial(t)
|
|
receiver.register("dmreceiver")
|
|
|
|
// Give relay goroutines time to start.
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
sender.send("PRIVMSG dmreceiver :hello privately")
|
|
|
|
lines := receiver.readUntil(func(line string) bool {
|
|
return strings.Contains(line, "hello privately")
|
|
})
|
|
|
|
assertContains(
|
|
t, lines, "hello privately", "direct PRIVMSG",
|
|
)
|
|
}
|
|
|
|
func TestCAPNegotiation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.send("CAP LS 302")
|
|
|
|
line := client.readLine()
|
|
if !strings.Contains(line, "CAP") {
|
|
t.Errorf("expected CAP response, got: %s", line)
|
|
}
|
|
|
|
client.send("CAP END")
|
|
lines := client.register("capuser")
|
|
assertContains(t, lines, " 001 ", "registration after CAP")
|
|
}
|
|
|
|
func TestPartChannel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("parter")
|
|
client.joinAndDrain("#parttest")
|
|
lines := client.sendAndExpect(
|
|
"PART #parttest :leaving", "PART",
|
|
)
|
|
|
|
assertContains(t, lines, "#parttest", "PART echo")
|
|
}
|
|
|
|
func TestModeQuery(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("modeuser")
|
|
client.joinAndDrain("#modetest")
|
|
lines := client.sendAndExpect(
|
|
"MODE #modetest", " 324 ",
|
|
)
|
|
|
|
assertContains(
|
|
t, lines, " 324 ", "RPL_CHANNELMODEIS",
|
|
)
|
|
}
|
|
|
|
func TestWhoChannel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("whouser")
|
|
client.joinAndDrain("#whotest")
|
|
lines := client.sendAndExpect("WHO #whotest", " 315 ")
|
|
assertContains(t, lines, " 352 ", "RPL_WHOREPLY")
|
|
}
|
|
|
|
func TestLusers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("luseruser")
|
|
lines := client.sendAndExpect("LUSERS", " 255 ")
|
|
assertContains(t, lines, " 251 ", "RPL_LUSERCLIENT")
|
|
}
|
|
|
|
func TestMotd(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("motduser")
|
|
lines := client.sendAndExpect("MOTD", " 376 ")
|
|
assertContains(t, lines, " 376 ", "RPL_ENDOFMOTD")
|
|
}
|
|
|
|
func TestAwaySetAndClear(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("awayuser")
|
|
|
|
setLines := client.sendAndExpect(
|
|
"AWAY :brb lunch", " 306 ",
|
|
)
|
|
assertContains(t, setLines, " 306 ", "RPL_NOWAWAY")
|
|
|
|
clearLines := client.sendAndExpect("AWAY", " 305 ")
|
|
assertContains(t, clearLines, " 305 ", "RPL_UNAWAY")
|
|
}
|
|
|
|
func TestHandlePassPostRegistration(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("passuser")
|
|
lines := client.sendAndExpect(
|
|
"PASS :mypassword123", "Password set",
|
|
)
|
|
|
|
assertContains(
|
|
t, lines, "Password set", "password confirmation",
|
|
)
|
|
}
|
|
|
|
func TestPreRegistrationNotRegistered(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.send("PRIVMSG #test :hello")
|
|
|
|
line := client.readLine()
|
|
if !strings.Contains(line, " 451 ") {
|
|
t.Errorf(
|
|
"expected ERR_NOTREGISTERED (451), got: %s",
|
|
line,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestNamesNonExistentChannel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("namesuser")
|
|
lines := client.sendAndExpect(
|
|
"NAMES #doesnotexist", " 366 ",
|
|
)
|
|
|
|
assertContains(
|
|
t, lines, " 366 ",
|
|
"RPL_ENDOFNAMES for non-existent channel",
|
|
)
|
|
}
|
|
|
|
// TestRelayStopsWhenConnectionCloses checks that closing a
|
|
// registered client's connection stops the goroutine that
|
|
// relays its messages.
|
|
//
|
|
//nolint:paralleltest // counts every goroutine in the process
|
|
func TestRelayStopsWhenConnectionCloses(t *testing.T) {
|
|
env := newTestEnv(t)
|
|
client := env.dial(t)
|
|
|
|
client.register("relaystop")
|
|
waitForRelayGoroutines(t, 1)
|
|
|
|
err := client.conn.Close()
|
|
if err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
|
|
waitForRelayGoroutines(t, 0)
|
|
}
|
|
|
|
const (
|
|
stackDumpSize = 1 << 20
|
|
relayCheckStep = 10 * time.Millisecond
|
|
)
|
|
|
|
// waitForRelayGoroutines waits until exactly want goroutines
|
|
// are running the relay loop, and fails the test if that does
|
|
// not happen within testTimeout.
|
|
func waitForRelayGoroutines(t *testing.T, want int) {
|
|
t.Helper()
|
|
|
|
deadline := time.Now().Add(testTimeout)
|
|
stacks := make([]byte, stackDumpSize)
|
|
|
|
for {
|
|
n := runtime.Stack(stacks, true)
|
|
got := strings.Count(string(stacks[:n]), "(*Conn).relayMessages(")
|
|
|
|
if got == want {
|
|
return
|
|
}
|
|
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("relay goroutines: got %d, want %d", got, want)
|
|
}
|
|
|
|
time.Sleep(relayCheckStep)
|
|
}
|
|
}
|
|
|
|
func BenchmarkParseMessage(b *testing.B) {
|
|
line := ":nick!user@host PRIVMSG #channel :Hello, world!"
|
|
|
|
b.ResetTimer()
|
|
|
|
for range b.N {
|
|
_ = ircserver.ParseMessage(line)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFormatMessage(b *testing.B) {
|
|
b.ResetTimer()
|
|
|
|
for range b.N {
|
|
_ = ircserver.FormatMessage(
|
|
"nick!user@host", "PRIVMSG",
|
|
"#channel", "Hello, world!",
|
|
)
|
|
}
|
|
}
|