import { describe, expect, test } from "bun:test"; import type { Context } from "@wrnexus/core"; import { createDb } from "@wrnexus/db"; import { sqlite } from "@wrnexus/db/sqlite"; import { dbPermissionStore, ensureAuthzTables } from "../src/db.ts"; import { authzMiddleware, can, cachedPermissionStore, defineAuthz, guardPermission, memoryAuditSink, mergeCatalogs, } from "../src/index.ts"; // Exercises the full composition end to end: db-backed store -> cache // decorator -> merged catalog -> per-request middleware -> can()/guardPermission() // -> audit sink. Each piece already has unit coverage elsewhere; this file is // only about the seams between them. const catalog = mergeCatalogs([ { source: "showcase.ts", module: defineAuthz({ permissions: { "post:read": { public: true }, "post:write": {}, "post:delete": { risk: "high" }, }, roles: { editor: ["post:write"], admin: ["role:editor", "post:delete"] }, policies: { ownsPost: async (s: { id?: string }, r?: { authorId?: string }) => r?.authorId === s?.id ? { allowed: true } : { allowed: false, reason: "not owner" }, }, bindings: { "post:delete": ["ownsPost"] }, }), }, ]); function makeCtx(user: unknown, tenantId?: string): Context { return { user, tenant: tenantId ? { id: tenantId } : undefined, locals: {}, url: new URL("http://localhost/"), req: new Request("http://localhost/"), } as unknown as Context; } describe("end-to-end authorization", () => { test("db store, cache, catalog, middleware, and audit compose", async () => { const db = createDb(sqlite(":memory:")); await ensureAuthzTables(db, "sqlite"); const store = cachedPermissionStore(dbPermissionStore(db), { ttlMs: 1_000 }); const audit = memoryAuditSink(); await store.assignRole("alice", "admin", { tenantId: "acme" }); const alice = makeCtx({ id: "alice" }, "acme"); await authzMiddleware({ catalog, store, audit, strict: true })( alice, async () => new Response("ok"), ); expect(await can(alice, "post:write")).toBe(true); expect(await can(alice, "post:delete", { id: 1, authorId: "alice" })).toBe(true); expect(await can(alice, "post:delete", { id: 2, authorId: "bob" })).toBe(false); // Wrong tenant: the admin role was scoped to acme. const elsewhere = makeCtx({ id: "alice" }, "other"); await authzMiddleware({ catalog, store, strict: true })( elsewhere, async () => new Response("ok"), ); expect(await can(elsewhere, "post:write")).toBe(false); // Anonymous can still read, because post:read is public. const guest = makeCtx(null); await authzMiddleware({ catalog, store, strict: true })(guest, async () => new Response("ok")); expect(await can(guest, "post:read")).toBe(true); expect(await can(guest, "post:write")).toBe(false); // Only denials were audited, and only alice's requests used the resolver // that was wired to this audit sink. expect(audit.events.length).toBeGreaterThan(0); expect(audit.events.every((event) => !event.allowed)).toBe(true); }); test("revoking a role takes effect immediately through the cache", async () => { const db = createDb(sqlite(":memory:")); await ensureAuthzTables(db, "sqlite"); const store = cachedPermissionStore(dbPermissionStore(db), { ttlMs: 60_000 }); await store.assignRole("bob", "editor"); const before = makeCtx({ id: "bob" }); await authzMiddleware({ catalog, store, strict: true })(before, async () => new Response("ok")); expect(await can(before, "post:write")).toBe(true); await store.revokeRole("bob", "editor"); const after = makeCtx({ id: "bob" }); await authzMiddleware({ catalog, store, strict: true })(after, async () => new Response("ok")); expect(await can(after, "post:write")).toBe(false); }); test("guardPermission returns an opaque 403", async () => { const db = createDb(sqlite(":memory:")); await ensureAuthzTables(db, "sqlite"); const ctx = makeCtx({ id: "carol" }); await authzMiddleware({ catalog, store: dbPermissionStore(db), strict: true })( ctx, async () => new Response("ok"), ); const res = await guardPermission("post:write")(ctx, async () => new Response("passed")); expect(res.status).toBe(403); expect(await res.json()).toEqual({ ok: false, error: "Forbidden" }); }); test("a public permission still runs its bound policy, including for an anonymous caller", async () => { const publicPolicyCatalog = mergeCatalogs([ { source: "public-policy.ts", module: defineAuthz({ permissions: { "post:preview": { public: true } }, policies: { notBanned: async (_s: { id?: string } | null | undefined, r?: { banned?: boolean }) => r?.banned ? { allowed: false, reason: "resource banned" } : { allowed: true }, }, bindings: { "post:preview": ["notBanned"] }, }), }, ]); const db = createDb(sqlite(":memory:")); await ensureAuthzTables(db, "sqlite"); const store = dbPermissionStore(db); const guest = makeCtx(null); await authzMiddleware({ catalog: publicPolicyCatalog, store, strict: true })( guest, async () => new Response("ok"), ); expect(await can(guest, "post:preview", { banned: false })).toBe(true); expect(await can(guest, "post:preview", { banned: true })).toBe(false); }); });