All checks were successful
check / check (push) Successful in 58s
IRC commands (PRIVMSG, JOIN, PART, NICK, TOPIC, etc.) now respond with proper IRC numeric replies delivered through the message queue instead of HTTP status codes. HTTP error codes are now reserved exclusively for transport-level concerns: auth failures (401), malformed requests (400), and server errors (500). Changes: - Add params column to messages table for IRC-style parameters - Add Params field to IRCMessage struct and update all queries - Add respondIRCError helper for consistent IRC error delivery - Add RPL_WELCOME (001) on session creation and login - Add RPL_TOPIC/RPL_NOTOPIC (332/331), RPL_NAMREPLY (353), RPL_ENDOFNAMES (366) on JOIN - Add RPL_TOPIC (332) on TOPIC set - Replace HTTP 404 with ERR_NOSUCHCHANNEL (403) and ERR_NOSUCHNICK (401) - Replace HTTP 409 with ERR_NICKNAMEINUSE (433) - Replace HTTP 403 with ERR_NOTONCHANNEL (442) - Replace HTTP 400 with ERR_NEEDMOREPARAMS (461), ERR_ERRONEUSNICKNAME (432), and ERR_UNKNOWNCOMMAND (421) where appropriate - Change PRIVMSG/NOTICE success from HTTP 201 to HTTP 200 - Update all tests to verify IRC numerics in message queue - Add new tests for RPL_WELCOME and JOIN numerics - Update README to document new numeric reply behavior closes #54
1985 lines
37 KiB
Go
1985 lines
37 KiB
Go
// 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/neoirc/internal/config"
|
|
"git.eeqj.de/sneak/neoirc/internal/db"
|
|
"git.eeqj.de/sneak/neoirc/internal/globals"
|
|
"git.eeqj.de/sneak/neoirc/internal/handlers"
|
|
"git.eeqj.de/sneak/neoirc/internal/healthcheck"
|
|
"git.eeqj.de/sneak/neoirc/internal/logger"
|
|
"git.eeqj.de/sneak/neoirc/internal/middleware"
|
|
"git.eeqj.de/sneak/neoirc/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: "neoirc-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
|
|
}
|
|
|
|
func findNumeric(
|
|
msgs []map[string]any,
|
|
numeric string,
|
|
) bool {
|
|
for _, msg := range msgs {
|
|
if msg[commandKey] == numeric {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// --- Tests ---
|
|
|
|
func TestCreateSessionValid(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("alice")
|
|
|
|
if token == "" {
|
|
t.Fatal("expected token")
|
|
}
|
|
}
|
|
|
|
func TestWelcomeNumeric(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("welcomer")
|
|
|
|
msgs, _ := tserver.pollMessages(token, 0)
|
|
|
|
if !findNumeric(msgs, "001") {
|
|
t.Fatalf(
|
|
"expected RPL_WELCOME (001), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestJoinNumerics(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("jnumtest")
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd, toKey: "#numtest",
|
|
})
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "353") {
|
|
t.Fatalf(
|
|
"expected RPL_NAMREPLY (353), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
|
|
if !findNumeric(msgs, "366") {
|
|
t.Fatalf(
|
|
"expected RPL_ENDOFNAMES (366), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
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")
|
|
|
|
// Drain initial MOTD/welcome numerics.
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
status, _ := tserver.sendCommand(
|
|
token, map[string]any{commandKey: joinCmd},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "461") {
|
|
t.Fatalf(
|
|
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
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: "#test",
|
|
})
|
|
tserver.sendCommand(bobToken, map[string]any{
|
|
commandKey: joinCmd, toKey: "#test",
|
|
})
|
|
|
|
_, _ = tserver.pollMessages(aliceToken, 0)
|
|
_, bobLastID := tserver.pollMessages(bobToken, 0)
|
|
|
|
status, result := tserver.sendCommand(
|
|
aliceToken,
|
|
map[string]any{
|
|
commandKey: privmsgCmd,
|
|
toKey: "#test",
|
|
bodyKey: []string{"hello world"},
|
|
},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf(
|
|
"expected 200, 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: "#test",
|
|
})
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
status, _ := tserver.sendCommand(token, map[string]any{
|
|
commandKey: privmsgCmd, toKey: "#test",
|
|
})
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "461") {
|
|
t.Fatalf(
|
|
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestMessageMissingTo(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("noto")
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
status, _ := tserver.sendCommand(token, map[string]any{
|
|
commandKey: privmsgCmd,
|
|
bodyKey: []string{"hello"},
|
|
})
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "461") {
|
|
t.Fatalf(
|
|
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
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",
|
|
})
|
|
|
|
_, lastID := tserver.pollMessages(aliceToken, 0)
|
|
|
|
// Alice tries to send without joining.
|
|
status, _ := tserver.sendCommand(
|
|
aliceToken,
|
|
map[string]any{
|
|
commandKey: privmsgCmd,
|
|
toKey: "#private",
|
|
bodyKey: []string{"sneaky"},
|
|
},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(aliceToken, lastID)
|
|
|
|
if !findNumeric(msgs, "442") {
|
|
t.Fatalf(
|
|
"expected ERR_NOTONCHANNEL (442), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
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.StatusOK {
|
|
t.Fatalf(
|
|
"expected 200, 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")
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
status, _ := tserver.sendCommand(token, map[string]any{
|
|
commandKey: privmsgCmd,
|
|
toKey: "nobody",
|
|
bodyKey: []string{"hello?"},
|
|
})
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "401") {
|
|
t.Fatalf(
|
|
"expected ERR_NOSUCHNICK (401), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
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")
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
status, _ := tserver.sendCommand(token, map[string]any{
|
|
commandKey: "NICK",
|
|
bodyKey: []string{"taken_nick"},
|
|
})
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "433") {
|
|
t.Fatalf(
|
|
"expected ERR_NICKNAMEINUSE (433), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestNickInvalid(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("nickval")
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
status, _ := tserver.sendCommand(token, map[string]any{
|
|
commandKey: "NICK",
|
|
bodyKey: []string{"bad nick!"},
|
|
})
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "432") {
|
|
t.Fatalf(
|
|
"expected ERR_ERRONEUSNICKNAME (432), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestNickEmptyBody(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("nicknobody")
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
status, _ := tserver.sendCommand(
|
|
token, map[string]any{commandKey: "NICK"},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "461") {
|
|
t.Fatalf(
|
|
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
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")
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
status, _ := tserver.sendCommand(token, map[string]any{
|
|
commandKey: "TOPIC",
|
|
bodyKey: []string{"topic"},
|
|
})
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "461") {
|
|
t.Fatalf(
|
|
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestTopicMissingBody(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("topicnobody")
|
|
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd, toKey: "#topictest",
|
|
})
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
status, _ := tserver.sendCommand(token, map[string]any{
|
|
commandKey: "TOPIC", toKey: "#topictest",
|
|
})
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "461") {
|
|
t.Fatalf(
|
|
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
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")
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
status, _ := tserver.sendCommand(
|
|
token, map[string]any{commandKey: "BOGUS"},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "421") {
|
|
t.Fatalf(
|
|
"expected ERR_UNKNOWNCOMMAND (421), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
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")
|
|
|
|
// Drain initial welcome/MOTD numerics.
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
start := time.Now()
|
|
|
|
resp, err := doRequestAuth(
|
|
t,
|
|
http.MethodGet,
|
|
tserver.url(fmt.Sprintf(
|
|
"%s?timeout=1&after=%d",
|
|
apiMessages, lastID,
|
|
)),
|
|
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 TestRegisterValid(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
|
|
body, err := json.Marshal(map[string]string{
|
|
"nick": "reguser", "password": "password123",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := doRequest(
|
|
t,
|
|
http.MethodPost,
|
|
tserver.url("/api/v1/register"),
|
|
bytes.NewReader(body),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf(
|
|
"expected 201, got %d: %s",
|
|
resp.StatusCode, respBody,
|
|
)
|
|
}
|
|
|
|
var result map[string]any
|
|
|
|
_ = json.NewDecoder(resp.Body).Decode(&result)
|
|
|
|
if result["token"] == nil || result["token"] == "" {
|
|
t.Fatal("expected token in response")
|
|
}
|
|
|
|
if result["nick"] != "reguser" {
|
|
t.Fatalf(
|
|
"expected reguser, got %v", result["nick"],
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestRegisterDuplicate(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
|
|
body, err := json.Marshal(map[string]string{
|
|
"nick": "dupuser", "password": "password123",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := doRequest(
|
|
t,
|
|
http.MethodPost,
|
|
tserver.url("/api/v1/register"),
|
|
bytes.NewReader(body),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
resp2, err := doRequest(
|
|
t,
|
|
http.MethodPost,
|
|
tserver.url("/api/v1/register"),
|
|
bytes.NewReader(body),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() { _ = resp2.Body.Close() }()
|
|
|
|
if resp2.StatusCode != http.StatusConflict {
|
|
t.Fatalf("expected 409, got %d", resp2.StatusCode)
|
|
}
|
|
}
|
|
|
|
func postJSONExpectStatus(
|
|
t *testing.T,
|
|
tserver *testServer,
|
|
path string,
|
|
payload map[string]string,
|
|
expectedStatus int,
|
|
) {
|
|
t.Helper()
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := doRequest(
|
|
t,
|
|
http.MethodPost,
|
|
tserver.url(path),
|
|
bytes.NewReader(body),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != expectedStatus {
|
|
t.Fatalf(
|
|
"expected %d, got %d",
|
|
expectedStatus, resp.StatusCode,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestRegisterShortPassword(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
|
|
postJSONExpectStatus(
|
|
t, tserver, "/api/v1/register",
|
|
map[string]string{
|
|
"nick": "shortpw", "password": "short",
|
|
},
|
|
http.StatusBadRequest,
|
|
)
|
|
}
|
|
|
|
func TestRegisterInvalidNick(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
|
|
postJSONExpectStatus(
|
|
t, tserver, "/api/v1/register",
|
|
map[string]string{
|
|
"nick": "bad nick!",
|
|
"password": "password123",
|
|
},
|
|
http.StatusBadRequest,
|
|
)
|
|
}
|
|
|
|
func TestLoginValid(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
|
|
// Register first.
|
|
regBody, err := json.Marshal(map[string]string{
|
|
"nick": "loginuser", "password": "password123",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := doRequest(
|
|
t,
|
|
http.MethodPost,
|
|
tserver.url("/api/v1/register"),
|
|
bytes.NewReader(regBody),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
// Login.
|
|
loginBody, err := json.Marshal(map[string]string{
|
|
"nick": "loginuser", "password": "password123",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp2, err := doRequest(
|
|
t,
|
|
http.MethodPost,
|
|
tserver.url("/api/v1/login"),
|
|
bytes.NewReader(loginBody),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() { _ = resp2.Body.Close() }()
|
|
|
|
if resp2.StatusCode != http.StatusOK {
|
|
respBody, _ := io.ReadAll(resp2.Body)
|
|
t.Fatalf(
|
|
"expected 200, got %d: %s",
|
|
resp2.StatusCode, respBody,
|
|
)
|
|
}
|
|
|
|
var result map[string]any
|
|
|
|
_ = json.NewDecoder(resp2.Body).Decode(&result)
|
|
|
|
if result["token"] == nil || result["token"] == "" {
|
|
t.Fatal("expected token in response")
|
|
}
|
|
|
|
// Verify token works.
|
|
token, ok := result["token"].(string)
|
|
if !ok {
|
|
t.Fatal("token not a string")
|
|
}
|
|
|
|
status, state := tserver.getState(token)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
if state["nick"] != "loginuser" {
|
|
t.Fatalf(
|
|
"expected loginuser, got %v",
|
|
state["nick"],
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestLoginWrongPassword(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
|
|
regBody, err := json.Marshal(map[string]string{
|
|
"nick": "wrongpwuser", "password": "correctpass1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, err := doRequest(
|
|
t,
|
|
http.MethodPost,
|
|
tserver.url("/api/v1/register"),
|
|
bytes.NewReader(regBody),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
loginBody, err := json.Marshal(map[string]string{
|
|
"nick": "wrongpwuser", "password": "wrongpass12",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp2, err := doRequest(
|
|
t,
|
|
http.MethodPost,
|
|
tserver.url("/api/v1/login"),
|
|
bytes.NewReader(loginBody),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() { _ = resp2.Body.Close() }()
|
|
|
|
if resp2.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf(
|
|
"expected 401, got %d", resp2.StatusCode,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestLoginNonexistentUser(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
|
|
postJSONExpectStatus(
|
|
t, tserver, "/api/v1/login",
|
|
map[string]string{
|
|
"nick": "ghostuser",
|
|
"password": "password123",
|
|
},
|
|
http.StatusUnauthorized,
|
|
)
|
|
}
|
|
|
|
func TestSessionStillWorks(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
|
|
// Verify anonymous session creation still works.
|
|
token := tserver.createSession("anon_user")
|
|
if token == "" {
|
|
t.Fatal("expected token for anonymous session")
|
|
}
|
|
|
|
status, state := tserver.getState(token)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
if state["nick"] != "anon_user" {
|
|
t.Fatalf(
|
|
"expected anon_user, got %v",
|
|
state["nick"],
|
|
)
|
|
}
|
|
}
|
|
|
|
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,
|
|
)
|
|
}
|
|
}
|