/** * Test realistic chain performance with actual graph relations */ import { test as _test } from 'node:test'; const test = process.env.RUN_PERF_TESTS === '1' ? _test : _test.skip; import assert from 'node:assert/strict'; import { BigGraphGenerator } from '../helpers/big-graph-generator.js'; import { ChainRule } from '../../src/authorization/rules/ChainRule.js'; test('measures realistic chain QPS with actual relations', async () => { if (process.env.TEST_DEBUG === '1') console.log('🎯 Testing realistic chain QPS with actual relations...'); const generator = new BigGraphGenerator(); const graphData = generator.generateGraph('enterprise'); const arbiter = generator.loadIntoArbiter(graphData); const rule = new ChainRule(arbiter); // Create a realistic chain rule using actual relations const chainRule = { type: 'chain', steps: [ { relation: 'member_of', direction: 'out' }, { relation: 'can_read', direction: 'out' } ], collectValues: false, valueAggregation: 'sum' }; // Find realistic chain paths using actual relations if (process.env.TEST_DEBUG === '1') console.log(' Finding realistic chain paths...'); // Get users with member_of relations const usersWithMemberships = graphData.relations .filter(r => r.relation === 'member_of' && r.src.startsWith('user:')) .map(r => r.src); // Get objects with can_read relations const objectsWithReadPermissions = graphData.relations .filter(r => r.relation === 'can_read' && r.dst.startsWith('doc:')) .map(r => r.dst); if (process.env.TEST_DEBUG === '1') console.log(` Found ${usersWithMemberships.length} users with memberships`); if (process.env.TEST_DEBUG === '1') console.log(` Found ${objectsWithReadPermissions.length} objects with read permissions`); // Create realistic test pairs const realisticPairs = []; for (let i = 0; i < Math.min(10, usersWithMemberships.length, objectsWithReadPermissions.length); i++) { realisticPairs.push({ user: usersWithMemberships[i], object: objectsWithReadPermissions[i] }); } if (process.env.TEST_DEBUG === '1') console.log(` Testing with ${realisticPairs.length} realistic pairs...`); // Test 1: Without caching (clear cache between queries) if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...'); const start1 = Date.now(); let queryCount1 = 0; let positiveResults1 = 0; const end1 = start1 + 3000; // 3 seconds while (Date.now() < end1) { for (const pair of realisticPairs) { // Clear cache to simulate no caching rule.chainResultCache.clear(); rule.chainPathCache.clear(); const result = rule.evaluate( arbiter.nodeIdByKey.get(pair.user), pair.user, arbiter.nodeIdByKey.get(pair.object), pair.object, chainRule, new Set(), 'can_read_via_membership', {} ); if (result.possibility > 0) { positiveResults1++; } queryCount1++; } } const duration1 = Date.now() - start1; const qps1 = (queryCount1 / duration1) * 1000; const positiveRate1 = (positiveResults1 / queryCount1) * 100; if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`); if (process.env.TEST_DEBUG === '1') console.log(` Positive results: ${positiveResults1} (${positiveRate1.toFixed(1)}%)`); // Test 2: With caching (let cache accumulate) if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...'); rule.chainResultCache.clear(); rule.chainPathCache.clear(); const start2 = Date.now(); let queryCount2 = 0; let positiveResults2 = 0; const end2 = start2 + 3000; // 3 seconds while (Date.now() < end2) { for (const pair of realisticPairs) { const result = rule.evaluate( arbiter.nodeIdByKey.get(pair.user), pair.user, arbiter.nodeIdByKey.get(pair.object), pair.object, chainRule, new Set(), 'can_read_via_membership', {} ); if (result.possibility > 0) { positiveResults2++; } queryCount2++; } } const duration2 = Date.now() - start2; const qps2 = (queryCount2 / duration2) * 1000; const positiveRate2 = (positiveResults2 / queryCount2) * 100; if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`); if (process.env.TEST_DEBUG === '1') console.log(` Positive results: ${positiveResults2} (${positiveRate2.toFixed(1)}%)`); if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`); // Calculate improvement const improvement = qps2 / qps1; if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`); // Verify improvement assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`); assert.ok(positiveRate1 > 0, 'Should have some positive results'); assert.ok(positiveRate2 > 0, 'Should have some positive results'); if (process.env.TEST_DEBUG === '1') console.log(' ✅ Realistic chain caching improves QPS'); }); test('measures chain QPS with role inheritance', async () => { if (process.env.TEST_DEBUG === '1') console.log('🔗 Testing chain QPS with role inheritance...'); const generator = new BigGraphGenerator(); const graphData = generator.generateGraph('enterprise'); const arbiter = generator.loadIntoArbiter(graphData); const rule = new ChainRule(arbiter); // Create a chain rule for role inheritance const chainRule = { type: 'chain', steps: [ { relation: 'member_of', direction: 'out' }, { relation: 'inherits_from', direction: 'out' } ], collectValues: false, valueAggregation: 'sum' }; // Find actual role inheritance chains if (process.env.TEST_DEBUG === '1') console.log(' Finding role inheritance chains...'); const roleChains = []; // Look for users with role memberships const roleMemberships = graphData.relations.filter(r => r.relation === 'member_of' && r.dst.startsWith('role:')); for (const membership of roleMemberships.slice(0, 5)) { const userId = membership.src; const roleId = membership.dst; // Find inheritance chains for this role const inheritanceChains = graphData.relations.filter(r => r.relation === 'inherits_from' && r.src === roleId ); for (const inheritance of inheritanceChains) { roleChains.push({ user: userId, role: roleId, inheritedRole: inheritance.dst }); } } if (process.env.TEST_DEBUG === '1') console.log(` Found ${roleChains.length} role inheritance chains`); if (roleChains.length === 0) { if (process.env.TEST_DEBUG === '1') console.log(' No role inheritance chains found, skipping test'); return; } // Test with role inheritance chains if (process.env.TEST_DEBUG === '1') console.log(' Testing with role inheritance chains...'); // Test 1: Without caching if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...'); const start1 = Date.now(); let queryCount1 = 0; let successfulChains1 = 0; const end1 = start1 + 2000; // 2 seconds while (Date.now() < end1) { for (const chain of roleChains) { // Clear cache to simulate no caching rule.chainResultCache.clear(); rule.chainPathCache.clear(); const result = rule.evaluate( arbiter.nodeIdByKey.get(chain.user), chain.user, arbiter.nodeIdByKey.get(chain.inheritedRole), chain.inheritedRole, chainRule, new Set(), 'inherits_via_membership', {} ); if (result.possibility > 0) { successfulChains1++; } queryCount2++; } } const duration1 = Date.now() - start1; const qps1 = (queryCount1 / duration1) * 1000; const successRate1 = (successfulChains1 / queryCount1) * 100; if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`); if (process.env.TEST_DEBUG === '1') console.log(` Successful chains: ${successfulChains1} (${successRate1.toFixed(1)}%)`); // Test 2: With caching if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...'); rule.chainResultCache.clear(); rule.chainPathCache.clear(); const start2 = Date.now(); let queryCount2 = 0; let successfulChains2 = 0; const end2 = start2 + 2000; // 2 seconds while (Date.now() < end2) { for (const chain of roleChains) { const result = rule.evaluate( arbiter.nodeIdByKey.get(chain.user), chain.user, arbiter.nodeIdByKey.get(chain.inheritedRole), chain.inheritedRole, chainRule, new Set(), 'inherits_via_membership', {} ); if (result.possibility > 0) { successfulChains2++; } queryCount2++; } } const duration2 = Date.now() - start2; const qps2 = (queryCount2 / duration2) * 1000; const successRate2 = (successfulChains2 / queryCount2) * 100; if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`); if (process.env.TEST_DEBUG === '1') console.log(` Successful chains: ${successfulChains2} (${successRate2.toFixed(1)}%)`); if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`); // Calculate improvement const improvement = qps2 / qps1; if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`); // Verify improvement assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`); assert.ok(successRate1 > 0, 'Should have some successful chains'); assert.ok(successRate2 > 0, 'Should have some successful chains'); if (process.env.TEST_DEBUG === '1') console.log(' ✅ Role inheritance chain caching improves QPS'); }); test('measures chain QPS with realistic workload', async () => { if (process.env.TEST_DEBUG === '1') console.log('📊 Testing chain QPS with realistic workload...'); const generator = new BigGraphGenerator(); const graphData = generator.generateGraph('enterprise'); const arbiter = generator.loadIntoArbiter(graphData); const rule = new ChainRule(arbiter); // Create a realistic chain rule const chainRule = { type: 'chain', steps: [ { relation: 'member_of', direction: 'out' }, { relation: 'can_read', direction: 'out' } ], collectValues: false, valueAggregation: 'sum' }; // Create a realistic workload: 70% positive, 30% negative if (process.env.TEST_DEBUG === '1') console.log(' Creating realistic workload (70% positive, 30% negative)...'); const workload = []; // 70% positive queries (should succeed) const positiveQueries = []; const memberOfRelations = graphData.relations.filter(r => r.relation === 'member_of'); const canReadRelations = graphData.relations.filter(r => r.relation === 'can_read'); for (let i = 0; i < Math.min(7, memberOfRelations.length, canReadRelations.length); i++) { positiveQueries.push({ user: memberOfRelations[i].src, object: canReadRelations[i].dst, expected: 'positive' }); } // 30% negative queries (should fail) const negativeQueries = []; const users = graphData.users.slice(0, 3); const objects = graphData.documents.slice(0, 3); for (let i = 0; i < 3; i++) { negativeQueries.push({ user: users[i].key, object: objects[i].key, expected: 'negative' }); } workload.push(...positiveQueries, ...negativeQueries); if (process.env.TEST_DEBUG === '1') console.log(` Workload: ${positiveQueries.length} positive, ${negativeQueries.length} negative queries`); // Test 1: Without caching if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...'); const start1 = Date.now(); let queryCount1 = 0; let positiveCount1 = 0; const end1 = start1 + 3000; // 3 seconds while (Date.now() < end1) { for (const query of workload) { // Clear cache to simulate no caching rule.chainResultCache.clear(); rule.chainPathCache.clear(); const result = rule.evaluate( arbiter.nodeIdByKey.get(query.user), query.user, arbiter.nodeIdByKey.get(query.object), query.object, chainRule, new Set(), 'can_read_via_membership', {} ); if (result.possibility > 0) { positiveCount1++; } queryCount1++; } } const duration1 = Date.now() - start1; const qps1 = (queryCount1 / duration1) * 1000; const positiveRate1 = (positiveCount1 / queryCount1) * 100; if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`); if (process.env.TEST_DEBUG === '1') console.log(` Positive rate: ${positiveRate1.toFixed(1)}%`); // Test 2: With caching if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...'); rule.chainResultCache.clear(); rule.chainPathCache.clear(); const start2 = Date.now(); let queryCount2 = 0; let positiveCount2 = 0; const end2 = start2 + 3000; // 3 seconds while (Date.now() < end2) { for (const query of workload) { const result = rule.evaluate( arbiter.nodeIdByKey.get(query.user), query.user, arbiter.nodeIdByKey.get(query.object), query.object, chainRule, new Set(), 'can_read_via_membership', {} ); if (result.possibility > 0) { positiveCount2++; } queryCount2++; } } const duration2 = Date.now() - start2; const qps2 = (queryCount2 / duration2) * 1000; const positiveRate2 = (positiveCount2 / queryCount2) * 100; if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`); if (process.env.TEST_DEBUG === '1') console.log(` Positive rate: ${positiveRate2.toFixed(1)}%`); if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`); // Calculate improvement const improvement = qps2 / qps1; if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`); // Verify improvement assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`); assert.ok(positiveRate1 > 0, 'Should have some positive results'); assert.ok(positiveRate2 > 0, 'Should have some positive results'); if (process.env.TEST_DEBUG === '1') console.log(' ✅ Realistic workload caching improves QPS'); });