import { randomUUID } from "node:crypto"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { createApp } from "../../apps/api/src/app.js"; import { ManagedStorage } from "../../apps/api/src/managed-storage.js"; import { RegistrationService } from "../../apps/api/src/registration.js"; import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js"; import { registrationNotice } from "../../packages/shared-contracts/src/registration-notice.js"; const now = Date.parse("2026-07-28T12:00:00.000Z"); const roots: string[] = []; const services: RegistrationService[] = []; const storages: ManagedStorage[] = []; const headers = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" }; async function harness() { const root = mkdtempSync(join(tmpdir(), "dada-wp1-05-api-")); roots.push(root); const databasePath = join(root, "db", "dada.sqlite3"); const storage = new ManagedStorage({ dataRoot: root, databasePath }); storages.push(storage); const resend = new MockResendAdapter(); const registration = new RegistrationService({ challengePepper: Buffer.alloc(32, 0x71), clock: () => now, codeGenerator: () => "539126", currentPrivacyNoticeVersion: registrationNotice.version, databasePath, inviteCodeGenerator: () => "DADA-WP1-05-API", invitePepper: Buffer.alloc(32, 0x72), resend, sessionPepper: Buffer.alloc(32, 0x73), }); services.push(registration); const email = "account-api@example.invalid"; const invite = registration.createInvite({ expiresAt: now + 86_400_000, maxUses: 1 }); const sent = await registration.sendRegistrationCode({ email, inviteCode: invite.code }); const completed = registration.completeRegistration({ code: resend.readLatestCode(email), creatorName: "Account API", idempotencyKey: `register-${randomUUID()}-${randomUUID()}`, privacyConsentAccepted: true, privacyNoticeVersion: registrationNotice.version, registrationId: sent.registrationId, socialId: "@account_api", }); const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration }); return { app, completed, email, registration, resend }; } afterEach(() => { for (const service of services.splice(0)) service.close(); for (const storage of storages.splice(0)) storage.close(); for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true }); }); describe("TDD-WP1-DEL-001 account settings and deletion API", () => { it("serves safe settings and deletes only after CSRF plus a fresh email code", async () => { const { app, completed, email, registration, resend } = await harness(); const cookie = `dada_session=${completed.sessionToken}`; const session = await app.inject({ headers: { ...headers, cookie }, method: "GET", url: "/api/v1/auth/session" }); const settings = await app.inject({ headers: { ...headers, cookie }, method: "GET", url: "/api/v1/account/settings" }); expect(settings.statusCode).toBe(200); expect(settings.json()).toMatchObject({ account: { email, status: "active" }, local_data: { backup_enabled: false, migration_supported: false, location: "configured_local_data_root" }, profile: { creator_name: "Account API", social_id: "@account_api" }, }); expect(JSON.stringify(settings.json())).not.toMatch(/[A-Z]:\\/i); const csrf = settings.json().csrf_token; const deletionSend = await app.inject({ headers: { ...headers, cookie, "x-csrf-token": csrf }, method: "POST", url: "/api/v1/account/deletion/send", }); expect(deletionSend.statusCode).toBe(200); const code = resend.readLatestCode(email); const rejected = await app.inject({ headers: { ...headers, cookie, "idempotency-key": `reject-${randomUUID()}-${randomUUID()}`, "x-csrf-token": csrf, }, method: "POST", payload: { confirmation: "注销账号", deletion_id: deletionSend.json().deletion_id, verification_code: "000000" }, url: "/api/v1/account/deletion/complete", }); expect(rejected.statusCode).toBe(400); expect(registration.database.prepare("SELECT failure_count FROM account_deletion_challenges").get().failure_count).toBe(1); expect(registration.readUserSession(completed.sessionToken)).toBeDefined(); const deletion = await app.inject({ headers: { ...headers, cookie, "idempotency-key": `delete-${randomUUID()}-${randomUUID()}`, "x-csrf-token": csrf, }, method: "POST", payload: { confirmation: "注销账号", deletion_id: deletionSend.json().deletion_id, verification_code: code }, url: "/api/v1/account/deletion/complete", }); expect(deletion.statusCode).toBe(200); expect(deletion.json()).toEqual({ status: "deleted" }); expect(deletion.headers["set-cookie"]).toContain("Max-Age=0"); expect(JSON.stringify(deletion.json())).not.toContain(email); const oldSession = await app.inject({ headers: { ...headers, cookie }, method: "GET", url: "/api/v1/auth/session" }); expect(oldSession.statusCode).toBe(401); expect(registration.readUserSession(completed.sessionToken)).toBeUndefined(); await app.close(); }); });