//go:build integration package inttest import ( "context" "errors" "testing" "time" "github.com/google/uuid" "scrabble/backend/internal/account" "scrabble/backend/internal/engine" "scrabble/backend/internal/game" ) // TestGuestReaper verifies the abandoned-guest reaper: it deletes guests with no // game seat once their account age is past the cutoff, while sparing guests that // are too young, guests seated in a game (the FK-protected opponent history), and // durable accounts. func TestGuestReaper(t *testing.T) { ctx := context.Background() store := account.NewStore(testDB) guestA := provisionGuest(t) // guest, no seat → reaped on a future cutoff guestB := provisionGuest(t) // guest, no seat → reaped on a future cutoff seated := provisionGuest(t) // guest seated in a game → kept durable := provisionAccount(t) // Seat the third guest in a game with a durable opponent (Create needs 2-4). opp := provisionAccount(t) if _, err := newGameService().Create(ctx, game.CreateParams{ Variant: engine.VariantEnglish, Seats: []uuid.UUID{seated, opp}, TurnTimeout: 24 * time.Hour, Seed: 1, }); err != nil { t.Fatalf("create game: %v", err) } // A cutoff in the past: every account is younger than the window, so the age // gate spares them all. if n, err := store.ReapAbandonedGuests(ctx, time.Now().Add(-time.Hour)); err != nil { t.Fatalf("reap (past cutoff): %v", err) } else if n != 0 { t.Fatalf("reap with a past cutoff deleted %d, want 0", n) } assertAccount(t, store, guestA, true) // A cutoff in the future: every account predates it, so the no-seat guests are // reaped and the seated guest and the durable account survive. if _, err := store.ReapAbandonedGuests(ctx, time.Now().Add(time.Hour)); err != nil { t.Fatalf("reap (future cutoff): %v", err) } assertAccount(t, store, guestA, false) assertAccount(t, store, guestB, false) assertAccount(t, store, seated, true) assertAccount(t, store, durable, true) } // assertAccount checks whether the account with id is present, failing the test // when its presence differs from want. func assertAccount(t *testing.T, store *account.Store, id uuid.UUID, want bool) { t.Helper() _, err := store.GetByID(context.Background(), id) switch { case err == nil: if !want { t.Errorf("account %s still exists, want reaped", id) } case errors.Is(err, account.ErrNotFound): if want { t.Errorf("account %s was reaped, want kept", id) } default: t.Fatalf("get account %s: %v", id, err) } }