Files
2026-05-27 23:47:53 +01:00

110 lines
3.9 KiB
TypeScript

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 }
);
});
});