Files
hope-events/e2e/privacy-retention.spec.ts
joshuaandClaude Opus 5.5 a1ecbb9617 POPIA toolkit part 3: retention, breach register, readiness
- Retention policies (Admin -> Privacy -> Retention) applied by a nightly
  `retention` job: inactive accounts anonymised after 5 years with a
  30-day warning (on by default; never staff roles or people with an
  upcoming registration), form answers, login history, payer details in
  payment notifications, audit/access logs
- User.lastLoginAt (set on login, backfilled) and inactivityWarnedAt
- Breach register (SecurityBreach) with drafts for the people affected
  and the Information Regulator
- Readiness checklist with a score
- Children's/medical flag in the access log; {retention} placeholder in
  the privacy notice
- Unit tests (retention.test.js), e2e (privacy-retention.spec.ts); docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
2026-10-23 20:25:58 +02:00

65 lines
3.9 KiB
TypeScript

import { test, expect, seed, loginAs, api } from "./fixtures";
type RunResult = { result: Record<string, Record<string, number>> };
type UserRow = { name: string; email: string; isActive: boolean };
type Readiness = { score: number; checks: { key: string; ok: boolean; applies: boolean }[] };
type Breach = { id: string; status: string; regulatorNotifiedAt: string | null };
test.describe("retention, breaches and readiness (POPIA)", () => {
test("the retention job warns inactive accounts first, and anonymises them 30 days after the warning", async ({ page }) => {
const { result } = await api<RunResult>(page, "admin", "POST", "/api/privacy/retention/run");
expect(result.inactive_accounts.warned).toBeGreaterThanOrEqual(1);
expect(result.inactive_accounts.anonymised).toBeGreaterThanOrEqual(1);
expect(result.login_history.deleted).toBeGreaterThanOrEqual(1);
const warned = await api<UserRow>(page, "admin", "GET", `/api/users/${seed().retention.toWarn}`);
expect(warned.email).toBe("dormant@e2e.test");
const gone = await api<UserRow>(page, "admin", "GET", `/api/users/${seed().retention.toAnonymise}`);
expect(gone.name).toBe("Deleted User");
expect(gone.email).toMatch(/@deleted\.invalid$/);
// Seeded accounts (staff roles, and the recently active user) are untouched.
expect((await api<UserRow>(page, "admin", "GET", `/api/users/${seed().users.user.id}`)).email).toBe("user@e2e.test");
});
test("retention periods are validated and saved; the notice follows them", async ({ page }) => {
const state = await api<{ policies: Record<string, { enabled: boolean; months: number }> }>(page, "admin", "GET", "/api/privacy/retention");
await expect(api(page, "admin", "PUT", "/api/privacy/retention", { policies: { ...state.policies, inactive_accounts: { enabled: true, months: 3 } } })).rejects.toThrow(/400/);
await api(page, "admin", "PUT", "/api/privacy/retention", { policies: { ...state.policies, form_answers: { enabled: true, months: 18 } } });
await page.goto("/legal/privacy");
await expect(page.getByText("Answers to event forms are deleted 18 months after the event.", { exact: false })).toBeVisible();
});
test("a breach is recorded, reported, and drafts are ready to copy", async ({ page }) => {
const breach = await api<Breach>(page, "admin", "POST", "/api/privacy/breaches", {
title: "Phone with the attendee list lost", discoveredAt: new Date().toISOString(),
description: "A leader's phone was stolen at the camp site.", dataAffected: "Names and phone numbers of 40 campers",
});
expect(breach.status).toBe("open");
await loginAs(page, "admin");
await page.goto("/dashboard/admin/privacy?tab=breaches");
await page.getByRole("button", { name: /Phone with the attendee list lost/ }).click();
await expect(page.getByText("Draft message to the people affected")).toBeVisible();
await expect(page.getByText("Personal information affected: Names and phone numbers of 40 campers")).toBeVisible();
await page.getByRole("button", { name: "Regulator told today" }).click();
await expect(page.getByRole("button", { name: "Regulator told today" })).toHaveCount(0);
const list = await api<Breach[]>(page, "admin", "GET", "/api/privacy/breaches");
expect(list.find(b => b.id === breach.id)?.regulatorNotifiedAt).toBeTruthy();
});
test("the readiness page scores the install", async ({ page }) => {
const r = await api<Readiness>(page, "admin", "GET", "/api/privacy/readiness");
expect(r.score).toBeGreaterThan(0);
expect(r.checks.find(c => c.key === "retention_reviewed")?.ok).toBe(true);
expect(r.checks.find(c => c.key === "retention_running")?.ok).toBe(false); // jobs are off in e2e
await loginAs(page, "admin");
await page.goto("/dashboard/admin/privacy");
await expect(page.getByText("POPIA readiness")).toBeVisible();
await expect(page.getByText(`${r.score}%`)).toBeVisible();
});
});