import { describe, it, expect, vi, beforeEach } from "vitest"; import { StdoutJsonAuditLog } from "./stdout-json-audit-log"; import type { AuditEntry } from "@repo/core-shared/audit"; const sample: AuditEntry = { actorId: "user_1", actorType: "user", actorRoles: ["admin"], action: "CREATE", resource: { type: "articles", id: "abc" }, at: new Date("2026-05-11T10:00:00.000Z"), scope: { feature: "blog", environment: "production", tenant: "default" }, from: { ipTruncated: "10.0.0.0", userAgent: "Mozilla/5.0" }, containsPii: false, outcome: "success", }; describe("StdoutJsonAuditLog", () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let writeSpy: ReturnType>; beforeEach(() => { writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); }); it("record() writes one JSON line per entry to stdout", async () => { const log = new StdoutJsonAuditLog(); await log.record(sample); expect(writeSpy).toHaveBeenCalledOnce(); const written = writeSpy.mock.calls[0]![0] as string; expect(written.endsWith("\n")).toBe(true); const parsed = JSON.parse(written.trimEnd()); expect(parsed._type).toBe("audit"); expect(parsed.actorId).toBe("user_1"); expect(parsed.action).toBe("CREATE"); expect(parsed.at).toBe("2026-05-11T10:00:00.000Z"); // ISO 8601 serialization }); it("eraseSubject() emits a tombstone with mode + actorId", async () => { const log = new StdoutJsonAuditLog(); await log.eraseSubject("user_1", "pseudonymize"); expect(writeSpy).toHaveBeenCalledOnce(); const written = writeSpy.mock.calls[0]![0] as string; const parsed = JSON.parse(written.trimEnd()); expect(parsed._type).toBe("audit-erasure"); expect(parsed.actorId).toBe("user_1"); expect(parsed.mode).toBe("pseudonymize"); expect(typeof parsed.at).toBe("string"); // ISO 8601 timestamp }); });