Stop the slow test packages waiting (closes #263)
check / check (push) Failing after 3s

- Tests hash passwords at bcrypt.MinCost, set through the
  PasswordCost field of db.Params. TestPasswordCost checks the
  cost with and without it.
- Test IRC servers check the queue only on broker notification;
  one test keeps the 100 ms fallback poll.
- The stop-timeout test shortens the timeout through a test-only
  setter.
- service and handlers tests build config.Config directly instead
  of through config.New, so they run in parallel. config exports
  DefaultTrustedProxies and ParseTrustedProxies for the handlers
  test config.
- Repeated race trials run as parallel subtests.
- Fixed sleeps become waits on the broker.
- A new test covers the relay's failed queue read, which only the
  100 ms poll had reached.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-10 23:10:22 +00:00
1 parent f42c6214a0
commit baaf54499d
15 files changed
+560 -263

No files matched your search

+176 -114
View File
@@ -145,34 +145,46 @@ type testServer struct {
handlers *handlers.Handlers
}
// viperMu makes tests start their servers one at a time,
// because config.New writes the global viper instance and
// the server reads it as it starts.
//
//nolint:gochecknoglobals // guards a global, so must be one
var viperMu sync.Mutex
const (
// maxMessageSize is MAX_MESSAGE_SIZE on every test server, the
// setting's default.
maxMessageSize = 4096
// loginRateLimit is LOGIN_RATE_LIMIT on every test server, the
// setting's default.
loginRateLimit = 1
// loginRateBurst is LOGIN_RATE_BURST on every test server, the
// setting's default.
loginRateBurst = 5
)
func newTestServer(
t *testing.T,
) *testServer {
t.Helper()
return newTestServerWith(t, 0, "", "")
return newTestServerWith(t, 0, "", "", config.DefaultTrustedProxies)
}
func newTestServerWith(
t *testing.T,
hashcashBits int,
operName, operPassword string,
operName, operPassword, trustedProxies string,
) *testServer {
t.Helper()
viperMu.Lock()
defer viperMu.Unlock()
dbURL := dbtest.NewURL(t)
copyTemplateDB(t, dbURL)
trusted, err := config.ParseTrustedProxies(trustedProxies)
if err != nil {
t.Fatalf("parse trusted proxies: %v", err)
}
// Each duration setting defaults to 30 days.
const thirtyDays = "720h"
var (
srv *server.Server
brk *broker.Broker
@@ -184,31 +196,25 @@ func newTestServerWith(
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,
)
// Built here rather than by config.New, which writes
// the global viper instance, so servers start in
// parallel.
func() *config.Config {
return &config.Config{
DBURL: dbURL,
// One line: each line is a database write per new session.
MOTD: "neoirc test server",
MaxMessageSize: maxMessageSize,
HashcashBits: hashcashBits,
OperName: operName,
OperPassword: operPassword,
MessageMaxAge: thirtyDays,
QueueMaxAge: thirtyDays,
SessionIdleTimeout: thirtyDays,
LoginRateLimit: loginRateLimit,
LoginRateBurst: loginRateBurst,
TrustedProxies: trusted,
}
cfg.DBURL = dbURL
cfg.Port = 0
// One line: each line is a database write per new session.
cfg.MOTD = "neoirc test server"
cfg.HashcashBits = hashcashBits
cfg.OperName = operName
cfg.OperPassword = operPassword
return cfg, nil
},
newTestDB,
broker.New,
@@ -255,7 +261,7 @@ func newTestDB(
cfg *config.Config,
) (*db.Database, error) {
database, err := db.New(lifecycle, db.Params{ //nolint:exhaustruct
Logger: log, Config: cfg,
Logger: log, Config: cfg, PasswordCost: bcrypt.MinCost,
})
if err != nil {
return nil, fmt.Errorf("test db: %w", err)
@@ -830,10 +836,7 @@ func (tserver *testServer) postBody(
func TestRequestBodyLimit(t *testing.T) {
t.Parallel()
const (
maxMessageSize = 4096 // the default MAX_MESSAGE_SIZE
nick = "alice_bodylimit"
)
const nick = "alice_bodylimit"
endpoints := []struct {
name, path, value string
@@ -1750,52 +1753,67 @@ func TestConcurrentQuitsSendOneQuit(t *testing.T) {
// that the second session then holds one QUIT from the
// first, and that the first session is deleted: its tokens
// are refused and its nick is free. It does all this
// quitRaceTrials times, each on a new server, because each
// trial logs in and logins are rate limited.
// quitRaceTrials times, at once, each on a new server, because
// each trial logs in and logins are rate limited.
func checkQuitRace(
t *testing.T,
leave func(tserver *testServer, tokens []string),
) {
t.Helper()
for trial := range quitRaceTrials {
t.Run(fmt.Sprintf("trial %d", trial), func(t *testing.T) {
t.Parallel()
checkQuitRaceOnce(t, newTestServer(t), leave)
})
}
}
// checkQuitRaceOnce runs one trial of checkQuitRace on
// tserver.
func checkQuitRaceOnce(
t *testing.T,
tserver *testServer,
leave func(tserver *testServer, tokens []string),
) {
t.Helper()
const nick = "racingquitter"
for trial := range quitRaceTrials {
tserver := newTestServer(t)
firstToken := tserver.createSession(nick)
observerToken := tserver.createSession("raceobserver")
firstToken := tserver.createSession(nick)
observerToken := tserver.createSession("raceobserver")
tserver.setPassword(nick)
tserver.setPassword(nick)
tokens := []string{firstToken, tserver.login(nick)}
tokens := []string{firstToken, tserver.login(nick)}
for _, token := range []string{firstToken, observerToken} {
tserver.sendCommand(token, map[string]any{
commandKey: joinCmd, toKey: "#quitrace",
})
}
_, lastID := tserver.pollMessages(observerToken, 0)
leave(tserver, tokens)
msgs, _ := tserver.pollMessages(observerToken, lastID)
quits := countQuits(msgs, nick)
if quits != 1 {
t.Errorf("trial %d: %d QUITs, want 1: %v", trial, quits, msgs)
}
for _, token := range tokens {
status, _ := tserver.getState(token)
if status != http.StatusUnauthorized {
t.Errorf("trial %d: state got %d, want 401", trial, status)
}
}
// createSession fails the test if the nick is taken.
tserver.createSession(nick)
for _, token := range []string{firstToken, observerToken} {
tserver.sendCommand(token, map[string]any{
commandKey: joinCmd, toKey: "#quitrace",
})
}
_, lastID := tserver.pollMessages(observerToken, 0)
leave(tserver, tokens)
msgs, _ := tserver.pollMessages(observerToken, lastID)
quits := countQuits(msgs, nick)
if quits != 1 {
t.Errorf("%d QUITs, want 1: %v", quits, msgs)
}
for _, token := range tokens {
status, _ := tserver.getState(token)
if status != http.StatusUnauthorized {
t.Errorf("state got %d, want 401", status)
}
}
// createSession fails the test if the nick is taken.
tserver.createSession(nick)
}
// TestLogoutOfOneClientKeepsSession checks that when one of
@@ -1933,11 +1951,28 @@ const (
// return once the session has left.
quitWakeDeadline = 5 * time.Second
// pollWaitStep is how often checkQuitWakesPoll checks
// pollWaitStep is how often waitForLongPoll checks
// whether the long poll is waiting yet.
pollWaitStep = 10 * time.Millisecond
)
// waitForLongPoll waits until a long poll of the session
// sessionID is waiting, and fails the test if none is within
// quitWakeDeadline.
func (tserver *testServer) waitForLongPoll(sessionID int64) {
tserver.t.Helper()
deadline := time.Now().Add(quitWakeDeadline)
for tserver.broker.ListenerCount(sessionID) == 0 {
if time.Now().After(deadline) {
tserver.t.Fatal("the long poll did not start waiting")
}
time.Sleep(pollWaitStep)
}
}
// TestQuitWakesPeerLongPoll checks that a session's QUIT
// wakes the long poll of a user who shares a channel with
// it.
@@ -2012,15 +2047,7 @@ func checkQuitWakesPoll(
msgs, pollErr = tserver.longPoll(peerToken, lastID, quitWakePollTimeout)
}()
deadline := time.Now().Add(quitWakeDeadline)
for tserver.broker.ListenerCount(peerID) == 0 {
if time.Now().After(deadline) {
t.Fatal("the long poll did not start waiting")
}
time.Sleep(pollWaitStep)
}
tserver.waitForLongPoll(peerID)
leave(tserver, token)
@@ -2850,7 +2877,12 @@ func TestLongPoll(t *testing.T) {
pollMsgs = result.Messages
}()
time.Sleep(200 * time.Millisecond)
bobID, err := tserver.database.GetSessionByNick(t.Context(), "lp_bob")
if err != nil {
t.Fatalf("get session of lp_bob: %v", err)
}
tserver.waitForLongPoll(bobID)
tserver.sendCommand(aliceToken, map[string]any{
commandKey: privmsgCmd,
@@ -3009,18 +3041,18 @@ func TestGetMessagesRemovesWaitChannel(t *testing.T) {
longTimeout = 10
// shortTimeout, in seconds, is the timeout of the
// requests that wait it out.
// request that waits it out.
shortTimeout = 1
)
// queued is whether a message is queued before the request,
// whose timeout is in seconds. failingRead is as for
// getMessagesFailingRead. wantReads is how many times the
// handler reads the queue: once if it answers at once,
// whose timeout is in seconds. failingRead and notified are
// as for getMessagesFailingRead. wantReads is how many times
// the handler reads the queue: once if it answers at once,
// twice if it waits first.
tests := []struct {
name string
queued bool
queued, notified bool
timeout, failingRead int
wantReads, wantStatus int
}{
@@ -3041,9 +3073,9 @@ func TestGetMessagesRemovesWaitChannel(t *testing.T) {
wantReads: 1, wantStatus: http.StatusInternalServerError,
},
{
name: "read after the wait failed", timeout: shortTimeout,
failingRead: 2, wantReads: 2,
wantStatus: http.StatusInternalServerError,
name: "read after the wait failed", notified: true,
timeout: handlers.MaxLongPollTimeout, failingRead: 2,
wantReads: 2, wantStatus: http.StatusInternalServerError,
},
}
@@ -3075,7 +3107,7 @@ func TestGetMessagesRemovesWaitChannel(t *testing.T) {
}
status, reads := tserver.getMessagesFailingRead(
token, lastID, tc.timeout, tc.failingRead,
token, sessionID, lastID, tc.timeout, tc.failingRead, tc.notified,
)
if status != tc.wantStatus {
t.Errorf("status %d, want %d", status, tc.wantStatus)
@@ -3092,14 +3124,17 @@ func TestGetMessagesRemovesWaitChannel(t *testing.T) {
}
// getMessagesFailingRead sends GET /api/v1/messages for the
// client token with timeout and after afterID. The handler's
// read of the queue numbered failingRead, counting from 1,
// fails; with 0, none does. It returns the response status
// and how many times the handler read the queue.
// client token of the session sessionID, with timeout and
// after afterID. The handler's read of the queue numbered
// failingRead, counting from 1, fails; with 0, none does.
// With notified, it notifies the broker once the request
// waits. It returns the response status and how many times
// the handler read the queue.
func (tserver *testServer) getMessagesFailingRead(
token string,
afterID int64,
sessionID, afterID int64,
timeout, failingRead int,
notified bool,
) (int, int) {
tserver.t.Helper()
@@ -3121,20 +3156,45 @@ func (tserver *testServer) getMessagesFailingRead(
)
})
resp, err := doRequestAuth(
tserver.t, http.MethodGet,
tserver.url(fmt.Sprintf(
"%s?timeout=%d&after=%d", apiMessages, timeout, afterID,
)),
token, nil,
var (
status int
err error
)
answered := make(chan struct{})
go func() {
defer close(answered)
var resp *http.Response
resp, err = doRequestAuth(
tserver.t, http.MethodGet,
tserver.url(fmt.Sprintf(
"%s?timeout=%d&after=%d", apiMessages, timeout, afterID,
)),
token, nil,
)
if err != nil {
return
}
status = resp.StatusCode
_ = resp.Body.Close()
}()
if notified {
tserver.waitForLongPoll(sessionID)
tserver.broker.Notify(sessionID)
}
<-answered
if err != nil {
tserver.t.Fatalf("get messages: %v", err)
}
_ = resp.Body.Close()
return resp.StatusCode, reads
return status, reads
}
// TestLongPollRemovesWaitChannelWhenClientGivesUp checks that
@@ -4343,12 +4403,11 @@ func TestLoginRateLimitIgnoresForwardedHeaders(t *testing.T) {
}
func TestLoginRateLimitPerClientBehindTrustedProxy(t *testing.T) {
// The test client connects from 127.0.0.1. Trusted as a proxy, it
// passes each client's address in X-Forwarded-For. t.Setenv rules
// out t.Parallel.
t.Setenv("TRUSTED_PROXIES", "127.0.0.1")
t.Parallel()
tserver := newTestServer(t)
// The test client connects from 127.0.0.1. Trusted as a proxy, it
// passes each client's address in X-Forwarded-For.
tserver := newTestServerWith(t, 0, "", "", "127.0.0.1")
// An empty JSON object is refused after the rate limit counts the
// attempt, so no attempt checks a password or looks up a hostname.
@@ -5079,7 +5138,9 @@ func TestRefusedSessionLeavesStampUnspent(t *testing.T) {
badNick = "bad nick"
)
tserver := newTestServerWith(t, bits, "", "")
tserver := newTestServerWith(
t, bits, "", "", config.DefaultTrustedProxies,
)
// A server with no SERVER_NAME takes stamps for "neoirc".
status := postSessionWithStamp(
@@ -5285,6 +5346,7 @@ func newTestServerWithOper(
return newTestServerWith(
t, 0, testOperName, testOperPassword,
config.DefaultTrustedProxies,
)
}