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.
This commit is contained in:
Abhishek S Lal
2026-02-25 16:35:48 +05:30
committed by GitHub
parent 0045b16e06
commit 757b635b0d
3 changed files with 257 additions and 29 deletions

View File

@@ -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) {

View File

@@ -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 };

View File

@@ -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');
});
});
});