Files
WRNexusJS/packages/authz/test/catalog.test.ts
T
ClintchizandClaude Opus 5 41b6e2ed2b fix(authz): freeze catalog values after boot; correct compile-time-check claims
frozenMap only blocked the Map's own mutators, so
catalog.roles.get("editor").push("*") escalated a role to a full wildcard
past an error string claiming the catalog is frozen after boot; the same
applied to permission/attribute metadata objects and binding arrays.
mergeCatalogs now stores frozen copies of each, so the original declaring
module's objects are never mutated either.

Also corrects two docstrings (codegen.ts, the design doc) that claimed
`wrnexus authz generate`'s output makes a permission typo a type error —
can(), guardPermission(), and decideFor() all take a bare string and nothing
consumes the generated union automatically. Documents what it actually is:
a Permission/Role union to type your own helpers/constants against. Also
adds a README note on the subject.id contract (must be a non-empty string;
owner() compares with Object.is).

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

136 lines
5.1 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { defineAuthz } from "../src/registry.ts";
import { emptyCatalog, mergeCatalogs } from "../src/catalog.ts";
describe("mergeCatalogs", () => {
test("merges disjoint modules", () => {
const catalog = mergeCatalogs([
{ source: "a.ts", module: defineAuthz({ permissions: { "post:read": {} } }) },
{ source: "b.ts", module: defineAuthz({ permissions: { "user:read": {} } }) },
]);
expect([...catalog.permissions.keys()].sort()).toEqual(["post:read", "user:read"]);
});
test("re-declaring a permission with deep-equal metadata is a no-op", () => {
const meta = { title: "View posts", risk: "low" as const };
const catalog = mergeCatalogs([
{ source: "a.ts", module: defineAuthz({ permissions: { "post:read": meta } }) },
{ source: "b.ts", module: defineAuthz({ permissions: { "post:read": { ...meta } } }) },
]);
expect(catalog.permissions.size).toBe(1);
});
test("conflicting metadata is a boot error naming both files", () => {
expect(() =>
mergeCatalogs([
{ source: "a.ts", module: defineAuthz({ permissions: { "post:read": { risk: "low" } } }) },
{ source: "b.ts", module: defineAuthz({ permissions: { "post:read": { risk: "high" } } }) },
]),
).toThrow(/a\.ts.*b\.ts|b\.ts.*a\.ts/s);
});
test("conflicting role definitions are a boot error", () => {
expect(() =>
mergeCatalogs([
{ source: "a.ts", module: defineAuthz({ roles: { editor: ["post:read"] } }) },
{ source: "b.ts", module: defineAuthz({ roles: { editor: ["post:write"] } }) },
]),
).toThrow(/editor/);
});
test("bindings for the same permission union across modules", () => {
const p1 = defineAuthz({
permissions: { "post:write": {} },
policies: { ownsPost: async () => ({ allowed: true }) },
bindings: { "post:write": ["ownsPost"] },
});
const p2 = defineAuthz({
policies: { notLocked: async () => ({ allowed: true }) },
bindings: { "post:write": ["notLocked"] },
});
const catalog = mergeCatalogs([
{ source: "a.ts", module: p1 },
{ source: "b.ts", module: p2 },
]);
expect([...catalog.bindings.get("post:write")!].sort()).toEqual(["notLocked", "ownsPost"]);
});
test("a binding referencing a policy no module declares is a boot error", () => {
expect(() =>
mergeCatalogs([
{
source: "a.ts",
module: { permissions: { "post:write": {} }, bindings: { "post:write": ["ghost"] } },
},
]),
).toThrow(/ghost/);
});
test("the merged catalog is frozen", () => {
const catalog = mergeCatalogs([]);
expect(() => (catalog.permissions as Map<string, never>).set("x:y", {} as never)).toThrow();
});
test("a role's granted-entries array cannot be mutated to escalate it after boot", () => {
// frozenMap only blocks the Map's own mutators (set/delete/clear) — the
// VALUES it holds are a separate concern. Without freezing them too,
// catalog.roles.get("editor").push("*") would succeed and silently
// escalate "editor" to a full wildcard past an error string that claims
// the catalog is frozen after boot.
const catalog = mergeCatalogs([
{ source: "a.ts", module: defineAuthz({ roles: { editor: ["post:write"] } }) },
]);
const editorRole = catalog.roles.get("editor")!;
expect(() => (editorRole as string[]).push("*")).toThrow();
expect(catalog.roles.get("editor")).toEqual(["post:write"]);
});
test("a permission's metadata object cannot be mutated after boot", () => {
const catalog = mergeCatalogs([
{
source: "a.ts",
module: defineAuthz({ permissions: { "post:delete": { risk: "low" } } }),
},
]);
const meta = catalog.permissions.get("post:delete")!;
expect(() => {
(meta as { risk?: string }).risk = "high";
}).toThrow();
expect(catalog.permissions.get("post:delete")!.risk).toBe("low");
});
test("an attribute's metadata object cannot be mutated after boot", () => {
const catalog = mergeCatalogs([
{
source: "a.ts",
module: defineAuthz({ attributes: { department: { description: "org unit" } } }),
},
]);
const meta = catalog.attributes.get("department")!;
expect(() => {
(meta as { description?: string }).description = "tampered";
}).toThrow();
expect(catalog.attributes.get("department")!.description).toBe("org unit");
});
test("a binding's policy-name array cannot be mutated after boot", () => {
const catalog = mergeCatalogs([
{
source: "a.ts",
module: defineAuthz({
permissions: { "post:write": {} },
policies: { ownsPost: async () => ({ allowed: true }) },
bindings: { "post:write": ["ownsPost"] },
}),
},
]);
const names = catalog.bindings.get("post:write")!;
expect(() => (names as string[]).push("injectedPolicy")).toThrow();
expect(catalog.bindings.get("post:write")).toEqual(["ownsPost"]);
});
test("emptyCatalog has no entries", () => {
expect(emptyCatalog().permissions.size).toBe(0);
});
});