331 lines
10 KiB
JavaScript
331 lines
10 KiB
JavaScript
|
|
/**
|
||
|
|
* Fact Declaration Tests
|
||
|
|
*
|
||
|
|
* Tests the fact declaration system of the Evidence DSL,
|
||
|
|
* including properties, caching, limits, and parameter types.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { DSLCompiler } from '../DSLCompiler.js';
|
||
|
|
|
||
|
|
export class FactTests {
|
||
|
|
constructor() {
|
||
|
|
this.arbiter = null;
|
||
|
|
this.compiler = null;
|
||
|
|
this.testResults = [];
|
||
|
|
}
|
||
|
|
|
||
|
|
setup(arbiter) {
|
||
|
|
this.arbiter = arbiter;
|
||
|
|
this.compiler = new DSLCompiler(arbiter);
|
||
|
|
}
|
||
|
|
|
||
|
|
runAllTests() {
|
||
|
|
console.log('=== Fact Declaration Tests ===\n');
|
||
|
|
|
||
|
|
this.testBasicFacts();
|
||
|
|
this.testFactProperties();
|
||
|
|
this.testFactCaching();
|
||
|
|
this.testFactLimits();
|
||
|
|
this.testParameterTypes();
|
||
|
|
this.testComplexFacts();
|
||
|
|
this.testFactErrors();
|
||
|
|
|
||
|
|
return this.getTestResults();
|
||
|
|
}
|
||
|
|
|
||
|
|
testBasicFacts() {
|
||
|
|
console.log('Testing 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 }) => {
|
||
|
|
try {
|
||
|
|
const result = this.compiler.compile(input, `test-basic-fact-${Date.now()}`);
|
||
|
|
this.assert(result.success, `${description} should parse successfully`);
|
||
|
|
this.assert(result.program.facts.length > 0, 'Should have facts');
|
||
|
|
console.log(` ✓ ${description}`);
|
||
|
|
} catch (error) {
|
||
|
|
this.fail(`Basic fact test: ${description}`, error);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
testFactProperties() {
|
||
|
|
console.log('Testing 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 }) => {
|
||
|
|
try {
|
||
|
|
const result = this.compiler.compile(input, `test-fact-property-${Date.now()}`);
|
||
|
|
this.assert(result.success, `${description} should parse successfully`);
|
||
|
|
console.log(` ✓ ${description}`);
|
||
|
|
} catch (error) {
|
||
|
|
this.fail(`Fact property test: ${description}`, error);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
testFactCaching() {
|
||
|
|
console.log('Testing 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 }) => {
|
||
|
|
try {
|
||
|
|
const result = this.compiler.compile(input, `test-fact-cache-${Date.now()}`);
|
||
|
|
this.assert(result.success, `${description} should parse successfully`);
|
||
|
|
console.log(` ✓ ${description}`);
|
||
|
|
} catch (error) {
|
||
|
|
this.fail(`Fact cache test: ${description}`, error);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
testFactLimits() {
|
||
|
|
console.log('Testing 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 }) => {
|
||
|
|
try {
|
||
|
|
const result = this.compiler.compile(input, `test-fact-limit-${Date.now()}`);
|
||
|
|
this.assert(result.success, `${description} should parse successfully`);
|
||
|
|
console.log(` ✓ ${description}`);
|
||
|
|
} catch (error) {
|
||
|
|
this.fail(`Fact limit test: ${description}`, error);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
testParameterTypes() {
|
||
|
|
console.log('Testing 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 }) => {
|
||
|
|
try {
|
||
|
|
const dsl = `fact test(param: ${type})`;
|
||
|
|
const result = this.compiler.compile(dsl, `test-param-type-${Date.now()}`);
|
||
|
|
this.assert(result.success, `${description} should parse successfully`);
|
||
|
|
console.log(` ✓ ${description}`);
|
||
|
|
} catch (error) {
|
||
|
|
this.fail(`Parameter type test: ${description}`, error);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
testComplexFacts() {
|
||
|
|
console.log('Testing 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 }) => {
|
||
|
|
try {
|
||
|
|
const result = this.compiler.compile(input, `test-complex-facts-${Date.now()}`);
|
||
|
|
this.assert(result.success, `${description} should parse successfully`);
|
||
|
|
this.assert(result.program.facts.length > 0, 'Should have facts');
|
||
|
|
console.log(` ✓ ${description}`);
|
||
|
|
} catch (error) {
|
||
|
|
this.fail(`Complex facts test: ${description}`, error);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
testFactErrors() {
|
||
|
|
console.log('Testing 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 = this.compiler.compile(input, `test-fact-error-${Date.now()}`);
|
||
|
|
this.assert(!result.success, `${description} should fail to parse`);
|
||
|
|
console.log(` ✓ ${description} (correctly failed)`);
|
||
|
|
} catch (error) {
|
||
|
|
// Expected to fail
|
||
|
|
console.log(` ✓ ${description} (correctly failed)`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
assert(condition, message) {
|
||
|
|
if (!condition) {
|
||
|
|
throw new Error(`Assertion failed: ${message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fail(testName, error) {
|
||
|
|
console.log(` ✗ ${testName} failed: ${error.message}`);
|
||
|
|
this.testResults.push({
|
||
|
|
test: testName,
|
||
|
|
status: 'FAILED',
|
||
|
|
error: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
getTestResults() {
|
||
|
|
const passed = this.testResults.filter(r => r.status === 'PASSED').length;
|
||
|
|
const failed = this.testResults.filter(r => r.status === 'FAILED').length;
|
||
|
|
const total = this.testResults.length;
|
||
|
|
|
||
|
|
return {
|
||
|
|
total: total,
|
||
|
|
passed: passed,
|
||
|
|
failed: failed,
|
||
|
|
success: failed === 0,
|
||
|
|
results: this.testResults
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function runFactTests(arbiter) {
|
||
|
|
const test = new FactTests();
|
||
|
|
test.setup(arbiter);
|
||
|
|
return test.runAllTests();
|
||
|
|
}
|