From 3d319855bc0bead4993218def86cf0a79bc50faa Mon Sep 17 00:00:00 2001 From: sanish-bruno Date: Thu, 22 Jan 2026 20:13:10 +0530 Subject: [PATCH] feat: enhance hook functionality with runSingleRequestByPathname - Introduced runSingleRequestByPathname to allow running requests at the collection level. - Updated executeCollectionHooks to utilize the new function for improved hook execution. - Added comprehensive tests for bru.runRequest in various hook scenarios, ensuring correct behavior and response handling. - Removed redundant code and optimized hook management in collection.js. --- packages/bruno-cli/src/commands/run.js | 79 ++++++++++--------- packages/bruno-cli/src/utils/collection.js | 57 +------------ .../hooks-comprehensive-tests/collection.bru | 30 +++++++ .../hooks/bru-run-request/helper-request.bru | 17 ++++ .../run-request-in-after-hook.bru | 49 ++++++++++++ .../run-request-in-before-hook.bru | 48 +++++++++++ .../verify-collection-run-request.bru | 28 +++++++ 7 files changed, 213 insertions(+), 95 deletions(-) create mode 100644 packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/helper-request.bru create mode 100644 packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/run-request-in-after-hook.bru create mode 100644 packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/run-request-in-before-hook.bru create mode 100644 packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/verify-collection-run-request.bru diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index b6cdc58b4..5ebf3487c 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -622,45 +622,7 @@ const handler = async function (argv) { console[type](...args); }; - // Helper function to execute collection-level hooks at runtime - const executeCollectionHooks = async (hookEvent, eventData) => { - collectionRoot = collection?.draft?.root || collection?.root || {}; - const collectionHooks = get(collectionRoot, 'request.script.hooks', ''); - - if (!collectionHooks || !collectionHooks.trim()) { - return; - } - - try { - const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime }); - const result = await hooksRuntime.runHooks({ - hooksFile: decomment(collectionHooks), - request: {}, // Placeholder request for collection-level hooks - envVariables: envVars, - runtimeVariables, - collectionPath, - onConsoleLog, - processEnvVars, - scriptingConfig, - runRequestByItemPathname: null, // Not available at collection level - collectionName - }); - - if (result?.hookManager) { - await result.hookManager.call(hookEvent, eventData); - // Dispose HookManager to free VM resources - if (result.hookManager && typeof result.hookManager.dispose === 'function') { - result.hookManager.dispose(); - } - } - } catch (error) { - console.error(`Error executing collection-level hooks for ${hookEvent}:`, error); - } - }; - - // Call onBeforeCollectionRun hook before starting to run requests - await executeCollectionHooks(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection }); - + // Define runSingleRequestByPathname before executeCollectionHooks so it's available at all hook levels const runSingleRequestByPathname = async (relativeItemPathname) => { const ext = FORMAT_CONFIG[collection.format].ext; return new Promise(async (resolve, reject) => { @@ -688,6 +650,45 @@ const handler = async function (argv) { }); }; + // Helper function to execute collection-level hooks at runtime + const executeCollectionHooks = async (hookEvent, eventData) => { + collectionRoot = collection?.draft?.root || collection?.root || {}; + const collectionHooks = get(collectionRoot, 'request.script.hooks', ''); + + if (!collectionHooks || !collectionHooks.trim()) { + return; + } + + try { + const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime }); + const result = await hooksRuntime.runHooks({ + hooksFile: decomment(collectionHooks), + request: {}, // Placeholder request for collection-level hooks + envVariables: envVars, + runtimeVariables, + collectionPath, + onConsoleLog, + processEnvVars, + scriptingConfig, + runRequestByItemPathname: runSingleRequestByPathname, + collectionName + }); + + if (result?.hookManager) { + await result.hookManager.call(hookEvent, eventData); + // Dispose HookManager to free VM resources + if (result.hookManager && typeof result.hookManager.dispose === 'function') { + result.hookManager.dispose(); + } + } + } catch (error) { + console.error(`Error executing collection-level hooks for ${hookEvent}:`, error); + } + }; + + // Call onBeforeCollectionRun hook before starting to run requests + await executeCollectionHooks(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection }); + let currentRequestIndex = 0; let nJumps = 0; // count the number of jumps to avoid infinite loops while (currentRequestIndex < requestItems.length) { diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js index 1f38fcb38..095a96cfb 100644 --- a/packages/bruno-cli/src/utils/collection.js +++ b/packages/bruno-cli/src/utils/collection.js @@ -429,60 +429,6 @@ const HOOK_EVENTS = Object.freeze({ RUNNER_AFTER_COLLECTION_RUN: 'runner:afterCollectionRun' }); -/** - * Get or create HookManager for a specific level (collection, folder, or request) - * @param {Map} hookManagersMap - Map storing HookManagers by key - * @param {string} key - Unique identifier (collection:${pathname}, folder:${pathname}, or request uid/pathname) - * @param {string} hooksFile - Hooks file content for this level - * @param {object} options - Options for hook registration - * @param {object} options.request - Request object - * @param {object} options.envVars - Environment variables (or envVariables) - * @param {object} options.runtimeVariables - Runtime variables - * @param {string} options.collectionPath - Collection path - * @param {function} options.onConsoleLog - Console log callback - * @param {object} options.processEnvVars - Process environment variables - * @param {object} options.scriptingConfig - Scripting configuration - * @param {function} options.runRequestByItemPathname - Function to run requests - * @param {string} options.collectionName - Collection name - * @returns {Promise} HookManager instance for this level - */ -const getOrCreateHookManager = async (hookManagersMap, key, hooksFile, options = {}) => { - // Return existing HookManager if already created - if (hookManagersMap.has(key)) { - return hookManagersMap.get(key); - } - - // Create new HookManager and register hooks - const hookManager = new HookManager(); - hookManagersMap.set(key, hookManager); - - if (hooksFile && hooksFile.trim()) { - const hooksRuntime = new HooksRuntime({ runtime: options.scriptingConfig?.runtime }); - try { - await hooksRuntime.runHooks({ - hooksFile: decomment(hooksFile), - hookManager, - request: options.request || {}, - envVariables: options.envVars || options.envVariables || {}, - runtimeVariables: options.runtimeVariables || {}, - collectionPath: options.collectionPath, - onConsoleLog: options.onConsoleLog, - processEnvVars: options.processEnvVars || {}, - scriptingConfig: options.scriptingConfig || {}, - runRequestByItemPathname: options.runRequestByItemPathname, - collectionName: options.collectionName - }); - } catch (error) { - console.error(`Error registering hooks for ${key}:`, error); - if (options.onConsoleLog) { - options.onConsoleLog('error', [`Error registering hooks for ${key}: ${error.message}`]); - } - } - } - - return hookManager; -}; - const getAllRequestsInFolder = (folderItems = [], recursive = true) => { let requests = []; @@ -714,6 +660,5 @@ module.exports = { getAllRequestsAtFolderRoot, getCallStack, extractHooks, - HOOK_EVENTS, - getOrCreateHookManager + HOOK_EVENTS }; diff --git a/packages/bruno-tests/hooks-comprehensive-tests/collection.bru b/packages/bruno-tests/hooks-comprehensive-tests/collection.bru index a3d982807..7209557ec 100644 --- a/packages/bruno-tests/hooks-comprehensive-tests/collection.bru +++ b/packages/bruno-tests/hooks-comprehensive-tests/collection.bru @@ -68,6 +68,32 @@ script:hooks { console.log('[onBeforeCollectionRun] Setup complete - token and counters initialized'); }); + // Test: bru.runRequest in onBeforeCollectionRun hook + bru.hooks.runner.onBeforeCollectionRun(async() => { + console.log('[onBeforeCollectionRun] Testing bru.runRequest in collection-level hook...'); + + try { + // Call another request using bru.runRequest + const response = await bru.runRequest('hooks/bru-run-request/helper-request'); + + // Store the response for verification in tests + if (response) { + bru.setVar('collection-run-request-response', response.data); + bru.setVar('collection-run-request-status', response.status); + bru.setVar('collection-run-request-tested', 'true'); + console.log('[onBeforeCollectionRun] bru.runRequest succeeded:', response.data); + } else { + bru.setVar('collection-run-request-error', 'No response received'); + console.log('[onBeforeCollectionRun] bru.runRequest failed - no response'); + } + } catch (error) { + bru.setVar('collection-run-request-error', error.message || String(error)); + console.log('[onBeforeCollectionRun] bru.runRequest error:', error); + } + + console.log('[onBeforeCollectionRun] bru.runRequest test completed'); + }); + // ============================================ // onAfterCollectionRun Hook Tests // ============================================ @@ -136,6 +162,10 @@ script:hooks { bru.deleteVar('success-count'); bru.deleteVar('final-stats'); bru.deleteVar('cleanup-performed'); + bru.deleteVar('collection-run-request-response'); + bru.deleteVar('collection-run-request-status'); + bru.deleteVar('collection-run-request-tested'); + bru.deleteVar('collection-run-request-error'); await bru.sleep(300); console.log('[onAfterCollectionRun] Cleanup complete - all test variables removed'); diff --git a/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/helper-request.bru b/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/helper-request.bru new file mode 100644 index 000000000..28973088f --- /dev/null +++ b/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/helper-request.bru @@ -0,0 +1,17 @@ +meta { + name: helper-request + type: http + seq: 1 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +tests { + test("helper request returns pong", function() { + expect(res.getBody()).to.equal('pong'); + }); +} diff --git a/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/run-request-in-after-hook.bru b/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/run-request-in-after-hook.bru new file mode 100644 index 000000000..d2e8e66f7 --- /dev/null +++ b/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/run-request-in-after-hook.bru @@ -0,0 +1,49 @@ +meta { + name: run-request-in-after-hook + type: http + seq: 3 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +script:hooks { + bru.hooks.http.onAfterResponse(async ({ res }) => { + console.log('[afterResponse] Testing bru.runRequest API'); + console.log('[afterResponse] Main request status:', res.getStatus()); + + // Call another request using bru.runRequest + const response = await bru.runRequest('hooks/bru-run-request/helper-request'); + + // Store the response body for verification in tests + if (response) { + bru.setVar('after-helper-response-body', response.data); + bru.setVar('after-helper-response-status', response.status); + console.log('[afterResponse] Helper request response:', response.data); + } else { + bru.setVar('after-helper-response-error', 'No response received'); + console.log('[afterResponse] Helper request failed - no response'); + } + + console.log('[afterResponse] bru.runRequest test completed'); + }); +} + +tests { + test("bru.runRequest in afterResponse hook should return helper response", function() { + const helperBody = bru.getVar('after-helper-response-body'); + expect(helperBody).to.equal('pong'); + }); + + test("bru.runRequest in afterResponse hook should return status 200", function() { + const helperStatus = bru.getVar('after-helper-response-status'); + expect(helperStatus).to.equal(200); + }); + + test("main request should succeed", function() { + expect(res.getBody()).to.equal('pong'); + }); +} diff --git a/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/run-request-in-before-hook.bru b/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/run-request-in-before-hook.bru new file mode 100644 index 000000000..d65c7469e --- /dev/null +++ b/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/run-request-in-before-hook.bru @@ -0,0 +1,48 @@ +meta { + name: run-request-in-before-hook + type: http + seq: 2 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +script:hooks { + bru.hooks.http.onBeforeRequest(async ({ req }) => { + console.log('[beforeRequest] Testing bru.runRequest API'); + + // Call another request using bru.runRequest + const response = await bru.runRequest('hooks/bru-run-request/helper-request'); + + // Store the response body for verification in tests + if (response) { + bru.setVar('helper-response-body', response.data); + bru.setVar('helper-response-status', response.status); + console.log('[beforeRequest] Helper request response:', response.data); + } else { + bru.setVar('helper-response-error', 'No response received'); + console.log('[beforeRequest] Helper request failed - no response'); + } + + console.log('[beforeRequest] bru.runRequest test completed'); + }); +} + +tests { + test("bru.runRequest in beforeRequest hook should return helper response", function() { + const helperBody = bru.getVar('helper-response-body'); + expect(helperBody).to.equal('pong'); + }); + + test("bru.runRequest in beforeRequest hook should return status 200", function() { + const helperStatus = bru.getVar('helper-response-status'); + expect(helperStatus).to.equal(200); + }); + + test("main request should still succeed", function() { + expect(res.getBody()).to.equal('pong'); + }); +} diff --git a/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/verify-collection-run-request.bru b/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/verify-collection-run-request.bru new file mode 100644 index 000000000..e93a23468 --- /dev/null +++ b/packages/bruno-tests/hooks-comprehensive-tests/hooks/bru-run-request/verify-collection-run-request.bru @@ -0,0 +1,28 @@ +meta { + name: verify-collection-run-request + type: http + seq: 4 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +tests { + test("collection-level bru.runRequest should have set response body", function() { + const responseBody = bru.getVar('collection-run-request-response'); + expect(responseBody).to.equal('pong'); + }); + + test("collection-level bru.runRequest should have set response status", function() { + const responseStatus = bru.getVar('collection-run-request-status'); + expect(responseStatus).to.equal(200); + }); + + test("collection-level bru.runRequest flag should be set", function() { + const flag = bru.getVar('collection-run-request-tested'); + expect(flag).to.equal('true'); + }); +}