diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index e2205c1f7..bf5cc766d 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -18,6 +18,7 @@ const constants = require('../constants'); const { findItemInCollection, createCollectionJsonFromPathname, getCallStack, FORMAT_CONFIG } = require('../utils/collection'); const { hasExecutableTestInScript } = require('../utils/request'); const { createSkippedFileResults } = require('../utils/run'); +const { sanitizeResultsForReporter } = require('../utils/sanitize-results'); const { getSystemProxy } = require('@usebruno/requests'); const command = 'run [paths...]'; const desc = 'Run one or more requests/folders'; @@ -200,6 +201,21 @@ const builder = async (yargs) => { description: 'Skip specific headers from the reporter output', default: [] }) + .option('reporter-skip-request-body', { + type: 'boolean', + description: 'Omit request body from the reporter output', + default: false + }) + .option('reporter-skip-response-body', { + type: 'boolean', + description: 'Omit response body from the reporter output', + default: false + }) + .option('reporter-skip-body', { + type: 'boolean', + description: 'Omit both request and response bodies from the reporter output', + default: false + }) .option('client-cert-config', { type: 'string', description: 'Path to the Client certificate config file used for securing the connection in the request' @@ -232,6 +248,9 @@ const builder = async (yargs) => { .example('$0 run folder -r', 'Run all requests in a folder recursively') .example('$0 run request.bru folder', 'Run a request and all requests in a folder') .example('$0 run --reporter-skip-all-headers', 'Run all requests in a folder recursively with omitted headers from the reporter output') + .example('$0 run --reporter-skip-request-body', 'Run all requests with request bodies omitted from the reporter output') + .example('$0 run --reporter-skip-response-body', 'Run all requests with response bodies omitted from the reporter output') + .example('$0 run --reporter-skip-body', 'Run all requests with both request and response bodies omitted from the reporter output') .example( '$0 run --reporter-skip-headers "Authorization"', 'Run all requests in a folder recursively with skipped headers from the reporter output' @@ -306,6 +325,9 @@ const handler = async function (argv) { bail, reporterSkipAllHeaders, reporterSkipHeaders, + reporterSkipRequestBody, + reporterSkipResponseBody, + reporterSkipBody, clientCertConfig, noproxy, delay, @@ -686,35 +708,12 @@ const handler = async function (argv) { path: result.test?.filename || path.relative(collectionPath, pathname) }); - if (reporterSkipAllHeaders) { - results.forEach((result) => { - result.request.headers = {}; - result.response.headers = {}; - }); - } - - const deleteHeaderIfExists = (headers, header) => { - Object.keys(headers).forEach((key) => { - if (key.toLowerCase() === header.toLowerCase()) { - delete headers[key]; - } - }); - }; - - if (reporterSkipHeaders?.length) { - results.forEach((result) => { - if (result.request?.headers) { - reporterSkipHeaders.forEach((header) => { - deleteHeaderIfExists(result.request.headers, header); - }); - } - if (result.response?.headers) { - reporterSkipHeaders.forEach((header) => { - deleteHeaderIfExists(result.response.headers, header); - }); - } - }); - } + sanitizeResultsForReporter(results, { + skipAllHeaders: reporterSkipAllHeaders, + skipHeaders: reporterSkipHeaders, + skipRequestBody: reporterSkipRequestBody || reporterSkipBody, + skipResponseBody: reporterSkipResponseBody || reporterSkipBody + }); // bail if option is set and there is a failure if (bail) { diff --git a/packages/bruno-cli/src/utils/sanitize-results.js b/packages/bruno-cli/src/utils/sanitize-results.js new file mode 100644 index 000000000..559d72504 --- /dev/null +++ b/packages/bruno-cli/src/utils/sanitize-results.js @@ -0,0 +1,45 @@ +const deleteHeaderIfExists = (headers, header) => { + Object.keys(headers).forEach((key) => { + if (key.toLowerCase() === header.toLowerCase()) { + delete headers[key]; + } + }); +}; + +const sanitizeResultsForReporter = (results, { skipAllHeaders = false, skipHeaders = [], skipRequestBody = false, skipResponseBody = false } = {}) => { + if (skipAllHeaders) { + results.forEach((result) => { + result.request.headers = {}; + result.response.headers = {}; + }); + } + + if (skipHeaders?.length) { + results.forEach((result) => { + if (result.request?.headers) { + skipHeaders.forEach((header) => { + deleteHeaderIfExists(result.request.headers, header); + }); + } + if (result.response?.headers) { + skipHeaders.forEach((header) => { + deleteHeaderIfExists(result.response.headers, header); + }); + } + }); + } + + if (skipRequestBody) { + results.forEach((result) => { + delete result.request?.data; + }); + } + + if (skipResponseBody) { + results.forEach((result) => { + delete result.response?.data; + }); + } +}; + +module.exports = { sanitizeResultsForReporter }; diff --git a/packages/bruno-cli/tests/reporters/skip-body.spec.js b/packages/bruno-cli/tests/reporters/skip-body.spec.js new file mode 100644 index 000000000..1efffd57e --- /dev/null +++ b/packages/bruno-cli/tests/reporters/skip-body.spec.js @@ -0,0 +1,184 @@ +const { describe, it, expect } = require('@jest/globals'); +const { generateHtmlReport } = require('@usebruno/common/runner'); + +const { sanitizeResultsForReporter } = require('../../src/utils/sanitize-results'); + +const REQUEST_DATA = { username: 'john', password: 'secret123' }; +const RESPONSE_DATA = { id: 1, username: 'john', email: 'john@example.com' }; + +const createMockResult = () => ({ + test: { filename: 'echo/echo-post.bru' }, + request: { + method: 'POST', + url: 'https://echo.usebruno.com', + headers: { 'content-type': 'application/json' }, + data: { ...REQUEST_DATA } + }, + response: { + status: 200, + statusText: 'OK', + headers: { 'content-type': 'application/json' }, + data: { ...RESPONSE_DATA }, + url: 'https://echo.usebruno.com', + responseTime: 150 + }, + error: null, + status: 'pass', + assertionResults: [ + { lhsExpr: 'res.status', rhsExpr: 'eq 200', status: 'pass' } + ], + testResults: [ + { description: 'should return user data', status: 'pass' } + ], + preRequestTestResults: [], + postResponseTestResults: [], + name: 'echo post', + path: 'echo/echo-post.bru', + runDuration: 0.150 +}); + +describe('reporter-skip-body', () => { + describe('JSON report', () => { + it('should exclude both request and response bodies with --reporter-skip-body', () => { + const results = [createMockResult()]; + // --reporter-skip-body sets both skipRequestBody and skipResponseBody to true + sanitizeResultsForReporter(results, { skipRequestBody: true, skipResponseBody: true }); + const json = JSON.parse(JSON.stringify({ summary: {}, results })); + + expect(json.results[0].request).not.toHaveProperty('data'); + expect(json.results[0].response).not.toHaveProperty('data'); + }); + }); + + describe('HTML report', () => { + const extractEmbeddedData = (htmlString) => { + const match = htmlString.match(/JSON\.parse\(decodeBase64\('([^']+)'\)\)/); + expect(match).not.toBeNull(); + const binary = atob(match[1]); + const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0)); + return JSON.parse(new TextDecoder().decode(bytes)); + }; + + const generateHtml = (results) => generateHtmlReport({ + runnerResults: [{ + iterationIndex: 0, + results, + summary: { totalRequests: 1, passedRequests: 1, failedRequests: 0, errorRequests: 0, skippedRequests: 0, totalAssertions: 1, passedAssertions: 1, failedAssertions: 0, totalTests: 1, passedTests: 1, failedTests: 0 } + }], + version: 'usebruno v1.16.0', + environment: null, + runCompletionTime: '2024-01-15T14:30:45.123Z' + }); + + it('should exclude both bodies from HTML report with --reporter-skip-body', () => { + const results = [createMockResult()]; + sanitizeResultsForReporter(results, { skipRequestBody: true, skipResponseBody: true }); + const embedded = extractEmbeddedData(generateHtml(results)); + const result = embedded.results[0].results[0]; + + expect(result.request).not.toHaveProperty('data'); + expect(result.response).not.toHaveProperty('data'); + }); + }); +}); + +describe('reporter-skip-request-body and reporter-skip-response-body', () => { + // --- JSON Report --- + describe('JSON report', () => { + it('should include both bodies by default', () => { + const results = [createMockResult()]; + const json = JSON.parse(JSON.stringify({ summary: {}, results })); + + expect(json.results[0].request.data).toEqual(REQUEST_DATA); + expect(json.results[0].response.data).toEqual(RESPONSE_DATA); + }); + + it('should exclude only request body with --reporter-skip-request-body', () => { + const results = [createMockResult()]; + sanitizeResultsForReporter(results, { skipRequestBody: true }); + const json = JSON.parse(JSON.stringify({ summary: {}, results })); + + expect(json.results[0].request).not.toHaveProperty('data'); + expect(json.results[0].response.data).toEqual(RESPONSE_DATA); + }); + + it('should exclude only response body with --reporter-skip-response-body', () => { + const results = [createMockResult()]; + sanitizeResultsForReporter(results, { skipResponseBody: true }); + const json = JSON.parse(JSON.stringify({ summary: {}, results })); + + expect(json.results[0].request.data).toEqual(REQUEST_DATA); + expect(json.results[0].response).not.toHaveProperty('data'); + }); + + it('should exclude both bodies when both flags are used', () => { + const results = [createMockResult()]; + sanitizeResultsForReporter(results, { skipRequestBody: true, skipResponseBody: true }); + const json = JSON.parse(JSON.stringify({ summary: {}, results })); + + expect(json.results[0].request).not.toHaveProperty('data'); + expect(json.results[0].response).not.toHaveProperty('data'); + }); + }); + + // --- HTML Report --- + describe('HTML report', () => { + const extractEmbeddedData = (htmlString) => { + const match = htmlString.match(/JSON\.parse\(decodeBase64\('([^']+)'\)\)/); + expect(match).not.toBeNull(); + const binary = atob(match[1]); + const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0)); + return JSON.parse(new TextDecoder().decode(bytes)); + }; + + const generateHtml = (results) => generateHtmlReport({ + runnerResults: [{ + iterationIndex: 0, + results, + summary: { totalRequests: 1, passedRequests: 1, failedRequests: 0, errorRequests: 0, skippedRequests: 0, totalAssertions: 1, passedAssertions: 1, failedAssertions: 0, totalTests: 1, passedTests: 1, failedTests: 0 } + }], + version: 'usebruno v1.16.0', + environment: null, + runCompletionTime: '2024-01-15T14:30:45.123Z' + }); + + it('should include both bodies by default', () => { + const results = [createMockResult()]; + const embedded = extractEmbeddedData(generateHtml(results)); + const result = embedded.results[0].results[0]; + + expect(result.request).toHaveProperty('data'); + expect(result.response).toHaveProperty('data'); + }); + + it('should exclude only request body with --reporter-skip-request-body', () => { + const results = [createMockResult()]; + sanitizeResultsForReporter(results, { skipRequestBody: true }); + const embedded = extractEmbeddedData(generateHtml(results)); + const result = embedded.results[0].results[0]; + + expect(result.request).not.toHaveProperty('data'); + expect(result.response).toHaveProperty('data'); + }); + + it('should exclude only response body with --reporter-skip-response-body', () => { + const results = [createMockResult()]; + sanitizeResultsForReporter(results, { skipResponseBody: true }); + const embedded = extractEmbeddedData(generateHtml(results)); + const result = embedded.results[0].results[0]; + + expect(result.request).toHaveProperty('data'); + expect(result.response).not.toHaveProperty('data'); + }); + + it('should exclude both bodies when both flags are used', () => { + const results = [createMockResult()]; + sanitizeResultsForReporter(results, { skipRequestBody: true, skipResponseBody: true }); + const embedded = extractEmbeddedData(generateHtml(results)); + const result = embedded.results[0].results[0]; + + expect(result.request).not.toHaveProperty('data'); + expect(result.response).not.toHaveProperty('data'); + }); + }); +});