import { describe, it } from "@suaveplan/testing/runner"; import * as fc from "fast-check"; import { PermissionCache } from "../src/cache/permission-cache/permission-cache.js"; import { compareValues } from "../src/conditions/condition-matcher/condition-matcher.js"; import { buildAbility } from "../src/core/ability/ability.js"; import type { ConditionOperator } from "../src/types/permissions.types.js"; describe("Property: Ability can/cannot consistency", () => { it("cannot is always negation of can for any action/resource", { timeout: 5000, }, () => { fc.assert( fc.property( fc.string({ minLength: 1, maxLength: 16 }), fc.string({ minLength: 1, maxLength: 16 }), fc.array(fc.string({ minLength: 1, maxLength: 16 }), { minLength: 0, maxLength: 5, }), (action, resource, grantedActions) => { const ability = buildAbility((b) => { for (const a of grantedActions) { b.can(a, resource); } }); const canResult = ability.can(action, resource); const cannotResult = ability.cannot(action, resource); return canResult !== cannotResult; } ), { numRuns: 200 } ); }); }); describe("Property: Cache invalidation completeness", () => { it("after invalidate(userId), no stale entry for that user", { timeout: 5000, }, () => { fc.assert( fc.property( fc.string({ minLength: 1, maxLength: 16 }), fc.array(fc.string({ minLength: 1, maxLength: 16 }), { minLength: 1, maxLength: 10, }), (userId, resources) => { const cache = new PermissionCache({ ttl: 99999 }); for (const r of resources) { cache.set(`${userId}:${r}:read`, true); } cache.invalidate(userId); return resources.every( (r) => cache.get(`${userId}:${r}:read`) === undefined ); } ), { numRuns: 100 } ); }); }); describe("Property: compareValues determinism", () => { it("same inputs always produce same result", { timeout: 5000 }, () => { const ops: ConditionOperator[] = [ "eq", "neq", "contains", "startsWith", ]; fc.assert( fc.property( fc.string({ minLength: 0, maxLength: 32 }), fc.string({ minLength: 0, maxLength: 32 }), fc.constantFrom(...ops), (a, b, op) => { const r1 = compareValues(a, b, op); const r2 = compareValues(a, b, op); return r1 === r2; } ), { numRuns: 300 } ); }); }); describe("Property: Role inheritance transitivity via Ability", () => { it("abilities compose additively across rule sets", { timeout: 5000, }, () => { fc.assert( fc.property( fc.array(fc.string({ minLength: 1, maxLength: 8 }), { minLength: 1, maxLength: 5, }), fc.string({ minLength: 1, maxLength: 8 }), (actions, resource) => { const ability = buildAbility((b) => { for (const a of actions) b.can(a, resource); }); // All granted actions must return can=true return actions.every((a) => ability.can(a, resource)); } ), { numRuns: 100 } ); }); });