import { randomUUID } from "node:crypto"; import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { Readable } from "node:stream"; import { afterEach, describe, expect, it } from "vitest"; 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 fixedNow = Date.parse("2026-07-28T10:00:00.000Z"); const roots: string[] = []; const services: RegistrationService[] = []; const storages: ManagedStorage[] = []; function writeEvidence(environmentName: string, file: string, value: unknown) { const directory = process.env[environmentName]; if (!directory) return; mkdirSync(directory, { recursive: true }); writeFileSync(resolve(directory, file), `${JSON.stringify(value, null, 2)}\n`); } async function createRegisteredHarness(email = "delete-me@example.invalid") { const root = mkdtempSync(join(tmpdir(), "dada-wp1-05-delete-")); 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 service = new RegistrationService({ challengePepper: Buffer.alloc(32, 0x51), clock: () => fixedNow, codeGenerator: () => "735104", currentPrivacyNoticeVersion: registrationNotice.version, databasePath, inviteCodeGenerator: () => `DADA-DELETE-${randomUUID()}`, invitePepper: Buffer.alloc(32, 0x52), resend, sessionPepper: Buffer.alloc(32, 0x53), }); services.push(service); const invite = service.createInvite({ expiresAt: fixedNow + 86_400_000, maxUses: 2 }); const sent = await service.sendRegistrationCode({ email, inviteCode: invite.code }); const completed = service.completeRegistration({ code: resend.readLatestCode(email), creatorName: "Delete Me", idempotencyKey: `register-${randomUUID()}-${randomUUID()}`, privacyConsentAccepted: true, privacyNoticeVersion: registrationNotice.version, registrationId: sent.registrationId, socialId: "@delete_me", }); return { completed, databasePath, email, resend, root, service, storage }; } 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-delete", () => { it("revokes every session, removes identity and queues owned files in one deletion transaction", async () => { const harness = await createRegisteredHarness(); const userId = harness.completed.user.userId; const secondSession = harness.service.issueAuthenticatedSession(userId, "user"); const csrfToken = harness.service.issueUserCsrfToken(harness.completed.sessionToken); const files = await Promise.all([ harness.storage.commitStream({ content: Readable.from(Buffer.from("reference")), expectedMimeType: "application/octet-stream", fileKind: "reference", fileName: "reference.bin", operationId: randomUUID(), ownerRef: userId, projectedWriteBytes: 9, }), harness.storage.commitStream({ content: Readable.from(Buffer.from("generated")), expectedMimeType: "application/octet-stream", fileKind: "generated", fileName: "generated.bin", operationId: randomUUID(), ownerRef: userId, projectedWriteBytes: 9, }), harness.storage.commitStream({ content: Readable.from(Buffer.from("export")), expectedMimeType: "application/octet-stream", fileKind: "export", fileName: "export.bin", operationId: randomUUID(), ownerRef: userId, projectedWriteBytes: 6, }), ]); const sent = await harness.service.sendAccountDeletionCode({ csrfToken, sessionToken: harness.completed.sessionToken, }); const result = harness.service.completeAccountDeletion({ code: harness.resend.readLatestCode(harness.email), confirmation: "注销账号", csrfToken, deletionId: sent.deletionId, idempotencyKey: `delete-${randomUUID()}-${randomUUID()}`, sessionToken: harness.completed.sessionToken, }); expect(result).toMatchObject({ status: "deleted" }); expect(harness.service.readUserSession(harness.completed.sessionToken)).toBeUndefined(); expect(harness.service.readUserSession(secondSession.sessionToken)).toBeUndefined(); const databaseState = { account: harness.service.database.prepare("SELECT COUNT(*) AS count FROM users WHERE user_id = ?").get(userId).count, consents: harness.service.database.prepare("SELECT COUNT(*) AS count FROM privacy_consents WHERE user_id = ?").get(userId).count, credits: harness.service.database.prepare("SELECT COUNT(*) AS count FROM credit_accounts WHERE user_id = ?").get(userId).count, ledger: harness.service.database.prepare("SELECT COUNT(*) AS count FROM credit_ledger WHERE user_id = ?").get(userId).count, profiles: harness.service.database.prepare("SELECT COUNT(*) AS count FROM user_profiles WHERE user_id = ?").get(userId).count, sessions: harness.service.database.prepare("SELECT COUNT(*) AS count FROM sessions WHERE user_id = ?").get(userId).count, }; expect(databaseState).toEqual({ account: 0, consents: 0, credits: 0, ledger: 0, profiles: 0, sessions: 0 }); expect(files.every((file) => harness.storage.resolveManagedFile(file.file_id) === undefined)).toBe(true); expect(harness.storage.inspectCounts().pending_cleanup).toBe(3); expect(harness.service.database.prepare("SELECT COUNT(*) AS count FROM users WHERE normalized_email = ?").get(harness.email).count).toBe(0); const freshInvite = harness.service.createInvite({ expiresAt: fixedNow + 86_400_000, maxUses: 1 }); const freshSent = await harness.service.sendRegistrationCode({ email: harness.email, inviteCode: freshInvite.code }); const fresh = harness.service.completeRegistration({ code: harness.resend.readLatestCode(harness.email), creatorName: "Fresh Account", idempotencyKey: `fresh-${randomUUID()}-${randomUUID()}`, privacyConsentAccepted: true, privacyNoticeVersion: registrationNotice.version, registrationId: freshSent.registrationId, socialId: "@fresh_account", }); expect(fresh.user.userId).not.toBe(userId); expect(fresh.credits).toEqual({ availableBalance: 10, reservedBalance: 0 }); const cleanup = await harness.storage.processCleanupQueue(); expect(cleanup).toEqual({ completed: 3, failed: 0 }); writeEvidence("DADA_EVIDENCE_DIR_DELETE", "response.json", { old_access: "revoked", old_resources: "not_found", same_email_registration: "fresh_account", status: result.status, }); writeEvidence("DADA_EVIDENCE_DIR_DELETE", "db-diff.json", { ...databaseState, cleanup_queued: 3, old_user_id_reused: false, }); writeEvidence("DADA_EVIDENCE_DIR_DELETE", "worker-events.json", { cleanup_completed: cleanup.completed, cleanup_failed: cleanup.failed, physical_files_remaining: files.filter((file) => existsSync(resolve(harness.root, file.relative_path))).length, }); }); }); describe("TDD-WP1-DEL-002-anonymous-retention", () => { it("projects identity-linked records onto the allowed anonymous fields without extending access audit expiry", async () => { const harness = await createRegisteredHarness("retention@example.invalid"); const userId = harness.completed.user.userId; const csrfToken = harness.service.issueUserCsrfToken(harness.completed.sessionToken); const accessActorRef = randomUUID(); const accessTargetRef = randomUUID(); const accessExpiresAt = fixedNow + 180 * 86_400_000; harness.service.database.prepare(` INSERT INTO private_content_access_logs ( log_id, actor_ref, subject_ref, target_ref, content_type, occurred_at, expires_at ) VALUES (?, ?, ?, ?, 'image', ?, ?) `).run(randomUUID(), accessActorRef, userId, accessTargetRef, fixedNow, accessExpiresAt); const sent = await harness.service.sendAccountDeletionCode({ csrfToken, sessionToken: harness.completed.sessionToken }); harness.service.completeAccountDeletion({ code: harness.resend.readLatestCode(harness.email), confirmation: "注销账号", csrfToken, deletionId: sent.deletionId, idempotencyKey: `retain-${randomUUID()}-${randomUUID()}`, sessionToken: harness.completed.sessionToken, }); const anonymous = harness.service.database.prepare("SELECT * FROM anonymous_retained_events").all(); expect(anonymous).toHaveLength(1); expect(Object.keys(anonymous[0]).sort()).toEqual([ "anonymous_subject_id", "credit_delta", "error_category", "event_id", "event_type", "expires_at", "model_id", "occurred_at", "outcome", ]); expect(anonymous[0]).toMatchObject({ credit_delta: 10, error_category: null, event_type: "registration_grant", expires_at: fixedNow + 180 * 86_400_000, model_id: null, outcome: "succeeded", }); expect(JSON.stringify(anonymous)).not.toContain(harness.email); expect(JSON.stringify(anonymous)).not.toContain(userId); expect(JSON.stringify(anonymous)).not.toContain(accessTargetRef); const access = harness.service.database.prepare("SELECT * FROM private_content_access_logs").get(); expect(access).toMatchObject({ actor_ref: accessActorRef, expires_at: accessExpiresAt }); expect(access.subject_ref).not.toBe(userId); expect(access.target_ref).not.toBe(accessTargetRef); expect(access.subject_ref).not.toBe(anonymous[0].anonymous_subject_id); writeEvidence("DADA_EVIDENCE_DIR_RETENTION", "db-diff.json", { access_actor_preserved: true, access_expiry_preserved: access.expires_at === accessExpiresAt, identity_relationships: 0, anonymous_events: anonymous.length, }); writeEvidence("DADA_EVIDENCE_DIR_RETENTION", "redaction.json", { forbidden_fields_present: [], original_identifiers_present: false, }); }); });