@apistudio/apim-cli
Version:
CLI for API Management Products
182 lines (181 loc) • 8.03 kB
JavaScript
/**
* 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;
}
}