mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 21:55:49 +00:00
refactor: streamline hook execution by merging scripts and removing unused functions
- Consolidated hook management by merging hooks from collection, folders, and requests into a single script using mergeScripts. - Removed the HooksExecutor and HooksConsolidator, simplifying the execution flow. - Updated runSingleRequest and network IPC to utilize the new merged hooks approach for improved performance and clarity. - Enhanced comments for better understanding of the hook execution process.
This commit is contained in:
@@ -6,10 +6,10 @@ const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lod
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { interpolateString, interpolateObject } = require('./interpolate-string');
|
||||
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime, HooksRuntime, HooksExecutor } = require('@usebruno/js');
|
||||
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime, HooksRuntime } = require('@usebruno/js');
|
||||
const { stripExtension } = require('../utils/filesystem');
|
||||
const { getOptions } = require('../utils/bru');
|
||||
const { extractHooks, getTreePathFromCollectionToItem, HOOK_EVENTS } = require('../utils/collection');
|
||||
const { getTreePathFromCollectionToItem, HOOK_EVENTS } = require('../utils/collection');
|
||||
const https = require('https');
|
||||
const { HttpProxyAgent } = require('http-proxy-agent');
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
@@ -211,17 +211,17 @@ const runSingleRequest = async function (
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = runtime;
|
||||
|
||||
// Get request tree path for hook extraction
|
||||
// Get request tree path for hook execution
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
const collectionName = collection?.brunoConfig?.name;
|
||||
|
||||
// Extract hooks for all levels
|
||||
const { collectionHooks, folderHooks, requestHooks } = extractHooks(collection, request, requestTreePath);
|
||||
|
||||
// Helper function to execute all hooks using consolidated approach
|
||||
const executeAllHooksConsolidated = async (extractedHooks, hookEvent, eventData) => {
|
||||
return HooksExecutor.executeAllHookLevels(extractedHooks, hookEvent, eventData, {
|
||||
// Helper function to execute merged hooks (hooks are merged in prepareRequest via mergeScripts)
|
||||
const executeHooks = async (hookEvent, eventData) => {
|
||||
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await hooksRuntime.runHooks({
|
||||
hooksFile: request.script?.hooks,
|
||||
request,
|
||||
response: eventData.response,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
@@ -231,15 +231,37 @@ const runSingleRequest = async function (
|
||||
runRequestByItemPathname: runSingleRequestByPathname,
|
||||
collectionName
|
||||
});
|
||||
|
||||
if (result?.hookManager) {
|
||||
// Enrich eventData with runtime-created req/res wrappers
|
||||
const enrichedEventData = {
|
||||
...eventData,
|
||||
req: result.req || eventData.req,
|
||||
res: result.res || eventData.res
|
||||
};
|
||||
await result.hookManager.call(hookEvent, enrichedEventData);
|
||||
|
||||
// Re-read runner control signals from bru instance AFTER handlers have executed
|
||||
// Handlers may have called bru.runner.setNextRequest(), skipRequest(), or stopExecution()
|
||||
// which update the bru instance but were not captured in the initial result
|
||||
const bru = result.__bru;
|
||||
if (bru) {
|
||||
result.nextRequestName = bru.nextRequest;
|
||||
result.skipRequest = bru.skipRequest;
|
||||
result.stopExecution = bru.stopExecution;
|
||||
}
|
||||
|
||||
result.hookManager.dispose();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Call beforeRequest hooks before running pre-request scripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
// Note: BrunoRequest is now created inside HooksRuntime for consistency with ScriptRuntime
|
||||
const beforeRequestEventData = { request, collection };
|
||||
|
||||
const beforeRequestHooksResult = await executeAllHooksConsolidated(
|
||||
{ collectionHooks, folderHooks, requestHooks },
|
||||
const beforeRequestHooksResult = await executeHooks(
|
||||
HOOK_EVENTS.HTTP_BEFORE_REQUEST,
|
||||
beforeRequestEventData
|
||||
);
|
||||
@@ -714,12 +736,9 @@ const runSingleRequest = async function (
|
||||
|
||||
// Call afterResponse hooks after response is received but before post-response scripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
// Uses consolidated execution when multiple levels have hooks (more efficient)
|
||||
// Note: BrunoRequest and BrunoResponse are now created inside HooksRuntime for consistency with ScriptRuntime
|
||||
const afterResponseEventData = { request, response, collection };
|
||||
|
||||
const afterResponseHooksResult = await executeAllHooksConsolidated(
|
||||
{ collectionHooks, folderHooks, requestHooks },
|
||||
const afterResponseHooksResult = await executeHooks(
|
||||
HOOK_EVENTS.HTTP_AFTER_RESPONSE,
|
||||
afterResponseEventData
|
||||
);
|
||||
|
||||
@@ -234,10 +234,12 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
let collectionPreReqScript = get(collectionRoot, 'request.script.req', '');
|
||||
let collectionPostResScript = get(collectionRoot, 'request.script.res', '');
|
||||
let collectionTests = get(collectionRoot, 'request.tests', '');
|
||||
let collectionHooks = get(collectionRoot, 'request.script.hooks', '');
|
||||
|
||||
let combinedPreReqScript = [];
|
||||
let combinedPostResScript = [];
|
||||
let combinedTests = [];
|
||||
let combinedHooks = [];
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
@@ -255,6 +257,11 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
if (tests && tests?.trim?.() !== '') {
|
||||
combinedTests.push(tests);
|
||||
}
|
||||
|
||||
let hooks = get(folderRoot, 'request.script.hooks', '');
|
||||
if (hooks && hooks.trim() !== '') {
|
||||
combinedHooks.push(hooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +311,21 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
];
|
||||
request.tests = compact(testScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
}
|
||||
|
||||
// Handle hooks - always merged sequentially: collection -> folders -> request
|
||||
let requestHooks = request?.script?.hooks || '';
|
||||
const hooksScripts = [
|
||||
collectionHooks,
|
||||
...combinedHooks,
|
||||
requestHooks
|
||||
];
|
||||
|
||||
// Ensure request.script exists
|
||||
if (!request.script) {
|
||||
request.script = {};
|
||||
}
|
||||
|
||||
request.script.hooks = compact(hooksScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
};
|
||||
|
||||
const findItem = (items = [], pathname) => {
|
||||
@@ -372,52 +394,6 @@ const mergeAuth = (collection, request, requestTreePath) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract hooks from collection, folders, and request for registration.
|
||||
* Unlike mergeScripts, this returns separate hooks for each level to allow
|
||||
* one-time registration at each level.
|
||||
*
|
||||
* @param {object} collection - Collection object
|
||||
* @param {object} request - Request object (prepared request, may not have hooks)
|
||||
* @param {array} requestTreePath - Path from collection to request
|
||||
* @returns {object} Object containing hooks at each level
|
||||
*/
|
||||
const extractHooks = (collection, request, requestTreePath) => {
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionHooks = get(collectionRoot, 'request.script.hooks', '');
|
||||
|
||||
const folderHooks = [];
|
||||
let requestHooks = '';
|
||||
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
const hooks = get(folderRoot, 'request.script.hooks', '');
|
||||
if (hooks && hooks.trim() !== '') {
|
||||
folderHooks.push({
|
||||
folderPathname: i.pathname, // Use pathname as unique identifier
|
||||
hooks: hooks
|
||||
});
|
||||
}
|
||||
} else if (i.type !== 'folder') {
|
||||
// This is the request item - get hooks from it
|
||||
const itemRoot = i?.draft || i?.root || i;
|
||||
requestHooks = get(itemRoot, 'request.script.hooks', '') || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to get from request object if not found in tree path
|
||||
if (!requestHooks) {
|
||||
requestHooks = get(request, 'script.hooks', '') || get(request, 'hooks', '') || '';
|
||||
}
|
||||
|
||||
return {
|
||||
collectionHooks,
|
||||
folderHooks,
|
||||
requestHooks
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook event names used throughout the application.
|
||||
* This object is frozen to prevent accidental modifications and improve maintainability.
|
||||
@@ -659,6 +635,5 @@ module.exports = {
|
||||
getAllRequestsInFolder,
|
||||
getAllRequestsAtFolderRoot,
|
||||
getCallStack,
|
||||
extractHooks,
|
||||
HOOK_EVENTS
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ const mime = require('mime-types');
|
||||
const { ipcMain } = require('electron');
|
||||
const { each, get, extend, cloneDeep, merge } = require('lodash');
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime, HooksRuntime, HooksExecutor } = require('@usebruno/js');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime, HooksRuntime } = require('@usebruno/js');
|
||||
const { encodeUrl } = require('@usebruno/common').utils;
|
||||
const { extractPromptVariables } = require('@usebruno/common').utils;
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
@@ -24,7 +24,7 @@ const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseData
|
||||
const { chooseFileToSave, writeFile, getCollectionFormat, hasRequestExtension } = require('../../utils/filesystem');
|
||||
const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');
|
||||
const { createFormData } = require('../../utils/form-data');
|
||||
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars, sortByNameThenSequence, extractHooks, HOOK_EVENTS } = require('../../utils/collection');
|
||||
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars, sortByNameThenSequence, HOOK_EVENTS } = require('../../utils/collection');
|
||||
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, updateCollectionOauth2Credentials } = require('../../utils/oauth2');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
@@ -497,10 +497,20 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
* @param {object} options - Configuration options
|
||||
* @returns {Promise<object|null>} Execution result or null if error
|
||||
*/
|
||||
const executeAllHooksConsolidated = async (extractedHooks, hookEvent, eventData, options) => {
|
||||
/**
|
||||
* Execute merged hooks for a specific event
|
||||
* @param {string} hookEvent - Hook event to trigger
|
||||
* @param {object} eventData - Data to pass to hook handlers
|
||||
* @param {object} options - Configuration options
|
||||
* @returns {Promise<object|null>} Execution result or null if error
|
||||
*/
|
||||
const executeHooks = async (hookEvent, eventData, options) => {
|
||||
try {
|
||||
const result = await HooksExecutor.executeAllHookLevels(extractedHooks, hookEvent, eventData, {
|
||||
const hooksRuntime = new HooksRuntime({ runtime: options.scriptingConfig?.runtime });
|
||||
const result = await hooksRuntime.runHooks({
|
||||
hooksFile: options.request?.script?.hooks,
|
||||
request: options.request || {},
|
||||
response: eventData.response,
|
||||
envVariables: options.envVars,
|
||||
runtimeVariables: options.runtimeVariables,
|
||||
collectionPath: options.collectionPath,
|
||||
@@ -511,6 +521,28 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionName: options.collectionName
|
||||
});
|
||||
|
||||
if (result?.hookManager) {
|
||||
// Enrich eventData with runtime-created req/res wrappers
|
||||
const enrichedEventData = {
|
||||
...eventData,
|
||||
req: result.req || eventData.req,
|
||||
res: result.res || eventData.res
|
||||
};
|
||||
await result.hookManager.call(hookEvent, enrichedEventData);
|
||||
|
||||
// Re-read runner control signals from bru instance AFTER handlers have executed
|
||||
// Handlers may have called bru.runner.setNextRequest(), skipRequest(), or stopExecution()
|
||||
// which update the bru instance but were not captured in the initial result
|
||||
const bru = result.__bru;
|
||||
if (bru) {
|
||||
result.nextRequestName = bru.nextRequest;
|
||||
result.skipRequest = bru.skipRequest;
|
||||
result.stopExecution = bru.stopExecution;
|
||||
}
|
||||
|
||||
result.hookManager.dispose();
|
||||
}
|
||||
|
||||
// Send UI updates if we have a result
|
||||
if (result) {
|
||||
await sendScriptEnvironmentUpdates({
|
||||
@@ -524,7 +556,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error executing consolidated hooks for ${hookEvent}:`, error);
|
||||
console.error(`Error executing hooks for ${hookEvent}:`, error);
|
||||
options.onConsoleLog?.('error', [`Error executing hooks for ${hookEvent}: ${error.message}`]);
|
||||
if (!options.runInBackground && options.notifyScriptExecution && typeof options.notifyScriptExecution === 'function') {
|
||||
options.notifyScriptExecution({
|
||||
@@ -735,19 +767,16 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
|
||||
// Get request tree path for hooks registration
|
||||
// Get request tree path for hooks execution
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
try {
|
||||
request.signal = abortController.signal;
|
||||
saveCancelToken(cancelTokenUid, abortController);
|
||||
|
||||
// Extract hooks for all levels
|
||||
const { collectionHooks, folderHooks, requestHooks } = extractHooks(collection, request, requestTreePath);
|
||||
|
||||
// Call beforeRequest hooks before running pre-request scripts
|
||||
// Hooks are merged in prepareRequest via mergeScripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
// Note: BrunoRequest is now created inside HooksRuntime for consistency with ScriptRuntime
|
||||
const beforeRequestEventData = { request, collection, collectionUid };
|
||||
const hookOptions = {
|
||||
request,
|
||||
@@ -767,9 +796,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
notifyScriptExecution
|
||||
};
|
||||
|
||||
// Call beforeRequest hooks using consolidated approach when multiple levels have hooks
|
||||
await executeAllHooksConsolidated(
|
||||
{ collectionHooks, folderHooks, requestHooks },
|
||||
// Call beforeRequest hooks using merged hooks
|
||||
await executeHooks(
|
||||
HOOK_EVENTS.HTTP_BEFORE_REQUEST,
|
||||
beforeRequestEventData,
|
||||
hookOptions
|
||||
@@ -941,12 +969,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// Call afterResponse hooks after response is received but before post-response scripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
// Note: BrunoRequest and BrunoResponse are now created inside HooksRuntime for consistency with ScriptRuntime
|
||||
const afterResponseEventData = { request, response, collection, collectionUid };
|
||||
|
||||
// Call afterResponse hooks using consolidated approach when multiple levels have hooks
|
||||
await executeAllHooksConsolidated(
|
||||
{ collectionHooks, folderHooks, requestHooks },
|
||||
// Call afterResponse hooks using merged hooks
|
||||
await executeHooks(
|
||||
HOOK_EVENTS.HTTP_AFTER_RESPONSE,
|
||||
afterResponseEventData,
|
||||
hookOptions
|
||||
@@ -1432,9 +1458,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get request tree path for hooks extraction
|
||||
// Get request tree path for hooks execution (hooks are merged in prepareRequest via mergeScripts)
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
const { collectionHooks, folderHooks, requestHooks } = extractHooks(collection, request, requestTreePath);
|
||||
|
||||
// Hook execution options
|
||||
const hookOptions = {
|
||||
@@ -1467,12 +1492,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
};
|
||||
|
||||
try {
|
||||
// Call beforeRequest hooks using consolidated approach when multiple levels have hooks
|
||||
// Note: BrunoRequest is now created inside HooksRuntime for consistency with ScriptRuntime
|
||||
// Call beforeRequest hooks using merged hooks
|
||||
const beforeRequestEventData = { request, collection, collectionUid };
|
||||
|
||||
const beforeRequestHooksResult = await executeAllHooksConsolidated(
|
||||
{ collectionHooks, folderHooks, requestHooks },
|
||||
const beforeRequestHooksResult = await executeHooks(
|
||||
HOOK_EVENTS.HTTP_BEFORE_REQUEST,
|
||||
beforeRequestEventData,
|
||||
hookOptions
|
||||
@@ -1723,12 +1746,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Call afterResponse hooks using consolidated approach when multiple levels have hooks
|
||||
// Note: BrunoRequest and BrunoResponse are now created inside HooksRuntime for consistency with ScriptRuntime
|
||||
// Call afterResponse hooks using merged hooks
|
||||
const afterResponseEventData = { request, response, collection, collectionUid };
|
||||
|
||||
const afterResponseHooksResult = await executeAllHooksConsolidated(
|
||||
{ collectionHooks, folderHooks, requestHooks },
|
||||
const afterResponseHooksResult = await executeHooks(
|
||||
HOOK_EVENTS.HTTP_AFTER_RESPONSE,
|
||||
afterResponseEventData,
|
||||
hookOptions
|
||||
|
||||
@@ -157,10 +157,12 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
let collectionPreReqScript = get(collectionRoot, 'request.script.req', '');
|
||||
let collectionPostResScript = get(collectionRoot, 'request.script.res', '');
|
||||
let collectionTests = get(collectionRoot, 'request.tests', '');
|
||||
let collectionHooks = get(collectionRoot, 'request.script.hooks', '');
|
||||
|
||||
let combinedPreReqScript = [];
|
||||
let combinedPostResScript = [];
|
||||
let combinedTests = [];
|
||||
let combinedHooks = [];
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
@@ -178,6 +180,11 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
if (tests && tests?.trim?.() !== '') {
|
||||
combinedTests.push(tests);
|
||||
}
|
||||
|
||||
let hooks = get(folderRoot, 'request.script.hooks', '');
|
||||
if (hooks && hooks.trim() !== '') {
|
||||
combinedHooks.push(hooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,52 +234,21 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
];
|
||||
request.tests = compact(testScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract hooks from collection, folders, and request for registration.
|
||||
* Unlike mergeScripts, this returns separate hooks for each level to allow
|
||||
* one-time registration at each level.
|
||||
*
|
||||
* @param {object} collection - Collection object
|
||||
* @param {object} request - Request object (prepared request, may not have hooks)
|
||||
* @param {array} requestTreePath - Path from collection to request
|
||||
* @returns {object} Object containing hooks at each level
|
||||
*/
|
||||
const extractHooks = (collection, request, requestTreePath) => {
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionHooks = get(collectionRoot, 'request.script.hooks', '');
|
||||
|
||||
const folderHooks = [];
|
||||
let requestHooks = '';
|
||||
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
const hooks = get(folderRoot, 'request.script.hooks', '');
|
||||
if (hooks && hooks.trim() !== '') {
|
||||
folderHooks.push({
|
||||
folderPathname: i.pathname, // Use pathname as unique identifier
|
||||
hooks: hooks
|
||||
});
|
||||
}
|
||||
} else if (i.type !== 'folder') {
|
||||
// This is the request item - get hooks from it
|
||||
const itemRoot = i?.draft || i?.root || i;
|
||||
requestHooks = get(itemRoot, 'request.script.hooks', '') || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to get from request object if not found in tree path
|
||||
if (!requestHooks) {
|
||||
requestHooks = get(request, 'script.hooks', '') || get(request, 'hooks', '') || '';
|
||||
}
|
||||
|
||||
return {
|
||||
// Handle hooks - always merged sequentially: collection -> folders -> request
|
||||
let requestHooks = request?.script?.hooks || '';
|
||||
const hooksScripts = [
|
||||
collectionHooks,
|
||||
folderHooks,
|
||||
...combinedHooks,
|
||||
requestHooks
|
||||
};
|
||||
];
|
||||
|
||||
// Ensure request.script exists
|
||||
if (!request.script) {
|
||||
request.script = {};
|
||||
}
|
||||
|
||||
request.script.hooks = compact(hooksScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -812,6 +788,5 @@ module.exports = {
|
||||
getFormattedCollectionOauth2Credentials,
|
||||
sortByNameThenSequence,
|
||||
resolveInheritedSettings,
|
||||
extractHooks,
|
||||
HOOK_EVENTS
|
||||
};
|
||||
|
||||
@@ -3,8 +3,6 @@ const TestRuntime = require('./runtime/test-runtime');
|
||||
const VarsRuntime = require('./runtime/vars-runtime');
|
||||
const AssertRuntime = require('./runtime/assert-runtime');
|
||||
const HooksRuntime = require('./runtime/hooks-runtime');
|
||||
const HooksConsolidator = require('./runtime/hooks-consolidator');
|
||||
const HooksExecutor = require('./runtime/hooks-executor');
|
||||
const HookManager = require('./hook-manager');
|
||||
const { runScriptInNodeVm } = require('./sandbox/node-vm');
|
||||
|
||||
@@ -14,8 +12,6 @@ module.exports = {
|
||||
VarsRuntime,
|
||||
AssertRuntime,
|
||||
HooksRuntime,
|
||||
HooksConsolidator,
|
||||
HooksExecutor,
|
||||
HookManager,
|
||||
runScriptInNodeVm
|
||||
};
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
/**
|
||||
* Hooks Consolidator Utility
|
||||
*
|
||||
* This module provides utilities to consolidate multiple hook scripts (collection, folder, request levels)
|
||||
* into a single IIFE execution. This improves performance by:
|
||||
* - Creating only one VM instance instead of multiple
|
||||
* - Executing all hook levels sequentially within that VM
|
||||
* - Maintaining proper variable scoping with nested IIFEs
|
||||
*
|
||||
* Each level's hooks are wrapped in their own IIFE to ensure:
|
||||
* - Isolated variable scope per level
|
||||
* - Each level gets its own Bru instance with appropriate variable context
|
||||
* - Handlers capture the correct Bru instance in their closure
|
||||
* - Errors in one level don't break other levels
|
||||
*/
|
||||
|
||||
const decomment = require('decomment');
|
||||
|
||||
/**
|
||||
* Hook level types for identification
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
const HOOK_LEVEL = Object.freeze({
|
||||
COLLECTION: 'collection',
|
||||
FOLDER: 'folder',
|
||||
REQUEST: 'request'
|
||||
});
|
||||
|
||||
/**
|
||||
* Represents a single hook level with its script and metadata
|
||||
* @typedef {Object} HookLevel
|
||||
* @property {string} level - The level type (collection, folder, request)
|
||||
* @property {string} script - The hooks script content
|
||||
* @property {string} [identifier] - Unique identifier (e.g., folder pathname)
|
||||
* @property {Object} [variables] - Level-specific variables
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configuration for building a consolidated hook script
|
||||
* @typedef {Object} ConsolidatorConfig
|
||||
* @property {string} [collectionHooks] - Collection-level hooks script
|
||||
* @property {Array<{folderPathname: string, hooks: string}>} [folderHooks] - Folder-level hooks
|
||||
* @property {string} [requestHooks] - Request-level hooks script
|
||||
* @property {boolean} [removeComments=true] - Whether to remove comments from scripts
|
||||
*/
|
||||
|
||||
/**
|
||||
* Result of consolidating hooks
|
||||
* @typedef {Object} ConsolidatedResult
|
||||
* @property {string} script - The consolidated IIFE script
|
||||
* @property {Array<HookLevel>} levels - Metadata about included levels
|
||||
* @property {boolean} hasHooks - Whether any hooks were found
|
||||
*/
|
||||
|
||||
/**
|
||||
* Escapes a string for safe inclusion in JavaScript code
|
||||
* Handles special characters that could break the script
|
||||
* @param {string} str - String to escape
|
||||
* @returns {string} Escaped string
|
||||
*/
|
||||
const escapeForTemplate = (str) => {
|
||||
if (!str) return '';
|
||||
// Replace backticks and ${} to prevent template literal injection
|
||||
return str
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/\$\{/g, '\\${');
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps a hook script in an IIFE with error handling
|
||||
* The IIFE captures the appropriate bru instance for that level
|
||||
* @param {string} script - Hook script content
|
||||
* @param {string} level - Hook level identifier for error reporting
|
||||
* @param {string} [identifier] - Additional identifier (e.g., folder path)
|
||||
* @returns {string} Wrapped script
|
||||
*/
|
||||
const wrapInIIFE = (script, level, identifier = '') => {
|
||||
if (!script || !script.trim()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const levelId = identifier ? `${level}:${identifier}` : level;
|
||||
|
||||
// The IIFE creates a scoped execution context
|
||||
// The bru variable is expected to be set in the outer scope before this IIFE runs
|
||||
// Each level will have its own bru instance with appropriate variable context
|
||||
return `
|
||||
// === ${level.toUpperCase()} HOOKS${identifier ? ` (${identifier})` : ''} ===
|
||||
await (async () => {
|
||||
const __hookLevel = '${escapeForTemplate(levelId)}';
|
||||
try {
|
||||
${script}
|
||||
} catch (__hookError) {
|
||||
__consolidatedErrors.push({
|
||||
level: __hookLevel,
|
||||
error: __hookError?.message || String(__hookError),
|
||||
stack: __hookError?.stack
|
||||
});
|
||||
if (typeof __onHookError === 'function') {
|
||||
__onHookError(__hookLevel, __hookError);
|
||||
}
|
||||
}
|
||||
})();
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes hook script by optionally removing comments
|
||||
* @param {string} script - Script to process
|
||||
* @param {boolean} removeComments - Whether to remove comments
|
||||
* @returns {string} Processed script
|
||||
*/
|
||||
const processScript = (script, removeComments = true) => {
|
||||
if (!script || !script.trim()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (removeComments) {
|
||||
try {
|
||||
return decomment(script);
|
||||
} catch (e) {
|
||||
// If decomment fails, return original script
|
||||
return script;
|
||||
}
|
||||
}
|
||||
|
||||
return script;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a consolidated hook script from multiple levels
|
||||
*
|
||||
* The consolidated script structure:
|
||||
* 1. Initializes shared state (error collection, bru instances tracking)
|
||||
* 2. Executes each level's hooks in an IIFE with its own scope
|
||||
* 3. Each level's handlers capture the bru instance available in that scope
|
||||
* 4. Returns collected errors and any other results
|
||||
*
|
||||
* @param {ConsolidatorConfig} config - Configuration for consolidation
|
||||
* @returns {ConsolidatedResult} Consolidated script and metadata
|
||||
*/
|
||||
const buildConsolidatedScript = (config) => {
|
||||
const {
|
||||
collectionHooks = '',
|
||||
folderHooks = [],
|
||||
requestHooks = '',
|
||||
removeComments = true
|
||||
} = config;
|
||||
|
||||
const levels = [];
|
||||
const scriptParts = [];
|
||||
|
||||
// Process collection hooks
|
||||
const processedCollectionHooks = processScript(collectionHooks, removeComments);
|
||||
if (processedCollectionHooks && processedCollectionHooks.trim()) {
|
||||
levels.push({
|
||||
level: HOOK_LEVEL.COLLECTION,
|
||||
script: processedCollectionHooks,
|
||||
identifier: 'root'
|
||||
});
|
||||
scriptParts.push(wrapInIIFE(processedCollectionHooks, HOOK_LEVEL.COLLECTION));
|
||||
}
|
||||
|
||||
// Process folder hooks (in order from collection to request)
|
||||
if (Array.isArray(folderHooks)) {
|
||||
for (const folder of folderHooks) {
|
||||
if (!folder || !folder.hooks) continue;
|
||||
|
||||
const processedFolderHooks = processScript(folder.hooks, removeComments);
|
||||
if (processedFolderHooks && processedFolderHooks.trim()) {
|
||||
levels.push({
|
||||
level: HOOK_LEVEL.FOLDER,
|
||||
script: processedFolderHooks,
|
||||
identifier: folder.folderPathname
|
||||
});
|
||||
scriptParts.push(wrapInIIFE(processedFolderHooks, HOOK_LEVEL.FOLDER, folder.folderPathname));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process request hooks
|
||||
const processedRequestHooks = processScript(requestHooks, removeComments);
|
||||
if (processedRequestHooks && processedRequestHooks.trim()) {
|
||||
levels.push({
|
||||
level: HOOK_LEVEL.REQUEST,
|
||||
script: processedRequestHooks,
|
||||
identifier: 'current'
|
||||
});
|
||||
scriptParts.push(wrapInIIFE(processedRequestHooks, HOOK_LEVEL.REQUEST));
|
||||
}
|
||||
|
||||
const hasHooks = levels.length > 0;
|
||||
|
||||
if (!hasHooks) {
|
||||
return {
|
||||
script: '',
|
||||
levels: [],
|
||||
hasHooks: false
|
||||
};
|
||||
}
|
||||
|
||||
// Build the full consolidated script
|
||||
// Note: The outer scope provides `bru` and `__onHookError` variables
|
||||
// The script assumes these are set up by the runtime before execution
|
||||
const consolidatedScript = `
|
||||
// Consolidated hooks script - generated by hooks-consolidator
|
||||
// Contains ${levels.length} hook level(s): ${levels.map((l) => l.level).join(', ')}
|
||||
|
||||
// Shared error collection for all levels
|
||||
const __consolidatedErrors = [];
|
||||
|
||||
// Execute all hook levels sequentially
|
||||
${scriptParts.join('\n')}
|
||||
|
||||
// Return collected errors (if any) for reporting
|
||||
if (__consolidatedErrors.length > 0) {
|
||||
__hookResult = { errors: __consolidatedErrors };
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
script: consolidatedScript.trim(),
|
||||
levels,
|
||||
hasHooks: true
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates level metadata for tracking what was included in consolidation
|
||||
* @param {ConsolidatorConfig} config - Configuration used for consolidation
|
||||
* @returns {Array<{level: string, identifier: string}>} Level metadata
|
||||
*/
|
||||
const getLevelMetadata = (config) => {
|
||||
const { collectionHooks, folderHooks, requestHooks } = config;
|
||||
const metadata = [];
|
||||
|
||||
if (collectionHooks && collectionHooks.trim()) {
|
||||
metadata.push({ level: HOOK_LEVEL.COLLECTION, identifier: 'root' });
|
||||
}
|
||||
|
||||
if (Array.isArray(folderHooks)) {
|
||||
for (const folder of folderHooks) {
|
||||
if (folder?.hooks && folder.hooks.trim()) {
|
||||
metadata.push({ level: HOOK_LEVEL.FOLDER, identifier: folder.folderPathname });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requestHooks && requestHooks.trim()) {
|
||||
metadata.push({ level: HOOK_LEVEL.REQUEST, identifier: 'current' });
|
||||
}
|
||||
|
||||
return metadata;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts hook configuration from extracted hooks object
|
||||
* Converts the format from extractHooks() to ConsolidatorConfig format
|
||||
* @param {Object} extractedHooks - Object from extractHooks() function
|
||||
* @param {string} extractedHooks.collectionHooks - Collection hooks script
|
||||
* @param {Array} extractedHooks.folderHooks - Array of folder hooks
|
||||
* @param {string} extractedHooks.requestHooks - Request hooks script
|
||||
* @returns {ConsolidatorConfig} Configuration for consolidation
|
||||
*/
|
||||
const fromExtractedHooks = (extractedHooks) => {
|
||||
const { collectionHooks = '', folderHooks = [], requestHooks = '' } = extractedHooks || {};
|
||||
|
||||
return {
|
||||
collectionHooks,
|
||||
folderHooks,
|
||||
requestHooks,
|
||||
removeComments: true
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// Main functions
|
||||
buildConsolidatedScript,
|
||||
|
||||
// Utility functions
|
||||
wrapInIIFE,
|
||||
processScript,
|
||||
escapeForTemplate,
|
||||
|
||||
// Analysis functions
|
||||
getLevelMetadata,
|
||||
fromExtractedHooks,
|
||||
|
||||
// Constants
|
||||
HOOK_LEVEL
|
||||
};
|
||||
@@ -1,272 +0,0 @@
|
||||
/**
|
||||
* Hooks Executor Utility
|
||||
*
|
||||
* This module provides a centralized hook execution logic that can be used by both
|
||||
* CLI and Electron implementations. It eliminates code duplication and ensures
|
||||
* consistent behavior across different execution contexts.
|
||||
*
|
||||
* Features:
|
||||
* - Unified hook execution for collection, folder, and request levels
|
||||
* - Support for both individual and consolidated execution modes
|
||||
* - Proper error isolation and reporting
|
||||
* - Hook manager lifecycle management
|
||||
*/
|
||||
|
||||
const HooksRuntime = require('./hooks-runtime');
|
||||
const decomment = require('decomment');
|
||||
|
||||
/**
|
||||
* Hook event names used throughout the application
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
const HOOK_EVENTS = Object.freeze({
|
||||
HTTP_BEFORE_REQUEST: 'http:beforeRequest',
|
||||
HTTP_AFTER_RESPONSE: 'http:afterResponse',
|
||||
RUNNER_BEFORE_COLLECTION_RUN: 'runner:beforeCollectionRun',
|
||||
RUNNER_AFTER_COLLECTION_RUN: 'runner:afterCollectionRun'
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates execution options for hooks
|
||||
* @typedef {Object} HookExecutionOptions
|
||||
* @property {object} request - The request object
|
||||
* @property {object} envVariables - Environment variables
|
||||
* @property {object} runtimeVariables - Runtime variables
|
||||
* @property {string} collectionPath - Collection path
|
||||
* @property {function} [onConsoleLog] - Console log callback
|
||||
* @property {object} processEnvVars - Process environment variables
|
||||
* @property {object} scriptingConfig - Scripting configuration
|
||||
* @property {function} [runRequestByItemPathname] - Function to run requests
|
||||
* @property {string} collectionName - Collection name
|
||||
* @property {HookManager} [hookManager] - Existing HookManager to use
|
||||
*/
|
||||
|
||||
/**
|
||||
* Result from hook execution
|
||||
* @typedef {Object} HookExecutionResult
|
||||
* @property {HookManager} hookManager - The HookManager instance
|
||||
* @property {object} envVariables - Updated environment variables
|
||||
* @property {object} runtimeVariables - Updated runtime variables
|
||||
* @property {object} persistentEnvVariables - Persistent environment variables
|
||||
* @property {object} globalEnvironmentVariables - Global environment variables
|
||||
* @property {Array} [errors] - Any errors that occurred during execution
|
||||
*/
|
||||
|
||||
/**
|
||||
* Executes hooks for a single level (collection, folder, or request)
|
||||
* This is the individual execution mode - one VM per level
|
||||
*
|
||||
* @param {string} hooksFile - The hooks script content
|
||||
* @param {string} hookEvent - The hook event to trigger (e.g., HOOK_EVENTS.HTTP_BEFORE_REQUEST)
|
||||
* @param {object} eventData - Data to pass to hook handlers
|
||||
* @param {HookExecutionOptions} options - Execution options
|
||||
* @returns {Promise<HookExecutionResult|null>} Execution result or null if no hooks
|
||||
*/
|
||||
const executeHooksForLevel = async (hooksFile, hookEvent, eventData, options) => {
|
||||
if (!hooksFile || !hooksFile.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
request,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
} = options;
|
||||
|
||||
// Extract response from eventData if available (for afterResponse hooks)
|
||||
const response = eventData?.response;
|
||||
|
||||
try {
|
||||
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await hooksRuntime.runHooks({
|
||||
hooksFile: decomment(hooksFile),
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
});
|
||||
|
||||
if (result?.hookManager) {
|
||||
// Enrich eventData with runtime-created req/res wrappers
|
||||
const enrichedEventData = {
|
||||
...eventData,
|
||||
req: result.req || eventData.req,
|
||||
res: result.res || eventData.res
|
||||
};
|
||||
await result.hookManager.call(hookEvent, enrichedEventData);
|
||||
// Dispose HookManager to free VM resources
|
||||
if (typeof result.hookManager.dispose === 'function') {
|
||||
result.hookManager.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (onConsoleLog) {
|
||||
onConsoleLog('error', [`Error executing hooks for ${hookEvent}: ${error.message}`]);
|
||||
}
|
||||
console.error(`Error executing hooks for ${hookEvent}:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes hooks for multiple levels using consolidated execution
|
||||
* This batches all hook levels into a single VM execution for better performance
|
||||
*
|
||||
* @param {object} extractedHooks - Hooks extracted from collection/folder/request
|
||||
* @param {string} extractedHooks.collectionHooks - Collection-level hooks
|
||||
* @param {Array} extractedHooks.folderHooks - Folder-level hooks array
|
||||
* @param {string} extractedHooks.requestHooks - Request-level hooks
|
||||
* @param {string} hookEvent - The hook event to trigger
|
||||
* @param {object} eventData - Data to pass to hook handlers
|
||||
* @param {HookExecutionOptions} options - Execution options
|
||||
* @returns {Promise<HookExecutionResult|null>} Execution result or null if no hooks
|
||||
*/
|
||||
const executeConsolidatedHooks = async (extractedHooks, hookEvent, eventData, options) => {
|
||||
const {
|
||||
request,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
} = options;
|
||||
|
||||
// Extract response from eventData if available (for afterResponse hooks)
|
||||
const response = eventData?.response;
|
||||
|
||||
try {
|
||||
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await hooksRuntime.runHooks({
|
||||
consolidated: true,
|
||||
consolidatedHooks: extractedHooks,
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
});
|
||||
|
||||
if (result?.hookManager) {
|
||||
// Enrich eventData with runtime-created req/res wrappers
|
||||
const enrichedEventData = {
|
||||
...eventData,
|
||||
req: result.req || eventData.req,
|
||||
res: result.res || eventData.res
|
||||
};
|
||||
await result.hookManager.call(hookEvent, enrichedEventData);
|
||||
|
||||
// IMPORTANT: Re-capture runner control values AFTER hooks have been called
|
||||
// The hooks may have called bru.runner.setNextRequest(), bru.runner.skipRequest(), etc.
|
||||
// These values are stored on the bru instance which is returned in result.__bru
|
||||
if (result.__bru) {
|
||||
result.nextRequestName = result.__bru.nextRequest;
|
||||
result.skipRequest = result.__bru.skipRequest;
|
||||
result.stopExecution = result.__bru.stopExecution;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (onConsoleLog) {
|
||||
onConsoleLog('error', [`Error executing consolidated hooks for ${hookEvent}: ${error.message}`]);
|
||||
}
|
||||
console.error(`Error executing consolidated hooks for ${hookEvent}:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes all hook levels in sequence (collection -> folders -> request)
|
||||
* Always uses consolidated execution for better performance
|
||||
*
|
||||
* @param {object} extractedHooks - Hooks extracted from all levels
|
||||
* @param {string} hookEvent - The hook event to trigger
|
||||
* @param {object} eventData - Data to pass to hook handlers
|
||||
* @param {HookExecutionOptions} options - Execution options
|
||||
* @returns {Promise<HookExecutionResult|null>} Execution result or null if no hooks
|
||||
*/
|
||||
const executeAllHookLevels = async (extractedHooks, hookEvent, eventData, options) => {
|
||||
// Always use consolidated execution - single VM for all levels
|
||||
const result = await executeConsolidatedHooks(extractedHooks, hookEvent, eventData, options);
|
||||
if (result?.hookManager && typeof result.hookManager.dispose === 'function') {
|
||||
result.hookManager.dispose();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a reusable hook executor with cached configuration
|
||||
* Useful for collection runs where the same configuration is used for multiple requests
|
||||
*
|
||||
* @param {HookExecutionOptions} baseOptions - Base options to use for all executions
|
||||
* @returns {object} Hook executor with pre-configured methods
|
||||
*/
|
||||
const createHookExecutor = (baseOptions) => {
|
||||
return {
|
||||
/**
|
||||
* Execute hooks for a single level
|
||||
*/
|
||||
executeLevel: (hooksFile, hookEvent, eventData, overrideOptions = {}) => {
|
||||
return executeHooksForLevel(hooksFile, hookEvent, eventData, {
|
||||
...baseOptions,
|
||||
...overrideOptions
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute consolidated hooks
|
||||
*/
|
||||
executeConsolidated: (extractedHooks, hookEvent, eventData, overrideOptions = {}) => {
|
||||
return executeConsolidatedHooks(extractedHooks, hookEvent, eventData, {
|
||||
...baseOptions,
|
||||
...overrideOptions
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute all hook levels
|
||||
*/
|
||||
executeAll: (extractedHooks, hookEvent, eventData, overrideOptions = {}) => {
|
||||
return executeAllHookLevels(extractedHooks, hookEvent, eventData, {
|
||||
...baseOptions,
|
||||
...overrideOptions
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// Core execution functions
|
||||
executeHooksForLevel,
|
||||
executeConsolidatedHooks,
|
||||
executeAllHookLevels,
|
||||
|
||||
// Factory function
|
||||
createHookExecutor,
|
||||
|
||||
// Constants
|
||||
HOOK_EVENTS
|
||||
};
|
||||
@@ -5,14 +5,15 @@ const BrunoResponse = require('../bruno-response');
|
||||
const HookManager = require('../hook-manager');
|
||||
const { cleanJson } = require('../utils');
|
||||
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
||||
const { buildConsolidatedScript, fromExtractedHooks } = require('./hooks-consolidator');
|
||||
|
||||
/**
|
||||
* HooksRuntime manages the execution of hook scripts in a sandboxed environment.
|
||||
*
|
||||
* Optimizations:
|
||||
* Hooks are now merged into a single script using mergeScripts() in collection.js,
|
||||
* following the same pattern as pre-request, post-response, and test scripts.
|
||||
*
|
||||
* Features:
|
||||
* - Lazy VM creation: VMs are only created when hooks are present
|
||||
* - Consolidated execution: Multiple hook levels can be batched into a single VM
|
||||
* - Shared HookManager: Can reuse an existing HookManager for handler registration
|
||||
*
|
||||
* @class
|
||||
@@ -28,8 +29,7 @@ class HooksRuntime {
|
||||
// Track statistics for performance monitoring
|
||||
this._stats = {
|
||||
vmCreations: 0,
|
||||
consolidatedRuns: 0,
|
||||
singleLevelRuns: 0,
|
||||
runs: 0,
|
||||
skippedRuns: 0
|
||||
};
|
||||
}
|
||||
@@ -55,7 +55,7 @@ class HooksRuntime {
|
||||
/**
|
||||
* Run hooks script to register event handlers
|
||||
* @param {object} options - Configuration options
|
||||
* @param {string} [options.hooksFile] - The hooks script content (for single-level execution)
|
||||
* @param {string} [options.hooksFile] - The merged hooks script content
|
||||
* @param {object} options.request - The request object (used for variable extraction and BrunoRequest creation)
|
||||
* @param {object} [options.response] - The response object (used for BrunoResponse creation, only for afterResponse hooks)
|
||||
* @param {object} options.envVariables - Environment variables
|
||||
@@ -67,11 +67,6 @@ class HooksRuntime {
|
||||
* @param {function} [options.runRequestByItemPathname] - Function to run requests
|
||||
* @param {string} options.collectionName - Collection name
|
||||
* @param {HookManager} [options.hookManager] - Existing HookManager instance to use (for shared hook registration)
|
||||
* @param {boolean} [options.consolidated=false] - Whether to use consolidated execution mode
|
||||
* @param {object} [options.consolidatedHooks] - Consolidated hooks data (when consolidated=true)
|
||||
* @param {string} [options.consolidatedHooks.collectionHooks] - Collection-level hooks script
|
||||
* @param {Array<object>} [options.consolidatedHooks.folderHooks] - Array of folder hooks
|
||||
* @param {string} [options.consolidatedHooks.requestHooks] - Request-level hooks script
|
||||
* @returns {object} Result containing the hookManager instance, and req/res wrapper objects
|
||||
*/
|
||||
async runHooks(options) {
|
||||
@@ -87,9 +82,7 @@ class HooksRuntime {
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName,
|
||||
hookManager,
|
||||
consolidated = false,
|
||||
consolidatedHooks
|
||||
hookManager
|
||||
} = options;
|
||||
const activeHookManager = hookManager || new HookManager();
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
@@ -99,31 +92,6 @@ class HooksRuntime {
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
|
||||
// Consolidated execution mode: build and execute consolidated script
|
||||
if (consolidated && consolidatedHooks) {
|
||||
return this._runConsolidatedHooks({
|
||||
consolidatedHooks,
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName,
|
||||
activeHookManager,
|
||||
globalEnvironmentVariables,
|
||||
oauth2CredentialVariables,
|
||||
collectionVariables,
|
||||
folderVariables,
|
||||
requestVariables,
|
||||
promptVariables
|
||||
});
|
||||
}
|
||||
|
||||
// Single-level execution mode (original behavior)
|
||||
// Pass activeHookManager to Bru so it uses the same instance (whether provided or newly created)
|
||||
const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, activeHookManager);
|
||||
|
||||
@@ -174,7 +142,7 @@ class HooksRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
this._stats.singleLevelRuns++;
|
||||
this._stats.runs++;
|
||||
this._stats.vmCreations++;
|
||||
|
||||
// Execute hooks script
|
||||
@@ -231,187 +199,6 @@ class HooksRuntime {
|
||||
res
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Run consolidated hooks execution - all levels in a single VM run
|
||||
* This is more efficient when there are multiple hook levels (collection, folder, request)
|
||||
* @private
|
||||
* @param {object} options - Execution options
|
||||
* @returns {Promise<object>} Result containing hookManager and variable states
|
||||
*/
|
||||
async _runConsolidatedHooks(options) {
|
||||
const {
|
||||
consolidatedHooks,
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName,
|
||||
activeHookManager,
|
||||
globalEnvironmentVariables,
|
||||
oauth2CredentialVariables,
|
||||
collectionVariables,
|
||||
folderVariables,
|
||||
requestVariables,
|
||||
promptVariables
|
||||
} = options;
|
||||
|
||||
// Build consolidated script from all hook levels
|
||||
const config = fromExtractedHooks(consolidatedHooks);
|
||||
const { script: consolidatedScript, hasHooks, levels } = buildConsolidatedScript(config);
|
||||
|
||||
// Lazy VM creation: If no hooks, return early without creating a VM
|
||||
if (!hasHooks || !consolidatedScript) {
|
||||
this._stats.skippedRuns++;
|
||||
const bru = new Bru(
|
||||
this.runtime,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath,
|
||||
collectionVariables,
|
||||
folderVariables,
|
||||
requestVariables,
|
||||
globalEnvironmentVariables,
|
||||
oauth2CredentialVariables,
|
||||
collectionName,
|
||||
promptVariables,
|
||||
activeHookManager
|
||||
);
|
||||
// Create BrunoRequest and BrunoResponse wrappers
|
||||
const req = request ? new BrunoRequest(request) : null;
|
||||
const res = response ? new BrunoResponse(response) : null;
|
||||
return {
|
||||
hookManager: activeHookManager,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
persistentEnvVariables: bru.persistentEnvVariables,
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution,
|
||||
__bru: bru,
|
||||
req,
|
||||
res
|
||||
};
|
||||
}
|
||||
|
||||
this._stats.consolidatedRuns++;
|
||||
this._stats.vmCreations++;
|
||||
|
||||
// Create a single Bru instance for consolidated execution
|
||||
// All hook levels will share this instance's HookManager
|
||||
const bru = new Bru(
|
||||
this.runtime,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath,
|
||||
collectionVariables,
|
||||
folderVariables,
|
||||
requestVariables,
|
||||
globalEnvironmentVariables,
|
||||
oauth2CredentialVariables,
|
||||
collectionName,
|
||||
promptVariables,
|
||||
activeHookManager
|
||||
);
|
||||
|
||||
// Create BrunoRequest and BrunoResponse wrappers (similar to ScriptRuntime)
|
||||
const req = request ? new BrunoRequest(request) : null;
|
||||
const res = response ? new BrunoResponse(response) : null;
|
||||
|
||||
// Prepare context with error handling callback
|
||||
const context = {
|
||||
bru,
|
||||
req,
|
||||
res,
|
||||
__hookResult: null,
|
||||
__onHookError: (level, error) => {
|
||||
if (onConsoleLog) {
|
||||
onConsoleLog('error', [`[Hook Error] ${level}: ${error?.message || error}`]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add custom console logger
|
||||
if (onConsoleLog && typeof onConsoleLog === 'function') {
|
||||
const customLogger = (type) => {
|
||||
return (...args) => {
|
||||
onConsoleLog(type, cleanJson(args));
|
||||
};
|
||||
};
|
||||
context.console = {
|
||||
log: customLogger('log'),
|
||||
debug: customLogger('debug'),
|
||||
info: customLogger('info'),
|
||||
warn: customLogger('warn'),
|
||||
error: customLogger('error')
|
||||
};
|
||||
}
|
||||
|
||||
// Add runRequest function if provided
|
||||
if (runRequestByItemPathname) {
|
||||
context.bru.runRequest = runRequestByItemPathname;
|
||||
}
|
||||
|
||||
// Execute consolidated script
|
||||
if (this.runtime === 'nodevm') {
|
||||
await runScriptInNodeVm({
|
||||
script: consolidatedScript,
|
||||
context,
|
||||
collectionPath,
|
||||
scriptingConfig
|
||||
});
|
||||
|
||||
return {
|
||||
hookManager: activeHookManager,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
persistentEnvVariables: bru.persistentEnvVariables,
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution,
|
||||
__bru: bru,
|
||||
req,
|
||||
res
|
||||
};
|
||||
}
|
||||
|
||||
// For QuickJS, persist the VM so hook handlers can be called later
|
||||
const result = await executeQuickJsVmAsync({
|
||||
script: consolidatedScript,
|
||||
context: context,
|
||||
collectionPath,
|
||||
persistVm: true
|
||||
});
|
||||
|
||||
// Register VM cleanup with HookManager
|
||||
if (result?.cleanup && typeof activeHookManager.registerCleanup === 'function') {
|
||||
activeHookManager.registerCleanup(result.cleanup);
|
||||
}
|
||||
|
||||
return {
|
||||
hookManager: activeHookManager,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
persistentEnvVariables: bru.persistentEnvVariables,
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution,
|
||||
// Include bru reference so callers can read updated values after hook execution
|
||||
__bru: bru,
|
||||
req,
|
||||
res
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HooksRuntime;
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
const {
|
||||
buildConsolidatedScript,
|
||||
wrapInIIFE,
|
||||
processScript,
|
||||
escapeForTemplate,
|
||||
getLevelMetadata,
|
||||
fromExtractedHooks,
|
||||
HOOK_LEVEL
|
||||
} = require('../src/runtime/hooks-consolidator');
|
||||
|
||||
describe('hooks-consolidator', () => {
|
||||
describe('HOOK_LEVEL constants', () => {
|
||||
it('should have correct level values', () => {
|
||||
expect(HOOK_LEVEL.COLLECTION).toBe('collection');
|
||||
expect(HOOK_LEVEL.FOLDER).toBe('folder');
|
||||
expect(HOOK_LEVEL.REQUEST).toBe('request');
|
||||
});
|
||||
|
||||
it('should be frozen', () => {
|
||||
expect(Object.isFrozen(HOOK_LEVEL)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeForTemplate', () => {
|
||||
it('should handle empty strings', () => {
|
||||
expect(escapeForTemplate('')).toBe('');
|
||||
expect(escapeForTemplate(null)).toBe('');
|
||||
expect(escapeForTemplate(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('should escape backticks', () => {
|
||||
expect(escapeForTemplate('Hello `world`')).toBe('Hello \\`world\\`');
|
||||
});
|
||||
|
||||
it('should escape template literals', () => {
|
||||
expect(escapeForTemplate('Value: ${value}')).toBe('Value: \\${value}');
|
||||
});
|
||||
|
||||
it('should escape backslashes', () => {
|
||||
expect(escapeForTemplate('path\\to\\file')).toBe('path\\\\to\\\\file');
|
||||
});
|
||||
|
||||
it('should handle complex strings', () => {
|
||||
const input = 'const msg = `Hello ${name}\\n`;';
|
||||
const expected = 'const msg = \\`Hello \\${name}\\\\n\\`;';
|
||||
expect(escapeForTemplate(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processScript', () => {
|
||||
it('should return empty string for empty input', () => {
|
||||
expect(processScript('')).toBe('');
|
||||
expect(processScript(null)).toBe('');
|
||||
expect(processScript(' ')).toBe('');
|
||||
});
|
||||
|
||||
it('should remove single-line comments by default', () => {
|
||||
const script = `
|
||||
// This is a comment
|
||||
const x = 1;
|
||||
`;
|
||||
const result = processScript(script);
|
||||
expect(result).not.toContain('This is a comment');
|
||||
expect(result).toContain('const x = 1');
|
||||
});
|
||||
|
||||
it('should remove multi-line comments by default', () => {
|
||||
const script = `
|
||||
/* This is a
|
||||
multi-line comment */
|
||||
const x = 1;
|
||||
`;
|
||||
const result = processScript(script);
|
||||
expect(result).not.toContain('multi-line comment');
|
||||
expect(result).toContain('const x = 1');
|
||||
});
|
||||
|
||||
it('should preserve script when removeComments is false', () => {
|
||||
const script = '// comment\nconst x = 1;';
|
||||
const result = processScript(script, false);
|
||||
expect(result).toContain('// comment');
|
||||
});
|
||||
});
|
||||
|
||||
describe('wrapInIIFE', () => {
|
||||
it('should return empty string for empty script', () => {
|
||||
expect(wrapInIIFE('', 'collection')).toBe('');
|
||||
expect(wrapInIIFE(' ', 'collection')).toBe('');
|
||||
expect(wrapInIIFE(null, 'collection')).toBe('');
|
||||
});
|
||||
|
||||
it('should wrap script in async IIFE', () => {
|
||||
const script = 'console.log("hello");';
|
||||
const result = wrapInIIFE(script, 'collection');
|
||||
expect(result).toContain('await (async () => {');
|
||||
expect(result).toContain('console.log("hello");');
|
||||
expect(result).toContain('})();');
|
||||
});
|
||||
|
||||
it('should include level identifier', () => {
|
||||
const result = wrapInIIFE('const x = 1;', 'collection');
|
||||
expect(result).toContain('__hookLevel = \'collection\'');
|
||||
expect(result).toContain('COLLECTION HOOKS');
|
||||
});
|
||||
|
||||
it('should include folder identifier when provided', () => {
|
||||
const result = wrapInIIFE('const x = 1;', 'folder', '/path/to/folder');
|
||||
expect(result).toContain('__hookLevel = \'folder:/path/to/folder\'');
|
||||
expect(result).toContain('FOLDER HOOKS');
|
||||
expect(result).toContain('/path/to/folder');
|
||||
});
|
||||
|
||||
it('should include error handling', () => {
|
||||
const result = wrapInIIFE('const x = 1;', 'request');
|
||||
expect(result).toContain('try {');
|
||||
expect(result).toContain('} catch (__hookError)');
|
||||
expect(result).toContain('__consolidatedErrors.push');
|
||||
expect(result).toContain('__onHookError');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildConsolidatedScript', () => {
|
||||
it('should return empty result for no hooks', () => {
|
||||
const result = buildConsolidatedScript({});
|
||||
expect(result.hasHooks).toBe(false);
|
||||
expect(result.script).toBe('');
|
||||
expect(result.levels).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle collection hooks only', () => {
|
||||
const result = buildConsolidatedScript({
|
||||
collectionHooks: 'bru.hooks.http.onBeforeRequest(() => {});'
|
||||
});
|
||||
expect(result.hasHooks).toBe(true);
|
||||
expect(result.levels).toHaveLength(1);
|
||||
expect(result.levels[0].level).toBe(HOOK_LEVEL.COLLECTION);
|
||||
expect(result.script).toContain('COLLECTION HOOKS');
|
||||
expect(result.script).toContain('bru.hooks.http.onBeforeRequest');
|
||||
});
|
||||
|
||||
it('should handle request hooks only', () => {
|
||||
const result = buildConsolidatedScript({
|
||||
requestHooks: 'bru.hooks.http.onAfterResponse(() => {});'
|
||||
});
|
||||
expect(result.hasHooks).toBe(true);
|
||||
expect(result.levels).toHaveLength(1);
|
||||
expect(result.levels[0].level).toBe(HOOK_LEVEL.REQUEST);
|
||||
expect(result.script).toContain('REQUEST HOOKS');
|
||||
});
|
||||
|
||||
it('should handle multiple folder hooks', () => {
|
||||
const result = buildConsolidatedScript({
|
||||
folderHooks: [
|
||||
{ folderPathname: '/folder1', hooks: 'const f1 = 1;' },
|
||||
{ folderPathname: '/folder2', hooks: 'const f2 = 2;' }
|
||||
]
|
||||
});
|
||||
expect(result.hasHooks).toBe(true);
|
||||
expect(result.levels).toHaveLength(2);
|
||||
expect(result.levels[0].level).toBe(HOOK_LEVEL.FOLDER);
|
||||
expect(result.levels[0].identifier).toBe('/folder1');
|
||||
expect(result.levels[1].identifier).toBe('/folder2');
|
||||
expect(result.script).toContain('/folder1');
|
||||
expect(result.script).toContain('/folder2');
|
||||
});
|
||||
|
||||
it('should consolidate all levels in correct order', () => {
|
||||
const result = buildConsolidatedScript({
|
||||
collectionHooks: 'const collectionVar = "collection";',
|
||||
folderHooks: [
|
||||
{ folderPathname: '/folder1', hooks: 'const folder1Var = "folder1";' },
|
||||
{ folderPathname: '/folder2', hooks: 'const folder2Var = "folder2";' }
|
||||
],
|
||||
requestHooks: 'const requestVar = "request";'
|
||||
});
|
||||
|
||||
expect(result.hasHooks).toBe(true);
|
||||
expect(result.levels).toHaveLength(4);
|
||||
expect(result.levels[0].level).toBe(HOOK_LEVEL.COLLECTION);
|
||||
expect(result.levels[1].level).toBe(HOOK_LEVEL.FOLDER);
|
||||
expect(result.levels[2].level).toBe(HOOK_LEVEL.FOLDER);
|
||||
expect(result.levels[3].level).toBe(HOOK_LEVEL.REQUEST);
|
||||
|
||||
// Verify order in script (collection before folders before request)
|
||||
const collectionIndex = result.script.indexOf('COLLECTION HOOKS');
|
||||
const folder1Index = result.script.indexOf('/folder1');
|
||||
const folder2Index = result.script.indexOf('/folder2');
|
||||
const requestIndex = result.script.indexOf('REQUEST HOOKS');
|
||||
|
||||
expect(collectionIndex).toBeLessThan(folder1Index);
|
||||
expect(folder1Index).toBeLessThan(folder2Index);
|
||||
expect(folder2Index).toBeLessThan(requestIndex);
|
||||
});
|
||||
|
||||
it('should skip empty folder hooks', () => {
|
||||
const result = buildConsolidatedScript({
|
||||
collectionHooks: 'const x = 1;',
|
||||
folderHooks: [
|
||||
{ folderPathname: '/folder1', hooks: '' },
|
||||
{ folderPathname: '/folder2', hooks: 'const y = 2;' },
|
||||
{ folderPathname: '/folder3', hooks: ' ' }
|
||||
]
|
||||
});
|
||||
|
||||
expect(result.levels).toHaveLength(2);
|
||||
expect(result.levels[0].level).toBe(HOOK_LEVEL.COLLECTION);
|
||||
expect(result.levels[1].level).toBe(HOOK_LEVEL.FOLDER);
|
||||
expect(result.levels[1].identifier).toBe('/folder2');
|
||||
});
|
||||
|
||||
it('should include error collection in script', () => {
|
||||
const result = buildConsolidatedScript({
|
||||
collectionHooks: 'const x = 1;'
|
||||
});
|
||||
expect(result.script).toContain('const __consolidatedErrors = []');
|
||||
expect(result.script).toContain('__hookResult');
|
||||
});
|
||||
|
||||
it('should remove comments by default', () => {
|
||||
const result = buildConsolidatedScript({
|
||||
collectionHooks: '// This is a comment\nconst x = 1;'
|
||||
});
|
||||
expect(result.script).not.toContain('This is a comment');
|
||||
expect(result.script).toContain('const x = 1');
|
||||
});
|
||||
|
||||
it('should preserve comments when removeComments is false', () => {
|
||||
const result = buildConsolidatedScript({
|
||||
collectionHooks: '// This is a comment\nconst x = 1;',
|
||||
removeComments: false
|
||||
});
|
||||
expect(result.script).toContain('This is a comment');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLevelMetadata', () => {
|
||||
it('should return empty array for no hooks', () => {
|
||||
expect(getLevelMetadata({})).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should include collection level', () => {
|
||||
const result = getLevelMetadata({ collectionHooks: 'const x = 1;' });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].level).toBe(HOOK_LEVEL.COLLECTION);
|
||||
expect(result[0].identifier).toBe('root');
|
||||
});
|
||||
|
||||
it('should include all levels', () => {
|
||||
const result = getLevelMetadata({
|
||||
collectionHooks: 'const x = 1;',
|
||||
folderHooks: [
|
||||
{ folderPathname: '/folder1', hooks: 'const y = 2;' }
|
||||
],
|
||||
requestHooks: 'const z = 3;'
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].level).toBe(HOOK_LEVEL.COLLECTION);
|
||||
expect(result[1].level).toBe(HOOK_LEVEL.FOLDER);
|
||||
expect(result[1].identifier).toBe('/folder1');
|
||||
expect(result[2].level).toBe(HOOK_LEVEL.REQUEST);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromExtractedHooks', () => {
|
||||
it('should handle empty input', () => {
|
||||
const result = fromExtractedHooks(null);
|
||||
expect(result.collectionHooks).toBe('');
|
||||
expect(result.folderHooks).toEqual([]);
|
||||
expect(result.requestHooks).toBe('');
|
||||
expect(result.removeComments).toBe(true);
|
||||
});
|
||||
|
||||
it('should convert extracted hooks format', () => {
|
||||
const extracted = {
|
||||
collectionHooks: 'collection script',
|
||||
folderHooks: [
|
||||
{ folderPathname: '/folder1', hooks: 'folder script' }
|
||||
],
|
||||
requestHooks: 'request script'
|
||||
};
|
||||
|
||||
const result = fromExtractedHooks(extracted);
|
||||
expect(result.collectionHooks).toBe('collection script');
|
||||
expect(result.folderHooks).toEqual(extracted.folderHooks);
|
||||
expect(result.requestHooks).toBe('request script');
|
||||
expect(result.removeComments).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration: consolidated script execution simulation', () => {
|
||||
it('should generate valid JavaScript', () => {
|
||||
const config = {
|
||||
collectionHooks: `
|
||||
bru.hooks.http.onBeforeRequest((data) => {
|
||||
console.log('Collection beforeRequest');
|
||||
});
|
||||
`,
|
||||
folderHooks: [
|
||||
{
|
||||
folderPathname: '/api/users',
|
||||
hooks: `
|
||||
bru.hooks.http.onBeforeRequest((data) => {
|
||||
console.log('Folder beforeRequest');
|
||||
});
|
||||
`
|
||||
}
|
||||
],
|
||||
requestHooks: `
|
||||
bru.hooks.http.onAfterResponse((data) => {
|
||||
console.log('Request afterResponse');
|
||||
});
|
||||
`
|
||||
};
|
||||
|
||||
const result = buildConsolidatedScript(config);
|
||||
|
||||
// The script should be syntactically valid (this is a basic check)
|
||||
expect(result.hasHooks).toBe(true);
|
||||
expect(result.script).toContain('__consolidatedErrors');
|
||||
expect(result.script).toContain('await (async () =>');
|
||||
expect(result.levels).toHaveLength(3);
|
||||
|
||||
// Try to parse it (without executing) to verify syntax
|
||||
// Note: We can't actually execute it without the bru context
|
||||
expect(() => {
|
||||
// This will throw if syntax is invalid
|
||||
new Function('bru', '__onHookError', '__hookResult', result.script);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,164 +0,0 @@
|
||||
/**
|
||||
* Unit tests for Hooks Executor
|
||||
*/
|
||||
const {
|
||||
executeHooksForLevel,
|
||||
executeConsolidatedHooks,
|
||||
executeAllHookLevels,
|
||||
createHookExecutor,
|
||||
HOOK_EVENTS
|
||||
} = require('../src/runtime/hooks-executor');
|
||||
|
||||
// Mock the HooksRuntime
|
||||
jest.mock('../src/runtime/hooks-runtime', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
runHooks: jest.fn().mockResolvedValue({
|
||||
hookManager: {
|
||||
call: jest.fn().mockResolvedValue(undefined),
|
||||
dispose: jest.fn()
|
||||
},
|
||||
envVariables: {},
|
||||
runtimeVariables: {},
|
||||
persistentEnvVariables: {},
|
||||
globalEnvironmentVariables: {}
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
describe('Hooks Executor', () => {
|
||||
const mockOptions = {
|
||||
request: { url: 'http://test.com' },
|
||||
envVariables: {},
|
||||
runtimeVariables: {},
|
||||
collectionPath: '/test/collection',
|
||||
onConsoleLog: jest.fn(),
|
||||
processEnvVars: {},
|
||||
scriptingConfig: { runtime: 'quickjs' },
|
||||
runRequestByItemPathname: jest.fn(),
|
||||
collectionName: 'Test Collection'
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('HOOK_EVENTS', () => {
|
||||
it('should have correct event names', () => {
|
||||
expect(HOOK_EVENTS.HTTP_BEFORE_REQUEST).toBe('http:beforeRequest');
|
||||
expect(HOOK_EVENTS.HTTP_AFTER_RESPONSE).toBe('http:afterResponse');
|
||||
expect(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN).toBe('runner:beforeCollectionRun');
|
||||
expect(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN).toBe('runner:afterCollectionRun');
|
||||
});
|
||||
|
||||
it('should be frozen', () => {
|
||||
expect(Object.isFrozen(HOOK_EVENTS)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeHooksForLevel()', () => {
|
||||
it('should return null for empty hooks file', async () => {
|
||||
const result = await executeHooksForLevel('', HOOK_EVENTS.HTTP_BEFORE_REQUEST, {}, mockOptions);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for whitespace-only hooks file', async () => {
|
||||
const result = await executeHooksForLevel(' ', HOOK_EVENTS.HTTP_BEFORE_REQUEST, {}, mockOptions);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should execute hooks for valid hooks file', async () => {
|
||||
const HooksRuntime = require('../src/runtime/hooks-runtime');
|
||||
await executeHooksForLevel('console.log("test")', HOOK_EVENTS.HTTP_BEFORE_REQUEST, {}, mockOptions);
|
||||
expect(HooksRuntime).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
const HooksRuntime = require('../src/runtime/hooks-runtime');
|
||||
HooksRuntime.mockImplementationOnce(() => ({
|
||||
runHooks: jest.fn().mockRejectedValue(new Error('Test error'))
|
||||
}));
|
||||
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
const result = await executeHooksForLevel('test()', HOOK_EVENTS.HTTP_BEFORE_REQUEST, {}, mockOptions);
|
||||
expect(result).toBeNull();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeConsolidatedHooks()', () => {
|
||||
it('should execute consolidated hooks', async () => {
|
||||
const HooksRuntime = require('../src/runtime/hooks-runtime');
|
||||
const extractedHooks = {
|
||||
collectionHooks: 'collection()',
|
||||
folderHooks: [{ hooks: 'folder()' }],
|
||||
requestHooks: 'request()'
|
||||
};
|
||||
|
||||
await executeConsolidatedHooks(extractedHooks, HOOK_EVENTS.HTTP_BEFORE_REQUEST, {}, mockOptions);
|
||||
expect(HooksRuntime).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
const HooksRuntime = require('../src/runtime/hooks-runtime');
|
||||
HooksRuntime.mockImplementationOnce(() => ({
|
||||
runHooks: jest.fn().mockRejectedValue(new Error('Test error'))
|
||||
}));
|
||||
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
const result = await executeConsolidatedHooks(
|
||||
{ collectionHooks: 'test()', folderHooks: [], requestHooks: '' },
|
||||
HOOK_EVENTS.HTTP_BEFORE_REQUEST,
|
||||
{},
|
||||
mockOptions
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeAllHookLevels()', () => {
|
||||
it('should always use consolidated execution', async () => {
|
||||
const extractedHooks = {
|
||||
collectionHooks: 'collection()',
|
||||
folderHooks: [{ hooks: 'folder()' }],
|
||||
requestHooks: 'request()'
|
||||
};
|
||||
|
||||
const HooksRuntime = require('../src/runtime/hooks-runtime');
|
||||
await executeAllHookLevels(
|
||||
extractedHooks,
|
||||
HOOK_EVENTS.HTTP_BEFORE_REQUEST,
|
||||
{},
|
||||
mockOptions
|
||||
);
|
||||
|
||||
// Should always use consolidated execution
|
||||
const runtimeInstance = HooksRuntime.mock.results[0].value;
|
||||
expect(runtimeInstance.runHooks).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ consolidated: true })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createHookExecutor()', () => {
|
||||
it('should create executor with base options', () => {
|
||||
const executor = createHookExecutor(mockOptions);
|
||||
expect(executor).toBeDefined();
|
||||
expect(typeof executor.executeLevel).toBe('function');
|
||||
expect(typeof executor.executeConsolidated).toBe('function');
|
||||
expect(typeof executor.executeAll).toBe('function');
|
||||
});
|
||||
|
||||
it('should allow overriding options', async () => {
|
||||
const executor = createHookExecutor(mockOptions);
|
||||
const newConsoleLog = jest.fn();
|
||||
|
||||
await executor.executeLevel('test()', HOOK_EVENTS.HTTP_BEFORE_REQUEST, {}, {
|
||||
onConsoleLog: newConsoleLog
|
||||
});
|
||||
|
||||
// The override should be merged with base options
|
||||
// Actual verification depends on implementation details
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user