UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

237 lines (218 loc) 7.5 kB
/** * Copyright IBM Corp. 2024, 2025 */ import { compileExpression } from 'filtrex'; import { performAssertion } from '../../handlers/assertion.handler.js'; import { RunExecutionAssertion } from '../../models/interface.js'; import { Assertions } from '../../schemas/test.schema.js'; import { VCM } from '../variable-context-manager/context-manager.js'; import { convertToExecutableFormat } from '../../helpers/condition-converter.js'; // Interface for wildcard match results interface WildcardResult { isWildcardResult: true; matches: any[]; originalPath: string; } export class AssertionEngine { async assert( assertions: Assertions, contextId: string, ): Promise<[RunExecutionAssertion[], boolean]> { const results: RunExecutionAssertion[] = []; let isStopOnFailTriggered = false; for (const assertion of assertions.assertions!) { for (const spec of assertion.spec) { let assertionExecutionCheck = true; const { name, action, key, value, if: conditionCheck, stopOnFail, } = spec; if ( conditionCheck !== undefined && typeof conditionCheck !== 'boolean' ) { try { // Convert human-readable format to executable format const executableCondition = convertToExecutableFormat(conditionCheck); const resolvedExpression = VCM.resolve( contextId, executableCondition, ); const exp = compileExpression(resolvedExpression); assertionExecutionCheck = exp(resolvedExpression); } catch (error) { console.error(error); assertionExecutionCheck = false; } } if (assertionExecutionCheck) { let actualValue; let expectedValue; try { expectedValue = this.resolveValue(value, contextId); actualValue = this.resolveKey(key, contextId); // Handle wildcard results differently if (this.isWildcardResult(actualValue)) { this.performWildcardAssertion( action, actualValue, expectedValue, name, ); } else { performAssertion(action, actualValue, expectedValue); } results.push({ metadata: assertion.metadata, assertion: name, skipped: false, actualValue: this.isWildcardResult(actualValue) ? actualValue.matches : actualValue, expectedValue, action, key, }); } catch (error: any) { results.push({ metadata: assertion.metadata, assertion: name, skipped: false, error: { name: error?.name || 'AssertionError', test: name, message: error?.message || '', stack: error?.stack || '', }, actualValue: this.isWildcardResult(actualValue) ? actualValue?.matches : actualValue, expectedValue, action, key, }); if (stopOnFail) { // Mark remaining assertions as skipped const remainingAssertions = assertions.assertions!.flatMap((a) => a.spec .filter((s) => !results.some((r) => r.assertion === s.name)) .map((s) => ({ metadata: a.metadata, assertion: s.name, skipped: true, error: undefined, action: s.action, key: s.key, })), ); results.push(...remainingAssertions); // Return a flag to indicate the test was terminated due to stop on fail // This flag needs to be checked in TestRunner to cancel all remaining requests isStopOnFailTriggered = true; break; } } } else { // if assertionExecutionCheck is not passed, it will skip the test. results.push({ metadata: assertion.metadata, assertion: name, skipped: true, error: undefined, action, key, }); } } } return [results, isStopOnFailTriggered]; } // For backward compatibility we check whether key have ${ to resolve it} private resolveKey(key: string, contextId: string): any { if (key.includes('*')) { return this.resolveWildcardKey(key, contextId); } return this.resolveValue(this.wrapKey(key), contextId); } private wrapKey(key: string): string { return key.includes('${') ? key : `\${${key}}`; } private resolveValue(key: any, contextId: string): any { return VCM.resolve(contextId, key); } /** * Helper method to check if a value is a wildcard result */ private isWildcardResult(value: any): value is WildcardResult { return ( value && typeof value === 'object' && value.isWildcardResult === true ); } /** * Performs assertions on each item in a wildcard result * If any assertion fails, the entire assertion fails */ private performWildcardAssertion( action: string, wildcardResult: WildcardResult, expected: any, assertionName: string, ): void { if (wildcardResult.matches.length === 0) { throw new Error( `No matches found for wildcard path: ${wildcardResult.originalPath}`, ); } // Check each match against the expected value for (const match of wildcardResult.matches) { try { performAssertion(action, match, expected); } catch (error: any) { throw new Error( `Assertion '${assertionName}' failed for path '${wildcardResult.originalPath}': ${error.message}`, ); } } } private resolveWildcardKey(key: string, contextId: string): WildcardResult { const parts = key.split('.'); const rootKey = parts[0]; const root = this.resolveValue(this.wrapKey(rootKey), contextId); if (!root) return { isWildcardResult: true, matches: [], originalPath: key }; const matches = this.collectWildcardMatches(root, parts.slice(1)); return { isWildcardResult: true, matches, originalPath: key }; } private collectWildcardMatches(node: any, pathParts: string[]): any[] { if (pathParts.length === 0) { return [node]; } const [head, ...tail] = pathParts; const results: any[] = []; if (head === '*') { if (Array.isArray(node)) { // Iterate array items for (const item of node) { results.push(...this.collectWildcardMatches(item, tail)); } } else if ( node && Object.prototype.toString.call(node) === '[object Object]' ) { // Iterate plain object values only (not arrays) for (const key of Object.keys(node)) { results.push(...this.collectWildcardMatches(node[key], tail)); } } // Skip if node is not iterable } else if (node && typeof node === 'object' && head in node) { results.push(...this.collectWildcardMatches(node[head], tail)); } return results; } }