All checks were successful
check / check (push) Successful in 2m11s
- Add busy_timeout PRAGMA and MaxOpenConns(1) for SQLite stability - Use per-test temp DB in handler tests to prevent state leaks - Pre-allocate migrations slice (prealloc lint) - Remove invalid linter names (wsl_v5, noinlineerr) from .golangci.yml - Remove unused //nolint:gosec directives - Replace context.Background() with t.Context() in tests - Use goimports formatting for all files - All make check passes with zero failures
1096 lines
24 KiB
Go
1096 lines
24 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"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"
|
|
)
|
|
|
|
// 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()
|
|
|
|
// Use a unique DB per test to avoid SQLite BUSY and state leaks.
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
t.Setenv("DBURL", "file:"+dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
|
|
|
|
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)
|
|
}
|
|
}
|