Broadcast a session's QUIT once when its clients leave together (closes #154)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Logout deleted the client, counted the session's clients and ran BroadcastQuit as separate statements, and BroadcastQuit read the peers, parted, deleted empty channels and deleted the session separately too. Two logouts or QUITs of one session at once both sent its QUIT, and a JOIN landing between a part and the channel's deletion got into a +k channel without the key. db.QuitSession now does all of that in one transaction, parting each channel as PartChannelAndDeleteIfEmpty does, and reports whether it deleted the session; only that caller sends the QUIT. db.LogoutClient deletes the client and counts the rest in the same transaction. Model: opus-5-5
This commit is contained in:
@@ -495,6 +495,78 @@ func TestJoinConcurrentWithLastPart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestJoinConcurrentWithLastQuit checks that a user who
|
||||
// sends JOIN without the key to a +k channel, again after
|
||||
// each ERR_BADCHANNELKEY, while the channel's only member
|
||||
// quits, gets in only once the channel is deleted, into a
|
||||
// new channel of that name with no key.
|
||||
func TestJoinConcurrentWithLastQuit(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
for trial := range concurrentJoinTrials {
|
||||
channel := fmt.Sprintf("#keyed%d", trial)
|
||||
leaverNick := fmt.Sprintf("leaver%d", trial)
|
||||
leaverSID := createSession(ctx, t, env.db, leaverNick)
|
||||
joinerNick := fmt.Sprintf("joiner%d", trial)
|
||||
joinerSID := createSession(ctx, t, env.db, joinerNick)
|
||||
|
||||
created, err := env.svc.JoinChannel(
|
||||
ctx, leaverSID, leaverNick, channel, "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("join %s: %v", leaverNick, err)
|
||||
}
|
||||
|
||||
err = env.db.SetChannelKey(ctx, created.ChannelID, "secret")
|
||||
if err != nil {
|
||||
t.Fatalf("set +k: %v", err)
|
||||
}
|
||||
|
||||
var joined *service.JoinResult
|
||||
|
||||
errs := runConcurrently(
|
||||
func() error {
|
||||
env.svc.BroadcastQuit(ctx, leaverSID, leaverNick, "")
|
||||
|
||||
return nil
|
||||
},
|
||||
// The database has one connection, so retried
|
||||
// JOINs take turns with the QUIT's statements
|
||||
// and land between any two that are not in one
|
||||
// transaction. A JOIN after the QUIT gets in, so
|
||||
// the loop ends.
|
||||
func() error {
|
||||
for {
|
||||
var (
|
||||
joinErr error
|
||||
ircErr *service.IRCError
|
||||
)
|
||||
|
||||
joined, joinErr = env.svc.JoinChannel(
|
||||
ctx, joinerSID, joinerNick, channel, "",
|
||||
)
|
||||
if !errors.As(joinErr, &ircErr) ||
|
||||
ircErr.Code != irc.ErrBadChannelKey {
|
||||
return joinErr
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
if errs[1] != nil {
|
||||
t.Fatalf("trial %d: JOIN: %v", trial, errs[1])
|
||||
}
|
||||
|
||||
key, _ := env.db.GetChannelKey(ctx, joined.ChannelID)
|
||||
if key != "" {
|
||||
t.Errorf(
|
||||
"trial %d: %s got into +k %s without the key",
|
||||
trial, joinerNick, channel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestJoinUsesUpInvite checks that the JOIN an invite let
|
||||
// into a +i channel uses the invite up, so that a JOIN
|
||||
// after leaving gets ERR_INVITEONLYCHAN.
|
||||
|
||||
Reference in New Issue
Block a user