import { beforeEach, describe, expect, test } from "bun:test"; import type { PermissionStore } from "../src/store.ts"; /** * Every PermissionStore adapter must pass this suite, so the memory and db * implementations cannot drift apart. */ export function runStoreConformance(name: string, makeStore: () => Promise): void { describe(`PermissionStore conformance: ${name}`, () => { let store: PermissionStore; beforeEach(async () => { store = await makeStore(); }); test("an unknown subject has empty assignments", async () => { expect(await store.assignmentsFor("nobody")).toEqual({ roles: [], grants: [], denies: [], }); }); test("assignRole then assignmentsFor round-trips", async () => { await store.assignRole("u1", "editor"); expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]); }); test("assignRole is idempotent", async () => { await store.assignRole("u1", "editor"); await store.assignRole("u1", "editor"); expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]); }); test("revokeRole removes only that role", async () => { await store.assignRole("u1", "editor"); await store.assignRole("u1", "admin"); await store.revokeRole("u1", "editor"); expect((await store.assignmentsFor("u1")).roles).toEqual(["admin"]); }); test("revoking a role that was never assigned is a no-op", async () => { await store.revokeRole("u1", "ghost"); expect((await store.assignmentsFor("u1")).roles).toEqual([]); }); test("scoped assignments do not leak across tenants", async () => { await store.assignRole("u1", "editor", { tenantId: "t1" }); expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["editor"]); expect((await store.assignmentsFor("u1", { tenantId: "t2" })).roles).toEqual([]); }); test("a global assignment is visible inside every tenant", async () => { await store.assignRole("u1", "superadmin"); expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["superadmin"]); }); test("global and scoped roles union within a tenant", async () => { await store.assignRole("u1", "viewer"); await store.assignRole("u1", "editor", { tenantId: "t1" }); expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles.sort()).toEqual([ "editor", "viewer", ]); }); test("grant with allow and deny land in the right buckets", async () => { await store.grant("u1", "post:write", "allow"); await store.grant("u1", "post:delete", "deny"); const assignments = await store.assignmentsFor("u1"); expect(assignments.grants).toEqual(["post:write"]); expect(assignments.denies).toEqual(["post:delete"]); }); test("re-granting the same permission replaces its effect", async () => { await store.grant("u1", "post:write", "allow"); await store.grant("u1", "post:write", "deny"); const assignments = await store.assignmentsFor("u1"); expect(assignments.grants).toEqual([]); expect(assignments.denies).toEqual(["post:write"]); }); test("revokeGrant removes the permission entirely", async () => { await store.grant("u1", "post:write", "allow"); await store.revokeGrant("u1", "post:write"); expect((await store.assignmentsFor("u1")).grants).toEqual([]); }); test("a tenant-scoped grant does not leak into another tenant", async () => { await store.grant("u1", "post:write", "allow", { tenantId: "t1" }); expect((await store.assignmentsFor("u1", { tenantId: "t1" })).grants).toEqual(["post:write"]); expect((await store.assignmentsFor("u1", { tenantId: "t2" })).grants).toEqual([]); }); test("a tenant-scoped deny does not leak into another tenant", async () => { await store.grant("u1", "post:delete", "deny", { tenantId: "t1" }); expect((await store.assignmentsFor("u1", { tenantId: "t1" })).denies).toEqual([ "post:delete", ]); expect((await store.assignmentsFor("u1", { tenantId: "t2" })).denies).toEqual([]); }); test("a global grant is visible inside every tenant", async () => { await store.grant("u1", "post:publish", "allow"); expect((await store.assignmentsFor("u1", { tenantId: "t1" })).grants).toEqual([ "post:publish", ]); }); test("revokeGrant is scope-isolated: revoking a tenant-scoped grant leaves the global grant intact", async () => { await store.grant("u1", "post:write", "allow"); await store.grant("u1", "post:write", "allow", { tenantId: "t1" }); await store.revokeGrant("u1", "post:write", { tenantId: "t1" }); expect((await store.assignmentsFor("u1")).grants).toEqual(["post:write"]); expect((await store.assignmentsFor("u1", { tenantId: "t1" })).grants).toEqual(["post:write"]); }); test("revokeRole is scope-isolated: revoking a tenant-scoped role leaves the global role intact", async () => { await store.assignRole("u1", "editor"); await store.assignRole("u1", "editor", { tenantId: "t1" }); await store.revokeRole("u1", "editor", { tenantId: "t1" }); expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]); expect((await store.assignmentsFor("u1", { tenantId: "t1" })).roles).toEqual(["editor"]); }); test("listSubjects returns everyone with an assignment in scope", async () => { await store.assignRole("u1", "editor", { tenantId: "t1" }); await store.assignRole("u2", "editor", { tenantId: "t1" }); await store.assignRole("u3", "editor", { tenantId: "t2" }); expect((await store.listSubjects({ tenantId: "t1" })).sort()).toEqual(["u1", "u2"]); }); test("an explicitly empty tenantId is refused, not treated as global", async () => { await store.assignRole("g1", "viewer"); // Otherwise a caller who controls the tenant id reaches global scope. await expect(store.assignmentsFor("g1", { tenantId: "" })).rejects.toThrow(/tenantId/); await expect(store.assignRole("g1", "admin", { tenantId: "" })).rejects.toThrow(/tenantId/); }); test("a non-string tenantId is refused", async () => { // Same class as the empty-string case: the caller controls this value. for (const bad of [null, 0, false, {}]) { await expect(store.assignmentsFor("u1", { tenantId: bad as never })).rejects.toThrow( /tenantId/, ); } }); test("concurrent identical assignRole calls all resolve", async () => { // Check-then-act loses this race; the UNIQUE constraint then rejects // every loser even though the desired end state was already reached. await Promise.all(Array.from({ length: 20 }, () => store.assignRole("u1", "editor"))); expect((await store.assignmentsFor("u1")).roles).toEqual(["editor"]); }); test("concurrent grants on distinct keys all resolve", async () => { await Promise.all([ store.grant("u1", "post:read", "allow"), store.grant("u1", "post:write", "allow"), store.grant("u1", "post:delete", "deny"), ]); const assignments = await store.assignmentsFor("u1"); expect(assignments.grants.sort()).toEqual(["post:read", "post:write"]); expect(assignments.denies).toEqual(["post:delete"]); }); test("a rejected write leaves unrelated state intact", async () => { await store.assignRole("victim", "admin"); await store.grant("victim", "post:read", "allow"); // An invalid effect must be refused without disturbing anything else. await expect(store.grant("victim", "post:write", "bogus" as never)).rejects.toThrow(); const assignments = await store.assignmentsFor("victim"); expect(assignments.roles).toEqual(["admin"]); expect(assignments.grants).toEqual(["post:read"]); }); // NOTE: the shared-connection rollback hazard - where one method's open // transaction sweeps in a concurrent bare write from another method and // discards it, so a revoke resolves successfully while the role survives - // is prevented STRUCTURALLY, by the store using no transactions at all. // It is deliberately not covered here: reproducing it needs the bare write // to land inside the open transaction, which a single-process Promise.all // does not reliably arrange, so any such test would pass against the // defective implementation and give false assurance. test("listSubjects with no scope returns global assignees only", async () => { await store.assignRole("g1", "viewer"); await store.assignRole("s1", "editor", { tenantId: "t1" }); expect(await store.listSubjects()).toEqual(["g1"]); }); test("listSubjects credits grant-only subjects", async () => { await store.grant("g1", "post:write", "allow", { tenantId: "t1" }); expect((await store.listSubjects({ tenantId: "t1" })).sort()).toEqual(["g1"]); }); }); }