- Tests hash passwords at bcrypt.MinCost through db.PasswordCost. - 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: service had run serially, and handlers started its servers one at a time. - 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:
1 parent
bc8aa6bb96
commit
c492e5969e
11 files changed
+356
-198
No files matched your search
@@ -1,7 +1,3 @@
|
||||
// Tests use a global viper instance for configuration,
|
||||
// making parallel execution unsafe.
|
||||
//
|
||||
//nolint:paralleltest
|
||||
package service_test
|
||||
|
||||
import (
|
||||
@@ -59,28 +55,12 @@ func newTestEnv(t *testing.T) *testEnv {
|
||||
}
|
||||
},
|
||||
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,
|
||||
)
|
||||
func() *config.Config {
|
||||
return &config.Config{
|
||||
DBURL: dbURL,
|
||||
OperName: "admin",
|
||||
OperPassword: "secret",
|
||||
}
|
||||
|
||||
cfg.DBURL = dbURL
|
||||
cfg.Port = 0
|
||||
cfg.OperName = "admin"
|
||||
cfg.OperPassword = "secret"
|
||||
|
||||
return cfg, nil
|
||||
},
|
||||
func(
|
||||
lifecycle fx.Lifecycle,
|
||||
@@ -136,6 +116,8 @@ func createSession(
|
||||
// environment starts on an empty database while another one
|
||||
// is still open.
|
||||
func TestEachTestEnvHasItsOwnDatabase(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
first := newTestEnv(t)
|
||||
@@ -146,6 +128,8 @@ func TestEachTestEnvHasItsOwnDatabase(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFanOut(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -173,6 +157,8 @@ func TestFanOut(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestJoinChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -213,6 +199,8 @@ func TestJoinChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPartChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -313,6 +301,8 @@ func joinConcurrently(
|
||||
// concurrent JOINs of a +l 2 channel with one member, one
|
||||
// gets in and the other gets ERR_CHANNELISFULL.
|
||||
func TestConcurrentJoinsRespectUserLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const userLimit = 2
|
||||
|
||||
env := newTestEnv(t)
|
||||
@@ -375,6 +365,8 @@ func TestConcurrentJoinsRespectUserLimit(t *testing.T) {
|
||||
// two concurrent JOINs that create a channel, exactly one
|
||||
// makes its session the operator.
|
||||
func TestConcurrentFirstJoinsMakeOneOperator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -426,6 +418,8 @@ func TestConcurrentFirstJoinsMakeOneOperator(t *testing.T) {
|
||||
// either joins a channel that still exists or gets
|
||||
// ERR_NOSUCHCHANNEL, and never fails otherwise.
|
||||
func TestJoinConcurrentWithLastPart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -491,6 +485,8 @@ func TestJoinConcurrentWithLastPart(t *testing.T) {
|
||||
// quits, gets in only once the channel is deleted, into a
|
||||
// new channel of that name with no key.
|
||||
func TestJoinConcurrentWithLastQuit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -569,81 +565,95 @@ const quitCrowdSize = 10
|
||||
// ahead of that JOIN. The quitting session is also on a
|
||||
// channel with quitCrowdSize other sessions, made before the
|
||||
// watcher's channel, so that its QUIT reaches the watcher
|
||||
// last.
|
||||
// last. Each trial runs on a database of its own.
|
||||
func TestQuitQueuedAheadOfNewNickHoldersJoin(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
t.Parallel()
|
||||
|
||||
for trial := range concurrentJoinTrials {
|
||||
nick := fmt.Sprintf("reused%d", trial)
|
||||
crowd := fmt.Sprintf("#crowd%d", trial)
|
||||
shared := fmt.Sprintf("#shared%d", trial)
|
||||
quitterSID := createSession(ctx, t, env.db, nick)
|
||||
t.Run(fmt.Sprintf("trial %d", trial), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, channel := range []string{crowd, shared} {
|
||||
_, err := env.svc.JoinChannel(ctx, quitterSID, 0, nick, channel, "")
|
||||
if err != nil {
|
||||
t.Fatalf("join %s: %v", channel, err)
|
||||
}
|
||||
checkQuitQueuedAheadOfJoin(t, newTestEnv(t))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// checkQuitQueuedAheadOfJoin runs one trial of
|
||||
// TestQuitQueuedAheadOfNewNickHoldersJoin on env.
|
||||
func checkQuitQueuedAheadOfJoin(t *testing.T, env *testEnv) {
|
||||
t.Helper()
|
||||
|
||||
const (
|
||||
nick = "reused"
|
||||
crowd = "#crowd"
|
||||
shared = "#shared"
|
||||
watcherNick = "watcher"
|
||||
)
|
||||
|
||||
ctx := t.Context()
|
||||
quitterSID := createSession(ctx, t, env.db, nick)
|
||||
|
||||
for _, channel := range []string{crowd, shared} {
|
||||
_, err := env.svc.JoinChannel(ctx, quitterSID, 0, nick, channel, "")
|
||||
if err != nil {
|
||||
t.Fatalf("join %s: %v", channel, err)
|
||||
}
|
||||
}
|
||||
|
||||
for member := range quitCrowdSize {
|
||||
memberNick := fmt.Sprintf("crowd%dx%d", trial, member)
|
||||
memberSID := createSession(ctx, t, env.db, memberNick)
|
||||
for member := range quitCrowdSize {
|
||||
memberNick := fmt.Sprintf("crowd%d", member)
|
||||
memberSID := createSession(ctx, t, env.db, memberNick)
|
||||
|
||||
_, err := env.svc.JoinChannel(ctx, memberSID, 0, memberNick, crowd, "")
|
||||
if err != nil {
|
||||
t.Fatalf("join %s: %v", memberNick, err)
|
||||
}
|
||||
_, err := env.svc.JoinChannel(ctx, memberSID, 0, memberNick, crowd, "")
|
||||
if err != nil {
|
||||
t.Fatalf("join %s: %v", memberNick, err)
|
||||
}
|
||||
}
|
||||
|
||||
watcherNick := fmt.Sprintf("watcher%d", trial)
|
||||
watcherSID, watcherClientID, _, err := env.db.CreateSession(
|
||||
ctx, watcherNick, watcherNick, "localhost", "127.0.0.1",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("create session %s: %v", watcherNick, err)
|
||||
}
|
||||
|
||||
watcherSID, watcherClientID, _, err := env.db.CreateSession(
|
||||
ctx, watcherNick, watcherNick, "localhost", "127.0.0.1",
|
||||
_, err = env.svc.JoinChannel(ctx, watcherSID, 0, watcherNick, shared, "")
|
||||
if err != nil {
|
||||
t.Fatalf("join %s: %v", watcherNick, err)
|
||||
}
|
||||
|
||||
_, lastQueueID, err := env.db.PollMessages(ctx, watcherClientID, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("poll: %v", err)
|
||||
}
|
||||
|
||||
errs := runConcurrently(
|
||||
func() error {
|
||||
env.svc.BroadcastQuit(ctx, quitterSID, nick, "")
|
||||
|
||||
return nil
|
||||
},
|
||||
func() error {
|
||||
return takeNickAndJoin(ctx, env, nick, shared)
|
||||
},
|
||||
)
|
||||
if errs[1] != nil {
|
||||
t.Fatalf("new session: %v", errs[1])
|
||||
}
|
||||
|
||||
msgs, _, err := env.db.PollMessages(
|
||||
ctx, watcherClientID, lastQueueID, 0,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("poll: %v", err)
|
||||
}
|
||||
|
||||
commands := commandsFrom(msgs, nick)
|
||||
if !slices.Equal(commands, []string{irc.CmdQuit, irc.CmdJoin}) {
|
||||
t.Errorf(
|
||||
"watcher got %v from %s, want QUIT then JOIN",
|
||||
commands, nick,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("create session %s: %v", watcherNick, err)
|
||||
}
|
||||
|
||||
_, err = env.svc.JoinChannel(ctx, watcherSID, 0, watcherNick, shared, "")
|
||||
if err != nil {
|
||||
t.Fatalf("join %s: %v", watcherNick, err)
|
||||
}
|
||||
|
||||
_, lastQueueID, err := env.db.PollMessages(ctx, watcherClientID, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("poll: %v", err)
|
||||
}
|
||||
|
||||
errs := runConcurrently(
|
||||
func() error {
|
||||
env.svc.BroadcastQuit(ctx, quitterSID, nick, "")
|
||||
|
||||
return nil
|
||||
},
|
||||
func() error {
|
||||
return takeNickAndJoin(ctx, env, nick, shared)
|
||||
},
|
||||
)
|
||||
if errs[1] != nil {
|
||||
t.Fatalf("trial %d: new session: %v", trial, errs[1])
|
||||
}
|
||||
|
||||
msgs, _, err := env.db.PollMessages(
|
||||
ctx, watcherClientID, lastQueueID, 0,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("poll: %v", err)
|
||||
}
|
||||
|
||||
commands := commandsFrom(msgs, nick)
|
||||
if !slices.Equal(commands, []string{irc.CmdQuit, irc.CmdJoin}) {
|
||||
t.Errorf(
|
||||
"trial %d: watcher got %v from %s, want QUIT then JOIN",
|
||||
trial, commands, nick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,6 +701,8 @@ func commandsFrom(msgs []db.IRCMessage, nick string) []string {
|
||||
// into a +i channel uses the invite up, so that a JOIN
|
||||
// after leaving gets ERR_INVITEONLYCHAN.
|
||||
func TestJoinUsesUpInvite(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const channel = "#club"
|
||||
|
||||
env := newTestEnv(t)
|
||||
@@ -736,6 +748,8 @@ func TestJoinUsesUpInvite(t *testing.T) {
|
||||
// session's nick, username and hostname refuses its JOIN
|
||||
// with ERR_BANNEDFROMCHAN.
|
||||
func TestJoinRefusedByHostmaskBan(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const channel = "#club"
|
||||
|
||||
env := newTestEnv(t)
|
||||
@@ -774,6 +788,8 @@ func TestJoinRefusedByHostmaskBan(t *testing.T) {
|
||||
// keeps a channel other members are still on and deletes
|
||||
// the channel when its last member parts.
|
||||
func TestPartDeletesChannelOnlyWhenEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const channel = "#room"
|
||||
|
||||
env := newTestEnv(t)
|
||||
@@ -821,6 +837,8 @@ func TestPartDeletesChannelOnlyWhenEmpty(t *testing.T) {
|
||||
// TestKickOfLastMemberDeletesChannel checks that a KICK
|
||||
// that leaves a channel with no members deletes it.
|
||||
func TestKickOfLastMemberDeletesChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const channel = "#room"
|
||||
|
||||
env := newTestEnv(t)
|
||||
@@ -845,6 +863,8 @@ func TestKickOfLastMemberDeletesChannel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSendChannelMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -892,6 +912,8 @@ func TestSendChannelMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBroadcastQuit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -929,6 +951,8 @@ func TestBroadcastQuit(t *testing.T) {
|
||||
// error as a DM to an unknown nick. The trigger deletes the
|
||||
// target's session just before the DM is stored.
|
||||
func TestSendDirectMessageTargetQuits(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -960,6 +984,8 @@ func TestSendDirectMessageTargetQuits(t *testing.T) {
|
||||
// trigger deletes the channel just before the message is
|
||||
// stored.
|
||||
func TestSendChannelMessageChannelDeleted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -991,6 +1017,8 @@ func TestSendChannelMessageChannelDeleted(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSendChannelMessage_Moderated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -1042,6 +1070,8 @@ func TestSendChannelMessage_Moderated(t *testing.T) {
|
||||
// not spend its hashcash stamp: the operator can then send
|
||||
// the same message with it.
|
||||
func TestRefusedChannelMessageLeavesStampUnspent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
channel = "#paid"
|
||||
bits = 2
|
||||
@@ -1067,6 +1097,8 @@ func TestRefusedChannelMessageLeavesStampUnspent(t *testing.T) {
|
||||
|
||||
for _, refusal := range refusals {
|
||||
t.Run(refusal.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -1119,6 +1151,8 @@ func TestRefusedChannelMessageLeavesStampUnspent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQueryUserMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -1169,6 +1203,8 @@ func TestQueryUserMode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUserhostReply(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -1274,10 +1310,14 @@ func applyUserModeTests() []applyUserModeTest {
|
||||
}
|
||||
|
||||
func TestApplyUserMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
for i, test := range applyUserModeTests() {
|
||||
t.Run(test.modeStr, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := t.Context()
|
||||
sid := createSession(
|
||||
ctx, t, env.db, fmt.Sprintf("user%d", i),
|
||||
|
||||
Reference in new issue
Block a user