Merge branch 'main' into fix/cli-not-following-redirects

This commit is contained in:
sanjai0py
2025-05-13 20:07:29 +05:30
58 changed files with 1700 additions and 560 deletions

View File

@@ -12,7 +12,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 { findItemInCollection, getAllRequestsInFolder, createCollectionJsonFromPathname } = require('../utils/collection');
const command = 'run [filename]';
const desc = 'Run a request';
@@ -22,6 +22,7 @@ const printRunSummary = (results) => {
passedRequests,
failedRequests,
skippedRequests,
errorRequests,
totalAssertions,
passedAssertions,
failedAssertions,
@@ -36,6 +37,9 @@ const printRunSummary = (results) => {
if (failedRequests > 0) {
requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`;
}
if (errorRequests > 0) {
requestSummary += `, ${chalk.red(`${errorRequests} error`)}`;
}
if (skippedRequests > 0) {
requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`;
}
@@ -62,6 +66,7 @@ const printRunSummary = (results) => {
passedRequests,
failedRequests,
skippedRequests,
errorRequests,
totalAssertions,
passedAssertions,
failedAssertions,
@@ -71,163 +76,6 @@ const printRunSummary = (results) => {
}
};
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';
};
@@ -320,7 +168,6 @@ const builder = async (yargs) => {
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')
@@ -390,25 +237,8 @@ const handler = async function (argv) {
} = 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
}
let collection = createCollectionJsonFromPathname(collectionPath);
const { root: collectionRoot, brunoConfig } = collection;
if (clientCertConfig) {
try {
@@ -444,7 +274,6 @@ const handler = async function (argv) {
}
}
if (filename && filename.length) {
const pathExists = await exists(filename);
if (!pathExists) {
@@ -566,54 +395,39 @@ const handler = async function (argv) {
const _isFile = isFile(filename);
let results = [];
let bruJsons = [];
let requestItems = [];
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 requestItem = bruToJson(bruContent);
requestItem.pathname = path.resolve(collectionPath, filename);
requestItems.push(requestItem);
}
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'));
}
const resolvedFilepath = path.resolve(filename);
if (resolvedFilepath === collectionPath) {
requestItems = getAllRequestsInFolder(collection?.items, recursive);
} else {
const folderItem = findItemInCollection(collection, resolvedFilepath);
if (folderItem) {
requestItems = getAllRequestsInFolder(folderItem.items, recursive);
}
}
bruJsons = getBruFilesRecursively(filename, testsOnly);
if (testsOnly) {
requestItems = requestItems.filter((iter) => {
const requestHasTests = iter.request?.tests;
const requestHasActiveAsserts = iter.request?.assertions.some((x) => x.enabled) || false;
return requestHasTests || requestHasActiveAsserts;
});
}
}
@@ -625,11 +439,10 @@ const handler = async function (argv) {
if (itemPathname && !itemPathname?.endsWith('.bru')) {
itemPathname = `${itemPathname}.bru`;
}
const bruJson = cloneDeep(findItemInCollection(collection, itemPathname));
if (bruJson) {
const requestItem = cloneDeep(findItemInCollection(collection, itemPathname));
if (requestItem) {
const res = await runSingleRequest(
itemPathname,
bruJson,
requestItem,
collectionPath,
runtimeVariables,
envVars,
@@ -648,14 +461,13 @@ const handler = async function (argv) {
let currentRequestIndex = 0;
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;
while (currentRequestIndex < requestItems.length) {
const requestItem = cloneDeep(requestItems[currentRequestIndex]);
const { pathname } = requestItem;
const start = process.hrtime();
const result = await runSingleRequest(
bruFilepath,
bruJson,
requestItem,
collectionPath,
runtimeVariables,
envVars,
@@ -667,7 +479,7 @@ const handler = async function (argv) {
runSingleRequestByPathname
);
const isLastRun = currentRequestIndex === bruJsons.length - 1;
const isLastRun = currentRequestIndex === requestItems.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.`));
@@ -681,7 +493,7 @@ const handler = async function (argv) {
results.push({
...result,
runtime: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9,
suitename: bruFilepath.replace('.bru', '')
suitename: pathname.replace('.bru', '')
});
if (reporterSkipAllHeaders) {
@@ -739,7 +551,7 @@ const handler = async function (argv) {
if (nextRequestName === null) {
break;
}
const nextRequestIdx = bruJsons.findIndex((iter) => iter.bruJson.name === nextRequestName);
const nextRequestIdx = requestItems.findIndex((iter) => iter.name === nextRequestName);
if (nextRequestIdx >= 0) {
currentRequestIndex = nextRequestIdx;
} else {

View File

@@ -156,6 +156,37 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
delete request.basicAuth;
}
if (request?.oauth2?.grantType) {
switch (request.oauth2.grantType) {
case 'password':
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';
request.oauth2.username = _interpolate(request.oauth2.username) || '';
request.oauth2.password = _interpolate(request.oauth2.password) || '';
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
break;
case 'client_credentials':
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
break;
default:
break;
}
}
if (request.awsv4config) {
request.awsv4config.accessKeyId = _interpolate(request.awsv4config.accessKeyId) || '';
request.awsv4config.secretAccessKey = _interpolate(request.awsv4config.secretAccessKey) || '';

View File

@@ -0,0 +1,6 @@
const { getOAuth2Token } = require('@usebruno/requests');
const tokenStore = require('./tokenStore');
module.exports = {
getOAuth2Token: (oauth2Config) => getOAuth2Token(oauth2Config, tokenStore)
};

View File

@@ -1,7 +1,7 @@
const { get, each, filter } = require('lodash');
const decomment = require('decomment');
const crypto = require('node:crypto');
const { mergeHeaders, mergeScripts, mergeVars, getTreePathFromCollectionToItem } = require('../utils/collection');
const { mergeHeaders, mergeScripts, mergeVars, mergeAuth, getTreePathFromCollectionToItem } = require('../utils/collection');
const { createFormData } = require('../utils/form-data');
const prepareRequest = (item = {}, collection = {}) => {
@@ -16,6 +16,7 @@ const prepareRequest = (item = {}, collection = {}) => {
mergeHeaders(collection, request, requestTreePath);
mergeScripts(collection, request, requestTreePath, scriptFlow);
mergeVars(collection, request, requestTreePath);
mergeAuth(collection, request, requestTreePath);
}
each(get(request, 'headers', []), (h) => {
@@ -72,6 +73,76 @@ const prepareRequest = (item = {}, collection = {}) => {
password: get(collectionAuth, 'digest.password')
};
}
if (collectionAuth.mode === 'oauth2') {
const grantType = get(collectionAuth, 'oauth2.grantType');
if (grantType === 'client_credentials') {
axiosRequest.oauth2 = {
grantType,
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
clientId: get(collectionAuth, 'oauth2.clientId'),
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
scope: get(collectionAuth, 'oauth2.scope'),
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey')
};
} else if (grantType === 'password') {
axiosRequest.oauth2 = {
grantType,
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
username: get(collectionAuth, 'oauth2.username'),
password: get(collectionAuth, 'oauth2.password'),
clientId: get(collectionAuth, 'oauth2.clientId'),
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
scope: get(collectionAuth, 'oauth2.scope'),
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey')
};
}
}
if (collectionAuth.mode === 'awsv4') {
axiosRequest.awsv4config = {
accessKeyId: get(collectionAuth, 'awsv4.accessKeyId'),
secretAccessKey: get(collectionAuth, 'awsv4.secretAccessKey'),
sessionToken: get(collectionAuth, 'awsv4.sessionToken'),
service: get(collectionAuth, 'awsv4.service'),
region: get(collectionAuth, 'awsv4.region'),
profileName: get(collectionAuth, 'awsv4.profileName')
};
}
if (collectionAuth.mode === 'ntlm') {
axiosRequest.ntlmConfig = {
username: get(collectionAuth, 'ntlm.username'),
password: get(collectionAuth, 'ntlm.password'),
domain: get(collectionAuth, 'ntlm.domain')
};
}
if (collectionAuth.mode === 'wsse') {
const username = get(collectionAuth, 'wsse.username', '');
const password = get(collectionAuth, 'wsse.password', '');
const ts = new Date().toISOString();
const nonce = crypto.randomBytes(16).toString('hex');
// Create the password digest using SHA-1 as required for WSSE
const hash = crypto.createHash('sha1');
hash.update(nonce + ts + password);
const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
// Construct the WSSE header
axiosRequest.headers[
'X-WSSE'
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
}
console.log('axiosRequest', axiosRequest);
}
if (request.auth && request.auth.mode !== 'inherit') {
@@ -129,6 +200,56 @@ const prepareRequest = (item = {}, collection = {}) => {
password: get(request, 'auth.digest.password')
};
}
if (request.auth.mode === 'oauth2') {
const grantType = get(request, 'auth.oauth2.grantType');
if (grantType === 'client_credentials') {
axiosRequest.oauth2 = {
grantType,
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'),
scope: get(request, 'auth.oauth2.scope'),
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey')
};
} else if (grantType === 'password') {
axiosRequest.oauth2 = {
grantType,
username: get(request, 'auth.oauth2.username'),
password: get(request, 'auth.oauth2.password'),
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'),
scope: get(request, 'auth.oauth2.scope'),
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey')
};
}
}
if (request.auth.mode === 'apikey') {
if (request.auth.apikey?.placement === 'header') {
axiosRequest.headers[request.auth.apikey?.key] = request.auth.apikey?.value;
}
if (request.auth.apikey?.placement === 'queryparams') {
if (axiosRequest.url && request.auth.apikey?.key) {
try {
const urlObj = new URL(request.url);
urlObj.searchParams.set(request.auth.apikey?.key, request.auth.apikey?.value);
axiosRequest.url = urlObj.toString();
} catch (error) {
console.error('Invalid URL:', request.url, error);
}
}
}
}
}
request.body = request.body || {};

View File

@@ -22,6 +22,7 @@ const path = require('path');
const { parseDataFromResponse } = require('../utils/common');
const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies');
const { createFormData } = require('../utils/form-data');
const { getOAuth2Token } = require('./oauth2');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const { NtlmClient } = require('axios-ntlm');
const { addDigestInterceptor } = require('@usebruno/requests');
@@ -31,8 +32,7 @@ const onConsoleLog = (type, args) => {
};
const runSingleRequest = async function (
filename,
bruJson,
item,
collectionPath,
runtimeVariables,
envVariables,
@@ -43,14 +43,12 @@ const runSingleRequest = async function (
collection,
runSingleRequestByPathname
) {
const { pathname: itemPathname } = item;
const relativeItemPathname = path.relative(collectionPath, itemPathname);
try {
let request;
let nextRequestName;
let shouldStopRunnerExecution = false;
let item = {
pathname: path.join(collectionPath, filename),
...bruJson
}
request = prepareRequest(item, collection);
request.__bruno__executionMode = 'cli';
@@ -84,7 +82,7 @@ const runSingleRequest = async function (
if (result?.skipRequest) {
return {
test: {
filename: filename
filename: relativeItemPathname
},
request: {
method: request.method,
@@ -98,7 +96,8 @@ const runSingleRequest = async function (
data: null,
responseTime: 0
},
error: 'Request has been skipped from pre-request script',
error: null,
status: 'skipped',
skipped: true,
assertionResults: [],
testResults: [],
@@ -313,6 +312,33 @@ const runSingleRequest = async function (
requestMaxRedirects = 5; // Default to 5 redirects
}
// Handle OAuth2 authentication
if (request.oauth2) {
try {
const token = await getOAuth2Token(request.oauth2);
if (token) {
const { tokenPlacement = 'header', tokenHeaderPrefix = 'Bearer', tokenQueryKey = 'access_token' } = request.oauth2;
if (tokenPlacement === 'header') {
request.headers['Authorization'] = `${tokenHeaderPrefix} ${token}`;
} 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);
}
// Remove oauth2 config from request to prevent it from being sent
delete request.oauth2;
}
let response, responseTime;
try {
@@ -370,10 +396,10 @@ const runSingleRequest = async function (
responseTime = response.headers.get('request-duration');
response.headers.delete('request-duration');
} else {
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));
return {
test: {
filename: filename
filename: relativeItemPathname
},
request: {
method: request.method,
@@ -382,13 +408,14 @@ const runSingleRequest = async function (
data: request.data
},
response: {
status: null,
status: 'error',
statusText: null,
headers: null,
data: null,
responseTime: 0
},
error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
status: 'error',
assertionResults: [],
testResults: [],
nextRequestName: nextRequestName,
@@ -400,12 +427,12 @@ const runSingleRequest = async function (
response.responseTime = responseTime;
console.log(
chalk.green(stripExtension(filename)) +
chalk.green(stripExtension(relativeItemPathname)) +
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
);
// run post-response vars
const postResponseVars = get(bruJson, 'request.vars.res');
const postResponseVars = get(item, 'request.vars.res');
if (postResponseVars?.length) {
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
varsRuntime.runPostResponseVars(
@@ -446,7 +473,7 @@ const runSingleRequest = async function (
// run assertions
let assertionResults = [];
const assertions = get(bruJson, 'request.assertions');
const assertions = get(item, 'request.assertions');
if (assertions) {
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
assertionResults = assertRuntime.runAssertions(
@@ -508,7 +535,7 @@ const runSingleRequest = async function (
return {
test: {
filename: filename
filename: relativeItemPathname
},
request: {
method: request.method,
@@ -524,16 +551,17 @@ const runSingleRequest = async function (
responseTime
},
error: null,
status: 'pass',
assertionResults,
testResults,
nextRequestName: nextRequestName,
shouldStopRunnerExecution
};
} catch (err) {
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));
return {
test: {
filename: filename
filename: relativeItemPathname
},
request: {
method: null,
@@ -542,12 +570,13 @@ const runSingleRequest = async function (
data: null
},
response: {
status: null,
status: 'error',
statusText: null,
headers: null,
data: null,
responseTime: 0
},
status: 'error',
error: err.message,
assertionResults: [],
testResults: []

View File

@@ -0,0 +1,22 @@
// In-memory token store implementation for OAuth2 tokens
const tokenStore = {
tokens: new Map(),
// Save a token with optional expiry information
async saveToken(serviceId, account, token) {
this.tokens.set(`${serviceId}:${account}`, token);
return true;
},
// Get a token
async getToken(serviceId, account) {
return this.tokens.get(`${serviceId}:${account}`);
},
// Delete a token
async deleteToken(serviceId, account) {
return this.tokens.delete(`${serviceId}:${account}`);
}
};
module.exports = tokenStore;

View File

@@ -15,6 +15,17 @@ const collectionBruToJson = (bru) => {
}
};
// add meta if it exists
// this is only for folder bru file
// in the future, all of this will be replaced by standard bru lang
const sequence = _.get(json, 'meta.seq');
if (json?.meta) {
transformedJson.meta = {
name: json.meta.name,
seq: !isNaN(sequence) ? Number(sequence) : 1
};
}
return transformedJson;
} catch (error) {
return Promise.reject(error);

View File

@@ -4,6 +4,112 @@ const fs = require('fs');
const path = require('path');
const { jsonToBruV2, envJsonToBruV2, jsonToCollectionBru } = require('@usebruno/lang');
const { sanitizeName } = require('./filesystem');
const { bruToJson, collectionBruToJson } = require('./bru');
const constants = require('../constants');
const chalk = require('chalk');
const createCollectionJsonFromPathname = (collectionPath) => {
const environmentsPath = path.join(collectionPath, `environments`);
// get the collection bruno json config [<collection-path>/bruno.json]
const brunoConfig = getCollectionBrunoJsonConfig(collectionPath);
// get the collection root [<collection-path>/collection.bru]
const collectionRoot = getCollectionRoot(collectionPath);
// get the collection items recursively
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()) {
if (filePath === environmentsPath) continue;
if (filePath.startsWith('.git') || filePath.startsWith('node_modules')) continue;
// get the folder root
let folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) }
const folderBruJson = getFolderRoot(filePath);
if (folderBruJson) {
folderItem.root = folderBruJson;
folderItem.seq = folderBruJson.meta.seq;
}
currentDirItems.push(folderItem);
}
else {
if (['collection.bru', 'folder.bru'].includes(file)) continue;
if (path.extname(filePath) !== '.bru') continue;
// get the request item
const bruContent = fs.readFileSync(filePath, 'utf8');
const requestItem = bruToJson(bruContent);
currentDirItems.push({
name: file,
pathname: filePath,
...requestItem
});
}
}
let currentDirFolderItems = currentDirItems?.filter((iter) => iter.type === 'folder');
let sortedFolderItems = currentDirFolderItems?.sort((a, b) => a.seq - b.seq);
let currentDirRequestItems = currentDirItems?.filter((iter) => iter.type !== 'folder');
let sortedRequestItems = currentDirRequestItems?.sort((a, b) => a.seq - b.seq);
return sortedFolderItems?.concat(sortedRequestItems);
};
let collectionItems = traverse(collectionPath);
let collection = {
brunoConfig,
root: collectionRoot,
pathname: collectionPath,
items: collectionItems
}
return collection;
};
const getCollectionBrunoJsonConfig = (dir) => {
// 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(dir, 'bruno.json');
const brunoJsonExists = fs.existsSync(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);
return brunoConfig;
}
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 null;
}
const content = fs.readFileSync(folderRootPath, 'utf8');
return collectionBruToJson(content);
};
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
@@ -204,6 +310,45 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
return path;
};
const mergeAuth = (collection, request, requestTreePath) => {
let collectionAuth = collection?.root?.request?.auth || { mode: 'none' };
let effectiveAuth = collectionAuth;
for (let i of requestTreePath) {
if (i.type === 'folder') {
const folderAuth = i?.root?.request?.auth;
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveAuth = folderAuth;
}
}
}
if (request.auth && request.auth.mode === 'inherit') {
request.auth = effectiveAuth;
}
}
const getAllRequestsInFolder = (folderItems = [], recursive = true) => {
let requests = [];
if (folderItems && folderItems.length) {
folderItems.forEach((item) => {
if (item.type !== 'folder') {
requests.push(item);
} else {
if (recursive) {
requests = requests.concat(getAllRequestsInFolder(item.items, recursive));
}
}
});
}
return requests;
};
const getAllRequestsAtFolderRoot = (folderItems = []) => {
return getAllRequestsInFolder(folderItems, false);
}
/**
* Safe write file implementation to handle errors
* @param {string} filePath - Path to write file
@@ -335,10 +480,14 @@ const processCollectionItems = async (items = [], currentPath) => {
};
module.exports = {
createCollectionJsonFromPathname,
mergeHeaders,
mergeVars,
mergeScripts,
findItemInCollection,
getTreePathFromCollectionToItem,
createCollectionFromBrunoObject
createCollectionFromBrunoObject,
mergeAuth,
getAllRequestsInFolder,
getAllRequestsAtFolderRoot
}