The slowest test packages spent their time waiting rather than testing. db.Params gains an optional PasswordCost (zero means bcrypt.DefaultCost), and the db, ircserver and handlers tests hash at bcrypt.MinCost. A test IRC server's relay checks its queue on broker notification instead of polling. The service and handlers tests build config.Config directly and run in parallel; config exports DefaultTrustedProxies and ParseTrustedProxies for them. Fixed sleeps give way to waits for the event. Production defaults are unchanged. What still takes time is CPU work under -race or a timeout a test is about. Model: opus-5-5
This commit was merged in pull request #267.
This commit is contained in:
1 parent
847f5c6fec
commit
ed5125606a
15 files changed
+558
-261
No files matched your search
+176
-114
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user