From 757b635b0daa1635a3de47feb365be6a7adb431e Mon Sep 17 00:00:00 2001 From: Abhishek S Lal Date: Wed, 25 Feb 2026 16:35:48 +0530 Subject: [PATCH] feat: add options to skip request and response bodies in reporter output (#7114) * feat: add options to skip request and response bodies in reporter output - Introduced `--reporter-skip-request-body` and `--reporter-skip-response-body` flags to omit respective bodies from the reporter output. - Updated examples in the CLI documentation to reflect new options. - Refactored result sanitization to handle new flags. * feat: add shorthand option to skip both request and response bodies in reporter output - Introduced `--reporter-skip-body` as a shorthand for omitting both request and response bodies from the reporter output. - Updated CLI documentation examples to include the new shorthand option. - Adjusted result sanitization to accommodate the new option. * refactor: simplify documentation and tests for reporter-skip-body option - Updated the description of the `--reporter-skip-body` option to remove redundancy. - Removed outdated shorthand references from the test suite for clarity. - Cleaned up examples in the CLI documentation to focus on the current functionality. * fix: handle optional chaining for request and response properties in result sanitization - Updated the `sanitizeResultsForReporter` function to use optional chaining when accessing request and response headers and data. - This change prevents potential errors when these properties are undefined. * test: enhance reporter-skip-body tests for JSON and HTML outputs - Added comprehensive tests for the `--reporter-skip-request-body` and `--reporter-skip-response-body` options in both JSON and HTML report formats. - Verified that the appropriate request and response bodies are included or excluded based on the specified flags. - Improved test coverage for scenarios where both flags are used simultaneously. * fix: remove optional chaining for request and response headers in result sanitization - Updated the `sanitizeResultsForReporter` function to directly assign empty objects to request and response headers, ensuring consistent behavior regardless of their initial state. - This change simplifies the code and maintains functionality for skipping headers. --- packages/bruno-cli/src/commands/run.js | 57 +++--- .../bruno-cli/src/utils/sanitize-results.js | 45 +++++ .../tests/reporters/skip-body.spec.js | 184 ++++++++++++++++++ 3 files changed, 257 insertions(+), 29 deletions(-) create mode 100644 packages/bruno-cli/src/utils/sanitize-results.js create mode 100644 packages/bruno-cli/tests/reporters/skip-body.spec.js 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'); + }); + }); +});