import { test, expect } from "bun:test"; import { createContext } from "@wrnexus/core"; import { defineRbac, hasRole, authorize, requireRole, requirePermission, any, all, attr, decision, owner, type Policy, type Subject, } from "../src/index.ts"; const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"], viewer: ["post:read"], moderator: ["role:editor", "comment:delete"], // inherits editor }); test("RBAC: roles, wildcards, namespaces, inheritance", () => { expect(rbac.can({ roles: ["viewer"] }, "post:read")).toBe(true); expect(rbac.can({ roles: ["viewer"] }, "post:write")).toBe(false); expect(rbac.can({ roles: ["admin"] }, "anything:goes")).toBe(true); // "*" expect(rbac.can({ roles: ["moderator"] }, "post:write")).toBe(true); // inherited from editor expect(rbac.can({ roles: ["moderator"] }, "comment:delete")).toBe(true); expect(rbac.can(undefined, "post:read")).toBe(false); expect(defineRbac({ ed: ["post:*"] }).can({ roles: ["ed"] }, "post:write")).toBe(true); // ns wildcard }); test("hasRole", () => { expect(hasRole({ roles: ["a", "b"] }, "a")).toBe(true); expect(hasRole({ roles: ["a"] }, "a", "b")).toBe(false); }); interface User extends Record { id?: string; roles?: string[]; tenant?: string; } interface Post { authorId: string; } test("PBAC/ABAC: policies compose (any/all) + attribute match", async () => { const ownsPost: Policy = (u, post) => u.id === post?.authorId; const isAdmin: Policy = (u) => (u.roles ?? []).includes("admin"); const canEdit = any(ownsPost, isAdmin); expect(await canEdit({ id: "u1" }, { authorId: "u1" })).toBe(true); // owner expect(await canEdit({ id: "u2", roles: ["admin"] }, { authorId: "u1" })).toBe(true); // admin expect(await canEdit({ id: "u2" }, { authorId: "u1" })).toBe(false); const sameTenant = all(isAdmin, attr("tenant", "acme")); expect(await sameTenant({ roles: ["admin"], tenant: "acme" })).toBe(true); expect(await sameTenant({ roles: ["admin"], tenant: "other" })).toBe(false); }); function ctx(user?: unknown) { const url = new URL("http://x/admin"); const c = createContext(new Request(url), url); c.user = user; return c; } test("guards: authorize / requireRole / requirePermission", async () => { const ok = () => new Response("ok"); expect((await requireRole("admin")(ctx({ roles: ["admin"] }), ok)).status).toBe(200); expect((await requireRole("admin")(ctx({ roles: ["viewer"] }), ok)).status).toBe(403); expect((await requirePermission(rbac, "post:write")(ctx({ roles: ["editor"] }), ok)).status).toBe( 200, ); expect((await requirePermission(rbac, "post:write")(ctx({ roles: ["viewer"] }), ok)).status).toBe( 403, ); expect( (await authorize((c) => (c.user as User)?.id === "u1")(ctx({ id: "u1" }), ok)).status, ).toBe(200); }); test("explainable decisions only include denial reasons when denied", async () => { const policy = decision("owner", (subject: User) => subject.id === "u1"); expect(await policy({ id: "u1" })).toEqual({ allowed: true, reason: undefined, policy: "owner", }); expect(await policy({ id: "u2" })).toEqual({ allowed: false, reason: "Policy denied access", policy: "owner", }); }); test("owner() denies rather than matching two absent ids", async () => { // A subject with no id, checked against a resource with no ownership key, // must never be treated as the owner: undefined !== undefined here means // "we don't know", not "match". const noId: Subject = {}; const resourceWithKey = { userId: "u1" }; const resourceWithoutKey: Record = { title: "t" }; const realSubject: Subject = { id: "u1" }; // Subject has no id at all. expect((await owner()(noId, resourceWithKey)).allowed).toBe(false); // Resource lacks the ownership key. expect((await owner()(realSubject, resourceWithoutKey)).allowed).toBe(false); // Both sides absent — the exact bug scenario (Object.is(undefined, undefined) === true). expect((await owner()(noId, resourceWithoutKey)).allowed).toBe(false); // Resource entirely absent. expect((await owner()(realSubject, undefined)).allowed).toBe(false); // A genuine match still allows. expect((await owner()(realSubject, resourceWithKey)).allowed).toBe(true); // Custom keys still work and still deny on absence. interface CustomResource extends Record { ownerId?: string; } const customOwns = owner("id", "ownerId"); expect((await customOwns({ id: "u1" }, { ownerId: "u1" })).allowed).toBe(true); expect((await customOwns({ id: "u1" }, {})).allowed).toBe(false); });