mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix: add size and duration fields to CLI bru.runRequest() response (#7429)
* fix: add size and duration fields to CLI bru.runRequest() response Add `size` and `duration` fields to the response object in CLI to match GUI behavior, ensuring consistent API for bru.runRequest() across both environments. - `duration` is an alias for `responseTime` for GUI compatibility - `size` is the byte length of the response buffer (0 for errors/skipped) Fixes #7352 * fix: address PR review feedback for CLI response consistency - Coerce responseTime header to number (was string from headers.get()) - Add comment explaining duration vs responseTime difference between GUI (wall-clock) and CLI (approximation using responseTime) - Add integration tests for duration/size fields across skipped, success, and network error response paths * fix: add missing setupProxyAgents mock in response-fields test The success path calls setupProxyAgents which was missing from the proxy-util mock, causing CI failure. --------- Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
This commit is contained in:
committed by
GitHub
parent
7ddd2d3f17
commit
765c9f1060
@@ -161,7 +161,9 @@ const runSingleRequest = async function (
|
||||
status: 'skipped',
|
||||
statusText: errorMsg,
|
||||
data: null,
|
||||
responseTime: 0
|
||||
responseTime: 0,
|
||||
duration: 0,
|
||||
size: 0
|
||||
},
|
||||
error: null,
|
||||
status: 'skipped',
|
||||
@@ -256,7 +258,9 @@ const runSingleRequest = async function (
|
||||
status: 'skipped',
|
||||
statusText: 'request skipped via pre-request script',
|
||||
data: null,
|
||||
responseTime: 0
|
||||
responseTime: 0,
|
||||
duration: 0,
|
||||
size: 0
|
||||
},
|
||||
error: null,
|
||||
status: 'skipped',
|
||||
@@ -307,7 +311,9 @@ const runSingleRequest = async function (
|
||||
headers: null,
|
||||
data: null,
|
||||
url: null,
|
||||
responseTime: 0
|
||||
responseTime: 0,
|
||||
duration: 0,
|
||||
size: 0
|
||||
},
|
||||
error: error?.message || 'An error occurred while executing the pre-request script.',
|
||||
status: 'error',
|
||||
@@ -667,7 +673,7 @@ const runSingleRequest = async function (
|
||||
response.dataBuffer = dataBuffer;
|
||||
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
responseTime = Number(response.headers.get('request-duration')) || 0;
|
||||
response.headers.delete('request-duration');
|
||||
|
||||
// save cookies if enabled
|
||||
@@ -707,7 +713,9 @@ const runSingleRequest = async function (
|
||||
headers: null,
|
||||
data: null,
|
||||
url: null,
|
||||
responseTime: 0
|
||||
responseTime: 0,
|
||||
duration: 0,
|
||||
size: 0
|
||||
},
|
||||
error: err?.message || err?.errors?.map((e) => e?.message)?.at(0) || err?.code || 'Request Failed!',
|
||||
status: 'error',
|
||||
@@ -902,7 +910,11 @@ const runSingleRequest = async function (
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null,
|
||||
responseTime
|
||||
responseTime,
|
||||
// In the GUI, duration is wall-clock time (timeEnd - timeStart).
|
||||
// In the CLI we use responseTime as a close approximation.
|
||||
duration: responseTime,
|
||||
size: response.dataBuffer ? Buffer.byteLength(response.dataBuffer) : 0
|
||||
},
|
||||
error: null,
|
||||
status: 'pass',
|
||||
@@ -931,7 +943,9 @@ const runSingleRequest = async function (
|
||||
headers: null,
|
||||
data: null,
|
||||
url: null,
|
||||
responseTime: 0
|
||||
responseTime: 0,
|
||||
duration: 0,
|
||||
size: 0
|
||||
},
|
||||
status: 'error',
|
||||
error: err.message,
|
||||
|
||||
193
packages/bruno-cli/tests/runner/response-fields.spec.js
Normal file
193
packages/bruno-cli/tests/runner/response-fields.spec.js
Normal file
@@ -0,0 +1,193 @@
|
||||
const { describe, it, expect, beforeEach } = require('@jest/globals');
|
||||
|
||||
// Mock all heavy dependencies before requiring the module
|
||||
jest.mock('../../src/runner/prepare-request', () => jest.fn());
|
||||
jest.mock('../../src/runner/interpolate-vars', () => jest.fn());
|
||||
jest.mock('../../src/runner/interpolate-string', () => ({
|
||||
interpolateString: jest.fn((s) => s),
|
||||
interpolateObject: jest.fn((o) => o)
|
||||
}));
|
||||
jest.mock('@usebruno/js', () => ({
|
||||
ScriptRuntime: jest.fn(),
|
||||
TestRuntime: jest.fn(),
|
||||
VarsRuntime: jest.fn(),
|
||||
AssertRuntime: jest.fn(),
|
||||
formatErrorWithContext: jest.fn(),
|
||||
SCRIPT_TYPES: { PRE_REQUEST: 'pre-request', POST_RESPONSE: 'post-response', TEST: 'test' }
|
||||
}));
|
||||
jest.mock('../../src/utils/filesystem', () => ({
|
||||
stripExtension: (p) => p.replace(/\.\w+$/, ''),
|
||||
getOptions: jest.fn(() => ({}))
|
||||
}));
|
||||
jest.mock('../../src/utils/bru', () => ({
|
||||
getOptions: jest.fn(() => ({}))
|
||||
}));
|
||||
jest.mock('../../src/utils/axios-instance', () => ({
|
||||
makeAxiosInstance: jest.fn()
|
||||
}));
|
||||
jest.mock('../../src/runner/awsv4auth-helper', () => ({
|
||||
addAwsV4Interceptor: jest.fn(),
|
||||
resolveAwsV4Credentials: jest.fn()
|
||||
}));
|
||||
jest.mock('../../src/utils/proxy-util', () => ({
|
||||
shouldUseProxy: jest.fn(() => false),
|
||||
setupProxyAgents: jest.fn(),
|
||||
PatchedHttpsProxyAgent: jest.fn()
|
||||
}));
|
||||
jest.mock('../../src/utils/common', () => ({
|
||||
parseDataFromResponse: jest.fn((res) => ({
|
||||
data: res.data,
|
||||
dataBuffer: Buffer.from(JSON.stringify(res.data || ''))
|
||||
}))
|
||||
}));
|
||||
jest.mock('../../src/utils/cookies', () => ({
|
||||
getCookieStringForUrl: jest.fn(() => ''),
|
||||
saveCookies: jest.fn()
|
||||
}));
|
||||
jest.mock('../../src/utils/form-data', () => ({
|
||||
createFormData: jest.fn()
|
||||
}));
|
||||
jest.mock('@usebruno/requests', () => ({
|
||||
addDigestInterceptor: jest.fn(),
|
||||
getHttpHttpsAgents: jest.fn(() => ({})),
|
||||
makeAxiosInstance: jest.fn(),
|
||||
getCACertificates: jest.fn(() => ({ caCertificates: [] })),
|
||||
transformProxyConfig: jest.fn(() => ({})),
|
||||
getOrCreateHttpsAgent: jest.fn(() => ({})),
|
||||
getOrCreateHttpAgent: jest.fn(() => ({}))
|
||||
}));
|
||||
jest.mock('../../src/utils/oauth2', () => ({
|
||||
getOAuth2Token: jest.fn(),
|
||||
getFormattedOauth2Credentials: jest.fn()
|
||||
}));
|
||||
jest.mock('../../src/store/tokenStore', () => ({
|
||||
getAll: jest.fn(() => ({})),
|
||||
put: jest.fn(),
|
||||
clearAll: jest.fn()
|
||||
}));
|
||||
|
||||
// Default: no prompt variables detected
|
||||
const mockExtractPromptVariables = jest.fn(() => []);
|
||||
jest.mock('@usebruno/common', () => ({
|
||||
utils: {
|
||||
encodeUrl: jest.fn((u) => u),
|
||||
buildFormUrlEncodedPayload: jest.fn(),
|
||||
extractPromptVariables: mockExtractPromptVariables,
|
||||
isFormData: jest.fn(() => false)
|
||||
}
|
||||
}));
|
||||
|
||||
const prepareRequest = require('../../src/runner/prepare-request');
|
||||
const { makeAxiosInstance } = require('../../src/utils/axios-instance');
|
||||
const { runSingleRequest } = require('../../src/runner/run-single-request');
|
||||
|
||||
const baseItem = {
|
||||
pathname: '/test-collection/request.bru',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'http://example.com/api',
|
||||
headers: [],
|
||||
body: { mode: 'none' },
|
||||
auth: { mode: 'none' },
|
||||
vars: {},
|
||||
script: {},
|
||||
tests: ''
|
||||
}
|
||||
};
|
||||
|
||||
const baseArgs = [
|
||||
baseItem, // item
|
||||
'/test-collection', // collectionPath
|
||||
{}, // runtimeVariables
|
||||
{}, // envVariables
|
||||
{}, // processEnvVars
|
||||
{}, // brunoConfig
|
||||
{}, // collectionRoot
|
||||
'vm2', // runtime
|
||||
{ items: [], pathname: '/test-collection' }, // collection
|
||||
jest.fn(), // runSingleRequestByPathname
|
||||
{} // globalEnvVars
|
||||
];
|
||||
|
||||
describe('runSingleRequest: duration and size fields (issue #7352)', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return duration=0 and size=0 when request is skipped due to prompt variables', async () => {
|
||||
prepareRequest.mockResolvedValue({
|
||||
method: 'GET',
|
||||
url: 'http://example.com/api/test',
|
||||
headers: {},
|
||||
data: null
|
||||
});
|
||||
// Simulate prompt variable detection
|
||||
mockExtractPromptVariables.mockReturnValueOnce(['prompt_var']);
|
||||
|
||||
const result = await runSingleRequest(...baseArgs);
|
||||
|
||||
expect(result.status).toBe('skipped');
|
||||
expect(result.response.duration).toBe(0);
|
||||
expect(result.response.size).toBe(0);
|
||||
expect(result.response.responseTime).toBe(0);
|
||||
expect(typeof result.response.duration).toBe('number');
|
||||
expect(typeof result.response.size).toBe('number');
|
||||
});
|
||||
|
||||
it('should return numeric duration and size on successful request', async () => {
|
||||
const responseBody = JSON.stringify({ message: 'ok' });
|
||||
const mockHeaders = new Map([['request-duration', '253']]);
|
||||
mockHeaders.delete = function (key) { this.delete(key); };
|
||||
// Use a plain object with get/delete to simulate axios headers
|
||||
const headers = {
|
||||
get: (key) => key === 'request-duration' ? '253' : null,
|
||||
delete: jest.fn()
|
||||
};
|
||||
|
||||
prepareRequest.mockResolvedValue({
|
||||
method: 'GET',
|
||||
url: 'http://example.com/api',
|
||||
headers: {},
|
||||
data: null,
|
||||
settings: {}
|
||||
});
|
||||
|
||||
const mockAxios = jest.fn().mockResolvedValue({
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers,
|
||||
data: responseBody,
|
||||
request: { protocol: 'http:', host: 'example.com', path: '/api' }
|
||||
});
|
||||
makeAxiosInstance.mockReturnValue(mockAxios);
|
||||
|
||||
const result = await runSingleRequest(...baseArgs);
|
||||
|
||||
expect(result.status).toBe('pass');
|
||||
expect(result.response.responseTime).toBe(253);
|
||||
expect(result.response.duration).toBe(253);
|
||||
expect(typeof result.response.duration).toBe('number');
|
||||
expect(typeof result.response.size).toBe('number');
|
||||
expect(result.response.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return duration=0 and size=0 on network error', async () => {
|
||||
prepareRequest.mockResolvedValue({
|
||||
method: 'GET',
|
||||
url: 'http://example.com/api',
|
||||
headers: {},
|
||||
data: null,
|
||||
settings: {}
|
||||
});
|
||||
|
||||
const mockAxios = jest.fn().mockRejectedValue(new Error('ECONNREFUSED'));
|
||||
makeAxiosInstance.mockReturnValue(mockAxios);
|
||||
|
||||
const result = await runSingleRequest(...baseArgs);
|
||||
|
||||
expect(result.status).toBe('error');
|
||||
expect(result.response.duration).toBe(0);
|
||||
expect(result.response.size).toBe(0);
|
||||
expect(result.response.responseTime).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user