package handlers_test import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "git.eeqj.de/sneak/chat/internal/config" "git.eeqj.de/sneak/chat/internal/db" "git.eeqj.de/sneak/chat/internal/globals" "git.eeqj.de/sneak/chat/internal/handlers" "git.eeqj.de/sneak/chat/internal/healthcheck" "git.eeqj.de/sneak/chat/internal/logger" "git.eeqj.de/sneak/chat/internal/middleware" "git.eeqj.de/sneak/chat/internal/server" "go.uber.org/fx" "go.uber.org/fx/fxtest" ) // testServer wraps a test HTTP server with helper methods. type testServer struct { srv *httptest.Server t *testing.T fxApp *fxtest.App } func newTestServer(t *testing.T) *testServer { t.Helper() var s *server.Server app := fxtest.New(t, fx.Provide( func() *globals.Globals { return &globals.Globals{ Appname: "chat-test", Version: "test", } }, logger.New, func( lc fx.Lifecycle, g *globals.Globals, l *logger.Logger, ) (*config.Config, error) { return config.New(lc, config.Params{ Globals: g, Logger: l, }) }, func( lc fx.Lifecycle, l *logger.Logger, c *config.Config, ) (*db.Database, error) { return db.New(lc, db.Params{ Logger: l, Config: c, }) }, func( lc fx.Lifecycle, g *globals.Globals, c *config.Config, l *logger.Logger, d *db.Database, ) (*healthcheck.Healthcheck, error) { return healthcheck.New(lc, healthcheck.Params{ Globals: g, Config: c, Logger: l, Database: d, }) }, func( lc fx.Lifecycle, l *logger.Logger, g *globals.Globals, c *config.Config, ) (*middleware.Middleware, error) { return middleware.New(lc, middleware.Params{ Logger: l, Globals: g, Config: c, }) }, func( lc fx.Lifecycle, l *logger.Logger, g *globals.Globals, c *config.Config, d *db.Database, hc *healthcheck.Healthcheck, ) (*handlers.Handlers, error) { return handlers.New(lc, handlers.Params{ Logger: l, Globals: g, Config: c, Database: d, Healthcheck: hc, }) }, func( lc fx.Lifecycle, l *logger.Logger, g *globals.Globals, c *config.Config, mw *middleware.Middleware, h *handlers.Handlers, ) (*server.Server, error) { return server.New(lc, server.Params{ Logger: l, Globals: g, Config: c, Middleware: mw, Handlers: h, }) }, ), fx.Populate(&s), ) app.RequireStart() time.Sleep(100 * time.Millisecond) ts := httptest.NewServer(s) t.Cleanup(func() { ts.Close() app.RequireStop() }) return &testServer{srv: ts, t: t, fxApp: app} } func (ts *testServer) url(path string) string { return ts.srv.URL + path } func (ts *testServer) doReq( method, url string, body io.Reader, ) (*http.Response, error) { ts.t.Helper() req, err := http.NewRequestWithContext( context.Background(), method, url, body, ) if err != nil { return nil, fmt.Errorf("new request: %w", err) } if body != nil { req.Header.Set("Content-Type", "application/json") } return http.DefaultClient.Do(req) } func (ts *testServer) doReqAuth( method, url, token string, body io.Reader, ) (*http.Response, error) { ts.t.Helper() req, err := http.NewRequestWithContext( context.Background(), method, url, body, ) if err != nil { return nil, fmt.Errorf("new request: %w", err) } if body != nil { req.Header.Set("Content-Type", "application/json") } if token != "" { req.Header.Set("Authorization", "Bearer "+token) } return http.DefaultClient.Do(req) } func (ts *testServer) createSession(nick string) string { ts.t.Helper() body, err := json.Marshal(map[string]string{"nick": nick}) if err != nil { ts.t.Fatalf("marshal session: %v", err) } resp, err := ts.doReq( http.MethodPost, ts.url("/api/v1/session"), bytes.NewReader(body), ) if err != nil { ts.t.Fatalf("create session: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { b, _ := io.ReadAll(resp.Body) ts.t.Fatalf("create session: status %d: %s", resp.StatusCode, b) } var result struct { ID int64 `json:"id"` Token string `json:"token"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { ts.t.Fatalf("decode session: %v", err) } return result.Token } func (ts *testServer) sendCommand( token string, cmd map[string]any, ) (int, map[string]any) { ts.t.Helper() body, err := json.Marshal(cmd) if err != nil { ts.t.Fatalf("marshal command: %v", err) } resp, err := ts.doReqAuth( http.MethodPost, ts.url("/api/v1/messages"), token, bytes.NewReader(body), ) if err != nil { ts.t.Fatalf("send command: %v", err) } defer func() { _ = resp.Body.Close() }() var result map[string]any _ = json.NewDecoder(resp.Body).Decode(&result) return resp.StatusCode, result } func (ts *testServer) getJSON( token, path string, //nolint:unparam ) (int, map[string]any) { ts.t.Helper() resp, err := ts.doReqAuth(http.MethodGet, ts.url(path), token, nil) if err != nil { ts.t.Fatalf("get: %v", err) } defer func() { _ = resp.Body.Close() }() var result map[string]any _ = json.NewDecoder(resp.Body).Decode(&result) return resp.StatusCode, result } func (ts *testServer) pollMessages( token string, afterID int64, ) ([]map[string]any, int64) { ts.t.Helper() url := fmt.Sprintf( "%s/api/v1/messages?timeout=0&after=%d", ts.srv.URL, afterID, ) resp, err := ts.doReqAuth(http.MethodGet, url, token, nil) if err != nil { ts.t.Fatalf("poll: %v", err) } defer func() { _ = resp.Body.Close() }() var result struct { Messages []map[string]any `json:"messages"` LastID json.Number `json:"last_id"` //nolint:tagliatelle } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { ts.t.Fatalf("decode poll: %v", err) } lastID, _ := result.LastID.Int64() return result.Messages, lastID } // --- Tests --- func TestCreateSession(t *testing.T) { ts := newTestServer(t) t.Run("valid nick", func(t *testing.T) { token := ts.createSession("alice") if token == "" { t.Fatal("expected token") } }) t.Run("duplicate nick", func(t *testing.T) { body, err := json.Marshal(map[string]string{"nick": "alice"}) if err != nil { t.Fatalf("marshal: %v", err) } resp, err := ts.doReq( http.MethodPost, ts.url("/api/v1/session"), bytes.NewReader(body), ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusConflict { t.Fatalf("expected 409, got %d", resp.StatusCode) } }) t.Run("empty nick", func(t *testing.T) { body, err := json.Marshal(map[string]string{"nick": ""}) if err != nil { t.Fatalf("marshal: %v", err) } resp, err := ts.doReq( http.MethodPost, ts.url("/api/v1/session"), bytes.NewReader(body), ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("invalid nick chars", func(t *testing.T) { body, err := json.Marshal(map[string]string{"nick": "hello world"}) if err != nil { t.Fatalf("marshal: %v", err) } resp, err := ts.doReq( http.MethodPost, ts.url("/api/v1/session"), bytes.NewReader(body), ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("nick starting with number", func(t *testing.T) { body, err := json.Marshal(map[string]string{"nick": "123abc"}) if err != nil { t.Fatalf("marshal: %v", err) } resp, err := ts.doReq( http.MethodPost, ts.url("/api/v1/session"), bytes.NewReader(body), ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("malformed json", func(t *testing.T) { resp, err := ts.doReq( http.MethodPost, ts.url("/api/v1/session"), strings.NewReader("{bad"), ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) } func TestAuth(t *testing.T) { ts := newTestServer(t) t.Run("no auth header", func(t *testing.T) { status, _ := ts.getJSON("", "/api/v1/state") if status != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", status) } }) t.Run("bad token", func(t *testing.T) { status, _ := ts.getJSON("invalid-token-12345", "/api/v1/state") if status != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", status) } }) t.Run("valid token", func(t *testing.T) { token := ts.createSession("authtest") status, result := ts.getJSON(token, "/api/v1/state") if status != http.StatusOK { t.Fatalf("expected 200, got %d", status) } if result["nick"] != "authtest" { t.Fatalf("expected nick authtest, got %v", result["nick"]) } }) } func TestJoinAndPart(t *testing.T) { ts := newTestServer(t) token := ts.createSession("bob") t.Run("join channel", func(t *testing.T) { status, result := ts.sendCommand(token, map[string]any{"command": "JOIN", "to": "#test"}) if status != http.StatusOK { t.Fatalf("expected 200, got %d: %v", status, result) } if result["channel"] != "#test" { t.Fatalf("expected #test, got %v", result["channel"]) } }) t.Run("join without hash", func(t *testing.T) { status, result := ts.sendCommand(token, map[string]any{"command": "JOIN", "to": "other"}) if status != http.StatusOK { t.Fatalf("expected 200, got %d: %v", status, result) } if result["channel"] != "#other" { t.Fatalf("expected #other, got %v", result["channel"]) } }) t.Run("part channel", func(t *testing.T) { status, result := ts.sendCommand(token, map[string]any{"command": "PART", "to": "#test"}) if status != http.StatusOK { t.Fatalf("expected 200, got %d: %v", status, result) } if result["channel"] != "#test" { t.Fatalf("expected #test, got %v", result["channel"]) } }) t.Run("join missing to", func(t *testing.T) { status, _ := ts.sendCommand(token, map[string]any{"command": "JOIN"}) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } }) } func TestPrivmsg(t *testing.T) { ts := newTestServer(t) aliceToken := ts.createSession("alice_msg") bobToken := ts.createSession("bob_msg") ts.sendCommand(aliceToken, map[string]any{"command": "JOIN", "to": "#chat"}) ts.sendCommand(bobToken, map[string]any{"command": "JOIN", "to": "#chat"}) _, _ = ts.pollMessages(aliceToken, 0) _, bobLastID := ts.pollMessages(bobToken, 0) t.Run("send channel message", func(t *testing.T) { status, result := ts.sendCommand(aliceToken, map[string]any{ "command": "PRIVMSG", "to": "#chat", "body": []string{"hello world"}, }) if status != http.StatusCreated { t.Fatalf("expected 201, got %d: %v", status, result) } if result["id"] == nil || result["id"] == "" { t.Fatal("expected message id") } }) t.Run("bob receives message", func(t *testing.T) { msgs, _ := ts.pollMessages(bobToken, bobLastID) found := false for _, m := range msgs { if m["command"] == "PRIVMSG" && m["from"] == "alice_msg" { found = true break } } if !found { t.Fatalf("bob didn't receive alice's message: %v", msgs) } }) t.Run("missing body", func(t *testing.T) { status, _ := ts.sendCommand(aliceToken, map[string]any{ "command": "PRIVMSG", "to": "#chat", }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } }) t.Run("missing to", func(t *testing.T) { status, _ := ts.sendCommand(aliceToken, map[string]any{ "command": "PRIVMSG", "body": []string{"hello"}, }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } }) } func TestDM(t *testing.T) { ts := newTestServer(t) aliceToken := ts.createSession("alice_dm") bobToken := ts.createSession("bob_dm") t.Run("send DM", func(t *testing.T) { status, result := ts.sendCommand(aliceToken, map[string]any{ "command": "PRIVMSG", "to": "bob_dm", "body": []string{"hey bob"}, }) if status != http.StatusCreated { t.Fatalf("expected 201, got %d: %v", status, result) } }) t.Run("bob receives DM", func(t *testing.T) { msgs, _ := ts.pollMessages(bobToken, 0) found := false for _, m := range msgs { if m["command"] == "PRIVMSG" && m["from"] == "alice_dm" { found = true } } if !found { t.Fatal("bob didn't receive DM") } }) t.Run("alice gets echo", func(t *testing.T) { msgs, _ := ts.pollMessages(aliceToken, 0) found := false for _, m := range msgs { if m["command"] == "PRIVMSG" && m["from"] == "alice_dm" && m["to"] == "bob_dm" { found = true } } if !found { t.Fatal("alice didn't get DM echo") } }) t.Run("DM to nonexistent user", func(t *testing.T) { status, _ := ts.sendCommand(aliceToken, map[string]any{ "command": "PRIVMSG", "to": "nobody", "body": []string{"hello?"}, }) if status != http.StatusNotFound { t.Fatalf("expected 404, got %d", status) } }) } func TestNick(t *testing.T) { ts := newTestServer(t) token := ts.createSession("nick_test") t.Run("change nick", func(t *testing.T) { status, result := ts.sendCommand(token, map[string]any{ "command": "NICK", "body": []string{"newnick"}, }) if status != http.StatusOK { t.Fatalf("expected 200, got %d: %v", status, result) } if result["nick"] != "newnick" { t.Fatalf("expected newnick, got %v", result["nick"]) } }) t.Run("nick same as current", func(t *testing.T) { status, _ := ts.sendCommand(token, map[string]any{ "command": "NICK", "body": []string{"newnick"}, }) if status != http.StatusOK { t.Fatalf("expected 200, got %d", status) } }) t.Run("nick collision", func(t *testing.T) { ts.createSession("taken_nick") status, _ := ts.sendCommand(token, map[string]any{ "command": "NICK", "body": []string{"taken_nick"}, }) if status != http.StatusConflict { t.Fatalf("expected 409, got %d", status) } }) t.Run("invalid nick", func(t *testing.T) { status, _ := ts.sendCommand(token, map[string]any{ "command": "NICK", "body": []string{"bad nick!"}, }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } }) t.Run("empty body", func(t *testing.T) { status, _ := ts.sendCommand(token, map[string]any{ "command": "NICK", }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } }) } func TestTopic(t *testing.T) { ts := newTestServer(t) token := ts.createSession("topic_user") ts.sendCommand(token, map[string]any{"command": "JOIN", "to": "#topictest"}) t.Run("set topic", func(t *testing.T) { status, result := ts.sendCommand(token, map[string]any{ "command": "TOPIC", "to": "#topictest", "body": []string{"Hello World Topic"}, }) if status != http.StatusOK { t.Fatalf("expected 200, got %d: %v", status, result) } if result["topic"] != "Hello World Topic" { t.Fatalf("expected topic, got %v", result["topic"]) } }) t.Run("missing to", func(t *testing.T) { status, _ := ts.sendCommand(token, map[string]any{ "command": "TOPIC", "body": []string{"topic"}, }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } }) t.Run("missing body", func(t *testing.T) { status, _ := ts.sendCommand(token, map[string]any{ "command": "TOPIC", "to": "#topictest", }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } }) } func TestPing(t *testing.T) { ts := newTestServer(t) token := ts.createSession("ping_user") status, result := ts.sendCommand(token, map[string]any{"command": "PING"}) if status != http.StatusOK { t.Fatalf("expected 200, got %d", status) } if result["command"] != "PONG" { t.Fatalf("expected PONG, got %v", result["command"]) } } func TestQuit(t *testing.T) { ts := newTestServer(t) token := ts.createSession("quitter") observerToken := ts.createSession("observer") ts.sendCommand(token, map[string]any{"command": "JOIN", "to": "#quitchan"}) ts.sendCommand(observerToken, map[string]any{"command": "JOIN", "to": "#quitchan"}) _, lastID := ts.pollMessages(observerToken, 0) status, result := ts.sendCommand(token, map[string]any{"command": "QUIT"}) if status != http.StatusOK { t.Fatalf("expected 200, got %d: %v", status, result) } msgs, _ := ts.pollMessages(observerToken, lastID) found := false for _, m := range msgs { if m["command"] == "QUIT" && m["from"] == "quitter" { found = true } } if !found { t.Fatalf("observer didn't get QUIT: %v", msgs) } status2, _ := ts.getJSON(token, "/api/v1/state") if status2 != http.StatusUnauthorized { t.Fatalf("expected 401 after quit, got %d", status2) } } func TestUnknownCommand(t *testing.T) { ts := newTestServer(t) token := ts.createSession("cmdtest") status, result := ts.sendCommand(token, map[string]any{"command": "BOGUS"}) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %v", status, result) } } func TestEmptyCommand(t *testing.T) { ts := newTestServer(t) token := ts.createSession("emptycmd") status, _ := ts.sendCommand(token, map[string]any{"command": ""}) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } } func TestHistory(t *testing.T) { ts := newTestServer(t) token := ts.createSession("historian") ts.sendCommand(token, map[string]any{"command": "JOIN", "to": "#history"}) for range 5 { ts.sendCommand(token, map[string]any{ "command": "PRIVMSG", "to": "#history", "body": []string{"test message"}, }) } resp, err := ts.doReqAuth( http.MethodGet, ts.url("/api/v1/history?target=%23history&limit=3"), token, nil, ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var msgs []map[string]any if err := json.NewDecoder(resp.Body).Decode(&msgs); err != nil { t.Fatalf("decode history: %v", err) } if len(msgs) != 3 { t.Fatalf("expected 3 messages, got %d", len(msgs)) } } func TestChannelList(t *testing.T) { ts := newTestServer(t) token := ts.createSession("lister") ts.sendCommand(token, map[string]any{"command": "JOIN", "to": "#listchan"}) resp, err := ts.doReqAuth( http.MethodGet, ts.url("/api/v1/channels"), token, nil, ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var channels []map[string]any if err := json.NewDecoder(resp.Body).Decode(&channels); err != nil { t.Fatalf("decode channels: %v", err) } found := false for _, ch := range channels { if ch["name"] == "#listchan" { found = true } } if !found { t.Fatal("channel not in list") } } func TestChannelMembers(t *testing.T) { ts := newTestServer(t) token := ts.createSession("membertest") ts.sendCommand(token, map[string]any{"command": "JOIN", "to": "#members"}) resp, err := ts.doReqAuth( http.MethodGet, ts.url("/api/v1/channels/members/members"), token, nil, ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestLongPoll(t *testing.T) { ts := newTestServer(t) aliceToken := ts.createSession("lp_alice") bobToken := ts.createSession("lp_bob") ts.sendCommand(aliceToken, map[string]any{"command": "JOIN", "to": "#longpoll"}) ts.sendCommand(bobToken, map[string]any{"command": "JOIN", "to": "#longpoll"}) _, lastID := ts.pollMessages(bobToken, 0) var wg sync.WaitGroup var pollMsgs []map[string]any wg.Add(1) go func() { defer wg.Done() url := fmt.Sprintf( "%s/api/v1/messages?timeout=5&after=%d", ts.srv.URL, lastID, ) resp, err := ts.doReqAuth(http.MethodGet, url, bobToken, nil) if err != nil { return } defer func() { _ = resp.Body.Close() }() var result struct { Messages []map[string]any `json:"messages"` } _ = json.NewDecoder(resp.Body).Decode(&result) pollMsgs = result.Messages }() time.Sleep(200 * time.Millisecond) ts.sendCommand(aliceToken, map[string]any{ "command": "PRIVMSG", "to": "#longpoll", "body": []string{"wake up!"}, }) wg.Wait() found := false for _, m := range pollMsgs { if m["command"] == "PRIVMSG" && m["from"] == "lp_alice" { found = true } } if !found { t.Fatalf("long-poll didn't receive message: %v", pollMsgs) } } func TestLongPollTimeout(t *testing.T) { ts := newTestServer(t) token := ts.createSession("lp_timeout") start := time.Now() resp, err := ts.doReqAuth( http.MethodGet, ts.url("/api/v1/messages?timeout=1"), token, nil, ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() elapsed := time.Since(start) if elapsed < 900*time.Millisecond { t.Fatalf("long-poll returned too fast: %v", elapsed) } if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestEphemeralChannelCleanup(t *testing.T) { ts := newTestServer(t) token := ts.createSession("ephemeral") ts.sendCommand(token, map[string]any{"command": "JOIN", "to": "#ephemeral"}) ts.sendCommand(token, map[string]any{"command": "PART", "to": "#ephemeral"}) resp, err := ts.doReqAuth( http.MethodGet, ts.url("/api/v1/channels"), token, nil, ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() var channels []map[string]any if err := json.NewDecoder(resp.Body).Decode(&channels); err != nil { t.Fatalf("decode channels: %v", err) } for _, ch := range channels { if ch["name"] == "#ephemeral" { t.Fatal("ephemeral channel should have been cleaned up") } } } func TestConcurrentSessions(t *testing.T) { ts := newTestServer(t) var wg sync.WaitGroup errs := make(chan error, 20) for i := range 20 { wg.Add(1) go func(i int) { defer wg.Done() nick := "concurrent_" + string(rune('a'+i)) body, err := json.Marshal(map[string]string{"nick": nick}) if err != nil { errs <- fmt.Errorf("marshal: %w", err) return } resp, err := ts.doReq( http.MethodPost, ts.url("/api/v1/session"), bytes.NewReader(body), ) if err != nil { errs <- err return } _ = resp.Body.Close() if resp.StatusCode != http.StatusCreated { errs <- fmt.Errorf( //nolint:err113 "status %d for %s", resp.StatusCode, nick, ) } }(i) } wg.Wait() close(errs) for err := range errs { if err != nil { t.Fatalf("concurrent session creation error: %v", err) } } } func TestServerInfo(t *testing.T) { ts := newTestServer(t) resp, err := ts.doReq(http.MethodGet, ts.url("/api/v1/server"), nil) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestHealthcheck(t *testing.T) { ts := newTestServer(t) resp, err := ts.doReq(http.MethodGet, ts.url("/.well-known/healthcheck.json"), nil) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var result map[string]any if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { t.Fatalf("decode healthcheck: %v", err) } if result["status"] != "ok" { t.Fatalf("expected ok status, got %v", result["status"]) } } func TestNickBroadcastToChannels(t *testing.T) { ts := newTestServer(t) aliceToken := ts.createSession("nick_a") bobToken := ts.createSession("nick_b") ts.sendCommand(aliceToken, map[string]any{"command": "JOIN", "to": "#nicktest"}) ts.sendCommand(bobToken, map[string]any{"command": "JOIN", "to": "#nicktest"}) _, lastID := ts.pollMessages(bobToken, 0) ts.sendCommand(aliceToken, map[string]any{ "command": "NICK", "body": []string{"nick_a_new"}, }) msgs, _ := ts.pollMessages(bobToken, lastID) found := false for _, m := range msgs { if m["command"] == "NICK" && m["from"] == "nick_a" { found = true } } if !found { t.Fatalf("bob didn't get nick change: %v", msgs) } }