UNPKG

@flamesshield/rules-engine

Version:

Independent rules engine for security analysis of Firebase

270 lines 11.6 kB
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