@flamesshield/rules-engine
Version:
Independent rules engine for security analysis of Firebase
270 lines • 11.6 kB
JavaScript
import { executeRules, createRuleExecutionContext } from '../rule-engine/index.js';
import { RuleValidator } from '../validation/rule-validator.js';
import { TestDataFactory } from './TestDataFactory.js';
/**
* Helper class for common rule testing operations.
* Provides utilities for running rule validations, asserting results,
* and setting up test contexts with minimal boilerplate.
*/
export class RuleTestHelper {
/**
* Validates a single rule's structure against project information
*/
static validateRule(rule, projectInfo) {
return this.validator.validateRule(rule, projectInfo);
}
/**
* Executes rules against project information and returns execution results
*/
static async executeRules(rules, projectInfo) {
const context = createRuleExecutionContext(projectInfo, rules);
return executeRules(context);
}
/**
* Validates a rule and asserts that it passes (no violations)
*/
static async assertRulePasses(rule, projectInfo, message) {
const results = await this.executeRules([rule], projectInfo);
const result = results[0];
if (result.triggered) {
const errorMessage = message ||
`Expected rule '${rule.name}' to pass, but it was triggered: ${result.message}`;
throw new Error(errorMessage);
}
}
/**
* Validates a rule and asserts that it fails (is triggered)
*/
static async assertRuleFails(rule, projectInfo, expectedMessage) {
const results = await this.executeRules([rule], projectInfo);
const result = results[0];
if (!result.triggered) {
throw new Error(expectedMessage || 'Expected rule to fail but it passed');
}
if (expectedMessage && !result.message.includes(expectedMessage)) {
throw new Error(`Expected rule message to contain '${expectedMessage}' for rule '${rule.name}', ` +
`but found: ${result.message}`);
}
return result;
}
/**
* Validates multiple rules and asserts expected pass/fail counts
*/
static async assertExecutionResults(rules, projectInfo, expectations) {
const results = await this.executeRules(rules, projectInfo);
const passingRules = results.filter((r) => !r.triggered);
const failingRules = results.filter((r) => r.triggered);
const totalTriggered = failingRules.length;
if (expectations.expectedPassing !== undefined && passingRules.length !== expectations.expectedPassing) {
throw new Error(`Expected ${expectations.expectedPassing} passing rules, but found ${passingRules.length}`);
}
if (expectations.expectedFailing !== undefined && failingRules.length !== expectations.expectedFailing) {
throw new Error(`Expected ${expectations.expectedFailing} failing rules, but found ${failingRules.length}`);
}
if (expectations.expectedTotalTriggered !== undefined && totalTriggered !== expectations.expectedTotalTriggered) {
throw new Error(`Expected ${expectations.expectedTotalTriggered} total triggered rules, but found ${totalTriggered}`);
}
return results;
}
/**
* Creates a test scenario with predefined project info and rules (legacy version)
*/
static async createTestScenarioLegacy(scenarioName, customProjectInfo, customRules) {
let projectInfo;
let rules;
switch (scenarioName) {
case 'appcheck-enabled':
projectInfo = await TestDataFactory.createProjectInfoWithAppCheck(true);
rules = [TestDataFactory.createAppCheckRule()];
break;
case 'appcheck-disabled':
projectInfo = await TestDataFactory.createProjectInfoWithAppCheck(false);
rules = [TestDataFactory.createAppCheckRule()];
break;
case 'auth-enabled':
projectInfo = await TestDataFactory.createProjectInfoWithAuth(true);
rules = [TestDataFactory.createAuthRule()];
break;
case 'auth-disabled':
projectInfo = await TestDataFactory.createProjectInfoWithAuth(false);
rules = [TestDataFactory.createAuthRule()];
break;
case 'custom':
projectInfo = customProjectInfo || await TestDataFactory.createEnrichedProjectInfo();
rules = customRules || TestDataFactory.createTestRuleSet();
break;
default:
throw new Error(`Unknown scenario: ${scenarioName}`);
}
return { projectInfo, rules };
}
/**
* Creates a test scenario with project info and expected results
*/
static createTestScenario(name, projectInfo, expectedResults, description) {
const autoDescription = description || `Expected rule outcomes: ${Object.entries(expectedResults)
.map(([ruleId, shouldTrigger]) => `${ruleId}=${shouldTrigger}`)
.join(', ')}`;
return {
name,
projectInfo,
expectedResults,
description: autoDescription
};
}
/**
* Runs a complete test scenario and returns results (legacy version)
*/
static async runTestScenario(scenarioName, customProjectInfo, customRules) {
const { projectInfo, rules } = await this.createTestScenarioLegacy(scenarioName, customProjectInfo, customRules);
return this.executeRules(rules, projectInfo);
}
/**
* Runs multiple test scenarios with the provided rules
*/
static async runTestScenarios(rules, scenarios) {
for (const scenario of scenarios) {
try {
const results = await this.executeRules(rules, scenario.projectInfo);
// Validate results against expected outcomes
const expectations = Object.entries(scenario.expectedResults).map(([ruleId, shouldTrigger]) => ({
ruleId,
shouldTrigger
}));
this.assertRuleResults(results, expectations);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Scenario '${scenario.name}' failed: ${errorMessage}`);
}
}
}
/**
* Utility for testing rule conditions with different field values
*/
static async testRuleWithFieldValues(baseRule, fieldPath, testValues) {
const results = [];
for (const testCase of testValues) {
// Create project info with the specific field value
const projectInfo = await TestDataFactory.createEnrichedProjectInfo();
// Set the field value (this is a simplified implementation)
// In a real implementation, you'd need a proper deep object setter
const fieldParts = fieldPath.split('.');
let current = projectInfo;
for (let i = 0; i < fieldParts.length - 1; i++) {
if (!current[fieldParts[i]]) {
current[fieldParts[i]] = {};
}
current = current[fieldParts[i]];
}
current[fieldParts[fieldParts.length - 1]] = testCase.value;
// Execute the rule against this modified project info
const executionResults = await this.executeRules([baseRule], projectInfo);
const result = executionResults[0];
const passed = !result.triggered;
results.push({
value: testCase.value,
result,
passed
});
// Optional assertion based on expected result
if (testCase.shouldPass !== undefined && passed !== testCase.shouldPass) {
const description = testCase.description || `value: ${testCase.value}`;
throw new Error(`Test case failed for ${description}. ` +
`Expected rule to ${testCase.shouldPass ? 'pass' : 'fail'}, but it ${passed ? 'passed' : 'failed'}`);
}
}
return results;
}
/**
* Creates a mock validation context for testing
*/
static async createValidationContext(projectInfo, rules) {
return {
projectInfo: projectInfo || await TestDataFactory.createEnrichedProjectInfo(),
rules: rules || TestDataFactory.createTestRuleSet(),
validator: this.validator,
executionId: `test-${Date.now()}`,
startTime: new Date()
};
}
/**
* Utility for performance testing of rule execution
*/
static async benchmarkRuleExecution(rules, projectInfo, iterations = 100) {
const times = [];
for (let i = 0; i < iterations; i++) {
const startTime = performance.now();
await this.executeRules(rules, projectInfo);
const endTime = performance.now();
times.push(endTime - startTime);
}
const totalTime = times.reduce((sum, time) => sum + time, 0);
const averageTime = totalTime / iterations;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
return {
averageTime,
minTime,
maxTime,
totalTime
};
}
/**
* Validates rule structure and syntax
*/
static validateRuleStructure(rule, projectInfo) {
return this.validator.validateRule(rule, projectInfo);
}
/**
* Utility for testing rule categories and severities
*/
static groupResultsByCategory(results) {
return results.reduce((acc, result) => {
const category = result.category || 'unknown';
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(result);
return acc;
}, {});
}
/**
* Utility for testing rule severities
*/
static groupResultsBySeverity(results) {
return results.reduce((acc, result) => {
const severity = result.severity || 'unknown';
if (!acc[severity]) {
acc[severity] = [];
}
acc[severity].push(result);
return acc;
}, {});
}
/**
* Asserts that rule execution results match expectations
*/
static assertRuleResults(results, expectations) {
const resultMap = new Map(results.map(r => [r.rule_id, r]));
const mismatches = [];
for (const expectation of expectations) {
const result = resultMap.get(expectation.ruleId);
if (!result) {
mismatches.push(`Expected result for rule '${expectation.ruleId}' not found in results`);
continue;
}
if (result.triggered !== expectation.shouldTrigger) {
mismatches.push(`Rule '${expectation.ruleId}' expectation failed: ` +
`expected ${expectation.shouldTrigger ? 'to trigger' : 'not to trigger'}, ` +
`but it ${result.triggered ? 'triggered' : 'did not trigger'}`);
}
}
// Throw an error if there are any mismatches
if (mismatches.length > 0) {
throw new Error(`Rule assertion failed:\n${mismatches.join('\n')}`);
}
}
}
RuleTestHelper.validator = new RuleValidator();
//# sourceMappingURL=RuleTestHelper.js.map