All checks were successful
check / check (push) Successful in 2m18s
Add per-channel hashcash requirement via MODE +H <bits>. When set, PRIVMSG to the channel must include a valid hashcash stamp in the meta.hashcash field bound to the channel name and message body hash. Server validates stamp format, difficulty, date freshness, channel binding, body hash binding, and proof-of-work. Spent stamps are persisted to SQLite with 1-year TTL for replay prevention. Stamp format: 1:bits:YYMMDD:channel:bodyhash:counter Changes: - Schema: add hashcash_bits column to channels, spent_hashcash table - DB: queries for get/set channel hashcash bits, spent token CRUD - Hashcash: ChannelValidator, BodyHash, StampHash, MintChannelStamp - Handlers: validate hashcash on PRIVMSG, MODE +H/-H support - Pass meta through fanOut chain to store in messages - Prune spent hashcash tokens in cleanup loop (1-year TTL) - Client: MintChannelHashcash helper for CLI - Tests: 12 new channel_test.go + 10 new api_test.go integration tests - README: document +H mode, stamp format, and usage
2386 lines
46 KiB
Go
2386 lines
46 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"
|
|
"strconv"
|
|
"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/hashcash"
|
|
"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
|
|
cfg.HashcashBits = 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",
|
|
StartTime: time.Now(),
|
|
}
|
|
}
|
|
|
|
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 {
|
|
want, _ := strconv.Atoi(numeric)
|
|
|
|
for _, msg := range msgs {
|
|
code, ok := msg["code"].(float64)
|
|
if ok && int(code) == want {
|
|
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, "412") {
|
|
t.Fatalf(
|
|
"expected ERR_NOTEXTTOSEND (412), 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, "411") {
|
|
t.Fatalf(
|
|
"expected ERR_NORECIPIENT (411), 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, "404") {
|
|
t.Fatalf(
|
|
"expected ERR_CANNOTSENDTOCHAN (404), 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,
|
|
)
|
|
}
|
|
}
|
|
|
|
// --- Channel Hashcash Tests ---
|
|
|
|
const (
|
|
metaKey = "meta"
|
|
modeCmd = "MODE"
|
|
hashcashKey = "hashcash"
|
|
)
|
|
|
|
func mintTestChannelHashcash(
|
|
tb testing.TB,
|
|
bits int,
|
|
channel string,
|
|
body json.RawMessage,
|
|
) string {
|
|
tb.Helper()
|
|
|
|
bodyHash := hashcash.BodyHash(body)
|
|
|
|
return hashcash.MintChannelStamp(bits, channel, bodyHash)
|
|
}
|
|
|
|
func TestChannelHashcashSetMode(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("hcmode_user")
|
|
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd, toKey: "#hctest",
|
|
})
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
// Set hashcash bits to 2 via MODE +H.
|
|
status, _ := tserver.sendCommand(
|
|
token,
|
|
map[string]any{
|
|
commandKey: modeCmd,
|
|
toKey: "#hctest",
|
|
bodyKey: []string{"+H", "2"},
|
|
},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
// Should get RPL_CHANNELMODEIS (324) confirming +H.
|
|
if !findNumeric(msgs, "324") {
|
|
t.Fatalf(
|
|
"expected RPL_CHANNELMODEIS (324), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestChannelHashcashQueryMode(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("hcquery_user")
|
|
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd, toKey: "#hcquery",
|
|
})
|
|
|
|
// Set hashcash bits.
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: modeCmd,
|
|
toKey: "#hcquery",
|
|
bodyKey: []string{"+H", "5"},
|
|
})
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
// Query mode — should show +nH.
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: modeCmd,
|
|
toKey: "#hcquery",
|
|
})
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
found := false
|
|
|
|
for _, msg := range msgs {
|
|
code, ok := msg["code"].(float64)
|
|
if ok && int(code) == 324 {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Fatalf(
|
|
"expected RPL_CHANNELMODEIS (324), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestChannelHashcashClearMode(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("hcclear_user")
|
|
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd, toKey: "#hcclear",
|
|
})
|
|
|
|
// Set hashcash bits.
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: modeCmd,
|
|
toKey: "#hcclear",
|
|
bodyKey: []string{"+H", "5"},
|
|
})
|
|
|
|
// Clear hashcash bits.
|
|
status, _ := tserver.sendCommand(token, map[string]any{
|
|
commandKey: modeCmd,
|
|
toKey: "#hcclear",
|
|
bodyKey: []string{"-H"},
|
|
})
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
// Now message should succeed without hashcash.
|
|
status, result := tserver.sendCommand(
|
|
token,
|
|
map[string]any{
|
|
commandKey: privmsgCmd,
|
|
toKey: "#hcclear",
|
|
bodyKey: []string{"test message"},
|
|
},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf(
|
|
"expected 200, got %d: %v", status, result,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestChannelHashcashRejectNoStamp(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("hcreject_user")
|
|
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd, toKey: "#hcreject",
|
|
})
|
|
|
|
// Set hashcash requirement.
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: modeCmd,
|
|
toKey: "#hcreject",
|
|
bodyKey: []string{"+H", "2"},
|
|
})
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
// Send message without hashcash — should fail.
|
|
status, _ := tserver.sendCommand(
|
|
token,
|
|
map[string]any{
|
|
commandKey: privmsgCmd,
|
|
toKey: "#hcreject",
|
|
bodyKey: []string{"spam message"},
|
|
},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
// Should get ERR_CANNOTSENDTOCHAN (404).
|
|
if !findNumeric(msgs, "404") {
|
|
t.Fatalf(
|
|
"expected ERR_CANNOTSENDTOCHAN (404), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestChannelHashcashAcceptValidStamp(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("hcaccept_user")
|
|
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd, toKey: "#hcaccept",
|
|
})
|
|
|
|
// Set hashcash requirement (2 bits = fast to mint).
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: modeCmd,
|
|
toKey: "#hcaccept",
|
|
bodyKey: []string{"+H", "2"},
|
|
})
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
// Mint a valid hashcash stamp.
|
|
msgBody, marshalErr := json.Marshal(
|
|
[]string{"hello world"},
|
|
)
|
|
if marshalErr != nil {
|
|
t.Fatal(marshalErr)
|
|
}
|
|
|
|
stamp := mintTestChannelHashcash(
|
|
t, 2, "#hcaccept", msgBody,
|
|
)
|
|
|
|
// Send message with valid hashcash.
|
|
status, result := tserver.sendCommand(
|
|
token,
|
|
map[string]any{
|
|
commandKey: privmsgCmd,
|
|
toKey: "#hcaccept",
|
|
bodyKey: []string{"hello world"},
|
|
metaKey: map[string]any{
|
|
hashcashKey: stamp,
|
|
},
|
|
},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf(
|
|
"expected 200, got %d: %v", status, result,
|
|
)
|
|
}
|
|
|
|
if result["id"] == nil || result["id"] == "" {
|
|
t.Fatal("expected message id for valid hashcash")
|
|
}
|
|
|
|
// Verify the message was delivered.
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
if !findMessage(msgs, privmsgCmd, "hcaccept_user") {
|
|
t.Fatalf(
|
|
"message not received: %v", msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestChannelHashcashRejectReplayedStamp(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("hcreplay_user")
|
|
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd, toKey: "#hcreplay",
|
|
})
|
|
|
|
// Set hashcash requirement.
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: modeCmd,
|
|
toKey: "#hcreplay",
|
|
bodyKey: []string{"+H", "2"},
|
|
})
|
|
|
|
_, _ = tserver.pollMessages(token, 0)
|
|
|
|
// Mint and send once — should succeed.
|
|
msgBody, marshalErr := json.Marshal(
|
|
[]string{"unique msg"},
|
|
)
|
|
if marshalErr != nil {
|
|
t.Fatal(marshalErr)
|
|
}
|
|
|
|
stamp := mintTestChannelHashcash(
|
|
t, 2, "#hcreplay", msgBody,
|
|
)
|
|
|
|
status, _ := tserver.sendCommand(
|
|
token,
|
|
map[string]any{
|
|
commandKey: privmsgCmd,
|
|
toKey: "#hcreplay",
|
|
bodyKey: []string{"unique msg"},
|
|
metaKey: map[string]any{
|
|
hashcashKey: stamp,
|
|
},
|
|
},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
// Replay the same stamp — should fail.
|
|
status, _ = tserver.sendCommand(
|
|
token,
|
|
map[string]any{
|
|
commandKey: privmsgCmd,
|
|
toKey: "#hcreplay",
|
|
bodyKey: []string{"unique msg"},
|
|
metaKey: map[string]any{
|
|
hashcashKey: stamp,
|
|
},
|
|
},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", status)
|
|
}
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
// Should get ERR_CANNOTSENDTOCHAN (404).
|
|
if !findNumeric(msgs, "404") {
|
|
t.Fatalf(
|
|
"expected replay rejection (404), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestChannelHashcashNoRequirementWorks(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("hcnone_user")
|
|
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd, toKey: "#nohashcash",
|
|
})
|
|
|
|
// No hashcash set — message should work.
|
|
status, result := tserver.sendCommand(
|
|
token,
|
|
map[string]any{
|
|
commandKey: privmsgCmd,
|
|
toKey: "#nohashcash",
|
|
bodyKey: []string{"free message"},
|
|
},
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf(
|
|
"expected 200, got %d: %v", status, result,
|
|
)
|
|
}
|
|
|
|
if result["id"] == nil || result["id"] == "" {
|
|
t.Fatal("expected message id")
|
|
}
|
|
}
|
|
|
|
func TestChannelHashcashInvalidBitsRange(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("hcbits_user")
|
|
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd, toKey: "#hcbits",
|
|
})
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
// Try to set bits to 0 — should fail.
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: modeCmd,
|
|
toKey: "#hcbits",
|
|
bodyKey: []string{"+H", "0"},
|
|
})
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "472") {
|
|
t.Fatalf(
|
|
"expected ERR_UNKNOWNMODE (472), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestChannelHashcashMissingBitsArg(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
token := tserver.createSession("hcnoarg_user")
|
|
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd, toKey: "#hcnoarg",
|
|
})
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
// Try to set +H without bits argument.
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: modeCmd,
|
|
toKey: "#hcnoarg",
|
|
bodyKey: []string{"+H"},
|
|
})
|
|
|
|
msgs, _ := tserver.pollMessages(token, lastID)
|
|
|
|
if !findNumeric(msgs, "461") {
|
|
t.Fatalf(
|
|
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
|
msgs,
|
|
)
|
|
}
|
|
}
|