import { describe, expect, test } from "bun:test"; import { pathToFileURL } from "node:url"; import { join } from "node:path"; import { defineAuthz } from "../src/registry.ts"; import { mergeCatalogs } from "../src/catalog.ts"; import { getAuthzCatalog, hasAuthzCatalog, setAuthzCatalog } from "../src/client.ts"; const CLIENT_URL = pathToFileURL(join(import.meta.dir, "..", "src", "client.ts")).href; describe("authz process-wide catalog singleton", () => { // `catalog` is module-level state, and bun test does NOT isolate module // instances between test files run in the same `bun test` invocation (a // single import in one file is visible to every other file in the run). So // "before any setAuthzCatalog call anywhere in the whole suite" cannot be // observed reliably in-process — a fresh subprocess is the only way to // guarantee the catalog genuinely has never been set. test("getAuthzCatalog throws a setup error before setAuthzCatalog is ever called, in a fresh process", async () => { const proc = Bun.spawn({ cmd: [ "bun", "-e", `const mod = await import(${JSON.stringify(CLIENT_URL)}); if (mod.hasAuthzCatalog()) { console.log("UNEXPECTED_HAS_CATALOG"); process.exit(1); } try { mod.getAuthzCatalog(); console.log("UNEXPECTED_NO_THROW"); process.exit(1); } catch (e) { console.log("THREW:" + (e instanceof Error ? e.message : String(e))); }`, ], stdout: "pipe", stderr: "pipe", cwd: join(import.meta.dir, ".."), }); const [stdout, stderr, exitCode] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited, ]); expect(stderr).toBe(""); expect(exitCode).toBe(0); expect(stdout).toContain("THREW:"); // Names the fix, like getDb()'s "No database configured. Add `db: ...`" message. expect(stdout).toContain("WRN-AUTHZ-SETUP"); expect(stdout).toContain("setAuthzCatalog"); }); test("setAuthzCatalog/getAuthzCatalog round-trip, and hasAuthzCatalog reflects the set state", () => { const catalog = mergeCatalogs([ { source: "client.test.ts", module: defineAuthz({ permissions: { "post:read": { title: "View posts" } } }), }, ]); const returned = setAuthzCatalog(catalog); expect(returned).toBe(catalog); expect(hasAuthzCatalog()).toBe(true); expect(getAuthzCatalog()).toBe(catalog); expect(getAuthzCatalog().permissions.get("post:read")).toEqual({ title: "View posts" }); }); test("setAuthzCatalog overwrites a previously set catalog", () => { const first = mergeCatalogs([ { source: "a.ts", module: defineAuthz({ permissions: { "a:read": {} } }) }, ]); const second = mergeCatalogs([ { source: "b.ts", module: defineAuthz({ permissions: { "b:read": {} } }) }, ]); setAuthzCatalog(first); expect(getAuthzCatalog()).toBe(first); setAuthzCatalog(second); expect(getAuthzCatalog()).toBe(second); expect(getAuthzCatalog().permissions.has("a:read")).toBe(false); expect(getAuthzCatalog().permissions.has("b:read")).toBe(true); }); });