Files
core/tests/rules/parent-rule.test.js
T
John Dvorak 717ae1031e initial commit: @arbiter/core authorization engine with js-rigor hardening
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/
binary modes, condensed snapshots, value relations) with 39 rigor test
campaigns. Includes fixes for snapshot binary writer/reader format
mismatch (snapshot-of-snapshot corruption), possibility write-boundary
validation, empty-graph snapshot serialization, relation lookup cache
direction collision, config-redefinition cache invalidation, binary
threshold semantics, defeasible compiled routing, and comparator
reason whitelisting.
2026-07-31 13:44:06 -07:00

124 lines
4.8 KiB
JavaScript

import { ParentRule } from '../../src/authorization/rules/ParentRule.js';
import { Arbiter } from '../../src/core/Arbiter.js';
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
describe('ParentRule', () => {
let arbiter;
let parentRule;
beforeEach(() => {
// Minimal mock Arbiter with relationManager and inference
arbiter = {
relationManager: {
getRelationsFromSrc: (id, rel) => {
if (rel === 'parent' && id === 'child1') {
return [{ src: 'child1', dst: 'parentA', rel: 'parent', possibility: 0.8 }];
}
if (rel === 'parent' && id === 'child2') {
return [];
}
return [];
},
getRelationsToDst: (id, rel) => {
if (rel === 'parent' && id === 'child1') {
return [{ src: 'parentA', dst: 'child1', rel: 'parent', possibility: 0.8 }];
}
if (rel === 'parent' && id === 'child2') {
return [];
}
return [];
},
getRelationsByName: (rel) => {
if (rel === 'parent') {
return [
{ src: 'parentA', dst: 'child1', rel: 'parent', possibility: 0.8 },
{ src: 'parentB', dst: 'child2', rel: 'parent', possibility: 0.4 }
];
}
return [];
}
},
keyByNodeId: new Map([
['parentA', 'parentA'],
['parentB', 'parentB'],
['child1', 'child1'],
['child2', 'child2']
]),
resolveKey: (id) => arbiter.keyByNodeId.get(id),
indices: {
getDirectRelation: (srcId, rel, dstId) => {
if (srcId === 'user' && dstId === 'parentA') return { possibility: 0.8 };
if (srcId === 'user' && dstId === 'parentB') return { possibility: 0.6 };
return null;
}
},
_getInferenceEngine: () => ({
params: { minCaseThreshold: 1 },
estimatePolicyElement: (src, rel, dst) => {
if (src === 'parentB' && rel === 'parent' && dst === 'child2') {
return { outcome: 'positive', totalCases: 2, possibility: 0.6 };
}
return { outcome: 'negative', totalCases: 1, possibility: 0.1 };
}
})
};
parentRule = new ParentRule(arbiter);
});
it('returns correct possibility and meta for direct parent', () => {
const rule = { type: 'parent', parentRelation: 'parent' };
const res = parentRule._evaluateRule('user', 'user', 'child1', 'child1', rule, {}, null, {});
assert.strictEqual(res.possibility, 0.8);
assert.ok(res.meta.parentRule);
assert.strictEqual(res.meta.parentRule.type, 'parent_access_checked');
assert.strictEqual(res.meta.parentRule.parentKey, 'parentA');
});
// it('returns correct possibility and meta for inferred parent', () => {
// // Mock inference result
// arbiter.inferencer = {
// inferRelation: () => ({
// possibility: 0.6, // Inferred possibility
// meta: { type: 'inferred', reliability: 0.8 }
// })
// };
// const res = parentRule._evaluateRule('child1', 'child1', 'grandparent', 'grandparent', ruleInferred, {}, null, {});
// assert.ok(res.possibility > 0.5 && res.possibility < 0.7); // Check within inferred range
// assert.deepStrictEqual(res.meta.type, 'inferred');
// });
it('returns 0 possibility if no parent and no inference', () => {
const rule = { type: 'parent', parentRelation: 'parent', allowInference: false };
const res = parentRule._evaluateRule('user', 'user', 'child2', 'child2', rule, {}, null, {});
assert.strictEqual(res.possibility, 0);
assert.ok(res.meta.parentRule);
assert.strictEqual(res.meta.parentRule.type, 'no_parent_relationship_found');
});
it('applies minPossibility threshold', () => {
const rule = { type: 'parent', parentRelation: 'parent' };
const res = parentRule._evaluateRule('user', 'user', 'child1', 'child1', rule, {}, null, { minPossibility: 0.9, fastPath: true });
assert.strictEqual(res.possibility, 0);
assert.ok(res.meta.parentRule.cutoffAppliedPostFusion);
});
it('applies OWA aggregation for multiple parents', () => {
// Add a second direct parent for child1
arbiter.relationManager.getRelationsToDst = (id, rel) => {
if (rel === 'parent' && id === 'child1') {
return [
{ src: 'parentA', dst: 'child1', rel: 'parent', possibility: 0.8 },
{ src: 'parentB', dst: 'child1', rel: 'parent', possibility: 0.6 }
];
}
return [];
};
const rule = { type: 'parent', parentRelation: 'parent', aggregator: 'mean' };
const res = parentRule._evaluateRule('user', 'user', 'child1', 'child1', rule, {}, null, {});
assert.ok(res.possibility > 0.6 && res.possibility < 0.8);
assert.ok(res.meta.parentRule.fusionMethod === 'mean');
assert.strictEqual(res.meta.parentRule.pathsConsidered, 2);
});
});