Files
bruno/packages/bruno-js/src/runtime/assert-runtime.js
2023-10-10 01:31:05 +02:00

336 lines
8.0 KiB
JavaScript

const _ = require('lodash');
const chai = require('chai');
const { nanoid } = require('nanoid');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
const { expect } = chai;
chai.use(require('chai-string'));
chai.use(function (chai, utils) {
// Custom assertion for checking if a variable is JSON
chai.Assertion.addProperty('json', function () {
const obj = this._obj;
const isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj) && obj.constructor === Object;
this.assert(isJson, `expected ${utils.inspect(obj)} to be JSON`, `expected ${utils.inspect(obj)} not to be JSON`);
});
});
// Custom assertion for matching regex
chai.use(function (chai, utils) {
chai.Assertion.addMethod('match', function (regex) {
const obj = this._obj;
let match = false;
if (obj === undefined) {
match = false;
} else {
match = regex.test(obj);
}
this.assert(
match,
`expected ${utils.inspect(obj)} to match ${regex}`,
`expected ${utils.inspect(obj)} not to match ${regex}`
);
});
});
/**
* Assertion operators
*
* eq : equal to
* neq : not equal to
* gt : greater than
* gte : greater than or equal to
* lt : less than
* lte : less than or equal to
* in : in
* notIn : not in
* contains : contains
* notContains : not contains
* length : length
* matches : matches
* notMatches : not matches
* startsWith : starts with
* endsWith : ends with
* between : between
* isEmpty : is empty
* isNull : is null
* isUndefined : is undefined
* isDefined : is defined
* isTruthy : is truthy
* isFalsy : is falsy
* isJson : is json
* isNumber : is number
* isString : is string
* isBoolean : is boolean
*/
const parseAssertionOperator = (str = '') => {
if (!str || typeof str !== 'string' || !str.length) {
return {
operator: 'eq',
value: str
};
}
const operators = [
'eq',
'neq',
'gt',
'gte',
'lt',
'lte',
'in',
'notIn',
'contains',
'notContains',
'length',
'matches',
'notMatches',
'startsWith',
'endsWith',
'between',
'isEmpty',
'isNull',
'isUndefined',
'isDefined',
'isTruthy',
'isFalsy',
'isJson',
'isNumber',
'isString',
'isBoolean'
];
const unaryOperators = [
'isEmpty',
'isNull',
'isUndefined',
'isDefined',
'isTruthy',
'isFalsy',
'isJson',
'isNumber',
'isString',
'isBoolean'
];
const [operator, ...rest] = str.trim().split(' ');
const value = rest.join(' ');
if (unaryOperators.includes(operator)) {
return {
operator,
value: ''
};
}
if (operators.includes(operator)) {
return {
operator,
value
};
}
return {
operator: 'eq',
value: str
};
};
const isUnaryOperator = (operator) => {
const unaryOperators = [
'isEmpty',
'isNull',
'isUndefined',
'isDefined',
'isTruthy',
'isFalsy',
'isJson',
'isNumber',
'isString',
'isBoolean'
];
return unaryOperators.includes(operator);
};
const evaluateRhsOperand = (rhsOperand, operator, context) => {
if (isUnaryOperator(operator)) {
return;
}
// gracefully allow both a,b as well as [a, b]
if (operator === 'in' || operator === 'notIn') {
if (rhsOperand.startsWith('[') && rhsOperand.endsWith(']')) {
rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);
}
return rhsOperand.split(',').map((v) => evaluateJsTemplateLiteral(v.trim(), context));
}
if (operator === 'between') {
const [lhs, rhs] = rhsOperand.split(',').map((v) => evaluateJsTemplateLiteral(v.trim(), context));
return [lhs, rhs];
}
// gracefully allow both ^[a-Z] as well as /^[a-Z]/
if (operator === 'matches' || operator === 'notMatches') {
if (rhsOperand.startsWith('/') && rhsOperand.endsWith('/')) {
rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);
}
return rhsOperand;
}
return evaluateJsTemplateLiteral(rhsOperand, context);
};
class AssertRuntime {
runAssertions(assertions, request, response, envVariables, collectionVariables, collectionPath) {
const enabledAssertions = _.filter(assertions, (a) => a.enabled);
if (!enabledAssertions.length) {
return [];
}
const bru = new Bru(envVariables, collectionVariables);
const req = new BrunoRequest(request);
const res = createResponseParser(response);
const bruContext = {
bru,
req,
res
};
const context = {
...envVariables,
...collectionVariables,
...bruContext
};
const assertionResults = [];
// parse assertion operators
for (const v of enabledAssertions) {
const lhsExpr = v.name;
const rhsExpr = v.value;
const { operator, value: rhsOperand } = parseAssertionOperator(rhsExpr);
try {
const lhs = evaluateJsExpression(lhsExpr, context);
const rhs = evaluateRhsOperand(rhsOperand, operator, context);
switch (operator) {
case 'eq':
expect(lhs).to.equal(rhs);
break;
case 'neq':
expect(lhs).to.not.equal(rhs);
break;
case 'gt':
expect(lhs).to.be.greaterThan(rhs);
break;
case 'gte':
expect(lhs).to.be.greaterThanOrEqual(rhs);
break;
case 'lt':
expect(lhs).to.be.lessThan(rhs);
break;
case 'lte':
expect(lhs).to.be.lessThanOrEqual(rhs);
break;
case 'in':
expect(lhs).to.be.oneOf(rhs);
break;
case 'notIn':
expect(lhs).to.not.be.oneOf(rhs);
break;
case 'contains':
expect(lhs).to.include(rhs);
break;
case 'notContains':
expect(lhs).to.not.include(rhs);
break;
case 'length':
expect(lhs).to.have.lengthOf(rhs);
break;
case 'matches':
expect(lhs).to.match(new RegExp(rhs));
break;
case 'notMatches':
expect(lhs).to.not.match(new RegExp(rhs));
break;
case 'startsWith':
expect(lhs).to.startWith(rhs);
break;
case 'endsWith':
expect(lhs).to.endWith(rhs);
break;
case 'between':
const [min, max] = rhs;
expect(lhs).to.be.within(min, max);
break;
case 'isEmpty':
expect(lhs).to.be.empty;
break;
case 'isNull':
expect(lhs).to.be.null;
break;
case 'isUndefined':
expect(lhs).to.be.undefined;
break;
case 'isDefined':
expect(lhs).to.not.be.undefined;
break;
case 'isTruthy':
expect(lhs).to.be.true;
break;
case 'isFalsy':
expect(lhs).to.be.false;
break;
case 'isJson':
expect(lhs).to.be.json;
break;
case 'isNumber':
expect(lhs).to.be.a('number');
break;
case 'isString':
expect(lhs).to.be.a('string');
break;
case 'isBoolean':
expect(lhs).to.be.a('boolean');
break;
default:
expect(lhs).to.equal(rhs);
break;
}
assertionResults.push({
uid: nanoid(),
lhsExpr,
rhsExpr,
rhsOperand,
operator,
status: 'pass'
});
} catch (err) {
assertionResults.push({
uid: nanoid(),
lhsExpr,
rhsExpr,
rhsOperand,
operator,
status: 'fail',
error: err.message
});
}
}
return assertionResults;
}
}
module.exports = AssertRuntime;