Files
WRNexusJS/packages/authz/test/store-conformance.ts
T
ClintchizandClaude Opus 5 1cc0b97a72 fix(authz): replace vacuous concurrency test, validate effect in memory store
The fix-round-1 test "a concurrent write is not lost to another
method's failure" was vacuous: a single-process Promise.all cannot
reliably land a bare write inside another method's open transaction,
so it passed against both the fixed and the (previously) defective
grant() implementation. The shared-connection rollback hazard it was
meant to catch is real (confirmed separately by forcing the
transaction open before the write), but this specific test could
never reach that state and gave false assurance either way.

Replaced it with "a rejected write leaves unrelated state intact",
which asserts a grant() call with an invalid effect is refused without
disturbing the subject's existing roles/grants, plus a NOTE
documenting that the rollback hazard is now prevented structurally (no
transactions) rather than by a dedicated concurrency test.

memoryPermissionStore.grant() had no effect validation, so it failed
the new test; added a guard mirroring the db adapter's CHECK
constraint so both adapters agree on rejecting anything other than
"allow"/"deny".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 20:52:18 +05:30

188 lines
8.6 KiB
TypeScript

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<PermissionStore>): 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("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"]);
});
});
}