mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 12:45:38 +00:00
* fix: oauth2 credential management improvements Add bru.resetOauth2Credential() API for programmatic credential invalidation from scripts, fix credential clearing to match on credentialsId, expose oauth2 credential variables in test runtime, and add input validation with deduplication to prevent redundant IPC messages. Remove unused collectionGetOauth2CredentialsByUrlAndCredentialsId reducer. * fix: handle invalid URLs in oauth2 callback redirect handler Wrap new URL() calls in try-catch within onWindowRedirect to prevent uncaught TypeError when redirect or callback URLs are invalid. * Update packages/bruno-app/src/utils/codemirror/autocomplete.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
971 lines
35 KiB
JavaScript
971 lines
35 KiB
JavaScript
const qs = require('qs');
|
|
const chalk = require('chalk');
|
|
const decomment = require('decomment');
|
|
const fs = require('fs');
|
|
const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash');
|
|
const prepareRequest = require('./prepare-request');
|
|
const interpolateVars = require('./interpolate-vars');
|
|
const { interpolateString, interpolateObject } = require('./interpolate-string');
|
|
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@usebruno/js');
|
|
const { stripExtension } = require('../utils/filesystem');
|
|
const { getOptions } = require('../utils/bru');
|
|
const https = require('https');
|
|
const { HttpProxyAgent } = require('http-proxy-agent');
|
|
const { SocksProxyAgent } = require('socks-proxy-agent');
|
|
const { makeAxiosInstance } = require('../utils/axios-instance');
|
|
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
|
|
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
|
|
const path = require('path');
|
|
const { parseDataFromResponse } = require('../utils/common');
|
|
const { getCookieStringForUrl, saveCookies } = require('../utils/cookies');
|
|
const { createFormData } = require('../utils/form-data');
|
|
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
|
const { NtlmClient } = require('axios-ntlm');
|
|
const { addDigestInterceptor, getHttpHttpsAgents, makeAxiosInstance: makeAxiosInstanceForOauth2 } = require('@usebruno/requests');
|
|
const { getCACertificates, transformProxyConfig } = require('@usebruno/requests');
|
|
const { getOAuth2Token, getFormattedOauth2Credentials } = require('../utils/oauth2');
|
|
const tokenStore = require('../store/tokenStore');
|
|
const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables, isFormData } = require('@usebruno/common').utils;
|
|
|
|
const onConsoleLog = (type, args) => {
|
|
console[type](...args);
|
|
};
|
|
|
|
const getCACertHostRegex = (domain) => {
|
|
return '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
|
|
};
|
|
|
|
/**
|
|
* Extract prompt variables from a request
|
|
* Tries to respect the hierarchy of the variables and avoid unnecessary prompts as much as possible
|
|
* Note: TO BE CALLED ONLY AFTER THE PREPARE REQUEST
|
|
*
|
|
* @param {*} request - request object built by prepareRequest
|
|
* @returns {string[]} An array of extracted prompt variables
|
|
*/
|
|
const extractPromptVariablesForRequest = ({ request, collection, envVariables, runtimeVariables, processEnvVars, brunoConfig }) => {
|
|
const { vars, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, ...requestObj } = request;
|
|
|
|
const allVariables = {
|
|
...globalEnvironmentVariables,
|
|
...envVariables,
|
|
...collectionVariables,
|
|
...folderVariables,
|
|
...requestVariables,
|
|
...runtimeVariables,
|
|
process: {
|
|
env: {
|
|
...processEnvVars
|
|
}
|
|
}
|
|
};
|
|
|
|
const prompts = extractPromptVariables(requestObj);
|
|
prompts.push(...extractPromptVariables(allVariables));
|
|
|
|
const interpolationOptions = {
|
|
globalEnvVars: globalEnvironmentVariables,
|
|
envVars: envVariables,
|
|
runtimeVariables,
|
|
processEnvVars
|
|
};
|
|
|
|
// client certificate config
|
|
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
|
|
for (let clientCert of clientCertConfig) {
|
|
const domain = interpolateString(clientCert?.domain, interpolationOptions);
|
|
if (domain) {
|
|
const hostRegex = getCACertHostRegex(domain);
|
|
if (request.url.match(hostRegex)) {
|
|
prompts.push(...extractPromptVariables(clientCert));
|
|
}
|
|
}
|
|
}
|
|
|
|
// return unique prompt variables
|
|
return Array.from(new Set(prompts));
|
|
};
|
|
|
|
const runSingleRequest = async function (
|
|
item,
|
|
collectionPath,
|
|
runtimeVariables,
|
|
envVariables,
|
|
processEnvVars,
|
|
brunoConfig,
|
|
collectionRoot,
|
|
runtime,
|
|
collection,
|
|
runSingleRequestByPathname,
|
|
globalEnvVars = {}
|
|
) {
|
|
const { pathname: itemPathname } = item;
|
|
const relativeItemPathname = path.relative(collectionPath, itemPathname);
|
|
|
|
const logResults = (results, title) => {
|
|
if (results?.length) {
|
|
if (title) {
|
|
console.log(chalk.dim(title));
|
|
}
|
|
each(results, (r) => {
|
|
const message = r.description || `${r.lhsExpr}: ${r.rhsExpr}`;
|
|
if (r.status === 'pass') {
|
|
console.log(chalk.green(` ✓ `) + chalk.dim(message));
|
|
} else {
|
|
console.log(chalk.red(` ✕ `) + chalk.red(message));
|
|
if (r.error) {
|
|
console.log(chalk.red(` ${r.error}`));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
try {
|
|
let request;
|
|
let nextRequestName;
|
|
let shouldStopRunnerExecution = false;
|
|
let preRequestTestResults = [];
|
|
let postResponseTestResults = [];
|
|
|
|
request = await prepareRequest(item, collection);
|
|
|
|
// Set global environment variables on the request for scripts to access via bru.getGlobalEnvVar()
|
|
request.globalEnvironmentVariables = globalEnvVars;
|
|
|
|
// Detect prompt variables before proceeding
|
|
const promptVars = extractPromptVariablesForRequest({ request, collection, envVariables, runtimeVariables, processEnvVars, brunoConfig });
|
|
|
|
if (promptVars.length > 0) {
|
|
const errorMsg = `Prompt variables detected in request. CLI execution is not supported for requests with prompt variables. \nPrompts: ${promptVars.join(', ')}`;
|
|
console.log(chalk.yellow(stripExtension(relativeItemPathname) + ' Skipped:') + chalk.dim(` (${errorMsg})`));
|
|
return {
|
|
test: {
|
|
filename: relativeItemPathname
|
|
},
|
|
request: {
|
|
method: request.method,
|
|
url: request.url,
|
|
headers: request.headers,
|
|
data: request.data
|
|
},
|
|
response: {
|
|
status: 'skipped',
|
|
statusText: errorMsg,
|
|
data: null,
|
|
responseTime: 0
|
|
},
|
|
error: null,
|
|
status: 'skipped',
|
|
skipped: true,
|
|
assertionResults: [],
|
|
testResults: [],
|
|
preRequestTestResults: [],
|
|
postResponseTestResults: [],
|
|
shouldStopRunnerExecution
|
|
};
|
|
}
|
|
|
|
request.__bruno__executionMode = 'cli';
|
|
|
|
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;
|
|
if (requestScriptFile?.length) {
|
|
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
|
try {
|
|
const result = await scriptRuntime.runRequestScript(decomment(requestScriptFile),
|
|
request,
|
|
envVariables,
|
|
runtimeVariables,
|
|
collectionPath,
|
|
onConsoleLog,
|
|
processEnvVars,
|
|
scriptingConfig,
|
|
runSingleRequestByPathname,
|
|
collectionName);
|
|
if (result?.nextRequestName !== undefined) {
|
|
nextRequestName = result.nextRequestName;
|
|
}
|
|
|
|
if (result?.stopExecution) {
|
|
shouldStopRunnerExecution = true;
|
|
}
|
|
|
|
if (result?.oauth2CredentialsToReset?.length) {
|
|
for (const credentialId of result.oauth2CredentialsToReset) {
|
|
tokenStore.deleteCredentialById(credentialId);
|
|
}
|
|
}
|
|
|
|
if (result?.skipRequest) {
|
|
return {
|
|
test: {
|
|
filename: relativeItemPathname
|
|
},
|
|
request: {
|
|
method: request.method,
|
|
url: request.url,
|
|
headers: request.headers,
|
|
data: request.data
|
|
},
|
|
response: {
|
|
status: 'skipped',
|
|
statusText: 'request skipped via pre-request script',
|
|
data: null,
|
|
responseTime: 0
|
|
},
|
|
error: null,
|
|
status: 'skipped',
|
|
skipped: true,
|
|
assertionResults: [],
|
|
testResults: [],
|
|
preRequestTestResults: result?.results || [],
|
|
postResponseTestResults: [],
|
|
shouldStopRunnerExecution
|
|
};
|
|
}
|
|
|
|
preRequestTestResults = result?.results || [];
|
|
} catch (error) {
|
|
// Pre-request errors are treated as request errors (we return early with status: 'error'), not as failures. Unlike post-response and test script errors, we do not add a synthetic fail and continue.
|
|
console.error('Pre-request script execution error:', error);
|
|
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${error.message})`));
|
|
|
|
// Extract partial results from the error (tests that passed before the error)
|
|
preRequestTestResults = error?.partialResults?.results || [];
|
|
|
|
// Preserve nextRequestName if it was set before the error
|
|
if (error?.partialResults?.nextRequestName !== undefined) {
|
|
nextRequestName = error.partialResults.nextRequestName;
|
|
}
|
|
|
|
// Preserve stopExecution if it was set before the error
|
|
if (error?.partialResults?.stopExecution) {
|
|
shouldStopRunnerExecution = true;
|
|
}
|
|
|
|
logResults(preRequestTestResults, 'Pre-Request Tests');
|
|
|
|
// Pre-request script error: execution didn't complete (request never sent). Return early so we don't run the HTTP request, post-response script, assertions, or tests.
|
|
return {
|
|
test: {
|
|
filename: relativeItemPathname
|
|
},
|
|
request: {
|
|
method: request.method,
|
|
url: request.url,
|
|
headers: request.headers,
|
|
data: request.data
|
|
},
|
|
response: {
|
|
status: 'error',
|
|
statusText: null,
|
|
headers: null,
|
|
data: null,
|
|
url: null,
|
|
responseTime: 0
|
|
},
|
|
error: error?.message || 'An error occurred while executing the pre-request script.',
|
|
status: 'error',
|
|
assertionResults: [],
|
|
testResults: [],
|
|
preRequestTestResults,
|
|
postResponseTestResults: [],
|
|
nextRequestName: nextRequestName,
|
|
shouldStopRunnerExecution
|
|
};
|
|
}
|
|
}
|
|
|
|
// interpolate variables inside request
|
|
interpolateVars(request, envVariables, runtimeVariables, processEnvVars);
|
|
|
|
// if this is a graphql request, parse the variables, only after interpolation
|
|
// https://github.com/usebruno/bruno/issues/884
|
|
if (request.mode === 'graphql' && typeof request.data?.variables === 'string') {
|
|
try {
|
|
request.data.variables = JSON.parse(request.data.variables);
|
|
} catch (err) {
|
|
throw new Error(`Failed to parse GraphQL variables: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
if (request.settings?.encodeUrl) {
|
|
request.url = encodeUrl(request.url);
|
|
}
|
|
|
|
if (!protocolRegex.test(request.url)) {
|
|
request.url = `http://${request.url}`;
|
|
}
|
|
|
|
const insecure = get(options, 'insecure', false);
|
|
const noproxy = get(options, 'noproxy', false);
|
|
const cachedSystemProxy = get(options, 'cachedSystemProxy', null);
|
|
const httpsAgentRequestFields = {};
|
|
|
|
if (insecure) {
|
|
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
|
} else {
|
|
const caCertFilePath = options['cacert'];
|
|
let caCertificatesData = getCACertificates({ caCertFilePath, shouldKeepDefaultCerts: !options['ignoreTruststore'] });
|
|
let caCertificates = caCertificatesData.caCertificates;
|
|
httpsAgentRequestFields['ca'] = caCertificates || [];
|
|
}
|
|
|
|
const interpolationOptions = {
|
|
globalEnvVars: request.globalEnvironmentVariables || {},
|
|
envVars: envVariables,
|
|
runtimeVariables,
|
|
processEnvVars
|
|
};
|
|
|
|
// client certificate config
|
|
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
|
|
for (let clientCert of clientCertConfig) {
|
|
const domain = interpolateString(clientCert?.domain, interpolationOptions);
|
|
const type = clientCert?.type || 'cert';
|
|
if (domain) {
|
|
const hostRegex = getCACertHostRegex(domain);
|
|
if (request.url.match(hostRegex)) {
|
|
if (type === 'cert') {
|
|
try {
|
|
let certFilePath = interpolateString(clientCert?.certFilePath, interpolationOptions);
|
|
certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath);
|
|
let keyFilePath = interpolateString(clientCert?.keyFilePath, interpolationOptions);
|
|
keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath);
|
|
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
|
|
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
|
|
} catch (err) {
|
|
console.log(chalk.red('Error reading cert/key file'), chalk.red(err?.message));
|
|
}
|
|
} else if (type === 'pfx') {
|
|
try {
|
|
let pfxFilePath = interpolateString(clientCert?.pfxFilePath, interpolationOptions);
|
|
pfxFilePath = path.isAbsolute(pfxFilePath) ? pfxFilePath : path.join(collectionPath, pfxFilePath);
|
|
httpsAgentRequestFields['pfx'] = fs.readFileSync(pfxFilePath);
|
|
} catch (err) {
|
|
console.log(chalk.red('Error reading pfx file'), chalk.red(err?.message));
|
|
}
|
|
}
|
|
httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let proxyMode = 'off';
|
|
let proxyConfig = {};
|
|
|
|
const collectionProxyConfig = transformProxyConfig(get(brunoConfig, 'proxy', {}));
|
|
const collectionProxyDisabled = get(collectionProxyConfig, 'disabled', false);
|
|
const collectionProxyInherit = get(collectionProxyConfig, 'inherit', true);
|
|
const collectionProxyConfigData = get(collectionProxyConfig, 'config', {});
|
|
|
|
if (noproxy || collectionProxyDisabled) {
|
|
// If noproxy flag is set or collection proxy is disabled, don't use any proxy
|
|
proxyMode = 'off';
|
|
} else if (!collectionProxyDisabled && !collectionProxyInherit) {
|
|
// Use collection-specific proxy
|
|
proxyConfig = collectionProxyConfigData;
|
|
proxyMode = 'on';
|
|
} else if (!collectionProxyDisabled && collectionProxyInherit) {
|
|
// Inherit from system proxy
|
|
if (cachedSystemProxy) {
|
|
const { http_proxy, https_proxy } = cachedSystemProxy;
|
|
if (http_proxy?.length || https_proxy?.length) {
|
|
proxyMode = 'system';
|
|
}
|
|
}
|
|
// else: no system proxy available, proxyMode stays 'off'
|
|
}
|
|
// else: collection proxy is disabled, proxyMode stays 'off'
|
|
|
|
if (proxyMode === 'on') {
|
|
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', ''));
|
|
if (shouldProxy) {
|
|
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
|
|
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
|
|
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
|
|
const proxyAuthEnabled = !get(proxyConfig, 'auth.disabled', false);
|
|
const socksEnabled = proxyProtocol.includes('socks');
|
|
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
|
let proxyUri;
|
|
if (proxyAuthEnabled) {
|
|
const proxyAuthUsername = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions));
|
|
const proxyAuthPassword = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions));
|
|
|
|
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
|
} else {
|
|
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
|
|
}
|
|
if (socksEnabled) {
|
|
request.httpsAgent = new SocksProxyAgent(
|
|
proxyUri,
|
|
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
|
);
|
|
request.httpAgent = new SocksProxyAgent(proxyUri);
|
|
} else {
|
|
request.httpsAgent = new PatchedHttpsProxyAgent(
|
|
proxyUri,
|
|
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
|
);
|
|
request.httpAgent = new HttpProxyAgent(proxyUri);
|
|
}
|
|
} else {
|
|
request.httpsAgent = new https.Agent({
|
|
...httpsAgentRequestFields
|
|
});
|
|
}
|
|
} else if (proxyMode === 'system') {
|
|
try {
|
|
const { http_proxy, https_proxy, no_proxy } = cachedSystemProxy || {};
|
|
const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || '');
|
|
const parsedUrl = new URL(request.url);
|
|
const isHttpsRequest = parsedUrl.protocol === 'https:';
|
|
if (shouldUseSystemProxy) {
|
|
try {
|
|
if (http_proxy?.length && !isHttpsRequest) {
|
|
new URL(http_proxy);
|
|
request.httpAgent = new HttpProxyAgent(http_proxy);
|
|
}
|
|
} catch (error) {
|
|
throw new Error('Invalid system http_proxy');
|
|
}
|
|
try {
|
|
if (https_proxy?.length && isHttpsRequest) {
|
|
new URL(https_proxy);
|
|
request.httpsAgent = new PatchedHttpsProxyAgent(https_proxy,
|
|
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined);
|
|
} else {
|
|
request.httpsAgent = new https.Agent({
|
|
...httpsAgentRequestFields
|
|
});
|
|
}
|
|
} catch (error) {
|
|
throw new Error('Invalid system https_proxy');
|
|
}
|
|
} else {
|
|
request.httpsAgent = new https.Agent({
|
|
...httpsAgentRequestFields
|
|
});
|
|
}
|
|
} catch (error) {
|
|
request.httpsAgent = new https.Agent({
|
|
...httpsAgentRequestFields
|
|
});
|
|
}
|
|
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
|
|
request.httpsAgent = new https.Agent({
|
|
...httpsAgentRequestFields
|
|
});
|
|
}
|
|
|
|
// set cookies if enabled
|
|
if (!options.disableCookies) {
|
|
const cookieString = getCookieStringForUrl(request.url);
|
|
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
|
const existingCookieHeaderName = Object.keys(request.headers).find(
|
|
(name) => name.toLowerCase() === 'cookie'
|
|
);
|
|
const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';
|
|
|
|
// Helper function to parse cookies into an object
|
|
const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {
|
|
const [name, ...rest] = cookie.split('=');
|
|
if (name && name.trim()) {
|
|
cookies[name.trim()] = rest.join('=').trim();
|
|
}
|
|
return cookies;
|
|
}, {});
|
|
|
|
const mergedCookies = {
|
|
...parseCookies(existingCookieString),
|
|
...parseCookies(cookieString)
|
|
};
|
|
|
|
const combinedCookieString = Object.entries(mergedCookies)
|
|
.map(([name, value]) => `${name}=${value}`)
|
|
.join('; ');
|
|
|
|
request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;
|
|
}
|
|
}
|
|
|
|
// stringify the request url encoded params
|
|
const contentTypeHeader = Object.keys(request.headers).find(
|
|
(name) => name.toLowerCase() === 'content-type'
|
|
);
|
|
|
|
if (contentTypeHeader && request.headers[contentTypeHeader] === 'application/x-www-form-urlencoded') {
|
|
if (Array.isArray(request.data)) {
|
|
request.data = buildFormUrlEncodedPayload(request.data);
|
|
} else if (typeof request.data !== 'string') {
|
|
request.data = qs.stringify(request.data, { arrayFormat: 'repeat' });
|
|
}
|
|
// if `data` is of string type - return as-is (assumes already encoded)
|
|
}
|
|
|
|
if (contentTypeHeader && request.headers[contentTypeHeader] === 'multipart/form-data') {
|
|
if (!isFormData(request?.data)) {
|
|
request._originalMultipartData = request.data;
|
|
request.collectionPath = collectionPath;
|
|
let form = createFormData(request.data, collectionPath);
|
|
request.data = form;
|
|
extend(request.headers, form.getHeaders());
|
|
}
|
|
}
|
|
|
|
// Get followRedirects setting, default to true for backward compatibility
|
|
const followRedirects = request.settings?.followRedirects ?? true;
|
|
|
|
// Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5
|
|
let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5;
|
|
|
|
// Ensure it's a valid number
|
|
if (typeof requestMaxRedirects !== 'number' || requestMaxRedirects < 0) {
|
|
requestMaxRedirects = 5; // Default to 5 redirects
|
|
}
|
|
|
|
// If followRedirects is disabled, set maxRedirects to 0 to disable all redirects
|
|
if (!followRedirects) {
|
|
requestMaxRedirects = 0;
|
|
}
|
|
|
|
request.maxRedirects = 0;
|
|
|
|
// Handle OAuth2 authentication
|
|
if (request.oauth2) {
|
|
try {
|
|
// Prepare interpolation options with all available variables
|
|
const oauth2InterpolationOptions = {
|
|
globalEnvVars: request.globalEnvironmentVariables || {},
|
|
envVars: envVariables,
|
|
runtimeVariables,
|
|
processEnvVars,
|
|
collectionVariables: request.collectionVariables || {},
|
|
folderVariables: request.folderVariables || {},
|
|
requestVariables: request.requestVariables || {}
|
|
};
|
|
|
|
const accessTokenUrl = request.oauth2.accessTokenUrl ? interpolateString(request.oauth2.accessTokenUrl, oauth2InterpolationOptions) : undefined;
|
|
const refreshTokenUrl = request.oauth2.refreshTokenUrl ? interpolateString(request.oauth2.refreshTokenUrl, oauth2InterpolationOptions) : undefined;
|
|
const oauth2RequestUrl = accessTokenUrl || refreshTokenUrl;
|
|
|
|
let token;
|
|
if (oauth2RequestUrl) {
|
|
const tlsOptions = {
|
|
noproxy: options.noproxy,
|
|
shouldVerifyTls: !insecure,
|
|
shouldUseCustomCaCertificate: !!options['cacert'],
|
|
customCaCertificateFilePath: options['cacert'],
|
|
shouldKeepDefaultCaCertificates: !options['ignoreTruststore']
|
|
};
|
|
|
|
const clientCertificates = get(brunoConfig, 'clientCertificates');
|
|
const proxyConfig = get(brunoConfig, 'proxy');
|
|
const interpolatedClientCertificates = clientCertificates ? interpolateObject(clientCertificates, oauth2InterpolationOptions) : undefined;
|
|
const interpolatedProxyConfig = proxyConfig ? interpolateObject(proxyConfig, oauth2InterpolationOptions) : undefined;
|
|
const systemProxyConfig = cachedSystemProxy;
|
|
|
|
const { httpAgent: oauth2HttpAgent, httpsAgent: oauth2HttpsAgent } = await getHttpHttpsAgents({
|
|
requestUrl: oauth2RequestUrl,
|
|
collectionPath,
|
|
options: tlsOptions,
|
|
clientCertificates: interpolatedClientCertificates,
|
|
collectionLevelProxy: interpolatedProxyConfig,
|
|
systemProxyConfig
|
|
});
|
|
|
|
const oauth2AxiosInstance = makeAxiosInstanceForOauth2({
|
|
requestMaxRedirects: requestMaxRedirects,
|
|
disableCookies: options.disableCookies,
|
|
httpAgent: oauth2HttpAgent,
|
|
httpsAgent: oauth2HttpsAgent
|
|
});
|
|
|
|
token = await getOAuth2Token(request.oauth2, oauth2AxiosInstance);
|
|
}
|
|
|
|
if (token) {
|
|
const { tokenPlacement = 'header', tokenHeaderPrefix = '', tokenQueryKey = 'access_token' } = request.oauth2;
|
|
|
|
if (tokenPlacement === 'header' && token) {
|
|
request.headers['Authorization'] = `${tokenHeaderPrefix} ${token}`.trim();
|
|
} else if (tokenPlacement === 'url') {
|
|
try {
|
|
const url = new URL(request.url);
|
|
url.searchParams.set(tokenQueryKey, token);
|
|
request.url = url.toString();
|
|
} catch (error) {
|
|
console.error('Error applying OAuth2 token to URL:', error.message);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('OAuth2 token fetch error:', error.message);
|
|
}
|
|
|
|
request.oauth2CredentialVariables = getFormattedOauth2Credentials();
|
|
|
|
// Remove oauth2 config from request to prevent it from being sent
|
|
delete request.oauth2;
|
|
}
|
|
|
|
let response, responseTime;
|
|
try {
|
|
// Set timeout from request settings, default to 0 (no timeout)
|
|
const requestTimeout = request.settings?.timeout || 0;
|
|
if (requestTimeout > 0) {
|
|
request.timeout = requestTimeout;
|
|
}
|
|
|
|
let axiosInstance = makeAxiosInstance({
|
|
requestMaxRedirects: requestMaxRedirects,
|
|
disableCookies: options.disableCookies,
|
|
followRedirects: followRedirects
|
|
});
|
|
|
|
if (request.ntlmConfig) {
|
|
axiosInstance = NtlmClient(request.ntlmConfig, axiosInstance.defaults);
|
|
delete request.ntlmConfig;
|
|
}
|
|
|
|
if (request.awsv4config) {
|
|
// todo: make this happen in prepare-request.js
|
|
// interpolate the aws v4 config
|
|
request.awsv4config.accessKeyId = interpolateString(request.awsv4config.accessKeyId, interpolationOptions);
|
|
request.awsv4config.secretAccessKey = interpolateString(
|
|
request.awsv4config.secretAccessKey,
|
|
interpolationOptions
|
|
);
|
|
request.awsv4config.sessionToken = interpolateString(request.awsv4config.sessionToken, interpolationOptions);
|
|
request.awsv4config.service = interpolateString(request.awsv4config.service, interpolationOptions);
|
|
request.awsv4config.region = interpolateString(request.awsv4config.region, interpolationOptions);
|
|
request.awsv4config.profileName = interpolateString(request.awsv4config.profileName, interpolationOptions);
|
|
|
|
request.awsv4config = await resolveAwsV4Credentials(request);
|
|
addAwsV4Interceptor(axiosInstance, request);
|
|
delete request.awsv4config;
|
|
}
|
|
|
|
if (request.digestConfig) {
|
|
addDigestInterceptor(axiosInstance, request);
|
|
delete request.digestConfig;
|
|
}
|
|
|
|
/** @type {import('axios').AxiosResponse} */
|
|
response = await axiosInstance(request);
|
|
|
|
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
|
response.data = data;
|
|
response.dataBuffer = dataBuffer;
|
|
|
|
// Prevents the duration on leaking to the actual result
|
|
responseTime = response.headers.get('request-duration');
|
|
response.headers.delete('request-duration');
|
|
|
|
// save cookies if enabled
|
|
if (!options.disableCookies) {
|
|
saveCookies(request.url, response.headers);
|
|
}
|
|
} catch (err) {
|
|
if (err?.response) {
|
|
const { data, dataBuffer } = parseDataFromResponse(err?.response);
|
|
err.response.data = data;
|
|
err.response.dataBuffer = dataBuffer;
|
|
response = err.response;
|
|
|
|
// Prevents the duration on leaking to the actual result
|
|
responseTime = response.headers.get('request-duration');
|
|
response.headers.delete('request-duration');
|
|
} else {
|
|
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));
|
|
return {
|
|
test: {
|
|
filename: relativeItemPathname
|
|
},
|
|
request: {
|
|
method: request.method,
|
|
url: request.url,
|
|
headers: request.headers,
|
|
data: request.data
|
|
},
|
|
response: {
|
|
status: 'error',
|
|
statusText: null,
|
|
headers: null,
|
|
data: null,
|
|
url: null,
|
|
responseTime: 0
|
|
},
|
|
error: err?.message || err?.errors?.map((e) => e?.message)?.at(0) || err?.code || 'Request Failed!',
|
|
status: 'error',
|
|
assertionResults: [],
|
|
testResults: [],
|
|
preRequestTestResults,
|
|
postResponseTestResults,
|
|
nextRequestName: nextRequestName,
|
|
shouldStopRunnerExecution
|
|
};
|
|
}
|
|
}
|
|
|
|
response.responseTime = responseTime;
|
|
|
|
console.log(
|
|
chalk.green(stripExtension(relativeItemPathname))
|
|
+ chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
|
);
|
|
|
|
// Log pre-request test results
|
|
logResults(preRequestTestResults, 'Pre-Request Tests');
|
|
|
|
// run post-response vars
|
|
const postResponseVars = get(item, 'request.vars.res');
|
|
if (postResponseVars?.length) {
|
|
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
|
|
varsRuntime.runPostResponseVars(
|
|
postResponseVars,
|
|
request,
|
|
response,
|
|
envVariables,
|
|
runtimeVariables,
|
|
collectionPath,
|
|
processEnvVars
|
|
);
|
|
}
|
|
|
|
// run post response script
|
|
const responseScriptFile = get(request, 'script.res');
|
|
if (responseScriptFile?.length) {
|
|
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
|
try {
|
|
const result = await scriptRuntime.runResponseScript(
|
|
decomment(responseScriptFile),
|
|
request,
|
|
response,
|
|
envVariables,
|
|
runtimeVariables,
|
|
collectionPath,
|
|
onConsoleLog,
|
|
processEnvVars,
|
|
scriptingConfig,
|
|
runSingleRequestByPathname,
|
|
collectionName
|
|
);
|
|
if (result?.nextRequestName !== undefined) {
|
|
nextRequestName = result.nextRequestName;
|
|
}
|
|
|
|
if (result?.stopExecution) {
|
|
shouldStopRunnerExecution = true;
|
|
}
|
|
|
|
if (result?.oauth2CredentialsToReset?.length) {
|
|
for (const credentialId of result.oauth2CredentialsToReset) {
|
|
tokenStore.deleteCredentialById(credentialId);
|
|
}
|
|
}
|
|
|
|
postResponseTestResults = result?.results || [];
|
|
logResults(postResponseTestResults, 'Post-Response Tests');
|
|
} catch (error) {
|
|
console.error('Post-response script execution error:', error);
|
|
|
|
const partialResults = error?.partialResults?.results || [];
|
|
postResponseTestResults = [
|
|
...partialResults,
|
|
{
|
|
status: 'fail',
|
|
description: 'Post-Response Script Error',
|
|
error: error.message || 'An error occurred while executing the post-response script.',
|
|
isScriptError: true
|
|
}
|
|
];
|
|
|
|
if (error?.partialResults?.nextRequestName !== undefined) {
|
|
nextRequestName = error.partialResults.nextRequestName;
|
|
}
|
|
|
|
if (error?.partialResults?.stopExecution) {
|
|
shouldStopRunnerExecution = true;
|
|
}
|
|
|
|
logResults(postResponseTestResults, 'Post-Response Tests');
|
|
}
|
|
}
|
|
|
|
let assertionResults = [];
|
|
const assertions = get(item, 'request.assertions');
|
|
if (assertions) {
|
|
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
|
|
assertionResults = assertRuntime.runAssertions(
|
|
assertions,
|
|
request,
|
|
response,
|
|
envVariables,
|
|
runtimeVariables,
|
|
processEnvVars
|
|
);
|
|
}
|
|
|
|
// run tests
|
|
let testResults = [];
|
|
const testFile = get(request, 'tests');
|
|
if (typeof testFile === 'string') {
|
|
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
|
try {
|
|
const result = await testRuntime.runTests(
|
|
decomment(testFile),
|
|
request,
|
|
response,
|
|
envVariables,
|
|
runtimeVariables,
|
|
collectionPath,
|
|
onConsoleLog,
|
|
processEnvVars,
|
|
scriptingConfig,
|
|
runSingleRequestByPathname,
|
|
collectionName
|
|
);
|
|
testResults = get(result, 'results', []);
|
|
|
|
if (result?.nextRequestName !== undefined) {
|
|
nextRequestName = result.nextRequestName;
|
|
}
|
|
|
|
if (result?.stopExecution) {
|
|
shouldStopRunnerExecution = true;
|
|
}
|
|
|
|
if (result?.oauth2CredentialsToReset?.length) {
|
|
for (const credentialId of result.oauth2CredentialsToReset) {
|
|
tokenStore.deleteCredentialById(credentialId);
|
|
}
|
|
}
|
|
|
|
logResults(testResults, 'Tests');
|
|
} catch (error) {
|
|
console.error('Test script execution error:', error);
|
|
|
|
const partialResults = error?.partialResults?.results || [];
|
|
testResults = [
|
|
...partialResults,
|
|
{
|
|
status: 'fail',
|
|
description: 'Test Script Error',
|
|
error: error.message || 'An error occurred while executing the test script.',
|
|
isScriptError: true
|
|
}
|
|
];
|
|
|
|
if (error?.partialResults?.nextRequestName !== undefined) {
|
|
nextRequestName = error.partialResults.nextRequestName;
|
|
}
|
|
|
|
if (error?.partialResults?.stopExecution) {
|
|
shouldStopRunnerExecution = true;
|
|
}
|
|
|
|
logResults(testResults, 'Tests');
|
|
}
|
|
}
|
|
|
|
logResults(assertionResults, 'Assertions');
|
|
|
|
return {
|
|
test: {
|
|
filename: relativeItemPathname
|
|
},
|
|
request: {
|
|
method: request.method,
|
|
url: request.url,
|
|
headers: request.headers,
|
|
data: request.data
|
|
},
|
|
response: {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: response.headers,
|
|
data: response.data,
|
|
url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null,
|
|
responseTime
|
|
},
|
|
error: null,
|
|
status: 'pass',
|
|
assertionResults,
|
|
testResults,
|
|
preRequestTestResults,
|
|
postResponseTestResults,
|
|
nextRequestName: nextRequestName,
|
|
shouldStopRunnerExecution
|
|
};
|
|
} catch (err) {
|
|
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));
|
|
return {
|
|
test: {
|
|
filename: relativeItemPathname
|
|
},
|
|
request: {
|
|
method: null,
|
|
url: null,
|
|
headers: null,
|
|
data: null
|
|
},
|
|
response: {
|
|
status: 'error',
|
|
statusText: null,
|
|
headers: null,
|
|
data: null,
|
|
url: null,
|
|
responseTime: 0
|
|
},
|
|
status: 'error',
|
|
error: err.message,
|
|
assertionResults: [],
|
|
testResults: [],
|
|
preRequestTestResults: [],
|
|
postResponseTestResults: []
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
runSingleRequest
|
|
};
|