import { BaseRule } from '../../src/authorization/rules/BaseRule.js'; import { Arbiter } from '../../src/core/Arbiter.js'; import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; // Create a concrete implementation of BaseRule for testing class TestRule extends BaseRule { constructor(arbiter) { super(arbiter); } canBatchProcess() { return true; } _evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) { // Simple test implementation return { possibility: 0.8, reliability: 0.9, collectedValues: [ { value: 100, possibility: 0.8, path: [userKey, objectKey], source: { entityKey: userKey, relation: 'test', step: 1 }, metadata: { timestamp: Date.now(), reliability: 0.9, decay: null } } ], meta: { ruleType: 'test', userKey, objectKey }, reason: 'test_evaluation' }; } } describe('BaseRule - Core Rule Infrastructure', () => { let testRule; let arbiter; beforeEach(() => { arbiter = new Arbiter({ embeddingDimensions: 256 }); testRule = new TestRule(arbiter); // Set up test entities arbiter.addNode('user:alice', 'user'); arbiter.addNode('project:secret', 'project'); }); describe('Constructor and Initialization', () => { it('prevents direct instantiation of BaseRule', () => { assert.throws(() => { new BaseRule(arbiter); }, Error, 'BaseRule is abstract and cannot be instantiated directly'); }); }); describe('Standard Evaluation Interface', () => { it('evaluates rules with proper input validation', () => { const userId = arbiter.nodeIdByKey.get('user:alice'); const objectId = arbiter.nodeIdByKey.get('project:secret'); const rule = { type: 'test' }; const visited = new Set(); const result = testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation'); assert.ok(result, 'Should return result'); assert.strictEqual(result.possibility, 0.8, 'Should return correct possibility'); assert.strictEqual(result.reliability, 0.9, 'Should return correct reliability'); assert.ok(Array.isArray(result.collectedValues), 'Should return collected values'); assert.strictEqual(result.reason, 'test_evaluation', 'Should return correct reason'); }); it('tracks performance metrics', () => { const userId = arbiter.nodeIdByKey.get('user:alice'); const objectId = arbiter.nodeIdByKey.get('project:secret'); const rule = { type: 'test' }; const visited = new Set(); testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation'); assert.strictEqual(testRule.evaluationCount, 1, 'Should increment evaluation count'); assert.ok(testRule.totalEvaluationTime >= 0, 'Should track evaluation time'); }); it('handles invalid inputs gracefully', () => { const result = testRule.evaluate(null, null, null, null, null, null, null); assert.ok(result, 'Should return error result'); assert.strictEqual(result.possibility_allow, 0, 'Should return zero possibility for invalid input'); assert.ok(result.reason.includes('missing'), 'Should indicate error in reason'); }); }); describe('Input Validation', () => { it('validates required parameters', () => { const userId = arbiter.nodeIdByKey.get('user:alice'); const objectId = arbiter.nodeIdByKey.get('project:secret'); const rule = { type: 'test' }; const visited = new Set(); // Valid inputs should pass const validation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation', {}); assert.ok(validation.valid, 'Should validate valid inputs'); // Invalid inputs should fail const invalidValidation = testRule._validateInputs(null, null, null, null, null, null, null, {}); assert.strictEqual(invalidValidation.valid, false, 'Should reject invalid inputs'); }); it('validates rule configuration', () => { const userId = arbiter.nodeIdByKey.get('user:alice'); const objectId = arbiter.nodeIdByKey.get('project:secret'); const visited = new Set(); // Missing rule type - BaseRule doesn't validate rule type, just that it's an object const noTypeValidation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', {}, visited, 'test_relation', {}); assert.ok(noTypeValidation.valid, 'Should accept rule without type'); // Valid rule const validRule = { type: 'test' }; const validValidation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', validRule, visited, 'test_relation', {}); assert.ok(validValidation.valid, 'Should accept valid rule'); }); }); describe('Options Normalization', () => { it('normalizes evaluation options', () => { const options = { fastPath: true, minAllowPossibility: 0.8, trackEvaluation: true }; const normalized = testRule._normalizeOptions(options, { type: 'test' }); assert.strictEqual(normalized.fastPath, true, 'Should preserve fastPath'); assert.strictEqual(normalized.minAllowPossibility, 0.8, 'Should preserve minAllowPossibility'); assert.strictEqual(normalized.trackEvaluation, true, 'Should preserve trackEvaluation'); }); it('provides default values for missing options', () => { const normalized = testRule._normalizeOptions({}, { type: 'test' }); assert.strictEqual(normalized.fastPath, false, 'Should default fastPath to false'); assert.strictEqual(normalized.minPossibility, null, 'Should default minPossibility to null'); assert.strictEqual(normalized.trackEvaluation, true, 'Should default trackEvaluation to true'); }); }); describe('Early Exit Optimization', () => { it('supports early exit with minAllowPossibility', () => { const options = { minAllowPossibility: 0.9, fastPath: true }; const earlyExit = testRule._checkEarlyExit(options, { type: 'test' }); // BaseRule doesn't implement early exit by default assert.strictEqual(earlyExit, null, 'Should return null for no early exit'); }); it('supports early exit with maxDenyPossibility', () => { const options = { maxDenyPossibility: 0.1, fastPath: true }; const earlyExit = testRule._checkEarlyExit(options, { type: 'test' }); // BaseRule doesn't implement early exit by default assert.strictEqual(earlyExit, null, 'Should return null for no early exit'); }); }); describe('Result Post-Processing', () => { it('post-processes rule evaluation results', () => { const rawResult = { possibility: 0.8, reliability: 0.9, collectedValues: [], meta: { ruleType: 'test' }, reason: 'test' }; const rule = { type: 'test' }; const options = { fastPath: false }; const processed = testRule._postProcessResult(rawResult, rule, options); assert.strictEqual(processed.possibility, 0.8, 'Should preserve possibility'); assert.strictEqual(processed.reliability, 0.9, 'Should preserve reliability'); assert.ok(Array.isArray(processed.collectedValues), 'Should preserve collected values'); }); it('applies minimum rule possibility threshold', () => { const rawResult = { possibility: 0.3, reliability: 0.9, collectedValues: [], meta: { ruleType: 'test' }, reason: 'test' }; const rule = { type: 'test', minRulePossibility: 0.5 }; const options = { fastPath: false }; const processed = testRule._postProcessResult(rawResult, rule, options); // BaseRule doesn't apply minimum threshold by default assert.strictEqual(processed.possibility, 0.3, 'Should preserve original possibility'); }); }); describe('Error Handling', () => { it('creates standardized error results', () => { const errorResult = testRule._createErrorResult('test_error', { details: 'test details' }); assert.strictEqual(errorResult.possibility_allow, 0, 'Should return zero possibility for errors'); assert.strictEqual(errorResult.reliability, 1.0, 'Should return reliability for errors'); assert.strictEqual(errorResult.reason, 'test_error', 'Should include error reason'); assert.ok(errorResult.details, 'Should include error details'); }); it('handles evaluation exceptions gracefully', () => { // Create a rule that throws an exception class ErrorRule extends BaseRule { constructor(arbiter) { super(arbiter); } _evaluateRule() { throw new Error('Test exception'); } } const errorRule = new ErrorRule(arbiter); const userId = arbiter.nodeIdByKey.get('user:alice'); const objectId = arbiter.nodeIdByKey.get('project:secret'); const rule = { type: 'error' }; const visited = new Set(); const result = errorRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation'); assert.strictEqual(result.possibility_allow, 0, 'Should return zero possibility for exceptions'); assert.ok(result.reason.includes('error'), 'Should indicate error in reason'); }); }); describe.skip('Batch Processing Support', () => { it('tracks batch evaluation performance', () => { const queries = [{ userId: arbiter.nodeIdByKey.get('user:alice'), userKey: 'user:alice', objectId: arbiter.nodeIdByKey.get('project:secret'), objectKey: 'project:secret', rule: { type: 'test' }, visited: new Set(), currentRelation: 'test_relation', options: {} }]; testRule.batchEvaluate(queries); assert.strictEqual(testRule.batchEvaluationCount, 1, 'Should increment batch count'); assert.ok(testRule.totalBatchEvaluationTime >= 0, 'Should track batch time'); }); }); describe('Performance Metrics', () => { it('tracks evaluation statistics', () => { const userId = arbiter.nodeIdByKey.get('user:alice'); const objectId = arbiter.nodeIdByKey.get('project:secret'); const rule = { type: 'test' }; const visited = new Set(); // Run multiple evaluations for (let i = 0; i < 5; i++) { testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation'); } assert.strictEqual(testRule.evaluationCount, 5, 'Should track evaluation count'); assert.ok(testRule.totalEvaluationTime >= 0, 'Should track total time'); }); it('provides performance summary', () => { const userId = arbiter.nodeIdByKey.get('user:alice'); const objectId = arbiter.nodeIdByKey.get('project:secret'); const rule = { type: 'test' }; const visited = new Set(); testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation'); const stats = testRule.getPerformanceStats(); assert.strictEqual(stats.evaluationCount, 1, 'Should report evaluation count'); assert.ok(stats.averageEvaluationTime >= 0, 'Should report average time'); }); }); describe('Value Context Integration', () => { it('passes value context to rule evaluation', () => { const userId = arbiter.nodeIdByKey.get('user:alice'); const objectId = arbiter.nodeIdByKey.get('project:secret'); const rule = { type: 'test' }; const visited = new Set(); const valueContext = { testValue: 42 }; const result = testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation', { valueContext }); assert.ok(result, 'Should handle value context'); assert.strictEqual(result.possibility, 0.8, 'Should return correct result'); }); }); describe('Abstract Method Requirements', () => { it('requires concrete implementations to define _evaluateRule', () => { class IncompleteRule extends BaseRule { constructor(arbiter) { super(arbiter); } // Missing _evaluateRule implementation } const incompleteRule = new IncompleteRule(arbiter); const userId = arbiter.nodeIdByKey.get('user:alice'); const objectId = arbiter.nodeIdByKey.get('project:secret'); const rule = { type: 'incomplete' }; const visited = new Set(); // Should handle missing implementation gracefully const result = incompleteRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation'); assert.ok(result, 'Should return result even with missing implementation'); }); }); });