66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
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"]);
|
|
});
|
|
});
|