From 89f418a11418efb2a9df5821aff7d5f0c33180cd Mon Sep 17 00:00:00 2001 From: Thomas Pyle Date: Mon, 25 Sep 2023 17:48:53 -0400 Subject: [PATCH] Adds an option to collect output from cli runs --- packages/bruno-cli/examples/report.json | 238 ++++++++++++++++++ packages/bruno-cli/readme.md | 19 +- packages/bruno-cli/src/commands/run.js | 106 ++++---- .../src/runner/run-single-request.js | 7 + 4 files changed, 321 insertions(+), 49 deletions(-) create mode 100644 packages/bruno-cli/examples/report.json diff --git a/packages/bruno-cli/examples/report.json b/packages/bruno-cli/examples/report.json new file mode 100644 index 0000000000..b933f43db0 --- /dev/null +++ b/packages/bruno-cli/examples/report.json @@ -0,0 +1,238 @@ +{ + "summary": { + "totalAssertions": 4, + "passedAssertions": 4, + "failedAssertions": 0, + "totalTests": 0, + "passedTests": 0, + "failedTests": 0 + }, + "requestResults": [ + { + "request": { + "method": "GET", + "url": "http://localhost:8080/test/v4", + "headers": {} + }, + "response": { + "status": 200, + "statusText": "OK", + "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", + "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": {} + } + }, + "assertionResults": [ + { + "uid": "mTrKBl5YU6jiAVG-phKT4", + "lhsExpr": "res.status", + "rhsExpr": "200", + "rhsOperand": "200", + "operator": "eq", + "status": "pass" + } + ], + "testResults": [] + }, + { + "request": { + "method": "GET", + "url": "http://localhost:8080/test/v2", + "headers": {} + }, + "response": { + "status": 200, + "statusText": "OK", + "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", + "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": {} + } + }, + "assertionResults": [ + { + "uid": "XsjjGx9cjt5t8tE_t69ZB", + "lhsExpr": "res.status", + "rhsExpr": "200", + "rhsOperand": "200", + "operator": "eq", + "status": "pass" + } + ], + "testResults": [] + }, + { + "request": { + "method": "GET", + "url": "http://localhost:8080/test/v3", + "headers": {} + }, + "response": { + "status": 200, + "statusText": "OK", + "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", + "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": {} + } + }, + "assertionResults": [ + { + "uid": "i_8MmDMtJA9YfvB_FrW15", + "lhsExpr": "res.status", + "rhsExpr": "200", + "rhsOperand": "200", + "operator": "eq", + "status": "pass" + } + ], + "testResults": [] + }, + { + "request": { + "method": "POST", + "url": "http://localhost:8080/test/v1", + "headers": { + "content-type": "application/json" + }, + "data": { + "test": "hello" + } + }, + "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", + "connection": "close" + }, + "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" + } + } + }, + "assertionResults": [ + { + "uid": "hNBSF_GBdSTFHNiyCcOn9", + "lhsExpr": "res.status", + "rhsExpr": "200", + "rhsOperand": "200", + "operator": "eq", + "status": "pass" + } + ], + "testResults": [] + } + ] +} diff --git a/packages/bruno-cli/readme.md b/packages/bruno-cli/readme.md index afe921d110..914f79092c 100644 --- a/packages/bruno-cli/readme.md +++ b/packages/bruno-cli/readme.md @@ -5,16 +5,21 @@ With Bruno CLI, you can now run your API collections with ease using simple comm This makes it easier to test your APIs in different environments, automate your testing process, and integrate your API tests with your continuous integration and deployment workflows. ## Installation + To install the Bruno CLI, use the node package manager of your choice, such as NPM: + ```bash npm install -g @usebruno/cli ``` ## Getting started + Navigate to the directory where your API collection resides, and then run: + ```bash bru run ``` + This command will run all the requests in your collection. You can also run a single request by specifying its filename: ```bash @@ -22,25 +27,37 @@ bru run request.bru ``` Or run all requests in a collection's subfolder: + ```bash bru run folder ``` If you need to use an environment, you can specify it with the --env option: + ```bash bru run folder --env Local ``` +If you need to collect the results of your API tests, you can specify the --output option: + +```bash +bru run folder --output results.json +``` + ## Demo + ![demo](assets/images/cli-demo.png) ## Support + If you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/usebruno/bruno) Thank you for using Bruno CLI! ## Changelog + See [here](packages/bruno-cli/changelog.md) ## License -[MIT](license.md) \ No newline at end of file + +[MIT](license.md) diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 62d53246ce..b2aaa8ebd6 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -127,6 +127,11 @@ const builder = async (yargs) => { describe: 'Overwrite a single environment variable, multiple usages possible', type: 'string' }) + .option('output', { + alias: 'o', + describe: 'Path to write JSON results to', + type: 'string' + }) .option('insecure', { type: 'boolean', description: 'Allow insecure server connections' @@ -138,12 +143,16 @@ const builder = async (yargs) => { .example( '$0 run request.bru --env local --env-var secret=xxx', 'Run a request with the environment set to local and overwrite the variable secret with value xxx' + ) + .example( + '$0 run request.bru --output results.json', + 'Run a request and write the results to results.json in the current directory' ); }; const handler = async function (argv) { try { - let { filename, cacert, env, envVar, insecure, r: recursive } = argv; + let { filename, cacert, env, envVar, insecure, r: recursive, output: outputPath } = argv; const collectionPath = process.cwd(); // todo @@ -243,36 +252,24 @@ const handler = async function (argv) { } const _isFile = await isFile(filename); + let assertionResults = []; + let testResults = []; + let requestResults = []; + + let bruJsons = []; + if (_isFile) { console.log(chalk.yellow('Running Request \n')); const bruContent = fs.readFileSync(filename, 'utf8'); const bruJson = bruToJson(bruContent); - const result = await runSingleRequest( - filename, - bruJson, - collectionPath, - collectionVariables, - envVars, - processEnvVars - ); - - if (result) { - const { assertionResults, testResults } = result; - - const summary = printRunSummary(assertionResults, testResults); - console.log(chalk.dim(chalk.grey('Done.'))); - - if (summary.failedAssertions > 0 || summary.failedTests > 0) { - process.exit(1); - } - } else { - process.exit(1); - } + bruJsons.push({ + bruFilepath: filename, + bruJson + }); } const _isDirectory = await isDirectory(filename); if (_isDirectory) { - let bruJsons = []; if (!recursive) { console.log(chalk.yellow('Running Folder \n')); const files = fs.readdirSync(filename); @@ -287,8 +284,6 @@ const handler = async function (argv) { bruJson }); } - - // order requests by sequence bruJsons.sort((a, b) => { const aSequence = a.bruJson.seq || 0; const bSequence = b.bruJson.seq || 0; @@ -299,35 +294,50 @@ const handler = async function (argv) { bruJsons = getBruFilesRecursively(filename); } + } - let assertionResults = []; - let testResults = []; - - for (const iter of bruJsons) { - const { bruFilepath, bruJson } = iter; - const result = await runSingleRequest( - bruFilepath, - bruJson, - collectionPath, - collectionVariables, - envVars, - processEnvVars - ); - - if (result) { - const { assertionResults: _assertionResults, testResults: _testResults } = result; - - assertionResults = assertionResults.concat(_assertionResults); - testResults = testResults.concat(_testResults); - } + for (const iter of bruJsons) { + const { bruFilepath, bruJson } = iter; + const result = await runSingleRequest( + bruFilepath, + bruJson, + collectionPath, + collectionVariables, + envVars, + processEnvVars + ); + + if (result) { + requestResults.push(result); + const { assertionResults: _assertionResults, testResults: _testResults } = result; + + assertionResults = assertionResults.concat(_assertionResults); + testResults = testResults.concat(_testResults); } + } - const summary = printRunSummary(assertionResults, testResults); - console.log(chalk.dim(chalk.grey('Ran all requests.'))); + const summary = printRunSummary(assertionResults, testResults); + console.log(chalk.dim(chalk.grey('Ran all requests.'))); - if (summary.failedAssertions > 0 || summary.failedTests > 0) { + if (outputPath && outputPath.length) { + const outputDir = path.dirname(outputPath); + const outputDirExists = await exists(outputDir); + if (!outputDirExists) { + console.error(chalk.red(`Output directory ${outputDir} does not exist`)); process.exit(1); } + + const outputJson = { + summary, + requestResults + }; + + 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) { + process.exit(1); } } catch (err) { console.log('Something went wrong'); diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 50bb7d546a..3f267fef90 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -171,6 +171,13 @@ const runSingleRequest = async function ( } return { + request: request, + response: { + status: response.status, + statusText: response.statusText, + headers: response.headers, + data: response.data + }, assertionResults, testResults };