Files
chat/internal/handlers/api_test.go
clawbot 704f5ecbbf fix: resolve all golangci-lint issues
- Refactor test helpers (sendCommand, getJSON) to return (int, map[string]any)
  instead of (*http.Response, map[string]any) to fix bodyclose warnings
- Add doReq/doReqAuth helpers using NewRequestWithContext to fix noctx
- Check all error returns (errcheck, errchkjson)
- Use integer range syntax (intrange) for Go 1.22+
- Use http.Method* constants (usestdlibvars)
- Replace fmt.Sprintf with string concatenation where possible (perfsprint)
- Reorder UI methods: exported before unexported (funcorder)
- Add lint target to Makefile
- Disable overly pedantic linters in .golangci.yml (paralleltest, dupl,
  noinlineerr, wsl_v5, nlreturn, lll, tagliatelle, goconst, funlen)
2026-02-26 20:17:02 -08:00

1091 lines
24 KiB
Go

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)
}
}