// Tests use a global viper instance for configuration, // making parallel execution unsafe. // //nolint:paralleltest package handlers_test import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "path/filepath" "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" ) const ( commandKey = "command" bodyKey = "body" toKey = "to" statusKey = "status" privmsgCmd = "PRIVMSG" joinCmd = "JOIN" apiMessages = "/api/v1/messages" apiSession = "/api/v1/session" apiState = "/api/v1/state" ) // testServer wraps a test HTTP server with helpers. type testServer struct { httpServer *httptest.Server t *testing.T fxApp *fxtest.App } func newTestServer( t *testing.T, ) *testServer { t.Helper() dbPath := filepath.Join( t.TempDir(), "test.db", ) dbURL := "file:" + dbPath + "?_journal_mode=WAL&_busy_timeout=5000" var srv *server.Server app := fxtest.New(t, fx.Provide( newTestGlobals, logger.New, func( lifecycle fx.Lifecycle, globs *globals.Globals, log *logger.Logger, ) (*config.Config, error) { cfg, err := config.New( lifecycle, config.Params{ //nolint:exhaustruct Globals: globs, Logger: log, }, ) if err != nil { return nil, fmt.Errorf( "test config: %w", err, ) } cfg.DBURL = dbURL cfg.Port = 0 return cfg, nil }, newTestDB, newTestHealthcheck, newTestMiddleware, newTestHandlers, newTestServerFx, ), fx.Populate(&srv), ) app.RequireStart() time.Sleep(100 * time.Millisecond) httpSrv := httptest.NewServer(srv) t.Cleanup(func() { httpSrv.Close() app.RequireStop() }) return &testServer{ httpServer: httpSrv, t: t, fxApp: app, } } func newTestGlobals() *globals.Globals { return &globals.Globals{ Appname: "chat-test", Version: "test", } } func newTestDB( lifecycle fx.Lifecycle, log *logger.Logger, cfg *config.Config, ) (*db.Database, error) { database, err := db.New(lifecycle, db.Params{ //nolint:exhaustruct Logger: log, Config: cfg, }) if err != nil { return nil, fmt.Errorf("test db: %w", err) } return database, nil } func newTestHealthcheck( lifecycle fx.Lifecycle, globs *globals.Globals, cfg *config.Config, log *logger.Logger, database *db.Database, ) (*healthcheck.Healthcheck, error) { hcheck, err := healthcheck.New(lifecycle, healthcheck.Params{ //nolint:exhaustruct Globals: globs, Config: cfg, Logger: log, Database: database, }) if err != nil { return nil, fmt.Errorf("test healthcheck: %w", err) } return hcheck, nil } func newTestMiddleware( lifecycle fx.Lifecycle, log *logger.Logger, globs *globals.Globals, cfg *config.Config, ) (*middleware.Middleware, error) { mware, err := middleware.New(lifecycle, middleware.Params{ //nolint:exhaustruct Logger: log, Globals: globs, Config: cfg, }) if err != nil { return nil, fmt.Errorf("test middleware: %w", err) } return mware, nil } func newTestHandlers( lifecycle fx.Lifecycle, log *logger.Logger, globs *globals.Globals, cfg *config.Config, database *db.Database, hcheck *healthcheck.Healthcheck, ) (*handlers.Handlers, error) { hdlr, err := handlers.New(lifecycle, handlers.Params{ //nolint:exhaustruct Logger: log, Globals: globs, Config: cfg, Database: database, Healthcheck: hcheck, }) if err != nil { return nil, fmt.Errorf("test handlers: %w", err) } return hdlr, nil } func newTestServerFx( lifecycle fx.Lifecycle, log *logger.Logger, globs *globals.Globals, cfg *config.Config, mware *middleware.Middleware, hdlr *handlers.Handlers, ) (*server.Server, error) { srv, err := server.New(lifecycle, server.Params{ //nolint:exhaustruct Logger: log, Globals: globs, Config: cfg, Middleware: mware, Handlers: hdlr, }) if err != nil { return nil, fmt.Errorf("test server: %w", err) } return srv, nil } func (tserver *testServer) url(path string) string { return tserver.httpServer.URL + path } func doRequest( t *testing.T, method, url string, body io.Reader, ) (*http.Response, error) { t.Helper() request, err := http.NewRequestWithContext( t.Context(), method, url, body, ) if err != nil { return nil, fmt.Errorf("new request: %w", err) } if body != nil { request.Header.Set( "Content-Type", "application/json", ) } resp, err := http.DefaultClient.Do(request) if err != nil { return nil, fmt.Errorf("do request: %w", err) } return resp, nil } func doRequestAuth( t *testing.T, method, url, token string, body io.Reader, ) (*http.Response, error) { t.Helper() request, err := http.NewRequestWithContext( t.Context(), method, url, body, ) if err != nil { return nil, fmt.Errorf("new request: %w", err) } if body != nil { request.Header.Set( "Content-Type", "application/json", ) } if token != "" { request.Header.Set( "Authorization", "Bearer "+token, ) } resp, err := http.DefaultClient.Do(request) if err != nil { return nil, fmt.Errorf("do request: %w", err) } return resp, nil } func (tserver *testServer) createSession( nick string, ) string { tserver.t.Helper() body, err := json.Marshal( map[string]string{"nick": nick}, ) if err != nil { tserver.t.Fatalf("marshal session: %v", err) } resp, err := doRequest( tserver.t, http.MethodPost, tserver.url(apiSession), bytes.NewReader(body), ) if err != nil { tserver.t.Fatalf("create session: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { respBody, _ := io.ReadAll(resp.Body) tserver.t.Fatalf( "create session: status %d: %s", resp.StatusCode, respBody, ) } var result struct { ID int64 `json:"id"` Token string `json:"token"` } decErr := json.NewDecoder(resp.Body).Decode(&result) if decErr != nil { tserver.t.Fatalf("decode session: %v", decErr) } return result.Token } func (tserver *testServer) sendCommand( token string, cmd map[string]any, ) (int, map[string]any) { tserver.t.Helper() body, err := json.Marshal(cmd) if err != nil { tserver.t.Fatalf("marshal command: %v", err) } resp, err := doRequestAuth( tserver.t, http.MethodPost, tserver.url(apiMessages), token, bytes.NewReader(body), ) if err != nil { tserver.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 (tserver *testServer) getState( token string, ) (int, map[string]any) { tserver.t.Helper() resp, err := doRequestAuth( tserver.t, http.MethodGet, tserver.url(apiState), token, nil, ) if err != nil { tserver.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 (tserver *testServer) pollMessages( token string, afterID int64, ) ([]map[string]any, int64) { tserver.t.Helper() pollURL := fmt.Sprintf( "%s"+apiMessages+"?timeout=0&after=%d", tserver.httpServer.URL, afterID, ) resp, err := doRequestAuth( tserver.t, http.MethodGet, pollURL, token, nil, ) if err != nil { tserver.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 } decErr := json.NewDecoder(resp.Body).Decode(&result) if decErr != nil { tserver.t.Fatalf("decode poll: %v", decErr) } lastID, _ := result.LastID.Int64() return result.Messages, lastID } func postSession( t *testing.T, tserver *testServer, nick string, ) *http.Response { t.Helper() body, err := json.Marshal( map[string]string{"nick": nick}, ) if err != nil { t.Fatalf("marshal: %v", err) } resp, err := doRequest( t, http.MethodPost, tserver.url(apiSession), bytes.NewReader(body), ) if err != nil { t.Fatal(err) } return resp } func findMessage( msgs []map[string]any, command, from string, ) bool { for _, msg := range msgs { if msg[commandKey] == command && msg["from"] == from { return true } } return false } // --- Tests --- func TestCreateSessionValid(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("alice") if token == "" { t.Fatal("expected token") } } func TestCreateSessionDuplicate(t *testing.T) { tserver := newTestServer(t) tserver.createSession("alice") resp := postSession(t, tserver, "alice") defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusConflict { t.Fatalf("expected 409, got %d", resp.StatusCode) } } func TestCreateSessionEmpty(t *testing.T) { tserver := newTestServer(t) resp := postSession(t, tserver, "") defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusBadRequest { t.Fatalf( "expected 400, got %d", resp.StatusCode, ) } } func TestCreateSessionInvalidChars(t *testing.T) { tserver := newTestServer(t) resp := postSession(t, tserver, "hello world") defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusBadRequest { t.Fatalf( "expected 400, got %d", resp.StatusCode, ) } } func TestCreateSessionNumericStart(t *testing.T) { tserver := newTestServer(t) resp := postSession(t, tserver, "123abc") defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusBadRequest { t.Fatalf( "expected 400, got %d", resp.StatusCode, ) } } func TestCreateSessionMalformed(t *testing.T) { tserver := newTestServer(t) resp, err := doRequest( t, http.MethodPost, tserver.url(apiSession), 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 TestAuthNoHeader(t *testing.T) { tserver := newTestServer(t) status, _ := tserver.getState("") if status != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", status) } } func TestAuthBadToken(t *testing.T) { tserver := newTestServer(t) status, _ := tserver.getState( "invalid-token-12345", ) if status != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", status) } } func TestAuthValidToken(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("authtest") status, result := tserver.getState(token) 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 TestJoinChannel(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("joiner") status, result := tserver.sendCommand( token, map[string]any{ commandKey: joinCmd, toKey: "#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"], ) } } func TestJoinWithoutHash(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("joiner2") status, result := tserver.sendCommand( token, map[string]any{ commandKey: joinCmd, toKey: "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"], ) } } func TestPartChannel(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("parter") tserver.sendCommand( token, map[string]any{ commandKey: joinCmd, toKey: "#test", }, ) status, result := tserver.sendCommand( token, map[string]any{ commandKey: "PART", toKey: "#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"], ) } } func TestJoinMissingTo(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("joiner3") status, _ := tserver.sendCommand( token, map[string]any{commandKey: joinCmd}, ) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } } func TestChannelMessage(t *testing.T) { tserver := newTestServer(t) aliceToken := tserver.createSession("alice_msg") bobToken := tserver.createSession("bob_msg") tserver.sendCommand(aliceToken, map[string]any{ commandKey: joinCmd, toKey: "#chat", }) tserver.sendCommand(bobToken, map[string]any{ commandKey: joinCmd, toKey: "#chat", }) _, _ = tserver.pollMessages(aliceToken, 0) _, bobLastID := tserver.pollMessages(bobToken, 0) status, result := tserver.sendCommand( aliceToken, map[string]any{ commandKey: privmsgCmd, toKey: "#chat", bodyKey: []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") } msgs, _ := tserver.pollMessages( bobToken, bobLastID, ) if !findMessage(msgs, privmsgCmd, "alice_msg") { t.Fatalf( "bob didn't receive alice's message: %v", msgs, ) } } func TestMessageMissingBody(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("nobody") tserver.sendCommand(token, map[string]any{ commandKey: joinCmd, toKey: "#chat", }) status, _ := tserver.sendCommand(token, map[string]any{ commandKey: privmsgCmd, toKey: "#chat", }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } } func TestMessageMissingTo(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("noto") status, _ := tserver.sendCommand(token, map[string]any{ commandKey: privmsgCmd, bodyKey: []string{"hello"}, }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } } func TestNonMemberCannotSend(t *testing.T) { tserver := newTestServer(t) aliceToken := tserver.createSession("alice_nosend") bobToken := tserver.createSession("bob_nosend") // Only bob joins the channel. tserver.sendCommand(bobToken, map[string]any{ commandKey: joinCmd, toKey: "#private", }) // Alice tries to send without joining. status, _ := tserver.sendCommand( aliceToken, map[string]any{ commandKey: privmsgCmd, toKey: "#private", bodyKey: []string{"sneaky"}, }, ) if status != http.StatusForbidden { t.Fatalf("expected 403, got %d", status) } } func TestDirectMessage(t *testing.T) { tserver := newTestServer(t) aliceToken := tserver.createSession("alice_dm") bobToken := tserver.createSession("bob_dm") status, result := tserver.sendCommand( aliceToken, map[string]any{ commandKey: privmsgCmd, toKey: "bob_dm", bodyKey: []string{"hey bob"}, }, ) if status != http.StatusCreated { t.Fatalf( "expected 201, got %d: %v", status, result, ) } msgs, _ := tserver.pollMessages(bobToken, 0) if !findMessage(msgs, privmsgCmd, "alice_dm") { t.Fatal("bob didn't receive DM") } aliceMsgs, _ := tserver.pollMessages(aliceToken, 0) found := false for _, msg := range aliceMsgs { if msg[commandKey] == privmsgCmd && msg["from"] == "alice_dm" && msg[toKey] == "bob_dm" { found = true } } if !found { t.Fatal("alice didn't get DM echo") } } func TestDMToNonexistentUser(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("dmsender") status, _ := tserver.sendCommand(token, map[string]any{ commandKey: privmsgCmd, toKey: "nobody", bodyKey: []string{"hello?"}, }) if status != http.StatusNotFound { t.Fatalf("expected 404, got %d", status) } } func TestNickChange(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("nick_test") status, result := tserver.sendCommand( token, map[string]any{ commandKey: "NICK", bodyKey: []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"], ) } } func TestNickSameAsCurrent(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("same_nick") status, _ := tserver.sendCommand(token, map[string]any{ commandKey: "NICK", bodyKey: []string{"same_nick"}, }) if status != http.StatusOK { t.Fatalf("expected 200, got %d", status) } } func TestNickCollision(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("nickuser") tserver.createSession("taken_nick") status, _ := tserver.sendCommand(token, map[string]any{ commandKey: "NICK", bodyKey: []string{"taken_nick"}, }) if status != http.StatusConflict { t.Fatalf("expected 409, got %d", status) } } func TestNickInvalid(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("nickval") status, _ := tserver.sendCommand(token, map[string]any{ commandKey: "NICK", bodyKey: []string{"bad nick!"}, }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } } func TestNickEmptyBody(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("nicknobody") status, _ := tserver.sendCommand( token, map[string]any{commandKey: "NICK"}, ) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } } func TestTopic(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("topic_user") tserver.sendCommand(token, map[string]any{ commandKey: joinCmd, toKey: "#topictest", }) status, result := tserver.sendCommand( token, map[string]any{ commandKey: "TOPIC", toKey: "#topictest", bodyKey: []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"], ) } } func TestTopicMissingTo(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("topicnoto") status, _ := tserver.sendCommand(token, map[string]any{ commandKey: "TOPIC", bodyKey: []string{"topic"}, }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } } func TestTopicMissingBody(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("topicnobody") tserver.sendCommand(token, map[string]any{ commandKey: joinCmd, toKey: "#topictest", }) status, _ := tserver.sendCommand(token, map[string]any{ commandKey: "TOPIC", toKey: "#topictest", }) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } } func TestPing(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("ping_user") status, result := tserver.sendCommand( token, map[string]any{commandKey: "PING"}, ) if status != http.StatusOK { t.Fatalf("expected 200, got %d", status) } if result[commandKey] != "PONG" { t.Fatalf( "expected PONG, got %v", result[commandKey], ) } } func TestQuit(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("quitter") observerToken := tserver.createSession("observer") tserver.sendCommand(token, map[string]any{ commandKey: joinCmd, toKey: "#quitchan", }) tserver.sendCommand(observerToken, map[string]any{ commandKey: joinCmd, toKey: "#quitchan", }) _, lastID := tserver.pollMessages(observerToken, 0) status, result := tserver.sendCommand( token, map[string]any{commandKey: "QUIT"}, ) if status != http.StatusOK { t.Fatalf( "expected 200, got %d: %v", status, result, ) } msgs, _ := tserver.pollMessages( observerToken, lastID, ) if !findMessage(msgs, "QUIT", "quitter") { t.Fatalf( "observer didn't get QUIT: %v", msgs, ) } afterStatus, _ := tserver.getState(token) if afterStatus != http.StatusUnauthorized { t.Fatalf( "expected 401 after quit, got %d", afterStatus, ) } } func TestUnknownCommand(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("cmdtest") status, _ := tserver.sendCommand( token, map[string]any{commandKey: "BOGUS"}, ) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } } func TestEmptyCommand(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("emptycmd") status, _ := tserver.sendCommand( token, map[string]any{commandKey: ""}, ) if status != http.StatusBadRequest { t.Fatalf("expected 400, got %d", status) } } func TestHistory(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("historian") tserver.sendCommand(token, map[string]any{ commandKey: joinCmd, toKey: "#history", }) for range 5 { tserver.sendCommand(token, map[string]any{ commandKey: privmsgCmd, toKey: "#history", bodyKey: []string{"test message"}, }) } histURL := tserver.url( "/api/v1/history?target=%23history&limit=3", ) resp, err := doRequestAuth( t, http.MethodGet, histURL, 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 decErr := json.NewDecoder(resp.Body).Decode(&msgs) if decErr != nil { t.Fatalf("decode history: %v", decErr) } if len(msgs) != 3 { t.Fatalf("expected 3 messages, got %d", len(msgs)) } } func TestHistoryNonMember(t *testing.T) { tserver := newTestServer(t) aliceToken := tserver.createSession("alice_hist") bobToken := tserver.createSession("bob_hist") // Alice creates and joins a channel. tserver.sendCommand(aliceToken, map[string]any{ commandKey: joinCmd, toKey: "#secret", }) tserver.sendCommand(aliceToken, map[string]any{ commandKey: privmsgCmd, toKey: "#secret", bodyKey: []string{"secret stuff"}, }) // Bob tries to read history without joining. histURL := tserver.url( "/api/v1/history?target=%23secret", ) resp, err := doRequestAuth( t, http.MethodGet, histURL, bobToken, nil, ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusForbidden { t.Fatalf( "expected 403, got %d", resp.StatusCode, ) } } func TestChannelList(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("lister") tserver.sendCommand(token, map[string]any{ commandKey: joinCmd, toKey: "#listchan", }) resp, err := doRequestAuth( t, http.MethodGet, tserver.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 decErr := json.NewDecoder(resp.Body).Decode( &channels, ) if decErr != nil { t.Fatalf("decode channels: %v", decErr) } found := false for _, channel := range channels { if channel["name"] == "#listchan" { found = true } } if !found { t.Fatal("channel not in list") } } func TestChannelMembers(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("membertest") tserver.sendCommand(token, map[string]any{ commandKey: joinCmd, toKey: "#members", }) resp, err := doRequestAuth( t, http.MethodGet, tserver.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) { tserver := newTestServer(t) aliceToken := tserver.createSession("lp_alice") bobToken := tserver.createSession("lp_bob") tserver.sendCommand(aliceToken, map[string]any{ commandKey: joinCmd, toKey: "#longpoll", }) tserver.sendCommand(bobToken, map[string]any{ commandKey: joinCmd, toKey: "#longpoll", }) _, lastID := tserver.pollMessages(bobToken, 0) var waitGroup sync.WaitGroup var pollMsgs []map[string]any waitGroup.Add(1) go func() { defer waitGroup.Done() pollURL := fmt.Sprintf( "%s"+apiMessages+"?timeout=5&after=%d", tserver.httpServer.URL, lastID, ) resp, err := doRequestAuth( t, http.MethodGet, pollURL, 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) tserver.sendCommand(aliceToken, map[string]any{ commandKey: privmsgCmd, toKey: "#longpoll", bodyKey: []string{"wake up!"}, }) waitGroup.Wait() if !findMessage(pollMsgs, privmsgCmd, "lp_alice") { t.Fatalf( "long-poll didn't receive message: %v", pollMsgs, ) } } func TestLongPollTimeout(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("lp_timeout") start := time.Now() resp, err := doRequestAuth( t, http.MethodGet, tserver.url(apiMessages+"?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) { tserver := newTestServer(t) token := tserver.createSession("ephemeral") tserver.sendCommand(token, map[string]any{ commandKey: joinCmd, toKey: "#ephemeral", }) tserver.sendCommand(token, map[string]any{ commandKey: "PART", toKey: "#ephemeral", }) resp, err := doRequestAuth( t, http.MethodGet, tserver.url("/api/v1/channels"), token, nil, ) if err != nil { t.Fatal(err) } defer func() { _ = resp.Body.Close() }() var channels []map[string]any decErr := json.NewDecoder(resp.Body).Decode( &channels, ) if decErr != nil { t.Fatalf("decode channels: %v", decErr) } for _, channel := range channels { if channel["name"] == "#ephemeral" { t.Fatal( "ephemeral channel should be cleaned up", ) } } } func TestConcurrentSessions(t *testing.T) { tserver := newTestServer(t) var waitGroup sync.WaitGroup const concurrency = 20 errs := make(chan error, concurrency) for idx := range concurrency { waitGroup.Add(1) go func(index int) { defer waitGroup.Done() nick := fmt.Sprintf("conc_%d", index) body, err := json.Marshal( map[string]string{"nick": nick}, ) if err != nil { errs <- fmt.Errorf( "marshal: %w", err, ) return } resp, err := doRequest( t, http.MethodPost, tserver.url(apiSession), 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, ) } }(idx) } waitGroup.Wait() close(errs) for err := range errs { if err != nil { t.Fatalf("concurrent error: %v", err) } } } func TestServerInfo(t *testing.T) { tserver := newTestServer(t) resp, err := doRequest( t, http.MethodGet, tserver.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) { tserver := newTestServer(t) resp, err := doRequest( t, http.MethodGet, tserver.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 decErr := json.NewDecoder(resp.Body).Decode(&result) if decErr != nil { t.Fatalf("decode healthcheck: %v", decErr) } if result[statusKey] != "ok" { t.Fatalf( "expected ok status, got %v", result[statusKey], ) } } func TestNickBroadcastToChannels(t *testing.T) { tserver := newTestServer(t) aliceToken := tserver.createSession("nick_a") bobToken := tserver.createSession("nick_b") tserver.sendCommand(aliceToken, map[string]any{ commandKey: joinCmd, toKey: "#nicktest", }) tserver.sendCommand(bobToken, map[string]any{ commandKey: joinCmd, toKey: "#nicktest", }) _, lastID := tserver.pollMessages(bobToken, 0) tserver.sendCommand(aliceToken, map[string]any{ commandKey: "NICK", bodyKey: []string{"nick_a_new"}, }) msgs, _ := tserver.pollMessages(bobToken, lastID) if !findMessage(msgs, "NICK", "nick_a") { t.Fatalf( "bob didn't get nick change: %v", msgs, ) } }