test: cover user-mode apply atomicity and non-blocking KILL
Some checks failed
check / check (push) Failing after 16s
Some checks failed
check / check (push) Failing after 16s
db: TestSetSessionUserModesIsAtomic installs a trigger that rejects the is_oper UPDATE after the is_wallops UPDATE has run, proving the '+w-o' partial-failure the old independent-UPDATE loop exhibited is gone. ircserver: TestDisconnectDoesNotBlockOnUnresponsiveVictim drives Disconnect against a net.Pipe victim that never reads and requires the call to return promptly; verified to fail against the synchronous implementation. Its counterpart asserts the notification is still delivered and the socket still closed.
This commit is contained in:
@@ -58,3 +58,19 @@ func (database *Database) Close() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecForTest runs a raw statement against the test
|
||||
// database. Tests use it to install SQLite triggers that
|
||||
// force a specific write to fail, so that the atomicity of
|
||||
// multi-statement helpers can be exercised.
|
||||
func (database *Database) ExecForTest(
|
||||
ctx context.Context,
|
||||
query string,
|
||||
) error {
|
||||
_, err := database.conn.ExecContext(ctx, query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec for test: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1488,3 +1488,127 @@ func TestChannelUserLimit(t *testing.T) {
|
||||
t.Fatalf("expected 0, got %d", limit)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetSessionUserModesIsAtomic proves that a multi-mode
|
||||
// change is all-or-nothing. A trigger makes the is_oper
|
||||
// UPDATE fail after the is_wallops UPDATE has already run,
|
||||
// which is exactly the "+w-o" partial-failure the previous
|
||||
// implementation exhibited: it issued the two UPDATEs
|
||||
// independently, so +w persisted while the caller reported
|
||||
// total failure.
|
||||
func TestSetSessionUserModesIsAtomic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sessionID, _, _, err := database.CreateSession(
|
||||
ctx, "alice", "", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = database.ExecForTest(ctx,
|
||||
`CREATE TRIGGER reject_oper
|
||||
BEFORE UPDATE OF is_oper ON sessions
|
||||
BEGIN SELECT RAISE(ABORT, 'oper write rejected');
|
||||
END`,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wallops := true
|
||||
oper := false
|
||||
|
||||
err = database.SetSessionUserModes(
|
||||
ctx, sessionID, &wallops, &oper,
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected the rejected oper write to fail")
|
||||
}
|
||||
|
||||
gotWallops, err := database.IsSessionWallops(
|
||||
ctx, sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if gotWallops {
|
||||
t.Error(
|
||||
"wallops persisted despite the transaction " +
|
||||
"failing; the apply stage is not atomic",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetSessionUserModesAppliesBoth is the success-path
|
||||
// counterpart: when nothing fails, both flags are written,
|
||||
// and a nil pointer leaves that flag untouched.
|
||||
func TestSetSessionUserModesAppliesBoth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sessionID, _, _, err := database.CreateSession(
|
||||
ctx, "alice", "", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := database.SetSessionOper(
|
||||
ctx, sessionID, true,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wallops := true
|
||||
oper := false
|
||||
|
||||
if err := database.SetSessionUserModes(
|
||||
ctx, sessionID, &wallops, &oper,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotWallops, err := database.IsSessionWallops(
|
||||
ctx, sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotOper, err := database.IsSessionOper(ctx, sessionID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !gotWallops || gotOper {
|
||||
t.Errorf(
|
||||
"want wallops=true oper=false, got %v/%v",
|
||||
gotWallops, gotOper,
|
||||
)
|
||||
}
|
||||
|
||||
// A nil pointer must leave the stored value alone.
|
||||
if err := database.SetSessionUserModes(
|
||||
ctx, sessionID, nil, nil,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotWallops, err = database.IsSessionWallops(
|
||||
ctx, sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !gotWallops {
|
||||
t.Error("nil pointers must not clear wallops")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user