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.
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
|
||||
|
||||
function createMockArbiter() {
|
||||
const relationConfigs = new Map();
|
||||
return {
|
||||
relationConfigs,
|
||||
setRelationConfig(relation, config) {
|
||||
relationConfigs.set(relation, config);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('Fact Declarations', () => {
|
||||
const arbiter = createMockArbiter();
|
||||
const compiler = new DSLCompiler(arbiter);
|
||||
|
||||
test('Basic facts', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string)`,
|
||||
description: 'Simple fact with two parameters'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group)`,
|
||||
description: 'Fact with custom types'
|
||||
},
|
||||
{
|
||||
input: `fact owns(user: User, doc: Document)`,
|
||||
description: 'Fact with multiple custom types'
|
||||
},
|
||||
{
|
||||
input: `fact isActive(user: User)`,
|
||||
description: 'Fact with single parameter'
|
||||
},
|
||||
{
|
||||
input: `fact hasPermission(user: User, resource: Resource, action: string)`,
|
||||
description: 'Fact with three parameters'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-basic-fact-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
assert.ok(result.program.facts.length > 0, 'Should have facts');
|
||||
});
|
||||
});
|
||||
|
||||
test('Fact properties', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive`,
|
||||
description: 'Transitive fact'
|
||||
},
|
||||
{
|
||||
input: `fact isFriend(user: User, friend: User) symmetrical`,
|
||||
description: 'Symmetrical fact'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive symmetrical`,
|
||||
description: 'Fact with multiple properties'
|
||||
},
|
||||
{
|
||||
input: `fact isColleague(user: User, colleague: User) symmetrical`,
|
||||
description: 'Symmetrical relationship fact'
|
||||
},
|
||||
{
|
||||
input: `fact isParentOf(parent: User, child: User) transitive`,
|
||||
description: 'Transitive hierarchical fact'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-fact-property-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Fact caching', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string) CACHE eager`,
|
||||
description: 'Eager cached fact'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) CACHE lazy`,
|
||||
description: 'Lazy cached fact'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive CACHE eager`,
|
||||
description: 'Transitive fact with eager caching'
|
||||
},
|
||||
{
|
||||
input: `fact isFriend(user: User, friend: User) symmetrical CACHE lazy`,
|
||||
description: 'Symmetrical fact with lazy caching'
|
||||
},
|
||||
{
|
||||
input: `fact hasPermission(user: User, resource: Resource, action: string) CACHE eager`,
|
||||
description: 'Multi-parameter fact with eager caching'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-fact-cache-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Fact limits', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) limit 10`,
|
||||
description: 'Fact with simple limit'
|
||||
},
|
||||
{
|
||||
input: `fact isFriend(user: User, friend: User) limit 100`,
|
||||
description: 'Fact with higher limit'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive limit 5`,
|
||||
description: 'Transitive fact with limit'
|
||||
},
|
||||
{
|
||||
input: `fact isFriend(user: User, friend: User) symmetrical limit 50`,
|
||||
description: 'Symmetrical fact with limit'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive CACHE lazy limit 3`,
|
||||
description: 'Fact with properties, caching, and limit'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-fact-limit-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Parameter types', () => {
|
||||
const testCases = [
|
||||
{ type: 'string', description: 'String parameter' },
|
||||
{ type: 'number', description: 'Number parameter' },
|
||||
{ type: 'boolean', description: 'Boolean parameter' },
|
||||
{ type: 'timestamp', description: 'Timestamp parameter' },
|
||||
{ type: 'User', description: 'Custom type parameter' },
|
||||
{ type: 'Group', description: 'Another custom type parameter' },
|
||||
{ type: 'Permission[]', description: 'Array type parameter' }
|
||||
];
|
||||
|
||||
testCases.forEach(({ type, description }) => {
|
||||
const dsl = `fact test(param: ${type})`;
|
||||
const result = compiler.compile(dsl, `test-param-type-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Complex facts', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string) CACHE eager
|
||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
|
||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
||||
fact owns(user: User, doc: Document) CACHE eager
|
||||
fact isSuspended(user: User) CACHE lazy`,
|
||||
description: 'Multiple facts with different configurations'
|
||||
},
|
||||
{
|
||||
input: `fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
||||
fact isAdmin(user: User) CACHE eager
|
||||
fact isOwner(user: User, resource: Resource) CACHE eager
|
||||
fact hasAccess(user: User, resource: Resource, level: string) CACHE lazy`,
|
||||
description: 'Permission-related facts'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive CACHE lazy limit 5
|
||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 50
|
||||
fact isColleague(user: User, colleague: User) symmetrical CACHE lazy limit 20
|
||||
fact isParentOf(parent: User, child: User) transitive CACHE eager limit 3`,
|
||||
description: 'Relationship facts with various properties'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-complex-facts-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
assert.ok(result.program.facts.length > 0, 'Should have facts');
|
||||
});
|
||||
});
|
||||
|
||||
test('Fact error handling', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string`,
|
||||
description: 'Missing closing parenthesis should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(user: User, )`,
|
||||
description: 'Missing parameter name should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(user: User, role: )`,
|
||||
description: 'Missing parameter type should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(, role: string)`,
|
||||
description: 'Missing parameter name should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string) CACHE`,
|
||||
description: 'Incomplete cache directive should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string) limit`,
|
||||
description: 'Incomplete limit should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string) invalid`,
|
||||
description: 'Invalid property should fail'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
try {
|
||||
const result = compiler.compile(input, `test-fact-error-${Date.now()}`);
|
||||
assert.ok(!result.success, `${description} should fail to parse`);
|
||||
} catch {
|
||||
// Expected to fail
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user