import { describe, it, expect, vi } from "vitest"; import { MultiSinkAuditLog } from "./multi-sink-audit-log"; import type { AuditEntry } from "@repo/core-shared/audit"; import type { IAuditLog } from "./audit-log.interface"; const sample: AuditEntry = { actorId: "user_1", actorType: "user", actorRoles: [], action: "VIEW", 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", }; function makeRecorder(): IAuditLog & { records: AuditEntry[]; erasures: string[] } { const records: AuditEntry[] = []; const erasures: string[] = []; return { records, erasures, async record(e) { records.push(e); }, async eraseSubject(actorId) { erasures.push(actorId); }, }; } describe("MultiSinkAuditLog", () => { it("record() fans out to every sink", async () => { const a = makeRecorder(); const b = makeRecorder(); const m = new MultiSinkAuditLog([a, b]); await m.record(sample); expect(a.records).toHaveLength(1); expect(b.records).toHaveLength(1); }); it("settle-all: one sink failing does not skip others", async () => { const errSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true); const a: IAuditLog = { record: async () => { throw new Error("a-fail"); }, eraseSubject: async () => {} }; const b = makeRecorder(); const m = new MultiSinkAuditLog([a, b]); await m.record(sample); expect(b.records).toHaveLength(1); // b still received the entry expect(errSpy).toHaveBeenCalledOnce(); const written = errSpy.mock.calls[0]![0] as string; const parsed = JSON.parse(written.trimEnd()); expect(parsed._type).toBe("audit-sink-error"); expect(parsed.error).toContain("a-fail"); errSpy.mockRestore(); }); it("eraseSubject() fans out to every sink", async () => { const a = makeRecorder(); const b = makeRecorder(); const m = new MultiSinkAuditLog([a, b]); await m.eraseSubject("user_1", "delete"); expect(a.erasures).toEqual(["user_1"]); expect(b.erasures).toEqual(["user_1"]); }); });