feat: implement TASK-WP1-05 account deletion
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { RetentionCleanup } from "../../apps/worker/src/retention-cleanup.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 baseNow = Date.parse("2026-07-28T11:00:00.000Z");
|
||||
const roots: string[] = [];
|
||||
const services: RegistrationService[] = [];
|
||||
|
||||
function writeEvidence(value: unknown) {
|
||||
const directory = process.env.DADA_EVIDENCE_DIR_RETENTION;
|
||||
if (!directory) return;
|
||||
mkdirSync(directory, { recursive: true });
|
||||
writeFileSync(resolve(directory, "retention-events.json"), `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const service of services.splice(0)) service.close();
|
||||
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
describe("TDD-WP1-DEL-002 retention worker", () => {
|
||||
it("refuses early/manual deletion and removes each row only at its original expiry", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "dada-wp1-05-retention-"));
|
||||
roots.push(root);
|
||||
const databasePath = join(root, "dada.sqlite3");
|
||||
const service = new RegistrationService({
|
||||
challengePepper: Buffer.alloc(32, 0x61), clock: () => baseNow, codeGenerator: () => "120984",
|
||||
currentPrivacyNoticeVersion: registrationNotice.version, databasePath,
|
||||
invitePepper: Buffer.alloc(32, 0x62), resend: new MockResendAdapter(), sessionPepper: Buffer.alloc(32, 0x63),
|
||||
});
|
||||
services.push(service);
|
||||
const accessExpiry = baseNow + 1_000;
|
||||
const anonymousExpiry = baseNow + 2_000;
|
||||
service.database.prepare(`
|
||||
INSERT INTO private_content_access_logs (log_id, actor_ref, subject_ref, target_ref, content_type, occurred_at, expires_at)
|
||||
VALUES (?, 'admin-fixture', ?, ?, 'prompt', ?, ?)
|
||||
`).run(randomUUID(), randomUUID(), randomUUID(), baseNow - 10_000, accessExpiry);
|
||||
service.database.prepare(`
|
||||
INSERT INTO anonymous_retained_events (
|
||||
event_id, anonymous_subject_id, event_type, model_id, outcome, error_category,
|
||||
credit_delta, occurred_at, expires_at
|
||||
) VALUES (?, ?, 'generation_commit', 'model-a', 'succeeded', NULL, -1, ?, ?)
|
||||
`).run(randomUUID(), randomUUID(), baseNow - 10_000, anonymousExpiry);
|
||||
|
||||
expect(() => service.database.prepare("DELETE FROM anonymous_retained_events").run()).toThrow();
|
||||
expect(() => service.database.prepare("DELETE FROM private_content_access_logs").run()).toThrow();
|
||||
|
||||
const early = new RetentionCleanup({ clock: () => baseNow, databasePath });
|
||||
expect(early.purgeExpired()).toEqual({ anonymous_events: 0, private_access_logs: 0 });
|
||||
early.close();
|
||||
const firstExpiry = new RetentionCleanup({ clock: () => accessExpiry, databasePath });
|
||||
expect(firstExpiry.purgeExpired()).toEqual({ anonymous_events: 0, private_access_logs: 1 });
|
||||
firstExpiry.close();
|
||||
const secondExpiry = new RetentionCleanup({ clock: () => anonymousExpiry, databasePath });
|
||||
expect(secondExpiry.purgeExpired()).toEqual({ anonymous_events: 1, private_access_logs: 0 });
|
||||
secondExpiry.close();
|
||||
|
||||
writeEvidence({
|
||||
anonymous_deleted_at_expiry: true,
|
||||
early_delete_blocked: true,
|
||||
private_access_deleted_at_original_expiry: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user