mirror of
https://github.com/usebruno/bruno.git
synced 2025-07-15 11:45:32 +02:00
336 lines
8.0 KiB
JavaScript
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;
|