Files
agentic-dev/packages/core-audit/src/hooks/audit-erasure-hook.test.ts
Danijel Martinek a2be5d5488 feat(core-cms): register audit-logs + wire GDPR audit erasure
The audit-logs collection was never registered (record() would throw),
bindAudit/createAuditErasureHook were unused, and DSR cascade-hard never
touched the audit trail (audit finding A6). core-cms now registers the
collection and wires a req-scoped afterDelete erasure hook on users;
bindAllProduction binds the audit log into consent/DSR; cascade-hard
pseudonymizes the subject's audit entries; the action select accepts
the full AuditAction enum so consent/DSR entries pass validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:25:46 +02:00

147 lines
4.6 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import {
createAuditErasureHook,
createReqScopedAuditErasureHook,
} from "./audit-erasure-hook";
import type { IAuditLog } from "../audit-log.interface";
function makeAuditLog(): IAuditLog {
return {
record: vi.fn().mockResolvedValue(undefined),
eraseSubject: vi.fn().mockResolvedValue(undefined),
};
}
/** Minimal CollectionAfterDeleteHook args shape (only `doc` matters here). */
function hookArgs(id: unknown) {
return {
doc: { id },
req: {} as never,
id: String(id),
collection: {} as never,
context: {},
};
}
describe("createAuditErasureHook", () => {
it("defaults to 'pseudonymize' mode", async () => {
const auditLog = makeAuditLog();
const hook = createAuditErasureHook({ auditLog });
await hook(hookArgs("user_1") as never);
expect(auditLog.eraseSubject).toHaveBeenCalledWith(
"user_1",
"pseudonymize",
);
});
it("respects explicit mode='delete'", async () => {
const auditLog = makeAuditLog();
const hook = createAuditErasureHook({ auditLog, mode: "delete" });
await hook(hookArgs("user_2") as never);
expect(auditLog.eraseSubject).toHaveBeenCalledWith("user_2", "delete");
});
it("coerces numeric id to string", async () => {
const auditLog = makeAuditLog();
const hook = createAuditErasureHook({ auditLog });
await hook(hookArgs(42) as never);
expect(auditLog.eraseSubject).toHaveBeenCalledWith("42", "pseudonymize");
});
it("skips when doc.id is undefined", async () => {
const auditLog = makeAuditLog();
const hook = createAuditErasureHook({ auditLog });
await hook(hookArgs(undefined) as never);
expect(auditLog.eraseSubject).not.toHaveBeenCalled();
});
it("skips when doc.id is null", async () => {
const auditLog = makeAuditLog();
const hook = createAuditErasureHook({ auditLog });
await hook(hookArgs(null) as never);
expect(auditLog.eraseSubject).not.toHaveBeenCalled();
});
it("skips when doc.id is an object", async () => {
const auditLog = makeAuditLog();
const hook = createAuditErasureHook({ auditLog });
await hook(hookArgs({ nested: true }) as never);
expect(auditLog.eraseSubject).not.toHaveBeenCalled();
});
});
describe("createReqScopedAuditErasureHook (A6)", () => {
function makeReqPayload(withAuditCollection: boolean) {
const find = vi.fn().mockResolvedValue({ docs: [{ id: "log-1" }] });
const update = vi.fn().mockResolvedValue({});
const del = vi.fn().mockResolvedValue({});
const payload = {
config: {
collections: withAuditCollection ? [{ slug: "audit-logs" }] : [],
},
find,
update,
delete: del,
};
return { payload, find, update, del };
}
function reqHookArgs(id: unknown, payload: unknown) {
return {
doc: { id },
req: { payload } as never,
id: String(id),
collection: {} as never,
context: {},
};
}
it("pseudonymizes the deleted subject's audit entries via req.payload", async () => {
const { payload, find, update } = makeReqPayload(true);
const hook = createReqScopedAuditErasureHook();
await hook(reqHookArgs("user_1", payload) as never);
expect(find).toHaveBeenCalledWith(
expect.objectContaining({
collection: "audit-logs",
where: { actorId: { equals: "user_1" } },
}),
);
expect(update).toHaveBeenCalledWith(
expect.objectContaining({
collection: "audit-logs",
id: "log-1",
data: { actorId: expect.stringMatching(/^erased-/) },
}),
);
});
it("respects mode='delete'", async () => {
const { payload, del } = makeReqPayload(true);
const hook = createReqScopedAuditErasureHook({ mode: "delete" });
await hook(reqHookArgs("user_2", payload) as never);
expect(del).toHaveBeenCalledWith(
expect.objectContaining({
collection: "audit-logs",
where: { actorId: { equals: "user_2" } },
}),
);
});
it("no-ops when the audit-logs collection is not registered", async () => {
const { payload, find, update, del } = makeReqPayload(false);
const hook = createReqScopedAuditErasureHook();
await hook(reqHookArgs("user_1", payload) as never);
expect(find).not.toHaveBeenCalled();
expect(update).not.toHaveBeenCalled();
expect(del).not.toHaveBeenCalled();
});
it("skips invalid doc ids", async () => {
const { payload, find } = makeReqPayload(true);
const hook = createReqScopedAuditErasureHook();
await hook(reqHookArgs(undefined, payload) as never);
expect(find).not.toHaveBeenCalled();
});
});