Merge remote-tracking branch 'upstream/main' into feat/oauth2-improvements

This commit is contained in:
lohxt1
2025-03-16 14:02:12 +05:30
250 changed files with 10845 additions and 1614 deletions

View File

@@ -46,13 +46,13 @@
"package.json"
],
"dependencies": {
"@aws-sdk/credential-providers": "3.658.1",
"@aws-sdk/credential-providers": "3.750.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
"@usebruno/vm2": "^3.9.13",
"aws4-axios": "^3.3.0",
"axios": "1.7.5",
"axios": "^1.8.3",
"axios-ntlm": "^1.4.2",
"chai": "^4.3.7",
"chalk": "^3.0.0",
@@ -66,7 +66,6 @@
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^4.1.3",
"@usebruno/vm2": "^3.9.13",
"xmlbuilder": "^15.1.1",
"yargs": "^17.6.2"
}

View File

@@ -11,6 +11,7 @@ const { rpad } = require('../utils/common');
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
const { dotenvToJson } = require('@usebruno/lang');
const constants = require('../constants');
const { findItemInCollection } = require('../utils/collection');
const command = 'run [filename]';
const desc = 'Run a request';
@@ -18,6 +19,7 @@ const printRunSummary = (results) => {
let totalRequests = 0;
let passedRequests = 0;
let failedRequests = 0;
let skippedRequests = 0;
let totalAssertions = 0;
let passedAssertions = 0;
let failedAssertions = 0;
@@ -49,7 +51,10 @@ const printRunSummary = (results) => {
failedAssertions += 1;
}
}
if (!hasAnyTestsOrAssertions && result.error) {
if (!hasAnyTestsOrAssertions && result.skipped) {
skippedRequests += 1;
}
else if (!hasAnyTestsOrAssertions && result.error) {
failedRequests += 1;
} else {
passedRequests += 1;
@@ -62,6 +67,9 @@ const printRunSummary = (results) => {
if (failedRequests > 0) {
requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`;
}
if (skippedRequests > 0) {
requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`;
}
requestSummary += `, ${totalRequests} total`;
let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`;
@@ -84,6 +92,7 @@ const printRunSummary = (results) => {
totalRequests,
passedRequests,
failedRequests,
skippedRequests,
totalAssertions,
passedAssertions,
failedAssertions,
@@ -144,7 +153,7 @@ const createCollectionFromPath = (collectionPath) => {
});
}
}
return currentDirItems
return currentDirItems;
};
collection.items = traverse(collectionPath);
return collection;
@@ -285,7 +294,7 @@ const builder = async (yargs) => {
type: 'string'
})
.option('sandbox', {
describe: 'Javscript sandbox to use; available sandboxes are "developer" (default) or "safe"',
describe: 'Javascript sandbox to use; available sandboxes are "developer" (default) or "safe"',
default: 'developer',
type: 'string'
})
@@ -338,6 +347,10 @@ const builder = async (yargs) => {
type: 'string',
description: 'Path to the Client certificate config file used for securing the connection in the request'
})
.option('delay', {
type:"number",
description: "Delay between each requests (in miliseconds)"
})
.example('$0 run request.bru', 'Run a request')
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
@@ -378,7 +391,8 @@ const builder = async (yargs) => {
'$0 run folder --cacert myCustomCA.pem --ignore-truststore',
'Use a custom CA certificate exclusively when validating the peers of the requests in the specified folder.'
)
.example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations');
.example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations')
.example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.');
};
const handler = async function (argv) {
@@ -402,7 +416,8 @@ const handler = async function (argv) {
bail,
reporterSkipAllHeaders,
reporterSkipHeaders,
clientCertConfig
clientCertConfig,
delay
} = argv;
const collectionPath = process.cwd();
@@ -634,6 +649,34 @@ const handler = async function (argv) {
}
const runtime = getJsSandboxRuntime(sandbox);
const runSingleRequestByPathname = async (relativeItemPathname) => {
return new Promise(async (resolve, reject) => {
let itemPathname = path.join(collectionPath, relativeItemPathname);
if (itemPathname && !itemPathname?.endsWith('.bru')) {
itemPathname = `${itemPathname}.bru`;
}
const bruJson = cloneDeep(findItemInCollection(collection, itemPathname));
if (bruJson) {
const res = await runSingleRequest(
itemPathname,
bruJson,
collectionPath,
runtimeVariables,
envVars,
processEnvVars,
brunoConfig,
collectionRoot,
runtime,
collection,
runSingleRequestByPathname
);
resolve(res?.response);
}
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
});
}
let currentRequestIndex = 0;
let nJumps = 0; // count the number of jumps to avoid infinite loops
while (currentRequestIndex < bruJsons.length) {
@@ -651,9 +694,21 @@ const handler = async function (argv) {
brunoConfig,
collectionRoot,
runtime,
collection
collection,
runSingleRequestByPathname
);
const isLastRun = currentRequestIndex === bruJsons.length - 1;
const isValidDelay = !Number.isNaN(delay) && delay > 0;
if(isValidDelay && !isLastRun){
console.log(chalk.yellow(`Waiting for ${delay}ms or ${(delay/1000).toFixed(3)}s before next request.`));
await new Promise((resolve) => setTimeout(resolve, delay));
}
if(Number.isNaN(delay) && !isLastRun){
console.log(chalk.red(`Ignoring delay because it's not a valid number.`));
}
results.push({
...result,
runtime: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9,
@@ -701,11 +756,16 @@ const handler = async function (argv) {
// determine next request
const nextRequestName = result?.nextRequestName;
if (result?.shouldStopRunnerExecution) {
break;
}
if (nextRequestName !== undefined) {
nJumps++;
if (nJumps > 10000) {
console.error(chalk.red(`Too many jumps, possible infinite loop`));
process.exit(constants.EXIT_STATUS.ERROR_INFINTE_LOOP);
process.exit(constants.EXIT_STATUS.ERROR_INFINITE_LOOP);
}
if (nextRequestName === null) {
break;

View File

@@ -10,7 +10,7 @@ const EXIT_STATUS = {
// The specified output dir does not exist
ERROR_MISSING_OUTPUT_DIR: 2,
// request chain caused an endless loop
ERROR_INFINTE_LOOP: 3,
ERROR_INFINITE_LOOP: 3,
// bru was called outside of a collection root
ERROR_NOT_IN_COLLECTION: 4,
// The specified file was not found

View File

@@ -62,7 +62,10 @@ const makeJUnitOutput = async (results, outputPath) => {
suite.testcase.push(testcase);
});
if (result.error) {
if (result?.skipped) {
suite['@skipped'] = 1;
}
else if (result.error) {
suite['@errors'] = 1;
suite['@tests'] = 1;
suite.testcase = [

View File

@@ -10,7 +10,8 @@ async function resolveAwsV4Credentials(request) {
if (isStrPresent(awsv4.profileName)) {
try {
credentialsProvider = fromIni({
profile: awsv4.profileName
profile: awsv4.profileName,
ignoreCache: true
});
credentials = await credentialsProvider();
awsv4.accessKeyId = credentials.accessKeyId;

View File

@@ -40,11 +40,13 @@ const runSingleRequest = async function (
brunoConfig,
collectionRoot,
runtime,
collection
collection,
runSingleRequestByPathname
) {
try {
let request;
let nextRequestName;
let shouldStopRunnerExecution = false;
let item = {
pathname: path.join(collectionPath, filename),
...bruJson
@@ -68,11 +70,41 @@ const runSingleRequest = async function (
collectionPath,
onConsoleLog,
processEnvVars,
scriptingConfig
scriptingConfig,
runSingleRequestByPathname
);
if (result?.nextRequestName !== undefined) {
nextRequestName = result.nextRequestName;
}
if (result?.stopExecution) {
shouldStopRunnerExecution = true;
}
if (result?.skipRequest) {
return {
test: {
filename: filename
},
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: 'Request has been skipped from pre-request script',
skipped: true,
assertionResults: [],
testResults: [],
shouldStopRunnerExecution
};
}
}
// interpolate variables inside request
@@ -233,7 +265,30 @@ const runSingleRequest = async function (
if (!options.disableCookies) {
const cookieString = getCookieStringForUrl(request.url);
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
request.headers['cookie'] = cookieString;
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;
}
}
@@ -323,7 +378,8 @@ const runSingleRequest = async function (
error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
assertionResults: [],
testResults: [],
nextRequestName: nextRequestName
nextRequestName: nextRequestName,
shouldStopRunnerExecution
};
}
}
@@ -363,11 +419,16 @@ const runSingleRequest = async function (
collectionPath,
null,
processEnvVars,
scriptingConfig
scriptingConfig,
runSingleRequestByPathname
);
if (result?.nextRequestName !== undefined) {
nextRequestName = result.nextRequestName;
}
if (result?.stopExecution) {
shouldStopRunnerExecution = true;
}
}
// run assertions
@@ -408,13 +469,18 @@ const runSingleRequest = async function (
collectionPath,
null,
processEnvVars,
scriptingConfig
scriptingConfig,
runSingleRequestByPathname
);
testResults = get(result, 'results', []);
if (result?.nextRequestName !== undefined) {
nextRequestName = result.nextRequestName;
}
if (result?.stopExecution) {
shouldStopRunnerExecution = true;
}
}
if (testResults?.length) {
@@ -447,7 +513,8 @@ const runSingleRequest = async function (
error: null,
assertionResults,
testResults,
nextRequestName: nextRequestName
nextRequestName: nextRequestName,
shouldStopRunnerExecution
};
} catch (err) {
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));

View File

@@ -204,5 +204,6 @@ module.exports = {
mergeHeaders,
mergeVars,
mergeScripts,
findItemInCollection,
getTreePathFromCollectionToItem
}

View File

@@ -34,14 +34,10 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
// If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed
if (!disableParsingResponseJson && !(typeof data === 'string' && data.startsWith('"') && data.endsWith('"'))) {
if (!disableParsingResponseJson) {
data = JSON.parse(data);
}
} catch {
}
} catch { }
return { data, dataBuffer };
};