diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 7c096e710..86a9e0ebd 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -29,5 +29,7 @@ jobs: run: npm run test --workspace=packages/bruno-app - name: Test Package bruno-js run: npm run test --workspace=packages/bruno-js + - name: Test Package bruno-cli + run: npm run test --workspace=packages/bruno-cli - name: Test Package bruno-electron run: npm run test --workspace=packages/bruno-electron diff --git a/packages/bruno-cli/examples/report.json b/packages/bruno-cli/examples/report.json index 3dfa641f2..4cb7586b1 100644 --- a/packages/bruno-cli/examples/report.json +++ b/packages/bruno-cli/examples/report.json @@ -1,8 +1,11 @@ { "summary": { + "totalRequests": 10, + "passedRequests": 10, + "failedRequests": 0, "totalAssertions": 4, - "passedAssertions": 4, - "failedAssertions": 0, + "passedAssertions": 0, + "failedAssertions": 4, "totalTests": 0, "passedTests": 0, "failedTests": 0 @@ -11,53 +14,33 @@ { "request": { "method": "GET", - "url": "http://localhost:8080/test/v4", + "url": "http://localhost:3000/test/v4", "headers": {} }, "response": { - "status": 200, - "statusText": "OK", + "status": 404, + "statusText": "Not Found", "headers": { "x-powered-by": "Express", - "content-type": "application/json; charset=utf-8", - "content-length": "497", - "etag": "W/\"1f1-08gGpUcq2NTnMCVT5AuXxQ0DzGE\"", - "date": "Mon, 25 Sep 2023 21:43:02 GMT", + "content-security-policy": "default-src 'none'", + "x-content-type-options": "nosniff", + "content-type": "text/html; charset=utf-8", + "content-length": "146", + "date": "Fri, 29 Sep 2023 00:37:50 GMT", "connection": "close" }, - "data": { - "path": "/test/v4", - "headers": { - "accept": "application/json, text/plain, */*", - "user-agent": "axios/1.5.0", - "accept-encoding": "gzip, compress, deflate, br", - "host": "localhost:8080", - "connection": "close" - }, - "method": "GET", - "body": "", - "fresh": false, - "hostname": "localhost", - "ip": "", - "ips": [], - "protocol": "http", - "query": {}, - "subdomains": [], - "xhr": false, - "os": { - "hostname": "05512cb2102c" - }, - "connection": {} - } + "data": "\n\n
\n\nCannot GET /test/v4\n\n\n" }, + "error": null, "assertionResults": [ { - "uid": "mTrKBl5YU6jiAVG-phKT4", + "uid": "oidgfXLiyD8Jv0NBAHUHF", "lhsExpr": "res.status", "rhsExpr": "200", "rhsOperand": "200", "operator": "eq", - "status": "pass" + "status": "fail", + "error": "expected 404 to equal 200" } ], "testResults": [] @@ -65,53 +48,33 @@ { "request": { "method": "GET", - "url": "http://localhost:8080/test/v2", + "url": "http://localhost:3000/test/v2", "headers": {} }, "response": { - "status": 200, - "statusText": "OK", + "status": 404, + "statusText": "Not Found", "headers": { "x-powered-by": "Express", - "content-type": "application/json; charset=utf-8", - "content-length": "497", - "etag": "W/\"1f1-lMqxZgVOJiQXjF5yk3AFEU8O9Ro\"", - "date": "Mon, 25 Sep 2023 21:43:02 GMT", + "content-security-policy": "default-src 'none'", + "x-content-type-options": "nosniff", + "content-type": "text/html; charset=utf-8", + "content-length": "146", + "date": "Fri, 29 Sep 2023 00:37:50 GMT", "connection": "close" }, - "data": { - "path": "/test/v2", - "headers": { - "accept": "application/json, text/plain, */*", - "user-agent": "axios/1.5.0", - "accept-encoding": "gzip, compress, deflate, br", - "host": "localhost:8080", - "connection": "close" - }, - "method": "GET", - "body": "", - "fresh": false, - "hostname": "localhost", - "ip": "", - "ips": [], - "protocol": "http", - "query": {}, - "subdomains": [], - "xhr": false, - "os": { - "hostname": "05512cb2102c" - }, - "connection": {} - } + "data": "\n\n\n\n
Cannot GET /test/v2\n\n\n" }, + "error": null, "assertionResults": [ { - "uid": "XsjjGx9cjt5t8tE_t69ZB", + "uid": "IgliYuHd9wKp6JNyqyHFK", "lhsExpr": "res.status", "rhsExpr": "200", "rhsOperand": "200", "operator": "eq", - "status": "pass" + "status": "fail", + "error": "expected 404 to equal 200" } ], "testResults": [] @@ -119,53 +82,33 @@ { "request": { "method": "GET", - "url": "http://localhost:8080/test/v3", + "url": "http://localhost:3000/test/v3", "headers": {} }, "response": { - "status": 200, - "statusText": "OK", + "status": 404, + "statusText": "Not Found", "headers": { "x-powered-by": "Express", - "content-type": "application/json; charset=utf-8", - "content-length": "497", - "etag": "W/\"1f1-tSiYu0/vWz3r+NYRCaed0aW1waw\"", - "date": "Mon, 25 Sep 2023 21:43:02 GMT", + "content-security-policy": "default-src 'none'", + "x-content-type-options": "nosniff", + "content-type": "text/html; charset=utf-8", + "content-length": "146", + "date": "Fri, 29 Sep 2023 00:37:50 GMT", "connection": "close" }, - "data": { - "path": "/test/v3", - "headers": { - "accept": "application/json, text/plain, */*", - "user-agent": "axios/1.5.0", - "accept-encoding": "gzip, compress, deflate, br", - "host": "localhost:8080", - "connection": "close" - }, - "method": "GET", - "body": "", - "fresh": false, - "hostname": "localhost", - "ip": "", - "ips": [], - "protocol": "http", - "query": {}, - "subdomains": [], - "xhr": false, - "os": { - "hostname": "05512cb2102c" - }, - "connection": {} - } + "data": "\n\n\n\n
Cannot GET /test/v3\n\n\n" }, + "error": null, "assertionResults": [ { - "uid": "i_8MmDMtJA9YfvB_FrW15", + "uid": "u-3sRebrCyuUbZOkwS0z8", "lhsExpr": "res.status", "rhsExpr": "200", "rhsOperand": "200", "operator": "eq", - "status": "pass" + "status": "fail", + "error": "expected 404 to equal 200" } ], "testResults": [] @@ -173,7 +116,7 @@ { "request": { "method": "POST", - "url": "http://localhost:8080/test/v1", + "url": "http://localhost:3000/test/v1", "headers": { "content-type": "application/json" }, @@ -181,57 +124,201 @@ "test": "hello" } }, + "response": { + "status": 404, + "statusText": "Not Found", + "headers": { + "x-powered-by": "Express", + "content-security-policy": "default-src 'none'", + "x-content-type-options": "nosniff", + "content-type": "text/html; charset=utf-8", + "content-length": "147", + "date": "Fri, 29 Sep 2023 00:37:50 GMT", + "connection": "close" + }, + "data": "\n\n\n\n
Cannot POST /test/v1\n\n\n" + }, + "error": null, + "assertionResults": [ + { + "uid": "PpKLK6I38I5_ibw4lZqLb", + "lhsExpr": "res.status", + "rhsExpr": "eq 200", + "rhsOperand": "200", + "operator": "eq", + "status": "fail", + "error": "expected 404 to equal 200" + } + ], + "testResults": [] + }, + { + "request": { + "method": "POST", + "url": "http://localhost:3000/test", + "headers": {} + }, + "response": { + "status": 404, + "statusText": "Not Found", + "headers": { + "x-powered-by": "Express", + "content-security-policy": "default-src 'none'", + "x-content-type-options": "nosniff", + "content-type": "text/html; charset=utf-8", + "content-length": "144", + "date": "Fri, 29 Sep 2023 00:37:50 GMT", + "connection": "close" + }, + "data": "\n\n\n\n
Cannot POST /test\n\n\n" + }, + "error": null, + "assertionResults": [], + "testResults": [] + }, + { + "request": { + "method": "HEAD", + "url": "http://localhost:3000/", + "headers": {} + }, "response": { "status": 200, "statusText": "OK", "headers": { "x-powered-by": "Express", - "content-type": "application/json; charset=utf-8", - "content-length": "623", - "etag": "W/\"26f-ku5QGz4p9f02u79vJIve7JH3QYM\"", - "date": "Mon, 25 Sep 2023 21:43:02 GMT", + "content-type": "text/html; charset=utf-8", + "content-length": "12", + "etag": "W/\"c-Lve95gjOVATpfV8EL5X4nxwjKHE\"", + "date": "Fri, 29 Sep 2023 00:37:50 GMT", "connection": "close" }, + "data": "" + }, + "error": null, + "assertionResults": [], + "testResults": [] + }, + { + "request": { + "method": "POST", + "url": "http://localhost:3000", + "headers": {} + }, + "response": { + "status": 404, + "statusText": "Not Found", + "headers": { + "x-powered-by": "Express", + "content-security-policy": "default-src 'none'", + "x-content-type-options": "nosniff", + "content-type": "text/html; charset=utf-8", + "content-length": "140", + "date": "Fri, 29 Sep 2023 00:37:50 GMT", + "connection": "close" + }, + "data": "\n\n\n\n
Cannot POST /\n\n\n" + }, + "error": null, + "assertionResults": [], + "testResults": [] + }, + { + "request": { + "method": "POST", + "url": "http://localhost:3000/", + "headers": { + "content-type": "multipart/form-data; boundary=--------------------------897965859410704836065858" + }, "data": { - "path": "/test/v1", - "headers": { - "accept": "application/json, text/plain, */*", - "content-type": "application/json", - "user-agent": "axios/1.5.0", - "content-length": "16", - "accept-encoding": "gzip, compress, deflate, br", - "host": "localhost:8080", - "connection": "close" - }, - "method": "POST", - "body": "{\"test\":\"hello\"}", - "fresh": false, - "hostname": "localhost", - "ip": "", - "ips": [], - "protocol": "http", - "query": {}, - "subdomains": [], - "xhr": false, - "os": { - "hostname": "05512cb2102c" - }, - "connection": {}, - "json": { - "test": "hello" - } + "_overheadLength": 103, + "_valueLength": 3, + "_valuesToMeasure": [], + "writable": false, + "readable": true, + "dataSize": 0, + "maxDataSize": 2097152, + "pauseStreams": true, + "_released": true, + "_streams": [], + "_currentStream": null, + "_insideLoop": false, + "_pendingNext": false, + "_boundary": "--------------------------897965859410704836065858", + "_events": {}, + "_eventsCount": 3 } }, - "assertionResults": [ - { - "uid": "hNBSF_GBdSTFHNiyCcOn9", - "lhsExpr": "res.status", - "rhsExpr": "200", - "rhsOperand": "200", - "operator": "eq", - "status": "pass" - } - ], + "response": { + "status": 404, + "statusText": "Not Found", + "headers": { + "x-powered-by": "Express", + "content-security-policy": "default-src 'none'", + "x-content-type-options": "nosniff", + "content-type": "text/html; charset=utf-8", + "content-length": "140", + "date": "Fri, 29 Sep 2023 00:37:50 GMT", + "connection": "close" + }, + "data": "\n\n\n\n
Cannot POST /\n\n\n" + }, + "error": null, + "assertionResults": [], + "testResults": [] + }, + { + "request": { + "method": "POST", + "url": "http://localhost:3000/", + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "data": "a=b&c=d" + }, + "response": { + "status": 404, + "statusText": "Not Found", + "headers": { + "x-powered-by": "Express", + "content-security-policy": "default-src 'none'", + "x-content-type-options": "nosniff", + "content-type": "text/html; charset=utf-8", + "content-length": "140", + "date": "Fri, 29 Sep 2023 00:37:50 GMT", + "connection": "close" + }, + "data": "\n\n\n\n
Cannot POST /\n\n\n" + }, + "error": null, + "assertionResults": [], + "testResults": [] + }, + { + "request": { + "method": "POST", + "url": "http://localhost:3000/test", + "headers": { + "content-type": "text/xml" + }, + "data": "
Cannot POST /test\n\n\n" + }, + "error": null, + "assertionResults": [], "testResults": [] } ] diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index 7cc382688..abb99627b 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -13,6 +13,9 @@ "type": "git", "url": "git+https://github.com/usebruno/bruno.git" }, + "scripts": { + "test": "jest" + }, "files": [ "src", "bin", diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 087b85d48..ff0ab42ea 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -12,17 +12,56 @@ const { dotenvToJson } = require('@usebruno/lang'); const command = 'run [filename]'; const desc = 'Run a request'; -const printRunSummary = (assertionResults, testResults) => { - // 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 printRunSummary = (results) => { + let totalRequests = 0; + let passedRequests = 0; + let failedRequests = 0; + let totalAssertions = 0; + let passedAssertions = 0; + let failedAssertions = 0; + let totalTests = 0; + let passedTests = 0; + let failedTests = 0; + + for (const result of results) { + totalRequests += 1; + totalTests += result.testResults.length; + totalAssertions += result.assertionResults.length; + let anyFailed = false; + let hasAnyTestsOrAssertions = false; + for (const testResult of result.testResults) { + hasAnyTestsOrAssertions = true; + if (testResult.status === 'pass') { + passedTests += 1; + } else { + anyFailed = true; + failedTests += 1; + } + } + for (const assertionResult of result.assertionResults) { + hasAnyTestsOrAssertions = true; + if (assertionResult.status === 'pass') { + passedAssertions += 1; + } else { + anyFailed = true; + failedAssertions += 1; + } + } + if (!hasAnyTestsOrAssertions && result.error) { + failedRequests += 1; + } else { + passedRequests += 1; + } + } - const totalTests = testResults.length; - const passedTests = testResults.filter((result) => result.status === 'pass').length; - const failedTests = totalTests - passedTests; const maxLength = 12; + let requestSummary = `${rpad('Requests:', maxLength)} ${chalk.green(`${passedRequests} passed`)}`; + if (failedRequests > 0) { + requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`; + } + requestSummary += `, ${totalRequests} total`; + let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`; if (failedTests > 0) { assertSummary += `, ${chalk.red(`${failedTests} failed`)}`; @@ -35,10 +74,14 @@ const printRunSummary = (assertionResults, testResults) => { } testSummary += `, ${totalAssertions} total`; - console.log('\n' + chalk.bold(assertSummary)); + console.log('\n' + chalk.bold(requestSummary)); + console.log(chalk.bold(assertSummary)); console.log(chalk.bold(testSummary)); return { + totalRequests, + passedRequests, + failedRequests, totalAssertions, passedAssertions, failedAssertions, @@ -255,9 +298,7 @@ const handler = async function (argv) { } const _isFile = await isFile(filename); - let assertionResults = []; - let testResults = []; - let testrunResults = []; + let results = []; let bruJsons = []; @@ -311,16 +352,10 @@ const handler = async function (argv) { brunoConfig ); - if (result) { - testrunResults.push(result); - const { assertionResults: _assertionResults, testResults: _testResults } = result; - - assertionResults = assertionResults.concat(_assertionResults); - testResults = testResults.concat(_testResults); - } + results.push(result); } - const summary = printRunSummary(assertionResults, testResults); + const summary = printRunSummary(results); console.log(chalk.dim(chalk.grey('Ran all requests.'))); if (outputPath && outputPath.length) { @@ -333,14 +368,14 @@ const handler = async function (argv) { const outputJson = { summary, - results: testrunResults + results }; fs.writeFileSync(outputPath, JSON.stringify(outputJson, null, 2)); console.log(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`))); } - if (summary.failedAssertions > 0 || summary.failedTests > 0) { + if (summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) { process.exit(1); } } catch (err) { @@ -354,5 +389,6 @@ module.exports = { command, desc, builder, - handler + handler, + printRunSummary }; diff --git a/packages/bruno-cli/src/commands/run.test.js b/packages/bruno-cli/src/commands/run.test.js new file mode 100644 index 000000000..979aec5a3 --- /dev/null +++ b/packages/bruno-cli/src/commands/run.test.js @@ -0,0 +1,67 @@ +const { describe, it, expect } = require('@jest/globals'); + +const { printRunSummary } = require('./run'); + +describe('printRunSummary', () => { + // Suppress console.log output + jest.spyOn(console, 'log').mockImplementation(() => {}); + + it('should produce the correct summary for a successful run', () => { + const results = [ + { + testResults: [{ status: 'pass' }, { status: 'pass' }, { status: 'pass' }], + assertionResults: [{ status: 'pass' }, { status: 'pass' }], + error: null + }, + { + testResults: [{ status: 'pass' }, { status: 'pass' }], + assertionResults: [{ status: 'pass' }, { status: 'pass' }, { status: 'pass' }], + error: null + } + ]; + + const summary = printRunSummary(results); + + expect(summary.totalRequests).toBe(2); + expect(summary.passedRequests).toBe(2); + expect(summary.failedRequests).toBe(0); + expect(summary.totalAssertions).toBe(5); + expect(summary.passedAssertions).toBe(5); + expect(summary.failedAssertions).toBe(0); + expect(summary.totalTests).toBe(5); + expect(summary.passedTests).toBe(5); + expect(summary.failedTests).toBe(0); + }); + + it('should produce the correct summary for a failed run', () => { + const results = [ + { + testResults: [{ status: 'fail' }, { status: 'pass' }, { status: 'pass' }], + assertionResults: [{ status: 'pass' }, { status: 'fail' }], + error: null + }, + { + testResults: [{ status: 'pass' }, { status: 'fail' }], + assertionResults: [{ status: 'pass' }, { status: 'fail' }, { status: 'fail' }], + error: null + }, + { + testResults: [], + assertionResults: [], + error: new Error('Request failed') + } + ]; + + const summary = printRunSummary(results); + + expect(summary.totalRequests).toBe(3); + expect(summary.passedRequests).toBe(2); + expect(summary.failedRequests).toBe(1); + expect(summary.totalAssertions).toBe(5); + expect(summary.passedAssertions).toBe(2); + expect(summary.failedAssertions).toBe(3); + expect(summary.totalTests).toBe(5); + expect(summary.passedTests).toBe(3); + expect(summary.failedTests).toBe(2); + }); +}); diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 1f9b6ba41..79435785e 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -20,9 +20,9 @@ const runSingleRequest = async function ( processEnvVars, brunoConfig ) { - let request; - try { + let request; + request = prepareRequest(bruJson.request); // make axios work in node using form data @@ -122,8 +122,34 @@ const runSingleRequest = async function ( request.data = qs.stringify(request.data); } - // run request - const response = await axios(request); + let response; + try { + // run request + response = await axios(request); + } catch (err) { + if (err && err.response) { + response = err.response; + } else { + console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`)); + return { + request: { + method: request.method, + url: request.url, + headers: request.headers, + data: request.data + }, + response: { + status: null, + statusText: null, + headers: null, + data: null + }, + error: err.message, + assertionResults: [], + testResults: [] + }; + } + } console.log(chalk.green(stripExtension(filename)) + chalk.dim(` (${response.status} ${response.statusText})`)); @@ -223,100 +249,28 @@ const runSingleRequest = async function ( headers: response.headers, data: response.data }, + error: null, assertionResults, testResults }; } catch (err) { - if (err && err.response) { - console.log( - chalk.green(stripExtension(filename)) + chalk.dim(` (${err.response.status} ${err.response.statusText})`) - ); - - // run post-response vars - const postResponseVars = get(bruJson, 'request.vars.res'); - if (postResponseVars && postResponseVars.length) { - const varsRuntime = new VarsRuntime(); - varsRuntime.runPostResponseVars( - postResponseVars, - request, - err.response, - envVariables, - collectionVariables, - collectionPath, - processEnvVars - ); - } - - // run post response script - const responseScriptFile = get(bruJson, 'request.script.res'); - if (responseScriptFile && responseScriptFile.length) { - const scriptRuntime = new ScriptRuntime(); - await scriptRuntime.runResponseScript( - responseScriptFile, - request, - err.response, - envVariables, - collectionVariables, - collectionPath, - null, - processEnvVars - ); - } - - // run assertions - let assertionResults = []; - const assertions = get(bruJson, 'request.assertions'); - if (assertions) { - const assertRuntime = new AssertRuntime(); - assertionResults = assertRuntime.runAssertions( - assertions, - request, - err.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 - let testResults = []; - const testFile = get(bruJson, 'request.tests'); - if (typeof testFile === 'string') { - const testRuntime = new TestRuntime(); - const result = await testRuntime.runTests( - testFile, - request, - err.response, - envVariables, - collectionVariables, - collectionPath, - null, - processEnvVars - ); - testResults = get(result, 'results', []); - } - - if (testResults && testResults.length) { - each(testResults, (testResult) => { - if (testResult.status === 'pass') { - console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description)); - } else { - console.log(chalk.red(` ✕ `) + chalk.red(testResult.description)); - } - }); - } - } else { - console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`)); - } + return { + request: { + method: null, + url: null, + headers: null, + data: null + }, + response: { + status: null, + statusText: null, + headers: null, + data: null + }, + error: err.message, + assertionResults: [], + testResults: [] + }; } }; diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index ac2e92208..fb7fc45b4 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -34,7 +34,7 @@ app.on('ready', async () => { nodeIntegration: true, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), - webviewTag: true, + webviewTag: true } });