feat(core-audit): createAuditErasureHook Payload afterDelete factory

Adds createAuditErasureHook in core-audit/src/hooks/. The factory returns
a CollectionAfterDeleteHook that calls auditLog.eraseSubject() when a
document is deleted. Defaults to "pseudonymize" mode; coerces numeric ids
to string; skips undefined/null/object ids. Barrel at hooks/index.ts.
6 unit tests cover all guard branches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-11 16:23:08 +02:00
parent 18fddcc45f
commit 270897c550
3 changed files with 107 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import { describe, it, expect, vi } from "vitest";
import { createAuditErasureHook } 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();
});
});