Files
WRNexusJS/packages/dev-server/test/authz-prod.test.ts
ClintchizandClaude Opus 5 a7255fa1bd fix(dev-server): don't clobber a caller-set authz catalog; drop dead RuntimeDeps.authz
createProductionHandlers called setAuthzCatalog unconditionally, so a caller
using client.ts's documented escape hatch (setAuthzCatalog(catalog) before
importing anything that reads it) had that catalog silently wiped to empty
whenever opts.authz was omitted. Now only sets when opts.authz has entries to
contribute, or when nothing has been set yet; a non-empty opts.authz still
always sets and still throws on a genuine conflict.

Also removes RuntimeDeps.authz: nothing read it, and its doc comment
described a consumer that doesn't exist. The real wiring is
getAuthzCatalog()/setAuthzCatalog(), including the HMR hot-update path, which
is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 02:10:00 +05:30

235 lines
8.5 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { pathToFileURL } from "node:url";
import { join } from "node:path";
import { defineAuthz, getAuthzCatalog, mergeCatalogs, setAuthzCatalog } from "@wrnexus/authz";
import {
applyAuthzManifestEarly,
createProductionHandlers,
type ProdManifest,
} from "../src/prod.ts";
const EMPTY_MANIFEST: ProdManifest = {
pages: [],
api: [],
realtime: [],
middleware: [],
components: [],
layouts: [],
};
const PROD_URL = pathToFileURL(join(import.meta.dir, "..", "src", "prod.ts")).href;
describe("createProductionHandlers authorization wiring (the idempotent second pass)", () => {
test("an empty/absent authz array never throws, whatever the ambient catalog state", () => {
createProductionHandlers(EMPTY_MANIFEST, { authz: [] });
createProductionHandlers(EMPTY_MANIFEST, {});
});
test("starting from a genuinely unset catalog, an empty/absent authz array yields an empty catalog", async () => {
// bun test does NOT isolate module instances between test files run in
// the same invocation (see client.test.ts's comment on the same trap),
// so "no catalog set yet" cannot be observed reliably in-process — some
// other file's test may already have called setAuthzCatalog. A fresh
// subprocess is the only way to guarantee that.
const proc = Bun.spawn({
cmd: [
"bun",
"-e",
`const mod = await import(${JSON.stringify(PROD_URL)});
const manifest = { pages: [], api: [], realtime: [], middleware: [], components: [], layouts: [] };
mod.createProductionHandlers(manifest, { authz: [] });
const { getAuthzCatalog } = await import("@wrnexus/authz");
console.log("SIZE:" + getAuthzCatalog().permissions.size);`,
],
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("SIZE:0");
});
test("a declaration with no default export warns and is skipped, not fatal", () => {
const originalWarn = console.warn;
const warnings: unknown[][] = [];
console.warn = (...args: unknown[]) => {
warnings.push(args);
};
try {
createProductionHandlers(EMPTY_MANIFEST, {
authz: [
{ source: "broken.ts", module: undefined },
{
source: "ok.ts",
module: defineAuthz({ permissions: { "post:read": {} } }),
},
],
});
} finally {
console.warn = originalWarn;
}
expect(getAuthzCatalog().permissions.has("post:read")).toBe(true);
expect(getAuthzCatalog().permissions.size).toBe(1);
expect(warnings.some((args) => args.some((arg) => String(arg).includes("broken.ts")))).toBe(
true,
);
});
test("a conflicting pair of declarations throws, naming both source files", () => {
expect(() =>
createProductionHandlers(EMPTY_MANIFEST, {
authz: [
{
source: "a.ts",
module: defineAuthz({ permissions: { "post:read": { risk: "low" } } }),
},
{
source: "b.ts",
module: defineAuthz({ permissions: { "post:read": { risk: "high" } } }),
},
],
}),
).toThrow(/WRN-AUTHZ-CONFLICT/);
let thrown: unknown;
try {
createProductionHandlers(EMPTY_MANIFEST, {
authz: [
{
source: "a.ts",
module: defineAuthz({ permissions: { "post:read": { risk: "low" } } }),
},
{
source: "b.ts",
module: defineAuthz({ permissions: { "post:read": { risk: "high" } } }),
},
],
});
} catch (error) {
thrown = error;
}
expect(thrown).toBeInstanceOf(Error);
const message = (thrown as Error).message;
expect(message).toContain("a.ts");
expect(message).toContain("b.ts");
});
test("calling createProductionHandlers a second time with different declarations re-validates, not skips", () => {
// Regression guard for the "skip merging if a catalog is already set"
// trap: since setAuthzCatalog is a process-wide singleton, an earlier
// test (or an earlier createProductionHandlers call in the same process)
// can leave hasAuthzCatalog() true. This call must still independently
// merge+validate its OWN opts.authz, not silently trust a stale catalog
// left over from something else.
createProductionHandlers(EMPTY_MANIFEST, {
authz: [{ source: "first.ts", module: defineAuthz({ permissions: { "a:read": {} } }) }],
});
expect(getAuthzCatalog().permissions.has("a:read")).toBe(true);
createProductionHandlers(EMPTY_MANIFEST, {
authz: [{ source: "second.ts", module: defineAuthz({ permissions: { "b:read": {} } }) }],
});
expect(getAuthzCatalog().permissions.has("a:read")).toBe(false);
expect(getAuthzCatalog().permissions.has("b:read")).toBe(true);
});
test("a caller-set catalog survives when opts.authz is omitted (the client.ts escape hatch)", () => {
// client.ts documents that a direct caller of createProductionHandlers may
// call setAuthzCatalog(catalog) itself, before importing anything that
// reads it, when it bypasses the generated `.authz-setup.ts` entry. That
// catalog must not be wiped just because this call's own opts.authz is
// empty/absent.
const preset = mergeCatalogs([
{
source: "preset.ts",
module: defineAuthz({
permissions: { "preset:read": {}, "preset:write": {}, "preset:delete": {} },
}),
},
]);
setAuthzCatalog(preset);
expect(getAuthzCatalog().permissions.size).toBe(3);
createProductionHandlers(EMPTY_MANIFEST, {});
expect(getAuthzCatalog()).toBe(preset);
expect(getAuthzCatalog().permissions.size).toBe(3);
expect(getAuthzCatalog().permissions.has("preset:read")).toBe(true);
});
test("a non-empty opts.authz still sets (and still throws on a conflict), even over a pre-set catalog", () => {
const preset = mergeCatalogs([
{ source: "preset.ts", module: defineAuthz({ permissions: { "preset:read": {} } }) },
]);
setAuthzCatalog(preset);
// A non-empty authz array must still replace the pre-set catalog with the
// merged result of ITS OWN declarations, not defer to the pre-set one.
createProductionHandlers(EMPTY_MANIFEST, {
authz: [{ source: "own.ts", module: defineAuthz({ permissions: { "own:read": {} } }) }],
});
expect(getAuthzCatalog()).not.toBe(preset);
expect(getAuthzCatalog().permissions.has("own:read")).toBe(true);
expect(getAuthzCatalog().permissions.has("preset:read")).toBe(false);
// And a genuine conflict inside that non-empty array still throws, exactly
// as it did before this pass became conditional.
setAuthzCatalog(preset);
expect(() =>
createProductionHandlers(EMPTY_MANIFEST, {
authz: [
{
source: "a.ts",
module: defineAuthz({ permissions: { "post:read": { risk: "low" } } }),
},
{
source: "b.ts",
module: defineAuthz({ permissions: { "post:read": { risk: "high" } } }),
},
],
}),
).toThrow(/WRN-AUTHZ-CONFLICT/);
});
});
describe("applyAuthzManifestEarly (the eager, silent pass called only by the generated .authz-setup.ts)", () => {
test("sets the catalog from valid declarations", () => {
applyAuthzManifestEarly([
{ source: "early.ts", module: defineAuthz({ permissions: { "early:read": {} } }) },
]);
expect(getAuthzCatalog().permissions.has("early:read")).toBe(true);
});
test("silently skips a missing default export — no warning, no throw", () => {
const originalWarn = console.warn;
let warnCalls = 0;
console.warn = () => {
warnCalls++;
};
try {
expect(() =>
applyAuthzManifestEarly([{ source: "broken.ts", module: undefined }]),
).not.toThrow();
} finally {
console.warn = originalWarn;
}
expect(warnCalls).toBe(0);
expect(getAuthzCatalog().permissions.size).toBe(0);
});
test("still throws on a genuine conflict (fatal either way, just earlier)", () => {
expect(() =>
applyAuthzManifestEarly([
{ source: "a.ts", module: defineAuthz({ permissions: { "post:read": { risk: "low" } } }) },
{ source: "b.ts", module: defineAuthz({ permissions: { "post:read": { risk: "high" } } }) },
]),
).toThrow(/WRN-AUTHZ-CONFLICT/);
});
});