feat(core-audit): StdoutJsonAuditLog impl with audit + audit-erasure markers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-11 16:12:18 +02:00
parent 17996e9347
commit 03e3ef39cd
2 changed files with 83 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
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", () => {
let writeSpy: ReturnType<typeof vi.spyOn>;
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
});
});