73 lines
3.6 KiB
TypeScript
73 lines
3.6 KiB
TypeScript
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 (?, ?, ?, ?, 'prompt', ?, ?)
|
|
`).run(randomUUID(), randomUUID(), randomUUID(), randomUUID(), accessExpiry - 180 * 86_400_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({ admin_operation_logs: 0, anonymous_events: 0, private_access_logs: 0 });
|
|
early.close();
|
|
const firstExpiry = new RetentionCleanup({ clock: () => accessExpiry, databasePath });
|
|
expect(firstExpiry.purgeExpired()).toEqual({ admin_operation_logs: 0, anonymous_events: 0, private_access_logs: 1 });
|
|
firstExpiry.close();
|
|
const secondExpiry = new RetentionCleanup({ clock: () => anonymousExpiry, databasePath });
|
|
expect(secondExpiry.purgeExpired()).toEqual({ admin_operation_logs: 0, 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,
|
|
});
|
|
});
|
|
});
|