//go:build integration package inttest import ( "context" "database/sql" "testing" "time" "github.com/google/uuid" "scrabble/backend/internal/account" "scrabble/backend/internal/accountdelete" "scrabble/backend/internal/engine" "scrabble/backend/internal/game" ) // deletedFields reads a tombstoned account's retained real name and its deleted_at. func deletedFields(t *testing.T, accountID uuid.UUID) (name string, deletedAt sql.NullTime) { t.Helper() var dn sql.NullString err := testDB.QueryRowContext(context.Background(), "SELECT deleted_display_name, deleted_at FROM accounts WHERE account_id = $1", accountID). Scan(&dn, &deletedAt) if err != nil { t.Fatalf("read deleted fields %s: %v", accountID, err) } return dn.String, deletedAt } // TestAnonymizeAndTombstone: deletion journals + frees the credentials, tombstones the // account, scrubs the live name while retaining the real one, and frees the creds for a // new account to reuse. func TestAnonymizeAndTombstone(t *testing.T) { ctx := context.Background() store := account.NewStore(testDB) deleter := accountdelete.NewDeleter(testDB) acc, _, err := store.ProvisionTelegram(ctx, "tg-"+uuid.NewString(), "ru", "handle", "Иван", "+03:00") if err != nil { t.Fatalf("provision: %v", err) } email := "del-" + uuid.NewString() + "@example.com" if err := store.AttachIdentity(ctx, acc.ID, account.KindEmail, email, true); err != nil { t.Fatalf("attach email: %v", err) } before, err := store.GetByID(ctx, acc.ID) if err != nil { t.Fatalf("load before: %v", err) } if err := deleter.AnonymizeAndTombstone(ctx, acc.ID); err != nil { t.Fatalf("delete: %v", err) } // The live identities are gone. if ids, err := store.Identities(ctx, acc.ID); err != nil || len(ids) != 0 { t.Fatalf("identities after delete = %+v (err %v), want none", ids, err) } // Both credentials are journalled with reason=delete. got := retainedRows(t, acc.ID) if len(got) != 2 { t.Fatalf("retained rows = %+v, want 2", got) } for _, r := range got { if r.reason != "delete" { t.Errorf("retained reason = %q, want delete", r.reason) } } // The live name is scrubbed; the real one is retained; deleted_at is set. after, err := store.GetByID(ctx, acc.ID) if err != nil { t.Fatalf("load after: %v", err) } if after.DisplayName != accountdelete.AnonymizedName { t.Errorf("live display name = %q, want %q", after.DisplayName, accountdelete.AnonymizedName) } name, deletedAt := deletedFields(t, acc.ID) if name != before.DisplayName { t.Errorf("retained name = %q, want %q", name, before.DisplayName) } if !deletedAt.Valid || time.Since(deletedAt.Time) > time.Minute { t.Errorf("deleted_at = %+v, want a recent timestamp", deletedAt) } // The credentials are free: a new account can claim the same email. other, _, err := store.ProvisionTelegram(ctx, "tg-"+uuid.NewString(), "en", "", "Other", "") if err != nil { t.Fatalf("provision other: %v", err) } if err := store.AttachIdentity(ctx, other.ID, account.KindEmail, email, true); err != nil { t.Fatalf("email should be free after deletion, got: %v", err) } } // TestDropAllRobotGames drops the deletee's solo vs-AI game but keeps a game with a human // opponent. func TestDropAllRobotGames(t *testing.T) { ctx := context.Background() gsvc := newGameService() robots := newRobotService(t, gsvc) if err := robots.EnsurePool(ctx); err != nil { t.Fatalf("ensure pool: %v", err) } mm := newMatchmaker(t, robots, time.Minute, 0) deleter := accountdelete.NewDeleter(testDB) user := provisionAccount(t) other := provisionAccount(t) aiRes, err := mm.StartVsAI(ctx, user, engine.VariantEnglish, true) if err != nil { t.Fatalf("start vs AI: %v", err) } humanGame, err := gsvc.Create(ctx, game.CreateParams{ Variant: engine.VariantEnglish, Seats: []uuid.UUID{user, other}, TurnTimeout: time.Hour, Seed: 1, }) if err != nil { t.Fatalf("create human game: %v", err) } n, err := deleter.DropAllRobotGames(ctx, user) if err != nil { t.Fatalf("drop: %v", err) } if n != 1 { t.Fatalf("dropped %d games, want 1 (the vs-AI game)", n) } if _, err := gsvc.GameByID(ctx, aiRes.Game.ID); err == nil { t.Error("the vs-AI game should be dropped") } if _, err := gsvc.GameByID(ctx, humanGame.ID); err != nil { t.Errorf("the human game should be kept, got: %v", err) } }