diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 9b4fe111..809d84ab 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -4,6 +4,7 @@ const path = require('path'); const { exists, isFile } = require('../utils/filesystem'); const { runSingleRequest } = require('../runner/run-single-request'); const { bruToEnvJson, getEnvVars } = require('../utils/bru'); +const { rpad } = require('../utils/common'); const command = 'run '; const desc = 'Run a request'; @@ -55,11 +56,41 @@ const handler = async function (argv) { const _isFile = await isFile(filename); if(_isFile) { console.log(chalk.yellow('Running Request \n')); - await runSingleRequest(filename, collectionPath, collectionVariables, envVars); - console.log(chalk.green('\nDone!')); + const { + assertionResults, + testResults + } = await runSingleRequest(filename, collectionPath, collectionVariables, envVars); + + // display assertion results and test results summary + const totalAssertions = assertionResults.length; + const passedAssertions = assertionResults.filter((result) => result.status === 'pass').length; + const failedAssertions = totalAssertions - passedAssertions; + + const totalTests = testResults.length; + const passedTests = testResults.filter((result) => result.status === 'pass').length; + const failedTests = totalTests - passedTests; + const maxLength = 12; + + let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`; + if (failedTests > 0) { + assertSummary += `, ${chalk.red(`${failedTests} failed`)}`; + } + assertSummary += `, ${totalTests} total`; + + let testSummary = `${rpad('Assertions:', maxLength)} ${chalk.green(`${passedAssertions} passed`)}`; + if (failedAssertions > 0) { + testSummary += `, ${chalk.red(`${failedAssertions} failed`)}`; + } + testSummary += `, ${totalAssertions} total`; + + console.log("\n" + chalk.bold(assertSummary)); + console.log(chalk.bold(testSummary)); + + console.log(chalk.dim(chalk.grey('Ran all requests.'))); } } catch (err) { - // console.error(err.message); + console.log("Something went wrong"); + console.error(chalk.red(err.message)); } }; diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index b0380548..b89c6c6d 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -5,7 +5,7 @@ const FormData = require('form-data'); const axios = require('axios'); const prepareRequest = require('./prepare-request'); const interpolateVars = require('./interpolate-vars'); -const { ScriptRuntime, TestRuntime, VarsRuntime } = require('@usebruno/js'); +const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@usebruno/js'); const { bruToJson } = require('../utils/bru'); const { stripExtension } = require('../utils/filesystem'); @@ -47,6 +47,8 @@ const runSingleRequest = async function (filename, collectionPath, collectionVar // run request const response = await axios(request); + console.log(chalk.green(stripExtension(filename)) + chalk.dim(` (${response.status} ${response.statusText})`)); + // run post-response vars const postResponseVars = get(bruJson, 'request.vars.res'); if(postResponseVars && postResponseVars.length) { @@ -58,7 +60,24 @@ const runSingleRequest = async function (filename, collectionPath, collectionVar const responseScriptFile = get(bruJson, 'request.script.res'); if(responseScriptFile && responseScriptFile.length) { const scriptRuntime = new ScriptRuntime(); - scriptRuntime.runResponseScript(responseScriptFile, response, envVariables, collectionVariables, collectionPath); + scriptRuntime.runResponseScript(responseScriptFile, request, response, envVariables, collectionVariables, collectionPath); + } + + // run assertions + let assertionResults = []; + const assertions = get(bruJson, 'request.assert'); + if(assertions && assertions.length) { + const assertRuntime = new AssertRuntime(); + assertionResults = assertRuntime.runAssertions(assertions, request, response, envVariables, collectionVariables, collectionPath); + + each(assertionResults, (r) => { + if(r.status === 'pass') { + console.log(chalk.green(` ✓ `) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`)); + } else { + console.log(chalk.red(` ✕ `) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`)); + console.log(chalk.red(` ${r.error}`)); + } + }); } // run tests @@ -70,7 +89,6 @@ const runSingleRequest = async function (filename, collectionPath, collectionVar testResults = get(result, 'results', []); } - console.log(chalk.green(stripExtension(filename)) + chalk.dim(` (${response.status} ${response.statusText})`)); if(testResults && testResults.length) { each(testResults, (testResult) => { if(testResult.status === 'pass') { @@ -80,6 +98,11 @@ const runSingleRequest = async function (filename, collectionPath, collectionVar } }); } + + return { + assertionResults, + testResults + }; } catch (err) { console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`)); } diff --git a/packages/bruno-cli/src/utils/common.js b/packages/bruno-cli/src/utils/common.js new file mode 100644 index 00000000..152c287c --- /dev/null +++ b/packages/bruno-cli/src/utils/common.js @@ -0,0 +1,20 @@ +const lpad = (str, width) => { + let paddedStr = str; + while (paddedStr.length < width) { + paddedStr = ' ' + paddedStr; + } + return paddedStr; +}; + +const rpad = (str, width) => { + let paddedStr = str; + while (paddedStr.length < width) { + paddedStr = paddedStr + ' '; + } + return paddedStr; +} + +module.exports = { + lpad, + rpad +}; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index b675ae11..5a15f4d4 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -130,7 +130,7 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => { const responseScript = get(request, 'script.res'); if(responseScript && responseScript.length) { const scriptRuntime = new ScriptRuntime(); - const result = scriptRuntime.runResponseScript(responseScript, response, envVars, collectionVariables, collectionPath); + const result = scriptRuntime.runResponseScript(responseScript, request, response, envVars, collectionVariables, collectionPath); mainWindow.webContents.send('main:script-environment-update', { environment: result.environment, @@ -319,7 +319,7 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => { const responseScript = get(request, 'script.res'); if(responseScript && responseScript.length) { const scriptRuntime = new ScriptRuntime(); - const result = scriptRuntime.runResponseScript(responseScript, response, envVars, collectionVariables, collectionPath); + const result = scriptRuntime.runResponseScript(responseScript, request, response, envVars, collectionVariables, collectionPath); mainWindow.webContents.send('main:script-environment-update', { environment: result.environment, diff --git a/packages/bruno-js/src/index.js b/packages/bruno-js/src/index.js index a703682d..fe6447cf 100644 --- a/packages/bruno-js/src/index.js +++ b/packages/bruno-js/src/index.js @@ -1,17 +1,11 @@ -const { - ScriptRuntime -} = require('./script-runtime'); - -const { - TestRuntime -} = require('./test-runtime'); - -const { - VarsRuntime -} = require('./vars-runtime'); +const ScriptRuntime = require('./runtime/script-runtime'); +const TestRuntime = require('./runtime/test-runtime'); +const VarsRuntime = require('./runtime/vars-runtime'); +const AssertRuntime = require('./runtime/assert-runtime'); module.exports = { ScriptRuntime, TestRuntime, - VarsRuntime + VarsRuntime, + AssertRuntime }; diff --git a/packages/bruno-js/src/runtime/assert-runtime.js b/packages/bruno-js/src/runtime/assert-runtime.js new file mode 100644 index 00000000..646de880 --- /dev/null +++ b/packages/bruno-js/src/runtime/assert-runtime.js @@ -0,0 +1,226 @@ +const _ = require('lodash'); +const chai = require('chai'); +const { nanoid } = require('nanoid'); +const Bru = require('../bru'); +const BrunoRequest = require('../bruno-request'); +const { evaluateJsExpression, createResponseParser } = require('../utils'); + +const { expect } = chai; + +/** + * Assertion operators + * + * eq : equal to + * neq : not equal to + * like : like + * 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 + * count : count + * 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', 'like', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn', + 'contains', 'notContains', 'count', 'length', 'matches', 'notMatches', + 'startsWith', 'endsWith', 'between', 'isEmpty', 'isNull', 'isUndefined', + 'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean' + ]; + + const [operator, ...rest] = str.trim().split(' '); + const value = rest.join(' '); + + if(operators.includes(operator)) { + return { + operator, + value + }; + } + + return { + operator: 'eq', + value: str + }; +}; + +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 = evaluateJsExpression(rhsOperand, context); + + switch(operator) { + case 'eq': + expect(lhs).to.equal(rhs); + break; + case 'neq': + expect(lhs).to.not.equal(rhs); + break; + case 'like': + expect(lhs).to.match(new RegExp(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 'count': + expect(lhs).to.have.lengthOf(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] = value.split(' '); + 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({ + lhsExpr, + rhsExpr, + rhsOperand, + operator, + status: 'pass' + }); + } + catch (err) { + assertionResults.push({ + lhsExpr, + rhsExpr, + rhsOperand, + operator, + status: 'fail', + error: err.message + }); + } + } + + return assertionResults; + } +} + +module.exports = AssertRuntime; \ No newline at end of file diff --git a/packages/bruno-js/src/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js similarity index 84% rename from packages/bruno-js/src/script-runtime.js rename to packages/bruno-js/src/runtime/script-runtime.js index b8ef04cd..77a2b274 100644 --- a/packages/bruno-js/src/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -1,8 +1,8 @@ const { NodeVM } = require('vm2'); const path = require('path'); -const Bru = require('./bru'); -const BrunoRequest = require('./bruno-request'); -const BrunoResponse = require('./bruno-response'); +const Bru = require('../bru'); +const BrunoRequest = require('../bruno-request'); +const BrunoResponse = require('../bruno-response'); // Inbuilt Library Support const atob = require('atob'); @@ -52,12 +52,14 @@ class ScriptRuntime { }; } - runResponseScript(script, response, environment, collectionVariables, collectionPath) { + runResponseScript(script, request, response, environment, collectionVariables, collectionPath) { const bru = new Bru(environment, collectionVariables); + const req = new BrunoRequest(request); const res = new BrunoResponse(response); const context = { bru, + req, res }; const vm = new NodeVM({ @@ -88,6 +90,4 @@ class ScriptRuntime { } } -module.exports = { - ScriptRuntime -}; \ No newline at end of file +module.exports = ScriptRuntime; diff --git a/packages/bruno-js/src/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js similarity index 85% rename from packages/bruno-js/src/test-runtime.js rename to packages/bruno-js/src/runtime/test-runtime.js index f1940d18..cbb1050f 100644 --- a/packages/bruno-js/src/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -1,11 +1,11 @@ const { NodeVM } = require('vm2'); const chai = require('chai'); const path = require('path'); -const Bru = require('./bru'); -const BrunoRequest = require('./bruno-request'); -const BrunoResponse = require('./bruno-response'); -const Test = require('./test'); -const TestResults = require('./test-results'); +const Bru = require('../bru'); +const BrunoRequest = require('../bruno-request'); +const BrunoResponse = require('../bruno-response'); +const Test = require('../test'); +const TestResults = require('../test-results'); // Inbuilt Library Support const atob = require('atob'); @@ -67,6 +67,4 @@ class TestRuntime { } } -module.exports = { - TestRuntime -}; +module.exports = TestRuntime; diff --git a/packages/bruno-js/src/vars-runtime.js b/packages/bruno-js/src/runtime/vars-runtime.js similarity index 91% rename from packages/bruno-js/src/vars-runtime.js rename to packages/bruno-js/src/runtime/vars-runtime.js index f4d5e0c8..fdc07d38 100644 --- a/packages/bruno-js/src/vars-runtime.js +++ b/packages/bruno-js/src/runtime/vars-runtime.js @@ -1,7 +1,7 @@ const _ = require('lodash'); -const Bru = require('./bru'); -const BrunoRequest = require('./bruno-request'); -const { evaluateJsExpression, createResponseParser } = require('./utils'); +const Bru = require('../bru'); +const BrunoRequest = require('../bruno-request'); +const { evaluateJsExpression, createResponseParser } = require('../utils'); class VarsRuntime { runPreRequestVars(vars, request, envVariables, collectionVariables, collectionPath) { @@ -59,6 +59,4 @@ class VarsRuntime { } } -module.exports = { - VarsRuntime -}; \ No newline at end of file +module.exports = VarsRuntime; diff --git a/packages/bruno-js/src/test-results.js b/packages/bruno-js/src/test-results.js index 6f65d9b7..0d49c6ea 100644 --- a/packages/bruno-js/src/test-results.js +++ b/packages/bruno-js/src/test-results.js @@ -1,4 +1,4 @@ -const {nanoid} = require('nanoid'); +const { nanoid } = require('nanoid'); class TestResults { constructor() { diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index 19355862..0f456325 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -5,10 +5,18 @@ const evaluateJsExpression = (expression, context) => { return fn(...Object.values(context)); }; -const createResponseParser = (res = {}) => (expr) => { - const output = jsonQuery(expr, { data: res.data }); +const createResponseParser = (response = {}) => { + const res = (expr) => { + const output = jsonQuery(expr, { data: response.data }); + return output ? output.value : null; + } - return output ? output.value : null; + res.status = response.status; + res.statusText = response.statusText; + res.headers = response.headers; + res.body = response.data; + + return res; }; module.exports = {