feat(core-testing): RecordingAuditLog test double
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,3 +4,4 @@ export { RecordingMetrics, type RecordedMetric } from "./recording-metrics";
|
|||||||
export { RecordingJobQueue } from "./recording-job-queue";
|
export { RecordingJobQueue } from "./recording-job-queue";
|
||||||
export { RecordingEventBus } from "./recording-event-bus";
|
export { RecordingEventBus } from "./recording-event-bus";
|
||||||
export { RecordingRealtimeBroadcaster } from "./recording-realtime-broadcaster";
|
export { RecordingRealtimeBroadcaster } from "./recording-realtime-broadcaster";
|
||||||
|
export { RecordingAuditLog } from "./recording-audit-log";
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { RecordingAuditLog } from "./recording-audit-log";
|
||||||
|
import type { AuditEntry } from "@repo/core-shared/audit";
|
||||||
|
|
||||||
|
const sample: AuditEntry = {
|
||||||
|
actorId: "user_1",
|
||||||
|
actorType: "user",
|
||||||
|
actorRoles: [],
|
||||||
|
action: "CREATE",
|
||||||
|
resource: { type: "articles" },
|
||||||
|
at: new Date(),
|
||||||
|
scope: { feature: "blog", environment: "test", tenant: "default" },
|
||||||
|
from: { ipTruncated: "10.0.0.0", userAgent: "test" },
|
||||||
|
containsPii: false,
|
||||||
|
outcome: "success",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("RecordingAuditLog", () => {
|
||||||
|
it("record() pushes to recorded[]", async () => {
|
||||||
|
const log = new RecordingAuditLog();
|
||||||
|
await log.record(sample);
|
||||||
|
expect(log.recorded).toHaveLength(1);
|
||||||
|
expect(log.recorded[0]!.actorId).toBe("user_1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("eraseSubject(pseudonymize) tracks erasure + rewrites actorId in recorded", async () => {
|
||||||
|
const log = new RecordingAuditLog();
|
||||||
|
await log.record(sample);
|
||||||
|
await log.eraseSubject("user_1", "pseudonymize");
|
||||||
|
expect(log.erasures).toEqual([{ actorId: "user_1", mode: "pseudonymize" }]);
|
||||||
|
expect(log.recorded[0]!.actorId).toBe("erased-user_1"); // sentinel rewrite
|
||||||
|
});
|
||||||
|
|
||||||
|
it("eraseSubject(delete) removes matching entries from recorded", async () => {
|
||||||
|
const log = new RecordingAuditLog();
|
||||||
|
await log.record(sample);
|
||||||
|
await log.record({ ...sample, actorId: "user_2" });
|
||||||
|
await log.eraseSubject("user_1", "delete");
|
||||||
|
expect(log.recorded.map((r) => r.actorId)).toEqual(["user_2"]);
|
||||||
|
expect(log.erasures).toEqual([{ actorId: "user_1", mode: "delete" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reset() clears recorded + erasures", async () => {
|
||||||
|
const log = new RecordingAuditLog();
|
||||||
|
await log.record(sample);
|
||||||
|
await log.eraseSubject("user_1", "pseudonymize");
|
||||||
|
log.reset();
|
||||||
|
expect(log.recorded).toEqual([]);
|
||||||
|
expect(log.erasures).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { AuditEntry } from "@repo/core-shared/audit";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test-side recording double for IAuditLog. Mirrors Payload semantics in
|
||||||
|
* eraseSubject (pseudonymize rewrites in place; delete filters out) so tests
|
||||||
|
* can assert against the same observable state the real impl produces.
|
||||||
|
*
|
||||||
|
* Use directly via constructor injection in factory-function tests — no
|
||||||
|
* container manipulation needed.
|
||||||
|
*/
|
||||||
|
export class RecordingAuditLog {
|
||||||
|
public recorded: AuditEntry[] = [];
|
||||||
|
public erasures: { actorId: string; mode: "pseudonymize" | "delete" }[] = [];
|
||||||
|
|
||||||
|
async record(entry: AuditEntry): Promise<void> {
|
||||||
|
// Shallow copy to avoid mutating caller's object during eraseSubject
|
||||||
|
this.recorded.push({ ...entry });
|
||||||
|
}
|
||||||
|
|
||||||
|
async eraseSubject(actorId: string, mode: "pseudonymize" | "delete"): Promise<void> {
|
||||||
|
this.erasures.push({ actorId, mode });
|
||||||
|
if (mode === "pseudonymize") {
|
||||||
|
for (const r of this.recorded) {
|
||||||
|
if (r.actorId === actorId) {
|
||||||
|
r.actorId = `erased-${actorId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.recorded = this.recorded.filter((r) => r.actorId !== actorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.recorded = [];
|
||||||
|
this.erasures = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user