//go:build integration package inttest import ( "context" "database/sql" "testing" "time" "github.com/google/uuid" "scrabble/backend/internal/account" "scrabble/backend/internal/accountdelete" ) // 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) } }