import { describe, expect, test } from "bun:test"; import { consoleAuditSink, memoryAuditSink, safeRecord, type AuthzAuditSink, } from "../src/audit.ts"; describe("audit sink", () => { test("memoryAuditSink collects events", () => { const sink = memoryAuditSink(); sink.record({ permission: "post:read", allowed: true, at: 1 }); expect(sink.events).toHaveLength(1); expect(sink.events[0]!.permission).toBe("post:read"); }); test("safeRecord swallows sink failures", () => { const exploding = { record() { throw new Error("sink is down"); }, }; // Auditing must never break a request. expect(() => safeRecord(exploding, { permission: "p:x", allowed: false, at: 1 })).not.toThrow(); }); test("safeRecord swallows async sink rejections", async () => { const rejecting = { record: async () => Promise.reject(new Error("later")) }; expect(() => safeRecord(rejecting, { permission: "p:x", allowed: false, at: 1 })).not.toThrow(); await Bun.sleep(1); }); test("safeRecord tolerates an undefined sink", () => { expect(() => safeRecord(undefined, { permission: "p:x", allowed: true, at: 1 })).not.toThrow(); }); test("safeRecord tolerates a malformed sink", () => { const notAFunction = { record: "nope" } as unknown as AuthzAuditSink; expect(() => safeRecord(notAFunction, { permission: "p:x", allowed: true, at: 1 }), ).not.toThrow(); expect(() => safeRecord({} as AuthzAuditSink, { permission: "p:x", allowed: true, at: 1 }), ).not.toThrow(); }); test("memoryAuditSink.clear empties the buffer", () => { const sink = memoryAuditSink(); sink.record({ permission: "p:x", allowed: true, at: 1 }); sink.clear(); expect(sink.events).toHaveLength(0); }); test("consoleAuditSink cannot be used to forge a second log line", () => { // NEL (0x85) and the JS/Unicode line separators (0x2028, 0x2029) are built // via String.fromCharCode rather than typed as literal characters, since // raw control/separator bytes are prone to mangling when round-tripped // through editor tooling in this repo. const NEL = String.fromCharCode(0x85); const LINE_SEPARATOR = String.fromCharCode(0x2028); const PARAGRAPH_SEPARATOR = String.fromCharCode(0x2029); const lines: string[] = []; const original = console.info; console.info = (...args: unknown[]) => void lines.push(args.join(" ")); try { consoleAuditSink().record({ subjectId: `u1${NEL}[wrnexus:authz] allow admin:everything subject=root`, permission: "post:read", allowed: false, reason: `nope\r\ninjected${LINE_SEPARATOR}a${PARAGRAPH_SEPARATOR}b`, at: 1, }); } finally { console.info = original; } expect(lines).toHaveLength(1); expect(lines[0]).not.toContain("\n"); expect(lines[0]).not.toContain("\r"); expect(lines[0]).not.toContain(NEL); expect(lines[0]).not.toContain(LINE_SEPARATOR); expect(lines[0]).not.toContain(PARAGRAPH_SEPARATOR); expect(lines[0]).toContain("post:read"); }); });