brackt/app/models/__tests__/user.test.ts
Claude 388071fef8
Add account deletion and multi-section settings page
- Rename /user-profile → /settings with 301 redirect from old URL
- Add multi-section settings nav (Profile, Account, API placeholder, Data & Privacy)
  reusing existing SettingsDesktopNav/SettingsMobileGridNav components
- Implement account deletion via anonymization: wipes all PII from users row,
  releases team ownerships, removes commissioner records, deletes sessions/accounts
- Add data export request form that emails privacy@brackt.com via Resend
- Add deletedAt timestamp column to users table (migration 0100)
- Add anonymizeUserAccount() to user model
- Add removeAllCommissionersByUserId() to commissioner model
- Tests for both new model functions
- Update UserMenu "Profile" link → "Settings" at /settings

https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X
2026-05-10 20:28:23 +00:00

82 lines
2.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("~/database/context", () => ({
database: vi.fn(),
}));
import { anonymizeUserAccount } from "../user";
import { database } from "~/database/context";
const USER_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
function makeDeleteChain() {
return { where: vi.fn().mockResolvedValue(undefined) };
}
function makeUpdateChain() {
const whereFn = vi.fn().mockResolvedValue(undefined);
const setFn = vi.fn().mockReturnValue({ where: whereFn });
return { set: setFn, _where: whereFn };
}
function makeAnonymizeDb() {
const deleteChains: ReturnType<typeof makeDeleteChain>[] = [];
const updateChain = makeUpdateChain();
const deleteFn = vi.fn().mockImplementation(() => {
const chain = makeDeleteChain();
deleteChains.push(chain);
return chain;
});
return {
db: { delete: deleteFn, update: vi.fn().mockReturnValue(updateChain) },
deleteChains,
updateChain,
deleteFn,
};
}
beforeEach(() => {
vi.clearAllMocks();
});
describe("anonymizeUserAccount", () => {
it("deletes sessions and accounts for the user", async () => {
const { db, deleteFn } = makeAnonymizeDb();
vi.mocked(database).mockReturnValue(db as never);
await anonymizeUserAccount(USER_ID);
expect(deleteFn).toHaveBeenCalledTimes(2);
});
it("calls update on the users table with anonymized fields", async () => {
const { db, updateChain } = makeAnonymizeDb();
vi.mocked(database).mockReturnValue(db as never);
await anonymizeUserAccount(USER_ID);
expect(db.update).toHaveBeenCalledTimes(1);
const setCall = updateChain.set.mock.calls[0][0];
expect(setCall.email).toMatch(/^deleted-[a-f0-9]{8}@deleted\.brackt\.com$/);
expect(setCall.displayName).toBeNull();
expect(setCall.username).toBeNull();
expect(setCall.imageUrl).toBeNull();
expect(setCall.customAvatarUrl).toBeNull();
expect(setCall.flagConfig).toBeNull();
expect(setCall.avatarType).toBe("flag");
expect(setCall.deletedAt).toBeInstanceOf(Date);
});
it("derives the short ID from the UUID", async () => {
const { db, updateChain } = makeAnonymizeDb();
vi.mocked(database).mockReturnValue(db as never);
await anonymizeUserAccount(USER_ID);
const setCall = updateChain.set.mock.calls[0][0];
// USER_ID without dashes starts with "a1b2c3d4" → shortId = "a1b2c3d4"
expect(setCall.email).toBe("deleted-a1b2c3d4@deleted.brackt.com");
});
});