import { describe, expect, it } from "@suaveplan/testing/runner"; import { MemoryAdapter } from "../src/adapters/memory/memory.js"; import { PermissionCache } from "../src/cache/permission-cache/permission-cache.js"; import { type Ability, buildAbility } from "../src/core/ability/ability.js"; import { PermissionManager } from "../src/core/permission-manager/permission-manager.js"; import { PolicyEngine } from "../src/core/policy-engine/policy-engine.js"; import { RoleManager } from "../src/core/role-manager/role-manager.js"; import { CircularInheritanceError } from "../src/errors/permission-error.js"; import { createAdminRole, createViewerRole, } from "../src/presets/role-presets/role-presets.js"; import type { Permission } from "../src/types/permissions.types.js"; function makePerm(id: string, resource = "posts", action = "read"): Permission { return { id, resource, action }; } describe("Full permissions flow", () => { it("role inheritance transitivity", { timeout: 5000 }, async () => { const adapter = new MemoryAdapter(); const roleManager = new RoleManager(adapter); const _manager = new PermissionManager(adapter); const cPerm = makePerm("p-c", "comments", "read"); const bPerm = makePerm("p-b", "posts", "write"); const aPerm = makePerm("p-a", "posts", "read"); await adapter.createRole({ id: "c", name: "C", permissions: [cPerm] }); await adapter.createRole({ id: "b", name: "B", permissions: [bPerm], inherits: ["c"], }); await adapter.createRole({ id: "a", name: "A", permissions: [aPerm], inherits: ["b"], }); const perms = await roleManager.getEffectivePermissions("a"); const permIds = perms.map((p) => p.id); expect(permIds).toContain("p-a"); expect(permIds).toContain("p-b"); expect(permIds).toContain("p-c"); }); it("policy engine with cache", { timeout: 5000 }, async () => { const adapter = new MemoryAdapter(); await adapter.createRole({ id: "r1", name: "R", permissions: [makePerm("p1")], }); await adapter.assignRole({ id: "ur1", userId: "u1", roleId: "r1", assignedAt: new Date(), }); const manager = new PermissionManager(adapter); const cache = new PermissionCache({ ttl: 60_000 }); const engine = new PolicyEngine(manager, { cache }); const r1 = await engine.evaluate("u1", "posts", "read"); const r2 = await engine.evaluate("u1", "posts", "read"); expect(r1.allowed).toBe(true); expect(r2.matchedRule).toBe("cached-allow"); // Invalidate cache and re-check cache.invalidate("u1"); const r3 = await engine.evaluate("u1", "posts", "read"); expect(r3.allowed).toBe(true); expect(r3.matchedRule).toBe("posts:read"); }); it("circular inheritance detection", { timeout: 5000 }, async () => { const adapter = new MemoryAdapter(); const roleManager = new RoleManager(adapter); await adapter.createRole({ id: "x", name: "X", permissions: [], inherits: ["y"], }); await adapter.createRole({ id: "y", name: "Y", permissions: [], inherits: ["x"], }); await expect( roleManager.getRoleInheritanceChain("x") ).rejects.toBeInstanceOf(CircularInheritanceError); }); it("preset roles work with PermissionManager", { timeout: 5000, }, async () => { const adapter = new MemoryAdapter(); await adapter.createRole(createAdminRole()); await adapter.assignRole({ id: "ur-admin", userId: "admin-user", roleId: "admin", assignedAt: new Date(), }); await adapter.createRole(createViewerRole()); await adapter.assignRole({ id: "ur-viewer", userId: "viewer-user", roleId: "viewer", assignedAt: new Date(), }); const manager = new PermissionManager(adapter); expect(await manager.hasPermission("admin-user", "any", "any")).toBe( true ); expect( await manager.hasPermission("viewer-user", "posts", "read") ).toBe(true); expect( await manager.hasPermission("viewer-user", "posts", "delete") ).toBe(false); }); }); describe("Ability system end-to-end", () => { it("can/cannot consistency", { timeout: 5000 }, () => { const ability: Ability = buildAbility((b) => { b.can("read", "posts"); b.cannot("delete", "posts"); }); expect(ability.can("read", "posts")).toBe(true); expect(ability.cannot("read", "posts")).toBe(false); expect(ability.can("delete", "posts")).toBe(false); expect(ability.cannot("delete", "posts")).toBe(true); }); });