/** * Exercises the REAL GDPR data-export and account-deletion paths against the * live (dev) DB to prove the system works end-to-end: seeds a throwaway user * with portfolio data + a stored file, exports, then runs the deletion drain * and asserts everything is gone (and the compliance evidence remains). * Run: npx tsx scripts/verify-gdpr.ts */ import { config } from "dotenv" config({ path: ".env.local" }) process.env.DATABASE_SSL = process.env.DATABASE_SSL ?? "disable" const TEST_USER_ID = "gdpr-verify-user" const TEST_EMAIL = "gdpr-verify@example.com" let failures = 0 function check(label: string, ok: boolean, detail?: string) { console.log(` ${ok ? "✓" : "✗"} ${label}${!ok && detail ? ` — ${detail}` : ""}`) if (!ok) failures++ } async function main() { const { db, pool } = await import("../lib/db") const s = await import("../lib/db/schema") const { eq, and } = await import("drizzle-orm") const storage = await import("../lib/storage") const { buildUserDataExport } = await import("../lib/gdpr/export") const { processDueDeletions } = await import("../lib/gdpr/delete") // ── cleanup any prior run ────────────────────────────────────────────────── await db.delete(s.user).where(eq(s.user.id, TEST_USER_ID)) await db.delete(s.consent_log).where(eq(s.consent_log.user_id, TEST_USER_ID)) await db.delete(s.account_deletion_requests).where(eq(s.account_deletion_requests.user_id, TEST_USER_ID)) await db.delete(s.admin_audit_log).where(eq(s.admin_audit_log.target_user_id, TEST_USER_ID)) // ── seed ─────────────────────────────────────────────────────────────────── console.log("── Seeding throwaway user + portfolio ───") await db.insert(s.user).values({ id: TEST_USER_ID, name: "GDPR Verify", email: TEST_EMAIL }) await db.insert(s.profiles).values({ id: TEST_USER_ID, email: TEST_EMAIL, full_name: "GDPR Verify" }) const [prop] = await db .insert(s.properties) .values({ user_id: TEST_USER_ID, name: "Test House", address_line1: "1 Test St", city: "Testville" }) .returning() const [tenant] = await db .insert(s.tenants) .values({ user_id: TEST_USER_ID, property_id: prop.id, first_name: "Tina", last_name: "Tenant", email: "tina@example.com" }) .returning() await db.insert(s.rent_payments).values({ user_id: TEST_USER_ID, tenant_id: tenant.id, property_id: prop.id, amount: 1200, due_date: "2026-07-01", }) await db.insert(s.api_keys).values({ user_id: TEST_USER_ID, name: "test key", key_hash: `hash-${Date.now()}`, key_prefix: "pmn_test", }) await db.insert(s.consent_log).values([ { user_id: TEST_USER_ID, email: TEST_EMAIL, kind: "terms", granted: true, source: "signup" }, { user_id: TEST_USER_ID, email: TEST_EMAIL, kind: "privacy", granted: true, source: "signup" }, ]) const { key: fileKey } = await storage.saveBuffer(Buffer.from("gdpr verify payload"), { userId: TEST_USER_ID, scope: "verify", ext: "txt", }) console.log(` seeded property=${prop.id.slice(0, 8)} tenant=${tenant.id.slice(0, 8)} file=${fileKey}`) // ── export ───────────────────────────────────────────────────────────────── console.log("── Data export (Articles 15/20) ─────────") const exp = await buildUserDataExport(TEST_USER_ID) check("subject user present", exp.data_subject.user?.email === TEST_EMAIL) check("profile present", exp.data_subject.profile?.id === TEST_USER_ID) check("property exported", exp.portfolio.properties.length === 1) check("tenant exported", exp.portfolio.tenants.length === 1) check("payment exported", exp.portfolio.rent_payments.length === 1) check("consent history exported", exp.privacy.consent_log.length === 2) check("api key exported WITHOUT hash", exp.automation.api_keys.length === 1 && !("key_hash" in exp.automation.api_keys[0])) const serialized = JSON.stringify(exp) check("no key_hash anywhere in export", !serialized.includes("key_hash")) check("no password field anywhere in export", !serialized.includes('"password"')) // ── deletion drain ───────────────────────────────────────────────────────── console.log("── Deletion (Article 17) ────────────────") await db.insert(s.account_deletion_requests).values({ user_id: TEST_USER_ID, email: null, // null → no completion email attempt from the drain scheduled_for: new Date(Date.now() - 60_000).toISOString(), // due 1 min ago }) const { processed, deleted } = await processDueDeletions(50) check("drain picked up the request", processed >= 1) check("drain deleted the account", deleted >= 1) const userAfter = await db.query.user.findFirst({ where: eq(s.user.id, TEST_USER_ID) }) check("user row deleted", !userAfter) const profileAfter = await db.query.profiles.findFirst({ where: eq(s.profiles.id, TEST_USER_ID) }) check("profile cascaded", !profileAfter) const propsAfter = await db.query.properties.findMany({ where: eq(s.properties.user_id, TEST_USER_ID) }) check("properties cascaded", propsAfter.length === 0) const tenantsAfter = await db.query.tenants.findMany({ where: eq(s.tenants.user_id, TEST_USER_ID) }) check("tenants cascaded", tenantsAfter.length === 0) const keysAfter = await db.query.api_keys.findMany({ where: eq(s.api_keys.user_id, TEST_USER_ID) }) check("api keys cascaded", keysAfter.length === 0) const request = await db.query.account_deletion_requests.findFirst({ where: eq(s.account_deletion_requests.user_id, TEST_USER_ID), }) check("request marked completed", request?.status === "completed") check("request email nulled", request?.email === null) const consents = await db.query.consent_log.findMany({ where: eq(s.consent_log.user_id, TEST_USER_ID) }) check("consent facts retained", consents.length === 2) check("consent PII anonymized", consents.every((c) => c.email === null && c.ip_address === null)) const audit = await db.query.admin_audit_log.findFirst({ where: and(eq(s.admin_audit_log.target_user_id, TEST_USER_ID), eq(s.admin_audit_log.action, "gdpr_delete_account")), }) check("audit evidence written (system action, admin_id null)", !!audit && audit.admin_id === null) const bytesAfter = await storage.getUserStorageBytes(TEST_USER_ID) check("stored files purged", bytesAfter === 0, `${bytesAfter} bytes remain`) // ── cleanup compliance rows from the test run ────────────────────────────── await db.delete(s.consent_log).where(eq(s.consent_log.user_id, TEST_USER_ID)) await db.delete(s.account_deletion_requests).where(eq(s.account_deletion_requests.user_id, TEST_USER_ID)) await db.delete(s.admin_audit_log).where(eq(s.admin_audit_log.target_user_id, TEST_USER_ID)) console.log(failures === 0 ? "\nALL CHECKS PASSED ✓" : `\n${failures} CHECK(S) FAILED ✗`) await pool.end() process.exit(failures === 0 ? 0 : 1) } main().catch((e) => { console.error(e) process.exit(1) })