UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

182 lines (181 loc) 8.03 kB
/** * Copyright IBM Corp. 2024, 2025 */ import { compileExpression } from 'filtrex'; import { performAssertion } from '../../handlers/assertion.handler.js'; import { VCM } from '../variable-context-manager/context-manager.js'; import { convertToExecutableFormat } from '../../helpers/condition-converter.js'; export class AssertionEngine { async assert(assertions, contextId) { const results = []; 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) { 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} resolveKey(key, contextId) { if (key.includes('*')) { return this.resolveWildcardKey(key, contextId); } return this.resolveValue(this.wrapKey(key), contextId); } wrapKey(key) { return key.includes('${') ? key : `\${${key}}`; } resolveValue(key, contextId) { return VCM.resolve(contextId, key); } /** * Helper method to check if a value is a wildcard result */ isWildcardResult(value) { 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 */ performWildcardAssertion(action, wildcardResult, expected, assertionName) { 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) { throw new Error(`Assertion '${assertionName}' failed for path '${wildcardResult.originalPath}': ${error.message}`); } } } resolveWildcardKey(key, contextId) { 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 }; } collectWildcardMatches(node, pathParts) { if (pathParts.length === 0) { return [node]; } const [head, ...tail] = pathParts; const results = []; 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; } }