233 lines
8.2 KiB
JavaScript
233 lines
8.2 KiB
JavaScript
|
|
import { test, describe } from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { Arbiter } from '../../src/index.js';
|
||
|
|
|
||
|
|
describe('Arbiter Core Functionality', () => {
|
||
|
|
test('adds nodes correctly', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
const nodeId = arbiter.addNode('user1', 'user');
|
||
|
|
|
||
|
|
assert.strictEqual(nodeId, 0);
|
||
|
|
assert.strictEqual(arbiter.nextNodeId, 1);
|
||
|
|
assert.ok(arbiter.nodes.has(0));
|
||
|
|
assert.strictEqual(arbiter.nodeIdByKey.get('user1'), 0);
|
||
|
|
assert.strictEqual(arbiter.keyByNodeId.get(0), 'user1');
|
||
|
|
|
||
|
|
const node = arbiter.nodes.get(0);
|
||
|
|
assert.strictEqual(node.key, 'user1');
|
||
|
|
assert.strictEqual(node.type, 'user');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('adds relations correctly', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
arbiter.addNode('project1', 'project');
|
||
|
|
|
||
|
|
const relationId = arbiter.addRelation('user1', 'can_read', 'project1', 0.8);
|
||
|
|
|
||
|
|
assert.strictEqual(relationId, 0);
|
||
|
|
assert.strictEqual(arbiter.relations.length, 1);
|
||
|
|
|
||
|
|
const relation = arbiter.relations[0];
|
||
|
|
assert.strictEqual(relation.src, 0); // user1 nodeId
|
||
|
|
assert.strictEqual(relation.dst, 1); // project1 nodeId
|
||
|
|
assert.strictEqual(relation.rel, 'can_read');
|
||
|
|
assert.strictEqual(relation.possibility, 0.8);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('performs basic authorization checks', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
|
||
|
|
// Setup
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
arbiter.addNode('project1', 'project');
|
||
|
|
arbiter.addRelation('user1', 'can_read', 'project1', 1.0);
|
||
|
|
arbiter.setRelationConfig('can_read', { type: 'direct' });
|
||
|
|
|
||
|
|
const result = arbiter.check('user1', 'can_read', 'project1');
|
||
|
|
|
||
|
|
assert.strictEqual(result.possibility, 1);
|
||
|
|
// CI-001 fix: with the fast path active, direct-type relations
|
||
|
|
// produce reason='direct_match' (AuthorizationChecker.js:161)
|
||
|
|
// instead of 'allow_rule_matched' (the slow-path reason).
|
||
|
|
assert.strictEqual(result.reason, 'direct_match');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('handles missing nodes gracefully', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
|
||
|
|
const result = arbiter.check('user1', 'can_read', 'nonexistent');
|
||
|
|
|
||
|
|
assert.strictEqual(result.possibility, 0);
|
||
|
|
assert.strictEqual(result.reason, 'missing_node');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('handles missing relations gracefully', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
arbiter.addNode('project1', 'project');
|
||
|
|
arbiter.setRelationConfig('can_read', { type: 'direct' });
|
||
|
|
|
||
|
|
const result = arbiter.check('user1', 'can_read', 'project1');
|
||
|
|
|
||
|
|
assert.strictEqual(result.possibility, 0);
|
||
|
|
// CI-001 fix: fast-path's no-relation branch sets 'no_relation'
|
||
|
|
// (AuthorizationChecker.js:174)
|
||
|
|
assert.strictEqual(result.reason, 'no_relation');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('supports binary mode checks', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
arbiter.addNode('project1', 'project');
|
||
|
|
arbiter.addRelation('user1', 'can_read', 'project1', 0.9);
|
||
|
|
arbiter.setRelationConfig('can_read', { type: 'direct' });
|
||
|
|
|
||
|
|
const result = arbiter.check('user1', 'can_read', 'project1', {
|
||
|
|
binary: true,
|
||
|
|
minAllowPossibility: 0.8
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.strictEqual(result.possibility, 0.9);
|
||
|
|
assert.strictEqual(result.allow, true);
|
||
|
|
assert.strictEqual(result.deny, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('supports fast path optimization', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
arbiter.addNode('project1', 'project');
|
||
|
|
arbiter.addRelation('user1', 'can_read', 'project1', 0.9);
|
||
|
|
arbiter.setRelationConfig('can_read', { type: 'direct' });
|
||
|
|
|
||
|
|
const result = arbiter.check('user1', 'can_read', 'project1', {
|
||
|
|
fastPath: true
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.strictEqual(result.possibility, 0.9);
|
||
|
|
// CI-001 fix: fast path returns 'direct_match'
|
||
|
|
assert.strictEqual(result.reason, 'direct_match');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('handles early exit conditions', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
arbiter.addNode('project1', 'project');
|
||
|
|
arbiter.addRelation('user1', 'can_read', 'project1', 0.9);
|
||
|
|
arbiter.setRelationConfig('can_read', { type: 'direct' });
|
||
|
|
|
||
|
|
const result = arbiter.check('user1', 'can_read', 'project1', {
|
||
|
|
minAllowPossibility: 0.95
|
||
|
|
});
|
||
|
|
|
||
|
|
// CI-001 fix: threshold-not-met zero-out the possibility since
|
||
|
|
// directRel.possibility (0.9) < effectiveThreshold (0.95).
|
||
|
|
// AuthorizationChecker.js:140-144.
|
||
|
|
assert.strictEqual(result.possibility, 0);
|
||
|
|
assert.strictEqual(result.reason, 'threshold_not_met');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('manages direct check cache', () => {
|
||
|
|
const arbiter = new Arbiter({ directCheckCacheSize: 2 });
|
||
|
|
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
arbiter.addNode('project1', 'project');
|
||
|
|
arbiter.addRelation('user1', 'can_read', 'project1', 1.0);
|
||
|
|
arbiter.setRelationConfig('can_read', { type: 'direct' });
|
||
|
|
|
||
|
|
// First check - should cache. CI-001 fix: with the fast path
|
||
|
|
// active, direct-type relations produce 'direct_match' reason.
|
||
|
|
const result1 = arbiter.check('user1', 'can_read', 'project1');
|
||
|
|
assert.strictEqual(result1.possibility, 1);
|
||
|
|
assert.strictEqual(result1.reason, 'direct_match');
|
||
|
|
|
||
|
|
// Second check - should hit the cache populated by the fast path.
|
||
|
|
const result2 = arbiter.check('user1', 'can_read', 'project1');
|
||
|
|
assert.strictEqual(result2.possibility, 1);
|
||
|
|
assert.strictEqual(result2.reason, 'direct_match');
|
||
|
|
|
||
|
|
assert.ok(arbiter.directCheckCache);
|
||
|
|
assert.strictEqual(typeof arbiter.directCheckCache.has, 'function');
|
||
|
|
// Cache should now have the entry.
|
||
|
|
assert.ok(arbiter.directCheckCache.has(
|
||
|
|
arbiter.keyManager.createCompositeKey(
|
||
|
|
arbiter.keyManager.getStringId('user1'),
|
||
|
|
'can_read',
|
||
|
|
arbiter.keyManager.getStringId('project1')
|
||
|
|
)
|
||
|
|
));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('handles cycle detection', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
arbiter.addNode('user2', 'user');
|
||
|
|
arbiter.addRelation('user1', 'parent', 'user2', 1.0);
|
||
|
|
arbiter.addRelation('user2', 'parent', 'user1', 1.0); // Creates cycle
|
||
|
|
arbiter.setRelationConfig('parent', { type: 'parent' });
|
||
|
|
|
||
|
|
const result = arbiter.check('user1', 'parent', 'user2');
|
||
|
|
|
||
|
|
assert.strictEqual(result.possibility, 0);
|
||
|
|
assert.strictEqual(result.reason, 'cycle');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('supports inference mode', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
arbiter.addNode('project1', 'project');
|
||
|
|
arbiter.addRelation('user1', 'can_read', 'project1', 0.7);
|
||
|
|
arbiter.setRelationConfig('can_read', { type: 'direct' });
|
||
|
|
|
||
|
|
const result = arbiter.check('user1', 'can_read', 'project1', {
|
||
|
|
noInfer: false
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.strictEqual(result.possibility, 0.7);
|
||
|
|
// CI-001 fix: direct-type relation, fast path → 'direct_match'
|
||
|
|
assert.strictEqual(result.reason, 'direct_match');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('handles multiple relation types', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
arbiter.addNode('group1', 'group');
|
||
|
|
arbiter.addNode('project1', 'project');
|
||
|
|
|
||
|
|
arbiter.addRelation('user1', 'member_of', 'group1', 1.0);
|
||
|
|
arbiter.addRelation('group1', 'can_access', 'project1', 1.0);
|
||
|
|
|
||
|
|
arbiter.setRelationConfig('member_of', { type: 'direct' });
|
||
|
|
arbiter.setRelationConfig('can_access', { type: 'direct' });
|
||
|
|
|
||
|
|
const result1 = arbiter.check('user1', 'member_of', 'group1');
|
||
|
|
const result2 = arbiter.check('group1', 'can_access', 'project1');
|
||
|
|
|
||
|
|
assert.strictEqual(result1.possibility, 1);
|
||
|
|
assert.strictEqual(result2.possibility, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('supports value collection', () => {
|
||
|
|
const arbiter = new Arbiter();
|
||
|
|
|
||
|
|
arbiter.addNode('user1', 'user');
|
||
|
|
arbiter.addNode('account1', 'account');
|
||
|
|
arbiter.addRelation('user1', 'has_account', 'account1', 1.0, { value: 1000 });
|
||
|
|
arbiter.setRelationConfig('has_account', { type: 'direct' });
|
||
|
|
|
||
|
|
const result = arbiter.check('user1', 'has_account', 'account1', { collectValues: true });
|
||
|
|
|
||
|
|
assert.strictEqual(result.possibility, 1);
|
||
|
|
assert.ok(result.collectedValues);
|
||
|
|
assert.strictEqual(result.collectedValues.length, 1);
|
||
|
|
assert.strictEqual(result.collectedValues[0].value, 1000);
|
||
|
|
});
|
||
|
|
});
|