Files

309 lines
16 KiB
JavaScript
Raw Permalink Normal View History

import { LogicalOperators } from '../../src/authorization/rules/LogicalOperators.js';
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
// Minimal mock ruleEvaluator for child rule evaluation
function makeMockRuleEvaluator(resultsByRule) {
return {
evaluateRule: (userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) => {
if (typeof rule === 'string') return resultsByRule[rule];
if (rule && rule.mockKey) return resultsByRule[rule.mockKey];
if (rule && rule.type && resultsByRule[rule.type]) return resultsByRule[rule.type];
return resultsByRule.default || { possibility: 0, reliability: 1.0, meta: { reason: 'no_rule' }, collectedValues: [] };
}
};
}
describe('LogicalOperators', () => {
let logicalOps;
let arbiter;
let ruleEvaluator;
beforeEach(() => {
arbiter = {};
});
it('evaluates union (max) correctly', () => {
ruleEvaluator = makeMockRuleEvaluator({
a: { possibility: 0.7, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
b: { possibility: 0.4, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] },
c: { possibility: 0.9, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'c' } }, collectedValues: [3] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }, { mockKey: 'c' }], aggregator: 'max' } };
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
assert.strictEqual(res.possibility, 0.9);
assert.ok(res.meta.operation === 'union');
assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
});
it('evaluates intersection (min) correctly', () => {
ruleEvaluator = makeMockRuleEvaluator({
a: { possibility: 0.7, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
b: { possibility: 0.4, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] },
c: { possibility: 0.9, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'c' } }, collectedValues: [3] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = { type: 'logical', intersection: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }, { mockKey: 'c' }], aggregator: 'min' } };
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
assert.strictEqual(res.possibility, 0.4);
assert.ok(res.meta.operation === 'intersection');
assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
});
it('evaluates union (mean/average) correctly', () => {
ruleEvaluator = makeMockRuleEvaluator({
a: { possibility: 0.6, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
b: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], aggregator: 'mean' } };
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
assert.ok(Math.abs(res.possibility - 0.7) < 1e-8);
assert.ok(res.meta.operation === 'union');
});
it('evaluates intersection (pessimistic) correctly', () => {
ruleEvaluator = makeMockRuleEvaluator({
a: { possibility: 0.9, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
b: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = { type: 'logical', intersection: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], aggregator: 'pessimistic' } };
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
assert.ok(res.possibility < 0.9 && res.possibility > 0.6);
assert.ok(res.meta.operation === 'intersection');
});
it('evaluates exclusion (A AND NOT B) correctly', () => {
ruleEvaluator = makeMockRuleEvaluator({
a: { possibility: 0.8, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
b: { possibility: 0.5, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = { type: 'logical', exclusion: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
assert.ok(Math.abs(res.possibility - (0.8 * (1 - 0.5))) < 1e-8);
assert.ok(res.meta.operation === 'exclusion');
});
it('evaluates custom OWA weights for union', () => {
ruleEvaluator = makeMockRuleEvaluator({
a: { possibility: 0.2, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
b: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], owaWeights: [0.7, 0.3] } };
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
assert.ok(Math.abs(res.possibility - (0.8 * 0.7 + 0.2 * 0.3)) < 1e-8);
assert.ok(res.meta.operation === 'union');
});
it('evaluates binary defeasible logic (never overrides everything)', () => {
// never wins over everything
ruleEvaluator = makeMockRuleEvaluator({
never: { possibility: 1, reliability: 0.95, meta: { ruleType: 'never' }, collectedValues: [1] },
strict: { possibility: 1, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [2] },
defeater: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = {
type: 'logical',
never: { union: [{ mockKey: 'never' }] },
always: { direct: { mockKey: 'strict' } },
unless: { union: [{ mockKey: 'defeater' }] },
when: { intersection: [{ mockKey: 'defeasible' }] },
mode: 'binary'
};
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
assert.strictEqual(res.possibility, 0);
assert.ok(res.meta.never);
assert.ok(res.meta.reason === 'never_rule_triggered');
assert.ok(res.meta.mode === 'binary');
});
it('evaluates binary defeasible logic (strict, defeater, defeasible)', () => {
// strict wins
ruleEvaluator = makeMockRuleEvaluator({
strict: { possibility: 1, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
defeater: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [2] },
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [3] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = {
type: 'logical',
always: { direct: { mockKey: 'strict' } },
unless: { union: [{ mockKey: 'defeater' }] },
when: { intersection: [{ mockKey: 'defeasible' }] },
mode: 'binary'
};
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
assert.strictEqual(res.possibility, 1);
assert.ok(res.meta.strict);
assert.ok(res.meta.mode === 'binary');
});
it('evaluates binary defeasible logic (requirements take precedence over defeaters)', () => {
// requirements not met should win over defeaters
ruleEvaluator = makeMockRuleEvaluator({
strict: { possibility: 0, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
requires: { possibility: 0.1, reliability: 0.9, meta: { ruleType: 'requires' }, collectedValues: [2] },
defeater: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = {
type: 'logical',
always: { direct: { mockKey: 'strict' } },
requires: { union: [{ mockKey: 'requires' }] },
unless: { union: [{ mockKey: 'defeater' }] },
when: { intersection: [{ mockKey: 'defeasible' }] },
mode: 'binary'
};
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
assert.strictEqual(res.possibility, 0);
assert.ok(res.meta.reason === 'requirements_not_met');
assert.ok(res.meta.mode === 'binary');
});
it('evaluates binary defeasible logic (defeater wins)', () => {
// defeater wins when requirements are met
ruleEvaluator = makeMockRuleEvaluator({
strict: { possibility: 0, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
requires: { possibility: 0.9, reliability: 0.9, meta: { ruleType: 'requires' }, collectedValues: [2] },
defeater: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = {
type: 'logical',
always: { direct: { mockKey: 'strict' } },
requires: { union: [{ mockKey: 'requires' }] },
unless: { union: [{ mockKey: 'defeater' }] },
when: { intersection: [{ mockKey: 'defeasible' }] },
mode: 'binary'
};
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
assert.strictEqual(res.possibility, 0);
assert.ok(res.meta.reason === 'defeated_by_unless');
assert.ok(res.meta.mode === 'binary');
});
it('evaluates threshold mode with never early exit', () => {
ruleEvaluator = makeMockRuleEvaluator({
never: { possibility: 0.9, reliability: 0.95, meta: { ruleType: 'never' }, collectedValues: [1] },
strict: { possibility: 0.85, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [2] },
defeater: { possibility: 0.2, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = {
type: 'logical',
never: { union: [{ mockKey: 'never' }] },
always: { direct: { mockKey: 'strict' } },
unless: { union: [{ mockKey: 'defeater' }] },
when: { intersection: [{ mockKey: 'defeasible' }] },
mode: 'threshold'
};
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
assert.strictEqual(res.possibility, 0);
assert.ok(res.meta.earlyExit === 'never_rule');
assert.ok(res.meta.never);
});
it('evaluates threshold mode with early exit', () => {
ruleEvaluator = makeMockRuleEvaluator({
strict: { possibility: 0.85, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
defeater: { possibility: 0.2, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [2] },
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [3] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = {
type: 'logical',
always: { direct: { mockKey: 'strict' } },
unless: { union: [{ mockKey: 'defeater' }] },
when: { intersection: [{ mockKey: 'defeasible' }] },
mode: 'threshold'
};
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
assert.strictEqual(res.possibility, 0.85);
assert.ok(res.meta.earlyExit === 'strict_rule');
});
it('handles empty union/intersection/exclusion', () => {
ruleEvaluator = makeMockRuleEvaluator({});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const unionRule = { type: 'logical', union: { rules: [] } };
const intersectionRule = { type: 'logical', intersection: { rules: [] } };
const exclusionRule = { type: 'logical', exclusion: { rules: [] } };
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', unionRule, {}, null, {}).possibility, 0);
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', intersectionRule, {}, null, {}).possibility, 0);
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', exclusionRule, {}, null, {}).possibility, 0);
});
it('handles all-zero and all-one cases', () => {
ruleEvaluator = makeMockRuleEvaluator({
a: { possibility: 0, reliability: 1.0, meta: {}, collectedValues: [] },
b: { possibility: 0, reliability: 1.0, meta: {}, collectedValues: [] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}).possibility, 0);
ruleEvaluator = makeMockRuleEvaluator({
a: { possibility: 1, reliability: 1.0, meta: {}, collectedValues: [] },
b: { possibility: 1, reliability: 1.0, meta: {}, collectedValues: [] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}).possibility, 1);
});
it('propagates meta and collectedValues', () => {
ruleEvaluator = makeMockRuleEvaluator({
a: { possibility: 0.5, reliability: 0.9, meta: { foo: 'bar' }, collectedValues: [1, 2] },
b: { possibility: 0.8, reliability: 0.8, meta: { baz: 'qux' }, collectedValues: [3] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
assert.ok(res.meta);
assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
});
it('handles missing rule types gracefully', () => {
ruleEvaluator = makeMockRuleEvaluator({});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = { type: 'logical', union: { rules: [{ mockKey: 'missing' }] } };
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
assert.strictEqual(res.possibility, 0);
assert.ok(res.meta && res.meta.reason === 'missing_rule');
});
it('supports fastPath/early exit logic in union', () => {
let callCount = 0;
ruleEvaluator = {
evaluateRule: (userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) => {
callCount++;
if (callCount === 1) return { possibility: 0.95, reliability: 0.9, meta: {}, collectedValues: [1] };
return { possibility: 0.2, reliability: 0.8, meta: {}, collectedValues: [2] };
}
};
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] }, fastPath: true, minPossibility: 0.9 };
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { fastPath: true, minPossibility: 0.9, collectValues: true });
assert.strictEqual(res.possibility, 0.95);
assert.deepStrictEqual(res.collectedValues, [1]);
});
it('supports priority/aggregator effects', () => {
ruleEvaluator = makeMockRuleEvaluator({
a: { possibility: 0.7, reliability: 0.9, meta: { rule: { priority: 10 } }, collectedValues: [1] },
b: { possibility: 0.6, reliability: 0.8, meta: { rule: { priority: 5 } }, collectedValues: [2] }
});
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a', priority: 10 }, { mockKey: 'b', priority: 5 }], aggregator: 'priority' } };
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
assert.ok(res.possibility > 0.6 && res.possibility < 0.8);
});
});