161 lines
5.7 KiB
JavaScript
161 lines
5.7 KiB
JavaScript
|
|
import { test, describe, it } from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { setupChainRuleTestGraph } from './helpers.js';
|
||
|
|
|
||
|
|
describe('ChainRule Comprehensive Tests', () => {
|
||
|
|
|
||
|
|
describe('Fast Path and Performance Optimization', () => {
|
||
|
|
it('supports early exit with minAllowPossibility threshold', () => {
|
||
|
|
const arbiter = setupChainRuleTestGraph();
|
||
|
|
|
||
|
|
arbiter.setRelationConfig('quick_access_check', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
// Test with fast path enabled and low threshold
|
||
|
|
const result = arbiter.check('user:alice', 'quick_access_check', 'project:web-app', {
|
||
|
|
fastPath: true,
|
||
|
|
minAllowPossibility: 0.5 // Exit early if possibility >= 0.5
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(result.possibility, 1);
|
||
|
|
assert.equal(result.reason, 'allow_threshold_met');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('supports early exit with maxDenyPossibility threshold', () => {
|
||
|
|
const arbiter = setupChainRuleTestGraph();
|
||
|
|
|
||
|
|
arbiter.setRelationConfig('deny_check', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
// Test denial with fast path
|
||
|
|
const result = arbiter.check('user:alice', 'deny_check', 'project:mobile-app', {
|
||
|
|
fastPath: true,
|
||
|
|
maxDenyPossibility: 0.8 // Exit early if denial >= 0.8
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(result.possibility, 0);
|
||
|
|
assert.equal(result.reason, 'no_chain_path_found');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Comparison with Other Rules', () => {
|
||
|
|
it('compares ChainRule vs ParentRule performance', () => {
|
||
|
|
const arbiter = setupChainRuleTestGraph();
|
||
|
|
|
||
|
|
// Set up equivalent rules using ChainRule and ParentRule
|
||
|
|
arbiter.setRelationConfig('chain_parent_check', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'manages', direction: 'in' }, // project ← group
|
||
|
|
{ relation: 'member_of', direction: 'in' } // group ← user
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
arbiter.setRelationConfig('traditional_parent_check', {
|
||
|
|
type: 'parent',
|
||
|
|
parentRelation: 'manages',
|
||
|
|
relation: 'member_of',
|
||
|
|
reverse: true
|
||
|
|
});
|
||
|
|
|
||
|
|
const startTime1 = Date.now();
|
||
|
|
const chainResult = arbiter.check('project:web-app', 'chain_parent_check', 'user:alice');
|
||
|
|
const chainTime = Date.now() - startTime1;
|
||
|
|
|
||
|
|
const startTime2 = Date.now();
|
||
|
|
const parentResult = arbiter.check('project:web-app', 'traditional_parent_check', 'user:alice');
|
||
|
|
const parentTime = Date.now() - startTime2;
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('ChainRule result:', chainResult.possibility);
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('ParentRule result:', parentResult.possibility);
|
||
|
|
|
||
|
|
// ChainRule should succeed (Alice is member of engineering, which manages web-app)
|
||
|
|
assert.equal(chainResult.possibility, 1);
|
||
|
|
|
||
|
|
// ParentRule might have different semantics - just verify it returns a valid result
|
||
|
|
assert.ok(typeof parentResult.possibility === 'number');
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log(`ChainRule time: ${chainTime}ms, ParentRule time: ${parentTime}ms`);
|
||
|
|
|
||
|
|
// ChainRule should be reasonably performant
|
||
|
|
assert.ok(chainTime < 100); // Should complete in reasonable time
|
||
|
|
});
|
||
|
|
|
||
|
|
it('compares ChainRule vs MultiHopRule for path finding', () => {
|
||
|
|
const arbiter = setupChainRuleTestGraph();
|
||
|
|
|
||
|
|
// ChainRule: specific path
|
||
|
|
arbiter.setRelationConfig('chain_access', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'belongs_to', direction: 'out' },
|
||
|
|
{ relation: 'has_access_level', direction: 'out' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
// MultiHopRule: flexible path finding
|
||
|
|
arbiter.setRelationConfig('multihop_access', {
|
||
|
|
type: 'multi_hop',
|
||
|
|
relation: 'member_of',
|
||
|
|
maxDepth: 3,
|
||
|
|
pathAggregation: 'max'
|
||
|
|
});
|
||
|
|
|
||
|
|
const chainResult = arbiter.check('user:alice', 'chain_access', 'access:level-5');
|
||
|
|
const multihopResult = arbiter.check('user:alice', 'multihop_access', 'access:level-5');
|
||
|
|
|
||
|
|
// ChainRule should give precise result for defined path
|
||
|
|
assert.equal(chainResult.possibility, 1);
|
||
|
|
|
||
|
|
// MultiHopRule might give different result based on path exploration
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Chain vs MultiHop results:', {
|
||
|
|
chain: chainResult.possibility,
|
||
|
|
multihop: multihopResult.possibility
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('demonstrates ChainRule semantic clarity vs other approaches', () => {
|
||
|
|
const arbiter = setupChainRuleTestGraph();
|
||
|
|
|
||
|
|
// ChainRule: explicit semantic path
|
||
|
|
arbiter.setRelationConfig('semantic_chain', {
|
||
|
|
type: 'chain',
|
||
|
|
steps: [
|
||
|
|
{ relation: 'member_of', direction: 'out' },
|
||
|
|
{ relation: 'manages', direction: 'out' },
|
||
|
|
{ relation: 'has_budget', direction: 'out' }
|
||
|
|
],
|
||
|
|
extractValues: true,
|
||
|
|
extractFrom: 2,
|
||
|
|
extractRelation: 'has_budget',
|
||
|
|
valueAggregation: 'sum'
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = arbiter.check('user:alice', 'semantic_chain', 'budget:tech-2024');
|
||
|
|
|
||
|
|
// Should provide clear semantic meaning:
|
||
|
|
// "user's budget access through group project management"
|
||
|
|
assert.equal(result.possibility, 1);
|
||
|
|
|
||
|
|
// Values are internal only, not exposed in authorization results
|
||
|
|
assert.equal(result.value, undefined);
|
||
|
|
|
||
|
|
if (process.env.TEST_DEBUG === '1') console.log('Semantic chain result:', {
|
||
|
|
access: result.possibility,
|
||
|
|
meaning: 'user → group → project → budget'
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|