@usebruno/cli
Version:
With Bruno CLI, you can now run your API collections with ease using simple command line commands.
988 lines (886 loc) • 33.7 kB
JavaScript
const fs = require('fs');
const chalk = require('chalk');
const path = require('path');
const { forOwn, cloneDeep } = require('lodash');
const { exists, isFile, isDirectory, parseCSV } = require('../utils/filesystem');
const { runSingleRequest } = require('../runner/run-single-request');
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
const makeJUnitOutput = require('../reporters/junit');
const makeHtmlOutput = require('../reporters/html');
const { rpad } = require('../utils/common');
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
const { dotenvToJson } = require('@usebruno/lang');
const { getExternalSecretsData } = require('../utils/external-secrets');
const { interpolateString } = require('../runner/interpolate-string');
const constants = require('../constants');
const { findItemInCollection } = require('../utils/collection');
const command = 'run [filename]';
const desc = 'Run a request';
const printRunSummary = (results) => {
let totalRequests = 0;
let passedRequests = 0;
let failedRequests = 0;
let skippedRequests = 0;
let totalAssertions = 0;
let passedAssertions = 0;
let failedAssertions = 0;
let totalTests = 0;
let passedTests = 0;
let failedTests = 0;
for (const result of results) {
totalRequests += 1;
totalTests += result.testResults.length;
totalAssertions += result.assertionResults.length;
let anyFailed = false;
let hasAnyTestsOrAssertions = false;
for (const testResult of result.testResults) {
hasAnyTestsOrAssertions = true;
if (testResult.status === 'pass') {
passedTests += 1;
} else {
anyFailed = true;
failedTests += 1;
}
}
for (const assertionResult of result.assertionResults) {
hasAnyTestsOrAssertions = true;
if (assertionResult.status === 'pass') {
passedAssertions += 1;
} else {
anyFailed = true;
failedAssertions += 1;
}
}
if (!hasAnyTestsOrAssertions && result.skipped) {
skippedRequests += 1;
}
else if (!hasAnyTestsOrAssertions && result.error) {
failedRequests += 1;
} else {
passedRequests += 1;
}
}
const maxLength = 12;
let requestSummary = `${rpad('Requests:', maxLength)} ${chalk.green(`${passedRequests} passed`)}`;
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`)}`;
if (failedTests > 0) {
assertSummary += `, ${chalk.red(`${failedTests} failed`)}`;
}
assertSummary += `, ${totalTests} total`;
let testSummary = `${rpad('Assertions:', maxLength)} ${chalk.green(`${passedAssertions} passed`)}`;
if (failedAssertions > 0) {
testSummary += `, ${chalk.red(`${failedAssertions} failed`)}`;
}
testSummary += `, ${totalAssertions} total`;
console.log('\n' + chalk.bold(requestSummary));
console.log(chalk.bold(assertSummary));
console.log(chalk.bold(testSummary));
return {
totalRequests,
passedRequests,
failedRequests,
skippedRequests,
totalAssertions,
passedAssertions,
failedAssertions,
totalTests,
passedTests,
failedTests
};
};
const createCollectionFromPath = (collectionPath) => {
const environmentsPath = path.join(collectionPath, `environments`);
const getFilesInOrder = (collectionPath) => {
let collection = {
pathname: collectionPath
};
const traverse = (currentPath) => {
const filesInCurrentDir = fs.readdirSync(currentPath);
if (currentPath.includes('node_modules')) {
return;
}
const currentDirItems = [];
for (const file of filesInCurrentDir) {
const filePath = path.join(currentPath, file);
const stats = fs.lstatSync(filePath);
if (
stats.isDirectory() &&
filePath !== environmentsPath &&
!filePath.startsWith('.git') &&
!filePath.startsWith('node_modules')
) {
let folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) }
const folderBruFilePath = path.join(filePath, 'folder.bru');
const folderBruFileExists = fs.existsSync(folderBruFilePath);
if(folderBruFileExists) {
const folderBruContent = fs.readFileSync(folderBruFilePath, 'utf8');
let folderBruJson = collectionBruToJson(folderBruContent);
folderItem.root = folderBruJson;
}
currentDirItems.push(folderItem);
}
}
for (const file of filesInCurrentDir) {
if (['collection.bru', 'folder.bru'].includes(file)) {
continue;
}
const filePath = path.join(currentPath, file);
const stats = fs.lstatSync(filePath);
if (!stats.isDirectory() && path.extname(filePath) === '.bru') {
const bruContent = fs.readFileSync(filePath, 'utf8');
const bruJson = bruToJson(bruContent);
currentDirItems.push({
name: file,
pathname: filePath,
...bruJson
});
}
}
return currentDirItems;
};
collection.items = traverse(collectionPath);
return collection;
};
return getFilesInOrder(collectionPath);
};
const getBruFilesRecursively = (dir, testsOnly) => {
const environmentsPath = 'environments';
const collection = {};
const getFilesInOrder = (dir) => {
let bruJsons = [];
const traverse = (currentPath) => {
const filesInCurrentDir = fs.readdirSync(currentPath);
if (currentPath.includes('node_modules')) {
return;
}
for (const file of filesInCurrentDir) {
const filePath = path.join(currentPath, file);
const stats = fs.statSync(filePath);
// todo: we might need a ignore config inside bruno.json
if (
stats.isDirectory() &&
filePath !== environmentsPath &&
!filePath.startsWith('.git') &&
!filePath.startsWith('node_modules')
) {
traverse(filePath);
}
}
const currentDirBruJsons = [];
for (const file of filesInCurrentDir) {
if (['collection.bru', 'folder.bru'].includes(file)) {
continue;
}
const filePath = path.join(currentPath, file);
const stats = fs.lstatSync(filePath);
if (!stats.isDirectory() && path.extname(filePath) === '.bru') {
const bruContent = fs.readFileSync(filePath, 'utf8');
const bruJson = bruToJson(bruContent);
const requestHasTests = bruJson.request?.tests;
const requestHasActiveAsserts = bruJson.request?.assertions.some((x) => x.enabled) || false;
if (testsOnly) {
if (requestHasTests || requestHasActiveAsserts) {
currentDirBruJsons.push({
bruFilepath: filePath,
bruJson
});
}
} else {
currentDirBruJsons.push({
bruFilepath: filePath,
bruJson
});
}
}
}
// order requests by sequence
currentDirBruJsons.sort((a, b) => {
const aSequence = a.bruJson.seq || 0;
const bSequence = b.bruJson.seq || 0;
return aSequence - bSequence;
});
bruJsons = bruJsons.concat(currentDirBruJsons);
};
traverse(dir);
return bruJsons;
};
return getFilesInOrder(dir);
};
const getCollectionRoot = (dir) => {
const collectionRootPath = path.join(dir, 'collection.bru');
const exists = fs.existsSync(collectionRootPath);
if (!exists) {
return {};
}
const content = fs.readFileSync(collectionRootPath, 'utf8');
return collectionBruToJson(content);
};
const getFolderRoot = (dir) => {
const folderRootPath = path.join(dir, 'folder.bru');
const exists = fs.existsSync(folderRootPath);
if (!exists) {
return {};
}
const content = fs.readFileSync(folderRootPath, 'utf8');
return collectionBruToJson(content);
};
const getJsSandboxRuntime = (sandbox) => {
return sandbox === 'safe' ? 'quickjs' : 'vm2';
};
const builder = async (yargs) => {
yargs
.option('r', {
describe: 'Indicates a recursive run',
type: 'boolean',
default: false
})
.option('cacert', {
type: 'string',
description: 'CA certificate to verify peer against'
})
.option('ignore-truststore', {
type: 'boolean',
default: false,
description:
'The specified custom CA certificate (--cacert) will be used exclusively and the default truststore is ignored, if this option is specified. Evaluated in combination with "--cacert" only.'
})
.option('disable-cookies', {
type: 'boolean',
default: false,
description: 'Automatically save and sent cookies with requests'
})
.option('env', {
describe: 'Environment variables',
type: 'string'
})
.option('env-var', {
describe: 'Overwrite a single environment variable, multiple usages possible',
type: 'string'
})
.option('sandbox', {
describe: 'Javascript sandbox to use; available sandboxes are "developer" (default) or "safe"',
default: 'developer',
type: 'string'
})
.option('output', {
alias: 'o',
describe: 'Path to write file results to',
type: 'string'
})
.option('format', {
alias: 'f',
describe: 'Format of the file results; available formats are "json" (default), "junit" or "html"',
default: 'json',
type: 'string'
})
.option('reporter-json', {
describe: 'Path to write json file results to',
type: 'string'
})
.option('reporter-junit', {
describe: 'Path to write junit file results to',
type: 'string'
})
.option('reporter-html', {
describe: 'Path to write html file results to',
type: 'string'
})
.option('insecure', {
type: 'boolean',
description: 'Allow insecure server connections'
})
.option('tests-only', {
type: 'boolean',
description: 'Only run requests that have a test or active assertion'
})
.option('bail', {
type: 'boolean',
description: 'Stop execution after a failure of a request, test, or assertion'
})
.option('verbose', {
type: 'boolean',
description: 'Allow verbose output for debugging purposes'
})
.option('csv-file-path', {
describe: 'Path to the CSV file',
type: 'string'
})
.option('json-file-path', {
describe: 'Path to the JSON data file',
type: 'string'
})
.option('iteration-count', {
describe: 'Number of iterations',
type: 'string'
})
.option('reporter-skip-all-headers', {
type: 'boolean',
description: 'Omit headers from the reporter output',
default: false
})
.option('reporter-skip-headers', {
type: 'array',
description: 'Skip specific headers from the reporter output',
default: []
})
.option('client-cert-config', {
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')
.example('$0 run folder', 'Run all requests in a folder')
.example('$0 run folder -r', 'Run all requests in a folder recursively')
.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-headers "Authorization"',
'Run all requests in a folder recursively with skipped headers from the reporter output'
)
.example(
'$0 run request.bru --env local --env-var secret=xxx',
'Run a request with the environment set to local and overwrite the variable secret with value xxx'
)
.example(
'$0 run request.bru --output results.json',
'Run a request and write the results to results.json in the current directory'
)
.example(
'$0 run request.bru --output results.xml --format junit',
'Run a request and write the results to results.xml in junit format in the current directory'
)
.example(
'$0 run request.bru --output results.html --format html',
'Run a request and write the results to results.html in html format in the current directory'
)
.example(
'$0 run request.bru --reporter-junit results.xml --reporter-html results.html',
'Run a request and write the results to results.html in html format and results.xml in junit format in the current directory'
)
.example('$0 run request.bru --tests-only', 'Run all requests that have a test')
.example(
'$0 run request.bru --cacert myCustomCA.pem',
'Use a custom CA certificate in combination with the default truststore when validating the peer of this request.'
)
.example(
'$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 folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.');
};
const handler = async function (argv) {
try {
let {
filename,
cacert,
ignoreTruststore,
disableCookies,
env,
envVar,
insecure,
r: recursive,
output: outputPath,
format,
reporterJson,
reporterJunit,
reporterHtml,
sandbox,
testsOnly,
bail,
verbose,
csvFilePath,
jsonFilePath,
iterationCount = 1,
reporterSkipAllHeaders,
reporterSkipHeaders,
clientCertConfig,
delay
} = argv;
const collectionPath = process.cwd();
// todo
// right now, bru must be run from the root of the collection
// will add support in the future to run it from anywhere inside the collection
const brunoJsonPath = path.join(collectionPath, 'bruno.json');
const brunoJsonExists = await exists(brunoJsonPath);
if (!brunoJsonExists) {
console.error(chalk.red(`You can run only at the root of a collection`));
process.exit(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION);
}
const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8');
const brunoConfig = JSON.parse(brunoConfigFile);
const collectionRoot = getCollectionRoot(collectionPath);
let collection = createCollectionFromPath(collectionPath);
collection = {
brunoConfig,
root: collectionRoot,
...collection
}
if (clientCertConfig) {
try {
const clientCertConfigExists = await exists(clientCertConfig);
if (!clientCertConfigExists) {
console.error(chalk.red(`Client Certificate Config file "${clientCertConfig}" does not exist.`));
process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);
}
const clientCertConfigFileContent = fs.readFileSync(clientCertConfig, 'utf8');
let clientCertConfigJson;
try {
clientCertConfigJson = JSON.parse(clientCertConfigFileContent);
} catch (err) {
console.error(chalk.red(`Failed to parse Client Certificate Config JSON: ${err.message}`));
process.exit(constants.EXIT_STATUS.ERROR_INVALID_JSON);
}
if (clientCertConfigJson?.enabled && Array.isArray(clientCertConfigJson?.certs)) {
if (brunoConfig.clientCertificates) {
brunoConfig.clientCertificates.certs.push(...clientCertConfigJson.certs);
} else {
brunoConfig.clientCertificates = { certs: clientCertConfigJson.certs };
}
console.log(chalk.green(`Client certificates has been added`));
} else {
console.warn(chalk.yellow(`Client certificate configuration is enabled, but it either contains no valid "certs" array or the added configuration has been set to false`));
}
} catch (err) {
console.error(chalk.red(`Unexpected error: ${err.message}`));
process.exit(constants.EXIT_STATUS.ERROR_UNKNOWN);
}
}
if (filename && filename.length) {
const pathExists = await exists(filename);
if (!pathExists) {
console.error(chalk.red(`File or directory ${filename} does not exist`));
process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);
}
} else {
filename = './';
recursive = true;
}
let runtimeVariables = {};
let envVars = {};
let externalSecretVariables = {};
if (env) {
const envFile = path.join(collectionPath, 'environments', `${env}.bru`);
const envPathExists = await exists(envFile);
if (!envPathExists) {
console.error(chalk.red(`Environment file not found: `) + chalk.dim(`environments/${env}.bru`));
process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND);
}
const envBruContent = fs.readFileSync(envFile, 'utf8');
const envJson = bruToEnvJson(envBruContent);
envVars = getEnvVars(envJson);
envVars.__name__ = env;
}
if (envVar) {
let processVars;
if (typeof envVar === 'string') {
processVars = [envVar];
} else if (typeof envVar === 'object' && Array.isArray(envVar)) {
processVars = envVar;
} else {
console.error(chalk.red(`overridable environment variables not parsable: use name=value`));
process.exit(constants.EXIT_STATUS.ERROR_MALFORMED_ENV_OVERRIDE);
}
if (processVars && Array.isArray(processVars)) {
for (const value of processVars.values()) {
// split the string at the first equals sign
const match = value.match(/^([^=]+)=(.*)$/);
if (!match) {
console.error(
chalk.red(`Overridable environment variable not correct: use name=value - presented: `) +
chalk.dim(`${value}`)
);
process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_ENV_OVERRIDE);
}
envVars[match[1]] = match[2];
}
}
}
const options = getOptions();
if (bail) {
options['bail'] = true;
}
if (insecure) {
options['insecure'] = true;
}
if (disableCookies) {
options['disableCookies'] = true;
}
if (cacert && cacert.length) {
if (insecure) {
console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`));
} else {
const pathExists = await exists(cacert);
if (pathExists) {
options['cacert'] = cacert;
} else {
console.error(chalk.red(`Cacert File ${cacert} does not exist`));
}
}
}
options['ignoreTruststore'] = ignoreTruststore;
if (verbose) {
options['verbose'] = true;
}
if (['json', 'junit', 'html'].indexOf(format) === -1) {
console.error(chalk.red(`Format must be one of "json", "junit or "html"`));
process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_OUTPUT_FORMAT);
}
let formats = {};
// Maintains back compat with --format and --output
if (outputPath && outputPath.length) {
formats[format] = outputPath;
}
if (reporterHtml && reporterHtml.length) {
formats['html'] = reporterHtml;
}
if (reporterJson && reporterJson.length) {
formats['json'] = reporterJson;
}
if (reporterJunit && reporterJunit.length) {
formats['junit'] = reporterJunit;
}
// load .env file at root of collection if it exists
const dotEnvPath = path.join(collectionPath, '.env');
const dotEnvExists = await exists(dotEnvPath);
const processEnvVars = {
...process.env
};
if (dotEnvExists) {
const content = fs.readFileSync(dotEnvPath, 'utf8');
const jsonData = dotenvToJson(content);
forOwn(jsonData, (value, key) => {
processEnvVars[key] = value;
});
}
if (env) {
const externalSecretsJsonPath = path.join(collectionPath, 'secrets.json');
const externalSecretsExists = await exists(externalSecretsJsonPath);
if (externalSecretsExists) {
const externalSecretsContent = fs.readFileSync(externalSecretsJsonPath, 'utf8');
const interpolationOptions = {
envVars,
runtimeVariables,
processEnvVars
};
const externalSecretsInterpolatedContent = interpolateString(externalSecretsContent, interpolationOptions);
const externalSecretsJson = JSON.parse(externalSecretsInterpolatedContent);
const {
cli: externalSecretsProviderConfig,
data: externalSecretsPathsData,
type: externalSecretsType
} = externalSecretsJson;
const externalSecretsForCurrentEnvironment = externalSecretsPathsData?.find(
(d) => d?.environment === env
)?.secrets;
const externalSecretsPathsForCurrentEnvironment = externalSecretsForCurrentEnvironment?.map((s) => s.path);
if (externalSecretsProviderConfig && externalSecretsPathsForCurrentEnvironment) {
verbose && console.log(chalk.yellow('Fetching external secrets... \n'));
try {
const secrets = await getExternalSecretsData({
type: externalSecretsType,
config: externalSecretsProviderConfig,
paths: externalSecretsPathsForCurrentEnvironment,
debug: verbose
});
secrets.forEach((s, idx) => {
if (s?.error) {
verbose &&
console.error(
chalk.red(`${idx + 1}. Couldn't fetch secret for path: ${s?.path}: `) +
chalk.dim(`${JSON.stringify(s?.error)}`)
);
return;
}
verbose && console.log(chalk.yellow(`${idx + 1}. Fetched secret for path: ${s?.path} \n`));
let secretName = externalSecretsForCurrentEnvironment?.find((x) => x.path === s.path)?.name;
Object.entries(s?.data).forEach(([key, value]) => {
externalSecretVariables[`$secrets.${secretName}.${key}`] = value;
});
});
} catch (err) {
verbose && console.error(chalk.red(`Fetching external secrets failed: `) + chalk.dim(`${err.message}`));
}
}
}
}
const _isFile = isFile(filename);
let results = [];
let bruJsons = [];
if (_isFile) {
console.log(chalk.yellow('Running Request \n'));
const bruContent = fs.readFileSync(filename, 'utf8');
const bruJson = bruToJson(bruContent);
bruJsons.push({
bruFilepath: filename,
bruJson
});
}
const _isDirectory = isDirectory(filename);
if (_isDirectory) {
if (!recursive) {
console.log(chalk.yellow('Running Folder \n'));
const files = fs.readdirSync(filename);
const bruFiles = files.filter((file) => !['folder.bru'].includes(file) && file.endsWith('.bru'));
for (const bruFile of bruFiles) {
const bruFilepath = path.join(filename, bruFile);
const bruContent = fs.readFileSync(bruFilepath, 'utf8');
const bruJson = bruToJson(bruContent);
const requestHasTests = bruJson.request?.tests;
const requestHasActiveAsserts = bruJson.request?.assertions.some((x) => x.enabled) || false;
if (testsOnly) {
if (requestHasTests || requestHasActiveAsserts) {
bruJsons.push({
bruFilepath,
bruJson
});
}
} else {
bruJsons.push({
bruFilepath,
bruJson
});
}
}
bruJsons.sort((a, b) => {
const aSequence = a.bruJson.seq || 0;
const bSequence = b.bruJson.seq || 0;
return aSequence - bSequence;
});
} else {
console.log(chalk.yellow('Running Folder Recursively \n'));
bruJsons = getBruFilesRecursively(filename, testsOnly);
}
}
let csvFileData = [];
if (csvFilePath) {
const csvPathExists = await exists(csvFilePath);
if (!csvPathExists) {
console.error(chalk.red(`CSV file ${csvFilePath} does not exist`));
process.exit(constants.EXIT_STATUS.ERROR_CSV_FILE_NOT_FOUND);
}
const csvData = fs.readFileSync(csvFilePath, 'utf8');
csvFileData = await parseCSV(csvData);
iterationCount = csvFileData?.length;
}
let jsonFileData = [];
if (jsonFilePath) {
const jsonPathExists = await exists(jsonFilePath);
if (!jsonPathExists) {
console.error(chalk.red(`CSV file ${jsonFilePath} does not exist`));
process.exit(constants.EXIT_STATUS.ERROR_JSON_FILE_NOT_FOUND);
}
const jsonData = fs.readFileSync(jsonFilePath, 'utf8');
jsonFileData = JSON.parse(jsonData);
iterationCount = jsonFileData?.length;
}
let iterationRunResults = [];
for (let iterationIndex = 0; iterationIndex < iterationCount; iterationIndex++) {
let currentRequestIndex = 0;
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,
externalSecretVariables,
runtime,
collection,
runSingleRequestByPathname
);
resolve(res?.response);
}
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
});
}
const csvDataVariables = csvFileData?.[iterationIndex] || {};
const jsonDataVariables = jsonFileData?.[iterationIndex] || {};
const hasCsvData = Object.keys(csvDataVariables).length > 0;
const hasJsonData = Object.keys(jsonDataVariables).length > 0;
const hasExternalData = hasCsvData || hasJsonData;
const resultsForCurrentIteration = [];
if (hasExternalData || iterationCount > 1) {
console.log(`\n${chalk.green('Iteration:', iterationIndex + 1)}\n`);
}
if (verbose && hasCsvData) {
console.log(`${chalk.green('CSV data:')}\n${chalk.yellow(JSON.stringify(csvDataVariables, null, 2))}\n`);
}
if (verbose && hasJsonData) {
console.log(`${chalk.green('JSON data:')}\n${chalk.yellow(JSON.stringify(jsonDataVariables, null, 2))}\n`);
}
let nJumps = 0; // count the number of jumps to avoid infinite loops
while (currentRequestIndex < bruJsons.length) {
const iter = cloneDeep(bruJsons[currentRequestIndex]);
const { bruFilepath, bruJson } = iter;
runtimeVariables = { ...runtimeVariables, ...csvDataVariables, ...jsonDataVariables };
const start = process.hrtime();
const result = await runSingleRequest(
bruFilepath,
bruJson,
collectionPath,
runtimeVariables,
envVars,
processEnvVars,
brunoConfig,
collectionRoot,
externalSecretVariables,
runtime,
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,
suitename: bruFilepath.replace('.bru', ''),
iterationIndex
});
resultsForCurrentIteration.push({
...result,
runtime: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9,
suitename: bruFilepath.replace('.bru', ''),
iterationIndex
});
if (reporterSkipAllHeaders) {
results.forEach((result) => {
result.request.headers = {};
result.response.headers = {};
});
}
const deleteHeaderIfExists = (headers, header) => {
if (headers && headers[header]) {
delete headers[header];
}
};
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);
});
}
});
}
// bail if option is set and there is a failure
if (bail) {
const requestFailure = result?.error;
const testFailure = result?.testResults?.find((iter) => iter.status === 'fail');
const assertionFailure = result?.assertionResults?.find((iter) => iter.status === 'fail');
if (requestFailure || testFailure || assertionFailure) {
break;
}
}
// determine next request
const nextRequestName = result?.nextRequestName;
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);
}
if (nextRequestName === null) {
break;
}
const nextRequestIdx = bruJsons.findIndex((iter) => iter.bruJson.name === nextRequestName);
if (nextRequestIdx >= 0) {
currentRequestIndex = nextRequestIdx;
} else {
console.error("Could not find request with name '" + nextRequestName + "'");
currentRequestIndex++;
}
} else {
currentRequestIndex++;
}
}
let iterationRunSummary = printRunSummary(resultsForCurrentIteration);
iterationRunResults.push({
iterationIndex,
summary: iterationRunSummary,
results: resultsForCurrentIteration
});
const totalTime = resultsForCurrentIteration.reduce((acc, res) => acc + res.response.responseTime, 0);
console.log(chalk.dim(chalk.grey(`Ran all requests - ${totalTime} ms`)));
}
// run summary for all iterations
let summary = printRunSummary(results);
const formatKeys = Object.keys(formats);
if (formatKeys && formatKeys.length > 0) {
const outputJson = iterationRunResults;
const reporters = {
'json': (path) => fs.writeFileSync(path, JSON.stringify(outputJson, null, 2)),
'junit': (path) => makeJUnitOutput(results, path),
'html': (path) => makeHtmlOutput(outputJson, path),
}
for (const formatter of Object.keys(formats))
{
const reportPath = formats[formatter];
const reporter = reporters[formatter];
// Skip formatters lacking an output path.
if (!reportPath || reportPath.length === 0) {
continue;
}
const outputDir = path.dirname(reportPath);
const outputDirExists = await exists(outputDir);
if (!outputDirExists) {
console.error(chalk.red(`Output directory ${outputDir} does not exist`));
process.exit(constants.EXIT_STATUS.ERROR_MISSING_OUTPUT_DIR);
}
if (!reporter) {
console.error(chalk.red(`Reporter ${formatter} does not exist`));
process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_OUTPUT_FORMAT);
}
reporter(reportPath);
console.log(chalk.dim(chalk.grey(`Wrote ${formatter} results to ${reportPath}`)));
}
}
if (summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) {
process.exit(constants.EXIT_STATUS.ERROR_FAILED_COLLECTION);
}
} catch (err) {
console.log('Something went wrong');
console.error(chalk.red(err.message));
process.exit(constants.EXIT_STATUS.ERROR_GENERIC);
}
};
module.exports = {
command,
desc,
builder,
handler,
printRunSummary
};