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>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { createAuditErasureHook } from "./audit-erasure-hook";
|
||||
import {
|
||||
createAuditErasureHook,
|
||||
createReqScopedAuditErasureHook,
|
||||
} from "./audit-erasure-hook";
|
||||
import type { IAuditLog } from "../audit-log.interface";
|
||||
|
||||
function makeAuditLog(): IAuditLog {
|
||||
@@ -25,7 +28,10 @@ describe("createAuditErasureHook", () => {
|
||||
const auditLog = makeAuditLog();
|
||||
const hook = createAuditErasureHook({ auditLog });
|
||||
await hook(hookArgs("user_1") as never);
|
||||
expect(auditLog.eraseSubject).toHaveBeenCalledWith("user_1", "pseudonymize");
|
||||
expect(auditLog.eraseSubject).toHaveBeenCalledWith(
|
||||
"user_1",
|
||||
"pseudonymize",
|
||||
);
|
||||
});
|
||||
|
||||
it("respects explicit mode='delete'", async () => {
|
||||
@@ -63,3 +69,78 @@ describe("createAuditErasureHook", () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user