Files
tyx_AI_xhs/tests/api/wp1-05-account-deletion.test.ts
T

102 lines
5.2 KiB
TypeScript

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();
});
});