110 lines
3.9 KiB
TypeScript
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 }
|
|
);
|
|
});
|
|
});
|