Broadcast a session's QUIT once when its clients leave together (closes #154)
check / check (push) Waiting to run
check / check (push) Waiting to run
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 stores and queues the QUIT in it only when it deleted the session, so the QUIT is queued before the nick is free. db.LogoutClient deletes the client and counts the rest in the same transaction. Model: opus-5-5
This commit was merged in pull request #168.
This commit is contained in:
+127
-30
@@ -2,6 +2,7 @@ package db_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
@@ -411,7 +412,7 @@ func TestJoinAndPart(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = database.PartChannel(ctx, chID, sid)
|
||||
err = database.PartChannelAndDeleteIfEmpty(ctx, chID, sid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -422,7 +423,10 @@ func TestJoinAndPart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteChannelIfEmpty(t *testing.T) {
|
||||
// TestPartChannelAndDeleteIfEmpty checks that a part keeps
|
||||
// a channel another session is still on and deletes the
|
||||
// channel when its last member parts.
|
||||
func TestPartChannelAndDeleteIfEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
@@ -435,22 +439,34 @@ func TestDeleteChannelIfEmpty(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sid, _, _, err := database.CreateSession(ctx, "temp", "", "", "")
|
||||
nicks := []string{"temp1", "temp2"}
|
||||
sids := make([]int64, 0, len(nicks))
|
||||
|
||||
for _, nick := range nicks {
|
||||
sid, _, _, createErr := database.CreateSession(ctx, nick, "", "", "")
|
||||
if createErr != nil {
|
||||
t.Fatal(createErr)
|
||||
}
|
||||
|
||||
err = database.JoinChannel(ctx, chID, sid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sids = append(sids, sid)
|
||||
}
|
||||
|
||||
err = database.PartChannelAndDeleteIfEmpty(ctx, chID, sids[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = database.JoinChannel(ctx, chID, sid)
|
||||
_, err = database.GetChannelByName(ctx, "#empty")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("channel deleted while a member is on it: %v", err)
|
||||
}
|
||||
|
||||
err = database.PartChannel(ctx, chID, sid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = database.DeleteChannelIfEmpty(ctx, chID)
|
||||
err = database.PartChannelAndDeleteIfEmpty(ctx, chID, sids[1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -777,6 +793,11 @@ func TestGetHistoryReusedNames(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = database.JoinChannel(ctx, oldChID, oldSID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
body := json.RawMessage(`["hi"]`)
|
||||
|
||||
_, _, err = database.InsertMessage(
|
||||
@@ -799,12 +820,9 @@ func TestGetHistoryReusedNames(t *testing.T) {
|
||||
t.Fatal("expected one DM and one channel message")
|
||||
}
|
||||
|
||||
err = database.DeleteSession(ctx, oldSID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = database.DeleteChannelIfEmpty(ctx, oldChID)
|
||||
// The session is the channel's only member, so quitting
|
||||
// deletes the channel too.
|
||||
_, err = database.QuitSession(ctx, oldSID, "reused", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -826,34 +844,42 @@ func TestGetHistoryReusedNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteSession(t *testing.T) {
|
||||
// TestQuitSession checks that QuitSession deletes the
|
||||
// session, keeps a channel another session is still on,
|
||||
// deletes a channel it leaves with no members, and queues
|
||||
// one QUIT to the other session; and that a second call for
|
||||
// the same session changes nothing.
|
||||
func TestQuitSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid, _, _, err := database.CreateSession(
|
||||
ctx, "deleteme", "", "", "",
|
||||
sid, sharedID, _ := createSessionWithChannels(
|
||||
t, database, "deleteme", "#shared", "#alone",
|
||||
)
|
||||
|
||||
peerSID, peerClientID, _, err := database.CreateSession(
|
||||
ctx, "peer", "", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
chID, err := database.GetOrCreateChannel(
|
||||
ctx, "#delchan",
|
||||
)
|
||||
err = database.JoinChannel(ctx, sharedID, peerSID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = database.JoinChannel(ctx, chID, sid)
|
||||
body := json.RawMessage(`["bye"]`)
|
||||
|
||||
peerIDs, err := database.QuitSession(ctx, sid, "deleteme", body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = database.DeleteSession(ctx, sid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if !slices.Equal(peerIDs, []int64{peerSID}) {
|
||||
t.Fatalf("got peers %v, want [%d]", peerIDs, peerSID)
|
||||
}
|
||||
|
||||
_, err = database.GetSessionByNick(ctx, "deleteme")
|
||||
@@ -861,9 +887,80 @@ func TestDeleteSession(t *testing.T) {
|
||||
t.Fatal("session should be deleted")
|
||||
}
|
||||
|
||||
ids, _ := database.GetChannelMemberIDs(ctx, chID)
|
||||
if len(ids) != 0 {
|
||||
t.Fatal("expected no members after deletion")
|
||||
ids, _ := database.GetChannelMemberIDs(ctx, sharedID)
|
||||
if !slices.Equal(ids, []int64{peerSID}) {
|
||||
t.Fatalf("#shared members: got %v, want [%d]", ids, peerSID)
|
||||
}
|
||||
|
||||
_, err = database.GetChannelByName(ctx, "#alone")
|
||||
if err == nil {
|
||||
t.Fatal("expected the emptied channel to be deleted")
|
||||
}
|
||||
|
||||
checkOneQuitQueued(t, database, peerClientID, "deleteme")
|
||||
|
||||
peerIDs, err = database.QuitSession(ctx, sid, "deleteme", body)
|
||||
if err != nil || len(peerIDs) != 0 {
|
||||
t.Fatalf("second quit: got %v, %v; want none", peerIDs, err)
|
||||
}
|
||||
|
||||
checkOneQuitQueued(t, database, peerClientID, "deleteme")
|
||||
}
|
||||
|
||||
// checkOneQuitQueued fails the test unless the queue of the
|
||||
// client clientID holds one message, a QUIT from nick.
|
||||
func checkOneQuitQueued(
|
||||
t *testing.T,
|
||||
database *db.Database,
|
||||
clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
msgs, _, err := database.PollMessages(t.Context(), clientID, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(msgs) != 1 || msgs[0].Command != "QUIT" || msgs[0].From != nick {
|
||||
t.Fatalf("queue: got %v, want one QUIT from %s", msgs, nick)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLogoutClientFailureKeepsClient checks that when the
|
||||
// session of a logout's last client cannot be deleted,
|
||||
// LogoutClient fails and the client stays logged in.
|
||||
func TestLogoutClientFailureKeepsClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid, clientID, token, err := database.CreateSession(
|
||||
ctx, "stuck", "", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = database.GetDB().ExecContext(ctx,
|
||||
`CREATE TRIGGER keep_sessions
|
||||
BEFORE DELETE ON sessions
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'sessions are kept');
|
||||
END`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = database.LogoutClient(ctx, sid, clientID, "stuck", nil)
|
||||
if err == nil {
|
||||
t.Fatal("logout succeeded although the session was kept")
|
||||
}
|
||||
|
||||
_, _, nick, err := database.GetSessionByToken(ctx, token)
|
||||
if err != nil || nick != "stuck" {
|
||||
t.Fatalf("client gone after a failed logout: %q, %v", nick, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user