mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-27 22:54:07 +00:00
feat: add certs and proxy config to bru.sendRequest API (#6988)
* feat: add certs and proxy config to bru.sendRequest API Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: handle URL string argument in bru.sendRequest When bru.sendRequest is called with a plain URL string instead of a config object, the function now normalizes it to { url: string } before processing. This fixes the case where spreading a string created an invalid config object. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add variable interpolation to bru.sendRequest certs and proxy config Interpolate environment variables in clientCertificates and proxy configuration for bru.sendRequest API, enabling use of variables like {{CERT_PATH}} or {{PROXY_HOST}} in certificate paths and proxy settings. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: use interpolateObject for certs and proxy config interpolation - Add interpolateObject to electron's interpolate-string.js using buildCombinedVars pattern (matches CLI implementation) - Simplify cert-utils.js by using interpolateObject instead of manual field-by-field interpolation - Add interpolation for clientCertificates and proxy config in CLI's run-single-request.js for bru.sendRequest Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: add all variable types to sendRequest interpolation options - Add globalEnvVars, collectionVariables, folderVariables, requestVariables to sendRequestInterpolationOptions for complete variable support - Use cached system proxy instead of redundant getSystemProxy() call - Remove duplicate getOptions() call Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: skip CA cert loading when TLS verification is disabled Only load CA certificates when shouldVerifyTls is true, since they are not used for validation when TLS verification is disabled. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,8 @@ const STATIC_API_HINTS = {
|
||||
'bru.setNextRequest(requestName)',
|
||||
'bru.getRequestVar(key)',
|
||||
'bru.runRequest(requestPathName)',
|
||||
'bru.sendRequest(requestConfig)',
|
||||
'bru.sendRequest(requestConfig, callback)',
|
||||
'bru.getAssertionResults()',
|
||||
'bru.getTestResults()',
|
||||
'bru.sleep(ms)',
|
||||
|
||||
@@ -170,6 +170,37 @@ const runSingleRequest = async function (
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = runtime;
|
||||
|
||||
// Build certsAndProxyConfig for bru.sendRequest
|
||||
const options = getOptions();
|
||||
const systemProxyConfig = options['cachedSystemProxy'];
|
||||
const sendRequestInterpolationOptions = {
|
||||
envVars: envVariables,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
globalEnvVars,
|
||||
collectionVariables: request.collectionVariables || {},
|
||||
folderVariables: request.folderVariables || {},
|
||||
requestVariables: request.requestVariables || {}
|
||||
};
|
||||
const rawClientCertificates = get(brunoConfig, 'clientCertificates');
|
||||
const rawProxyConfig = get(brunoConfig, 'proxy', {});
|
||||
const certsAndProxyConfig = {
|
||||
collectionPath,
|
||||
options: {
|
||||
noproxy: get(options, 'noproxy', false),
|
||||
shouldVerifyTls: !get(options, 'insecure', false),
|
||||
shouldUseCustomCaCertificate: !!options['cacert'],
|
||||
customCaCertificateFilePath: options['cacert'],
|
||||
shouldKeepDefaultCaCertificates: !options['ignoreTruststore']
|
||||
},
|
||||
clientCertificates: rawClientCertificates ? interpolateObject(rawClientCertificates, sendRequestInterpolationOptions) : undefined,
|
||||
collectionLevelProxy: transformProxyConfig(interpolateObject(rawProxyConfig, sendRequestInterpolationOptions)),
|
||||
systemProxyConfig
|
||||
};
|
||||
|
||||
// Add certsAndProxyConfig to request object for bru.sendRequest
|
||||
request.certsAndProxyConfig = certsAndProxyConfig;
|
||||
|
||||
// run pre request script
|
||||
const requestScriptFile = get(request, 'script.req');
|
||||
const collectionName = collection?.brunoConfig?.name;
|
||||
@@ -237,7 +268,6 @@ const runSingleRequest = async function (
|
||||
request.url = `http://${request.url}`;
|
||||
}
|
||||
|
||||
const options = getOptions();
|
||||
const insecure = get(options, 'insecure', false);
|
||||
const noproxy = get(options, 'noproxy', false);
|
||||
const cachedSystemProxy = get(options, 'cachedSystemProxy', null);
|
||||
|
||||
@@ -5,7 +5,7 @@ const { getCACertificates } = require('@usebruno/requests');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||
const { getCachedSystemProxy } = require('../../store/system-proxy');
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
const { interpolateString, interpolateObject } = require('./interpolate-string');
|
||||
|
||||
/**
|
||||
* Gets certificates and proxy configuration for a request
|
||||
@@ -155,4 +155,68 @@ const getCertsAndProxyConfig = async ({
|
||||
return { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions };
|
||||
};
|
||||
|
||||
module.exports = { getCertsAndProxyConfig };
|
||||
/**
|
||||
* Builds the certsAndProxyConfig object for bru.sendRequest
|
||||
* This allows bru.sendRequest to use the same proxy/certs config as the main request
|
||||
*/
|
||||
const buildCertsAndProxyConfig = async ({
|
||||
collectionUid,
|
||||
collection,
|
||||
collectionPath,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
request
|
||||
}) => {
|
||||
const brunoConfig = getBrunoConfig(collectionUid, collection);
|
||||
|
||||
// Build interpolation options (same pattern as getCertsAndProxyConfig)
|
||||
const globalEnvironmentVariables = collection.globalEnvironmentVariables || {};
|
||||
const { promptVariables } = collection;
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
|
||||
const interpolationOptions = {
|
||||
globalEnvironmentVariables,
|
||||
collectionVariables,
|
||||
envVars,
|
||||
folderVariables,
|
||||
requestVariables,
|
||||
runtimeVariables,
|
||||
promptVariables,
|
||||
processEnvVars
|
||||
};
|
||||
|
||||
// Build options for getHttpHttpsAgents
|
||||
const options = {
|
||||
noproxy: false,
|
||||
shouldVerifyTls: preferencesUtil.shouldVerifyTls(),
|
||||
shouldUseCustomCaCertificate: preferencesUtil.shouldUseCustomCaCertificate(),
|
||||
customCaCertificateFilePath: preferencesUtil.getCustomCaCertificateFilePath(),
|
||||
shouldKeepDefaultCaCertificates: preferencesUtil.shouldKeepDefaultCaCertificates()
|
||||
};
|
||||
|
||||
// Get client certificates from bruno config and interpolate
|
||||
const rawClientCertificates = get(brunoConfig, 'clientCertificates');
|
||||
const clientCertificates = rawClientCertificates
|
||||
? interpolateObject(rawClientCertificates, interpolationOptions)
|
||||
: undefined;
|
||||
|
||||
// Get proxy config from bruno config and interpolate
|
||||
const collectionProxyConfig = get(brunoConfig, 'proxy', {});
|
||||
const collectionLevelProxy = interpolateObject(collectionProxyConfig, interpolationOptions);
|
||||
|
||||
// Get system proxy config
|
||||
const systemProxyConfig = getCachedSystemProxy();
|
||||
|
||||
return {
|
||||
collectionPath,
|
||||
options,
|
||||
clientCertificates,
|
||||
collectionLevelProxy,
|
||||
systemProxyConfig
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { getCertsAndProxyConfig, buildCertsAndProxyConfig };
|
||||
|
||||
@@ -34,7 +34,7 @@ const { isRequestTagsIncluded } = require('@usebruno/common');
|
||||
const { cookiesStore } = require('../../store/cookies');
|
||||
const registerGrpcEventHandlers = require('./grpc-event-handlers');
|
||||
const { registerWsEventHandlers } = require('./ws-event-handlers');
|
||||
const { getCertsAndProxyConfig } = require('./cert-utils');
|
||||
const { getCertsAndProxyConfig, buildCertsAndProxyConfig } = require('./cert-utils');
|
||||
const { buildFormUrlEncodedPayload, isFormData } = require('@usebruno/common').utils;
|
||||
|
||||
const ERROR_OCCURRED_WHILE_EXECUTING_REQUEST = 'Error occurred while executing the request!';
|
||||
@@ -680,6 +680,20 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
request.signal = abortController.signal;
|
||||
saveCancelToken(cancelTokenUid, abortController);
|
||||
|
||||
// Build certsAndProxyConfig for bru.sendRequest
|
||||
const certsAndProxyConfig = await buildCertsAndProxyConfig({
|
||||
collectionUid,
|
||||
collection,
|
||||
collectionPath,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
request
|
||||
});
|
||||
|
||||
// Add certsAndProxyConfig to request object for bru.sendRequest
|
||||
request.certsAndProxyConfig = certsAndProxyConfig;
|
||||
|
||||
let preRequestScriptResult = null;
|
||||
let preRequestError = null;
|
||||
try {
|
||||
@@ -1288,6 +1302,20 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Build certsAndProxyConfig for bru.sendRequest
|
||||
const certsAndProxyConfig = await buildCertsAndProxyConfig({
|
||||
collectionUid,
|
||||
collection,
|
||||
collectionPath,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
request
|
||||
});
|
||||
|
||||
// Add certsAndProxyConfig to request object for bru.sendRequest
|
||||
request.certsAndProxyConfig = certsAndProxyConfig;
|
||||
|
||||
let preRequestScriptResult;
|
||||
let preRequestError = null;
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { forOwn, cloneDeep } = require('lodash');
|
||||
const { interpolate } = require('@usebruno/common');
|
||||
const { interpolate, interpolateObject: interpolateObjectCommon } = require('@usebruno/common');
|
||||
|
||||
const interpolateString = (str, {
|
||||
const buildCombinedVars = ({
|
||||
globalEnvironmentVariables,
|
||||
collectionVariables,
|
||||
envVars,
|
||||
@@ -11,10 +11,6 @@ const interpolateString = (str, {
|
||||
processEnvVars,
|
||||
promptVariables
|
||||
}) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
processEnvVars = processEnvVars || {};
|
||||
runtimeVariables = runtimeVariables || {};
|
||||
globalEnvironmentVariables = globalEnvironmentVariables || {};
|
||||
@@ -38,8 +34,7 @@ const interpolateString = (str, {
|
||||
});
|
||||
});
|
||||
|
||||
// runtimeVariables take precedence over envVars
|
||||
const combinedVars = {
|
||||
return {
|
||||
...globalEnvironmentVariables,
|
||||
...collectionVariables,
|
||||
...envVars,
|
||||
@@ -53,10 +48,26 @@ const interpolateString = (str, {
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const interpolateString = (str, interpolationOptions) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
const combinedVars = buildCombinedVars(interpolationOptions);
|
||||
return interpolate(str, combinedVars);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
interpolateString
|
||||
/**
|
||||
* Recursively interpolates all string values in an object
|
||||
*/
|
||||
const interpolateObject = (obj, interpolationOptions) => {
|
||||
const combinedVars = buildCombinedVars(interpolationOptions);
|
||||
return interpolateObjectCommon(obj, combinedVars);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
interpolateString,
|
||||
interpolateObject
|
||||
};
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
const { cloneDeep } = require('lodash');
|
||||
const xmlFormat = require('xml-formatter');
|
||||
const { interpolate: _interpolate } = require('@usebruno/common');
|
||||
const { sendRequest } = require('@usebruno/requests').scripting;
|
||||
const { sendRequest, createSendRequest } = require('@usebruno/requests').scripting;
|
||||
const { jar: createCookieJar } = require('@usebruno/requests').cookies;
|
||||
|
||||
const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
class Bru {
|
||||
constructor(runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables) {
|
||||
/**
|
||||
* @param {string} runtime - The runtime environment ('quickjs' or 'nodevm')
|
||||
* @param {object} envVariables - Environment variables
|
||||
* @param {object} runtimeVariables - Runtime variables
|
||||
* @param {object} processEnvVars - Process environment variables
|
||||
* @param {string} collectionPath - Path to the collection
|
||||
* @param {object} collectionVariables - Collection-level variables
|
||||
* @param {object} folderVariables - Folder-level variables
|
||||
* @param {object} requestVariables - Request-level variables
|
||||
* @param {object} globalEnvironmentVariables - Global environment variables
|
||||
* @param {object} oauth2CredentialVariables - OAuth2 credential variables
|
||||
* @param {string} collectionName - Name of the collection
|
||||
* @param {object} promptVariables - Prompt variables
|
||||
* @param {object} certsAndProxyConfig - Configuration for bru.sendRequest (proxy, certs, TLS)
|
||||
* @param {string} certsAndProxyConfig.collectionPath - Path to the collection
|
||||
* @param {object} certsAndProxyConfig.options - TLS and proxy options
|
||||
* @param {object} [certsAndProxyConfig.clientCertificates] - Client certificate configuration
|
||||
* @param {object} [certsAndProxyConfig.collectionLevelProxy] - Collection-level proxy settings
|
||||
* @param {object} [certsAndProxyConfig.systemProxyConfig] - System proxy configuration
|
||||
*/
|
||||
constructor(runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig) {
|
||||
this.envVariables = envVariables || {};
|
||||
this.runtimeVariables = runtimeVariables || {};
|
||||
this.promptVariables = promptVariables || {};
|
||||
@@ -19,7 +39,8 @@ class Bru {
|
||||
this.oauth2CredentialVariables = oauth2CredentialVariables || {};
|
||||
this.collectionPath = collectionPath;
|
||||
this.collectionName = collectionName;
|
||||
this.sendRequest = sendRequest;
|
||||
// Use createSendRequest with config if provided, otherwise use default sendRequest
|
||||
this.sendRequest = certsAndProxyConfig ? createSendRequest(certsAndProxyConfig) : sendRequest;
|
||||
this.runtime = runtime;
|
||||
this.cookies = {
|
||||
jar: () => {
|
||||
|
||||
@@ -256,6 +256,7 @@ class AssertRuntime {
|
||||
}
|
||||
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
const certsAndProxyConfig = request?.certsAndProxyConfig;
|
||||
const bru = new Bru(
|
||||
this.runtime,
|
||||
envVariables,
|
||||
@@ -268,7 +269,8 @@ class AssertRuntime {
|
||||
globalEnvironmentVariables,
|
||||
{},
|
||||
undefined,
|
||||
promptVariables
|
||||
promptVariables,
|
||||
certsAndProxyConfig
|
||||
);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = createResponseParser(response);
|
||||
|
||||
@@ -33,7 +33,8 @@ class ScriptRuntime {
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
const assertionResults = request?.assertionResults || [];
|
||||
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables);
|
||||
const certsAndProxyConfig = request?.certsAndProxyConfig;
|
||||
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig);
|
||||
const req = new BrunoRequest(request);
|
||||
|
||||
// extend bru with result getter methods
|
||||
@@ -128,7 +129,8 @@ class ScriptRuntime {
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
const assertionResults = request?.assertionResults || {};
|
||||
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables);
|
||||
const certsAndProxyConfig = request?.certsAndProxyConfig;
|
||||
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ class TestRuntime {
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
const assertionResults = request?.assertionResults || [];
|
||||
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, {}, collectionName, promptVariables);
|
||||
const certsAndProxyConfig = request?.certsAndProxyConfig;
|
||||
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, {}, collectionName, promptVariables, certsAndProxyConfig);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ class VarsRuntime {
|
||||
}
|
||||
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, undefined, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, undefined, promptVariables);
|
||||
const certsAndProxyConfig = request?.certsAndProxyConfig;
|
||||
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, undefined, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, undefined, promptVariables, certsAndProxyConfig);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = createResponseParser(response);
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as sendRequest } from './send-request';
|
||||
export { default as sendRequest, createSendRequest } from './send-request';
|
||||
|
||||
198
packages/bruno-requests/src/scripting/send-request.spec.ts
Normal file
198
packages/bruno-requests/src/scripting/send-request.spec.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import sendRequest, { createSendRequest } from './send-request';
|
||||
|
||||
jest.mock('../network', () => ({
|
||||
makeAxiosInstance: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock('../utils/http-https-agents', () => ({
|
||||
getHttpHttpsAgents: jest.fn()
|
||||
}));
|
||||
|
||||
import { makeAxiosInstance } from '../network';
|
||||
import { getHttpHttpsAgents } from '../utils/http-https-agents';
|
||||
|
||||
const mockMakeAxiosInstance = makeAxiosInstance as jest.Mock;
|
||||
const mockGetHttpHttpsAgents = getHttpHttpsAgents as jest.Mock;
|
||||
|
||||
describe('sendRequest', () => {
|
||||
let mockAxios: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockAxios = jest.fn();
|
||||
mockMakeAxiosInstance.mockReturnValue(mockAxios);
|
||||
mockGetHttpHttpsAgents.mockResolvedValue({ httpAgent: null, httpsAgent: null });
|
||||
});
|
||||
|
||||
describe('without callback', () => {
|
||||
test('should return response directly', async () => {
|
||||
const mockResponse = { data: 'test', status: 200 };
|
||||
mockAxios.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await sendRequest({ url: 'http://example.com' });
|
||||
|
||||
expect(result).toBe(mockResponse);
|
||||
});
|
||||
|
||||
test('should reject on request error', async () => {
|
||||
const error = new Error('Network error');
|
||||
mockAxios.mockRejectedValue(error);
|
||||
|
||||
await expect(sendRequest({ url: 'http://example.com' })).rejects.toThrow('Network error');
|
||||
});
|
||||
|
||||
test('should handle URL string instead of config object', async () => {
|
||||
const mockResponse = { data: 'pong', status: 200 };
|
||||
mockAxios.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await sendRequest('http://example.com/ping');
|
||||
|
||||
expect(result).toBe(mockResponse);
|
||||
expect(mockAxios).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: 'http://example.com/ping'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with callback', () => {
|
||||
test('should call callback with response and return response', async () => {
|
||||
const mockResponse = { data: 'test', status: 200 };
|
||||
mockAxios.mockResolvedValue(mockResponse);
|
||||
const callback = jest.fn();
|
||||
|
||||
const result = await sendRequest({ url: 'http://example.com' }, callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(null, mockResponse);
|
||||
expect(result).toBe(mockResponse);
|
||||
});
|
||||
|
||||
test('should call callback with error on request failure', async () => {
|
||||
const error = new Error('Network error');
|
||||
mockAxios.mockRejectedValue(error);
|
||||
const callback = jest.fn();
|
||||
|
||||
await sendRequest({ url: 'http://example.com' }, callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(error, null);
|
||||
});
|
||||
|
||||
test('should reject if callback throws on success', async () => {
|
||||
const mockResponse = { data: 'test', status: 200 };
|
||||
mockAxios.mockResolvedValue(mockResponse);
|
||||
const callbackError = new Error('Callback error');
|
||||
const callback = jest.fn().mockRejectedValue(callbackError);
|
||||
|
||||
await expect(sendRequest({ url: 'http://example.com' }, callback)).rejects.toThrow(
|
||||
'Callback error'
|
||||
);
|
||||
});
|
||||
|
||||
test('should reject if callback throws on error', async () => {
|
||||
const requestError = new Error('Network error');
|
||||
mockAxios.mockRejectedValue(requestError);
|
||||
const callbackError = new Error('Callback error');
|
||||
const callback = jest.fn().mockRejectedValue(callbackError);
|
||||
|
||||
await expect(sendRequest({ url: 'http://example.com' }, callback)).rejects.toThrow(
|
||||
'Callback error'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSendRequest', () => {
|
||||
let mockAxios: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockAxios = jest.fn();
|
||||
mockMakeAxiosInstance.mockReturnValue(mockAxios);
|
||||
});
|
||||
|
||||
test('should apply agents from config', async () => {
|
||||
const mockHttpAgent = { name: 'httpAgent' };
|
||||
const mockHttpsAgent = { name: 'httpsAgent' };
|
||||
mockGetHttpHttpsAgents.mockResolvedValue({
|
||||
httpAgent: mockHttpAgent,
|
||||
httpsAgent: mockHttpsAgent
|
||||
});
|
||||
const mockResponse = { data: 'test' };
|
||||
mockAxios.mockResolvedValue(mockResponse);
|
||||
|
||||
const customSendRequest = createSendRequest({ proxyConfig: {} });
|
||||
await customSendRequest({ url: 'https://example.com' });
|
||||
|
||||
expect(mockGetHttpHttpsAgents).toHaveBeenCalledWith({
|
||||
proxyConfig: {},
|
||||
requestUrl: 'https://example.com'
|
||||
});
|
||||
expect(mockAxios).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpAgent: mockHttpAgent,
|
||||
httpsAgent: mockHttpsAgent
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should not override agents if already set in requestConfig', async () => {
|
||||
const configHttpAgent = { name: 'configAgent' };
|
||||
const configHttpsAgent = { name: 'configHttpsAgent' };
|
||||
mockGetHttpHttpsAgents.mockResolvedValue({
|
||||
httpAgent: { name: 'ignored' },
|
||||
httpsAgent: { name: 'ignored' }
|
||||
});
|
||||
mockAxios.mockResolvedValue({ data: 'test' });
|
||||
|
||||
const customSendRequest = createSendRequest({ proxyConfig: {} });
|
||||
await customSendRequest({
|
||||
url: 'https://example.com',
|
||||
httpAgent: configHttpAgent,
|
||||
httpsAgent: configHttpsAgent
|
||||
});
|
||||
|
||||
expect(mockAxios).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
httpAgent: configHttpAgent,
|
||||
httpsAgent: configHttpsAgent
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should not call getHttpHttpsAgents when no config provided', async () => {
|
||||
mockAxios.mockResolvedValue({ data: 'test' });
|
||||
|
||||
const customSendRequest = createSendRequest();
|
||||
await customSendRequest({ url: 'https://example.com' });
|
||||
|
||||
expect(mockGetHttpHttpsAgents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle URL string and apply agents from config', async () => {
|
||||
const mockHttpAgent = { name: 'httpAgent' };
|
||||
const mockHttpsAgent = { name: 'httpsAgent' };
|
||||
mockGetHttpHttpsAgents.mockResolvedValue({
|
||||
httpAgent: mockHttpAgent,
|
||||
httpsAgent: mockHttpsAgent
|
||||
});
|
||||
const mockResponse = { data: 'pong' };
|
||||
mockAxios.mockResolvedValue(mockResponse);
|
||||
|
||||
const customSendRequest = createSendRequest({ collectionPath: '/test' });
|
||||
const result = await customSendRequest('https://example.com/ping');
|
||||
|
||||
expect(result).toBe(mockResponse);
|
||||
expect(mockGetHttpHttpsAgents).toHaveBeenCalledWith({
|
||||
collectionPath: '/test',
|
||||
requestUrl: 'https://example.com/ping'
|
||||
});
|
||||
expect(mockAxios).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: 'https://example.com/ping',
|
||||
httpAgent: mockHttpAgent,
|
||||
httpsAgent: mockHttpsAgent
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,27 +1,76 @@
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { makeAxiosInstance } from '../network';
|
||||
import { getHttpHttpsAgents } from '../utils/http-https-agents';
|
||||
import type { GetHttpHttpsAgentsParams } from '../utils/http-https-agents';
|
||||
|
||||
type T_SendRequestCallback = (error: any, response: any) => void;
|
||||
|
||||
const sendRequest = async (requestConfig: AxiosRequestConfig, callback: T_SendRequestCallback) => {
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
if (!callback) {
|
||||
return await axiosInstance(requestConfig);
|
||||
}
|
||||
try {
|
||||
const response = await axiosInstance(requestConfig);
|
||||
/**
|
||||
* Configuration for creating a sendRequest function with proxy/certs support.
|
||||
* This is the same config used by getHttpHttpsAgents, minus requestUrl which is
|
||||
* extracted from the actual request.
|
||||
*/
|
||||
type SendRequestConfig = Omit<GetHttpHttpsAgentsParams, 'requestUrl'>;
|
||||
|
||||
/**
|
||||
* Creates a sendRequest function configured with proxy and certificate settings.
|
||||
* This allows bru.sendRequest to use the same proxy/certs config as the main request.
|
||||
*
|
||||
* @param config - Configuration for proxy, certs, and TLS options (same as getHttpHttpsAgents)
|
||||
* @returns A sendRequest function that applies the config to each request
|
||||
*/
|
||||
const createSendRequest = (config?: SendRequestConfig) => {
|
||||
return async (requestConfig: AxiosRequestConfig | string, callback?: T_SendRequestCallback) => {
|
||||
// Handle case where requestConfig is a URL string
|
||||
const normalizedConfig: AxiosRequestConfig = typeof requestConfig === 'string'
|
||||
? { url: requestConfig }
|
||||
: { ...requestConfig };
|
||||
|
||||
// If config is provided, create agents with the request URL for proper proxy bypass
|
||||
if (config) {
|
||||
const requestUrl = normalizedConfig.url;
|
||||
|
||||
const { httpAgent, httpsAgent } = await getHttpHttpsAgents({
|
||||
...config,
|
||||
requestUrl
|
||||
});
|
||||
|
||||
// Apply agents if not explicitly set in normalizedConfig
|
||||
if (httpAgent && !normalizedConfig.httpAgent) {
|
||||
normalizedConfig.httpAgent = httpAgent;
|
||||
}
|
||||
if (httpsAgent && !normalizedConfig.httpsAgent) {
|
||||
normalizedConfig.httpsAgent = httpsAgent;
|
||||
}
|
||||
}
|
||||
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
|
||||
if (!callback) {
|
||||
return await axiosInstance(normalizedConfig);
|
||||
}
|
||||
|
||||
try {
|
||||
await callback(null, response);
|
||||
const response = await axiosInstance(normalizedConfig);
|
||||
try {
|
||||
await callback(null, response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
try {
|
||||
await callback(error, null);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
try {
|
||||
await callback(error, null);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Default sendRequest without config (for backward compatibility)
|
||||
const sendRequest = createSendRequest();
|
||||
|
||||
export default sendRequest;
|
||||
export { createSendRequest };
|
||||
export type { SendRequestConfig };
|
||||
|
||||
@@ -214,18 +214,18 @@ const getCertsAndProxyConfig = ({
|
||||
}: GetCertsAndProxyConfigParams): GetCertsAndProxyConfigResult => {
|
||||
const certsConfig: CertsConfig = {};
|
||||
|
||||
const caCertFilePath = options.shouldUseCustomCaCertificate && options.customCaCertificateFilePath ? options.customCaCertificateFilePath : undefined;
|
||||
const caCertificatesData = getCACertificates({
|
||||
caCertFilePath,
|
||||
shouldKeepDefaultCerts: options.shouldKeepDefaultCaCertificates
|
||||
});
|
||||
// Only load CA certificates when TLS verification is enabled
|
||||
if (options.shouldVerifyTls) {
|
||||
const caCertFilePath = options.shouldUseCustomCaCertificate && options.customCaCertificateFilePath ? options.customCaCertificateFilePath : undefined;
|
||||
const caCertificatesData = getCACertificates({
|
||||
caCertFilePath,
|
||||
shouldKeepDefaultCerts: options.shouldKeepDefaultCaCertificates
|
||||
});
|
||||
|
||||
const caCertificates = caCertificatesData.caCertificates;
|
||||
const caCertificatesCount = caCertificatesData.caCertificatesCount;
|
||||
|
||||
// configure HTTPS agent with aggregated CA certificates
|
||||
certsConfig.caCertificatesCount = caCertificatesCount;
|
||||
certsConfig.ca = caCertificates || [];
|
||||
// configure HTTPS agent with aggregated CA certificates
|
||||
certsConfig.caCertificatesCount = caCertificatesData.caCertificatesCount;
|
||||
certsConfig.ca = caCertificatesData.caCertificates || [];
|
||||
}
|
||||
|
||||
// client certificate config
|
||||
const clientCertConfig = get(clientCertificates, 'certs', []) as ClientCertificate[];
|
||||
@@ -443,3 +443,5 @@ const getHttpHttpsAgents = async ({
|
||||
};
|
||||
|
||||
export { getHttpHttpsAgents };
|
||||
|
||||
export type { GetHttpHttpsAgentsParams };
|
||||
|
||||
Reference in New Issue
Block a user