mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-29 07:34:07 +00:00
feat: hooks runtime
add: hooks component add support for hooks within bruno-lang fix: hooks is not getting save hooks implemtation add hooks component within folders, requests add: quick js shims for hooks fix: garbage collected hook managers send logs to main rm: hook manager store feat: introduce HOOK_EVENTS constant for improved hook management add folder start/end events add folder run events rm: folder run related events add cli support for hooks support script:hooks instead of hooks move hooks to script tab make outer scope available within callback in safemode added runner, req apis as an abstraction over event based hooks fix: crash while editing folder hooks rm: unused files fix: self review changes refactor, request specific hook manager deleted once add: cm rm: spaces add prompt var rm: indent fix: lint refactor: shims handling for hooks fix: enable async calling in dev mode for gui, cli fix: support async callbacks within safe mode rm: vm instance fix: review comments fix: review comments add cli tests for hooks rm: client certs fix: add hooks to oc yaml fix: rename uid ot path name for better clarity, app crash when saving folder hooks rm: console rm: vm2 runtime leftover rm: check add: handler cleanup function add: playwright test case for hooks rm: review fixes fix: review comments add fallback hook manager add fallback hook manager fix: show error from hooks scripts within response pane change: collection events name feat: add name spaced hooks fix: review comments add: hooks specific collection for testing use hooks manager as a private field fix: tests use collection from bruno-test within playwright rm: databuffer test fix: playwright test rm: unintended changes rm: file
This commit is contained in:
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@@ -100,12 +100,19 @@ jobs:
|
||||
npm install
|
||||
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer
|
||||
|
||||
- name: Run comprehensive hooks tests
|
||||
run: |
|
||||
cd packages/bruno-tests/hooks-comprehensive-tests
|
||||
node ../../bruno-cli/bin/bru.js run --env Prod --output junit-hooks.xml --format junit
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
check_name: CLI Test Results
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
files: |
|
||||
packages/bruno-tests/collection/junit.xml
|
||||
packages/bruno-tests/hooks-comprehensive-tests/junit-hooks.xml
|
||||
comment_mode: always
|
||||
e2e-test:
|
||||
name: Playwright E2E Tests
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateCollectionRequestScript, updateCollectionResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { updateCollectionRequestScript, updateCollectionResponseScript, updateCollectionHooksScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
@@ -14,8 +14,10 @@ const Script = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const preRequestEditorRef = useRef(null);
|
||||
const postResponseEditorRef = useRef(null);
|
||||
const hooksEditorRef = useRef(null);
|
||||
const requestScript = collection.draft?.root ? get(collection, 'draft.root.request.script.req', '') : get(collection, 'root.request.script.req', '');
|
||||
const responseScript = collection.draft?.root ? get(collection, 'draft.root.request.script.res', '') : get(collection, 'root.request.script.res', '');
|
||||
const hooks = collection.draft?.root ? get(collection, 'draft.root.request.script.hooks', '') : get(collection, 'root.request.script.hooks', '');
|
||||
|
||||
// Default to post-response if pre-request script is empty
|
||||
const getInitialTab = () => {
|
||||
@@ -45,6 +47,8 @@ const Script = ({ collection }) => {
|
||||
preRequestEditorRef.current.editor.refresh();
|
||||
} else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {
|
||||
postResponseEditorRef.current.editor.refresh();
|
||||
} else if (activeTab === 'hooks' && hooksEditorRef.current?.editor) {
|
||||
hooksEditorRef.current.editor.refresh();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
@@ -69,6 +73,13 @@ const Script = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onHooksEdit = (value) => {
|
||||
dispatch(updateCollectionHooksScript({
|
||||
hooks: value,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
};
|
||||
@@ -89,6 +100,10 @@ const Script = ({ collection }) => {
|
||||
Post Response
|
||||
{responseScript && responseScript.trim().length > 0 && <StatusDot />}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="hooks">
|
||||
Hooks
|
||||
{hooks && hooks.trim().length > 0 && <StatusDot />}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pre-request" className="mt-2">
|
||||
@@ -120,6 +135,21 @@ const Script = ({ collection }) => {
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="hooks" className="mt-2">
|
||||
<CodeEditor
|
||||
ref={hooksEditorRef}
|
||||
collection={collection}
|
||||
value={hooks || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onHooksEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12">
|
||||
|
||||
@@ -29,7 +29,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
};
|
||||
|
||||
const root = collection?.draft?.root || collection?.root;
|
||||
const hasScripts = root?.request?.script?.res || root?.request?.script?.req;
|
||||
const hasScripts = root?.request?.script?.res || root?.request?.script?.req || root?.request?.script?.hooks;
|
||||
const hasTests = root?.request?.tests;
|
||||
const hasDocs = root?.docs;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateFolderRequestScript, updateFolderResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { updateFolderRequestScript, updateFolderResponseScript, updateFolderHooksScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
@@ -14,8 +14,10 @@ const Script = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const preRequestEditorRef = useRef(null);
|
||||
const postResponseEditorRef = useRef(null);
|
||||
const hooksEditorRef = useRef(null);
|
||||
const requestScript = folder.draft ? get(folder, 'draft.request.script.req', '') : get(folder, 'root.request.script.req', '');
|
||||
const responseScript = folder.draft ? get(folder, 'draft.request.script.res', '') : get(folder, 'root.request.script.res', '');
|
||||
const hooks = folder.draft ? get(folder, 'draft.request.script.hooks', '') : get(folder, 'root.request.script.hooks', '');
|
||||
|
||||
// Default to post-response if pre-request script is empty
|
||||
const getInitialTab = () => {
|
||||
@@ -45,6 +47,8 @@ const Script = ({ collection, folder }) => {
|
||||
preRequestEditorRef.current.editor.refresh();
|
||||
} else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {
|
||||
postResponseEditorRef.current.editor.refresh();
|
||||
} else if (activeTab === 'hooks' && hooksEditorRef.current?.editor) {
|
||||
hooksEditorRef.current.editor.refresh();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
@@ -71,6 +75,14 @@ const Script = ({ collection, folder }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onHooksEdit = (value) => {
|
||||
dispatch(updateFolderHooksScript({
|
||||
hooks: value,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
@@ -91,6 +103,10 @@ const Script = ({ collection, folder }) => {
|
||||
Post Response
|
||||
{responseScript && responseScript.trim().length > 0 && <StatusDot />}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="hooks">
|
||||
Hooks
|
||||
{hooks && hooks.trim().length > 0 && <StatusDot />}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pre-request" className="mt-2">
|
||||
@@ -122,6 +138,21 @@ const Script = ({ collection, folder }) => {
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="hooks" className="mt-2">
|
||||
<CodeEditor
|
||||
ref={hooksEditorRef}
|
||||
collection={collection}
|
||||
value={hooks || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onHooksEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12">
|
||||
|
||||
@@ -21,7 +21,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
}
|
||||
|
||||
const folderRoot = folder?.draft || folder?.root;
|
||||
const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req;
|
||||
const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req || folderRoot?.request?.script?.hooks;
|
||||
const hasTests = folderRoot?.request?.tests;
|
||||
|
||||
const headers = folderRoot?.request?.headers || [];
|
||||
|
||||
@@ -87,7 +87,7 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
);
|
||||
|
||||
const indicators = useMemo(() => {
|
||||
const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;
|
||||
const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage || item.hooksScriptErrorMessage;
|
||||
const hasTestError = item.testScriptErrorMessage;
|
||||
|
||||
return {
|
||||
@@ -96,7 +96,7 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
headers: activeCounts.headers > 0 ? <sup className="font-medium">{activeCounts.headers}</sup> : null,
|
||||
auth: auth.mode !== 'none' ? <StatusDot /> : null,
|
||||
vars: activeCounts.vars > 0 ? <sup className="font-medium">{activeCounts.vars}</sup> : null,
|
||||
script: (script.req || script.res) ? (hasScriptError ? <StatusDot type="error" /> : <StatusDot />) : null,
|
||||
script: (script.req || script.res || script.hooks) ? (hasScriptError ? <StatusDot type="error" /> : <StatusDot />) : null,
|
||||
assert: activeCounts.assertions > 0 ? <sup className="font-medium">{activeCounts.assertions}</sup> : null,
|
||||
tests: tests?.length > 0 ? (hasTestError ? <StatusDot type="error" /> : <StatusDot />) : null,
|
||||
docs: docs?.length > 0 ? <StatusDot /> : null,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { updateRequestScript, updateResponseScript, updateRequestHooksScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
@@ -12,8 +12,10 @@ const Script = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const preRequestEditorRef = useRef(null);
|
||||
const postResponseEditorRef = useRef(null);
|
||||
const hooksEditorRef = useRef(null);
|
||||
const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');
|
||||
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
|
||||
const hooks = item.draft ? get(item, 'draft.request.script.hooks', '') : get(item, 'request.script.hooks', '');
|
||||
|
||||
// Default to post-response if pre-request script is empty
|
||||
const getInitialTab = () => {
|
||||
@@ -44,6 +46,8 @@ const Script = ({ item, collection }) => {
|
||||
preRequestEditorRef.current.editor.refresh();
|
||||
} else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {
|
||||
postResponseEditorRef.current.editor.refresh();
|
||||
} else if (activeTab === 'hooks' && hooksEditorRef.current?.editor) {
|
||||
hooksEditorRef.current.editor.refresh();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
@@ -70,6 +74,14 @@ const Script = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onHooksEdit = (value) => {
|
||||
dispatch(updateRequestHooksScript({
|
||||
hooks: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
@@ -88,6 +100,10 @@ const Script = ({ item, collection }) => {
|
||||
Post Response
|
||||
{hasPostResponseScript && <StatusDot />}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="hooks">
|
||||
Hooks
|
||||
{hooks && hooks.trim().length > 0 && <StatusDot />}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pre-request" className="mt-2" dataTestId="pre-request-script-editor">
|
||||
@@ -121,6 +137,22 @@ const Script = ({ item, collection }) => {
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="hooks" className="mt-2" dataTestId="hooks-editor">
|
||||
<CodeEditor
|
||||
ref={hooksEditorRef}
|
||||
collection={collection}
|
||||
value={hooks || ''}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onHooksEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,9 +4,10 @@ import ErrorBanner from 'ui/ErrorBanner';
|
||||
const ScriptError = ({ item, onClose }) => {
|
||||
const preRequestError = item?.preRequestScriptErrorMessage;
|
||||
const postResponseError = item?.postResponseScriptErrorMessage;
|
||||
const hooksError = item?.hookScriptErrorMessage;
|
||||
const testScriptError = item?.testScriptErrorMessage;
|
||||
|
||||
if (!preRequestError && !postResponseError && !testScriptError) return null;
|
||||
if (!preRequestError && !postResponseError && !testScriptError && !hooksError) return null;
|
||||
|
||||
const errors = [];
|
||||
|
||||
@@ -24,6 +25,13 @@ const ScriptError = ({ item, onClose }) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (hooksError) {
|
||||
errors.push({
|
||||
title: 'Hooks Script Error',
|
||||
message: hooksError
|
||||
});
|
||||
}
|
||||
|
||||
if (testScriptError) {
|
||||
errors.push({
|
||||
title: 'Test Script Error',
|
||||
|
||||
@@ -86,10 +86,10 @@ const ResponsePane = ({ item, collection }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage) {
|
||||
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage || item?.hookScriptErrorMessage) {
|
||||
setShowScriptErrorCard(true);
|
||||
}
|
||||
}, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage, item?.testScriptErrorMessage]);
|
||||
}, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage, item?.testScriptErrorMessage, item?.hookScriptErrorMessage]);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -116,7 +116,7 @@ const ResponsePane = ({ item, collection }) => {
|
||||
}, [response.size, response.dataBuffer]);
|
||||
const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;
|
||||
|
||||
const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage;
|
||||
const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage || item?.hookScriptErrorMessage;
|
||||
|
||||
const allTabs = useMemo(() => {
|
||||
return [
|
||||
|
||||
@@ -21,17 +21,17 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const { requestSent, responseReceived, testResults, assertionResults, preRequestTestResults, postResponseTestResults, error } = item;
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage) {
|
||||
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage || item?.hookScriptErrorMessage) {
|
||||
setShowScriptErrorCard(true);
|
||||
}
|
||||
}, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage, item?.testScriptErrorMessage]);
|
||||
}, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage, item?.testScriptErrorMessage, item?.hookScriptErrorMessage]);
|
||||
|
||||
const headers = get(item, 'responseReceived.headers', []);
|
||||
const status = get(item, 'responseReceived.status', 0);
|
||||
const size = get(item, 'responseReceived.size', 0);
|
||||
const duration = get(item, 'responseReceived.duration', 0);
|
||||
|
||||
const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage;
|
||||
const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage || item?.hookScriptErrorMessage;
|
||||
|
||||
const selectTab = (tab) => setSelectedTab(tab);
|
||||
|
||||
|
||||
@@ -1716,6 +1716,20 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestHooksScript: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
set(item.draft, 'request.script.hooks', action.payload.hooks);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestMethod: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -2078,6 +2092,18 @@ export const collectionsSlice = createSlice({
|
||||
set(collection, 'draft.root.request.tests', action.payload.tests);
|
||||
}
|
||||
},
|
||||
updateCollectionHooksScript: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
set(collection.draft, 'root.request.script.hooks', action.payload.hooks);
|
||||
}
|
||||
},
|
||||
updateCollectionDocs: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -2334,6 +2360,16 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'draft.request.tests', action.payload.tests);
|
||||
}
|
||||
},
|
||||
updateFolderHooksScript: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
set(folder.draft, 'request.script.hooks', action.payload.hooks);
|
||||
}
|
||||
},
|
||||
updateFolderAuth: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
if (!collection) return;
|
||||
@@ -2828,6 +2864,7 @@ export const collectionsSlice = createSlice({
|
||||
item.preRequestScriptErrorMessage = null;
|
||||
item.postResponseScriptErrorMessage = null;
|
||||
item.testScriptErrorMessage = null;
|
||||
item.hookScriptErrorMessage = null;
|
||||
},
|
||||
runRequestEvent: (state, action) => {
|
||||
const { itemUid, collectionUid, type, requestUid } = action.payload;
|
||||
@@ -2851,6 +2888,10 @@ export const collectionsSlice = createSlice({
|
||||
item.testScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if (type === 'hooks-script-execution') {
|
||||
item.hookScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if (type === 'request-queued') {
|
||||
const { cancelTokenUid } = action.payload;
|
||||
// ignore if request is already in progress or completed
|
||||
@@ -2990,6 +3031,11 @@ export const collectionsSlice = createSlice({
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.preRequestScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if (type === 'hooks-script-execution') {
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.hookScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
}
|
||||
},
|
||||
resetCollectionRunner: (state, action) => {
|
||||
@@ -3487,6 +3533,7 @@ export const {
|
||||
updateRequestScript,
|
||||
updateResponseScript,
|
||||
updateRequestTests,
|
||||
updateRequestHooksScript,
|
||||
updateRequestMethod,
|
||||
updateRequestProtoPath,
|
||||
addAssertion,
|
||||
@@ -3509,6 +3556,7 @@ export const {
|
||||
updateFolderRequestScript,
|
||||
updateFolderResponseScript,
|
||||
updateFolderTests,
|
||||
updateFolderHooksScript,
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader,
|
||||
@@ -3521,6 +3569,7 @@ export const {
|
||||
updateCollectionRequestScript,
|
||||
updateCollectionResponseScript,
|
||||
updateCollectionTests,
|
||||
updateCollectionHooksScript,
|
||||
updateCollectionDocs,
|
||||
updateCollectionProxy,
|
||||
updateCollectionClientCertificates,
|
||||
|
||||
@@ -87,6 +87,13 @@ const STATIC_API_HINTS = {
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.hooks',
|
||||
'bru.hooks.http',
|
||||
'bru.hooks.http.onBeforeRequest(callback)',
|
||||
'bru.hooks.http.onAfterResponse(callback)',
|
||||
'bru.hooks.runner',
|
||||
'bru.hooks.runner.onBeforeCollectionRun(callback)',
|
||||
'bru.hooks.runner.onAfterCollectionRun(callback)',
|
||||
'bru.interpolate(str)',
|
||||
'bru.cookies',
|
||||
'bru.cookies.jar()',
|
||||
|
||||
@@ -522,6 +522,9 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
if (script?.res?.length) {
|
||||
di.root.request.script.res = script?.res;
|
||||
}
|
||||
if (script?.hooks?.length) {
|
||||
di.root.request.script.hooks = script?.hooks;
|
||||
}
|
||||
}
|
||||
// folder level vars
|
||||
if (Object.keys(vars)?.length) {
|
||||
@@ -607,6 +610,9 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
if (script?.res?.length) {
|
||||
collectionToSave.root.request.script.res = script?.res;
|
||||
}
|
||||
if (script?.hooks?.length) {
|
||||
collectionToSave.root.request.script.hooks = script?.hooks;
|
||||
}
|
||||
}
|
||||
// collection level vars
|
||||
if (Object.keys(vars)?.length) {
|
||||
|
||||
@@ -2,7 +2,7 @@ const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
const { forOwn, cloneDeep } = require('lodash');
|
||||
const { forOwn, cloneDeep, get } = require('lodash');
|
||||
const { getRunnerSummary } = require('@usebruno/common/runner');
|
||||
const { exists, isFile, isDirectory } = require('../utils/filesystem');
|
||||
const { runSingleRequest } = require('../runner/run-single-request');
|
||||
@@ -15,9 +15,10 @@ const { rpad } = require('../utils/common');
|
||||
const { getOptions } = require('../utils/bru');
|
||||
const { parseDotEnv, parseEnvironment } = require('@usebruno/filestore');
|
||||
const constants = require('../constants');
|
||||
const { findItemInCollection, createCollectionJsonFromPathname, getCallStack, FORMAT_CONFIG } = require('../utils/collection');
|
||||
const { findItemInCollection, createCollectionJsonFromPathname, getCallStack, FORMAT_CONFIG, HOOK_EVENTS, getOrCreateHookManager } = require('../utils/collection');
|
||||
const { hasExecutableTestInScript } = require('../utils/request');
|
||||
const { createSkippedFileResults } = require('../utils/run');
|
||||
const HookManager = require('@usebruno/js/src/hook-manager');
|
||||
const command = 'run [paths...]';
|
||||
const desc = 'Run one or more requests/folders';
|
||||
|
||||
@@ -315,7 +316,7 @@ const handler = async function (argv) {
|
||||
const collectionPath = process.cwd();
|
||||
|
||||
let collection = createCollectionJsonFromPathname(collectionPath);
|
||||
const { root: collectionRoot, brunoConfig } = collection;
|
||||
let { root: collectionRoot, brunoConfig } = collection;
|
||||
|
||||
if (clientCertConfig) {
|
||||
try {
|
||||
@@ -612,6 +613,49 @@ const handler = async function (argv) {
|
||||
});
|
||||
|
||||
const runtime = getJsSandboxRuntime(sandbox);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = runtime;
|
||||
|
||||
// Create HookManager map to share HookManagers across requests
|
||||
const hookManagersMap = new Map();
|
||||
const collectionName = collection?.brunoConfig?.name;
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
};
|
||||
|
||||
// Register collection-level hooks once at the start
|
||||
collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionHooks = get(collectionRoot, 'request.script.hooks', '');
|
||||
const collectionHookManagerKey = `collection:${collection.pathname}`;
|
||||
let collectionHookManager = null;
|
||||
|
||||
if (collectionHooks && collectionHooks.trim()) {
|
||||
const hookManagerOptions = {
|
||||
request: {}, // Placeholder request for hook registration
|
||||
envVariables: envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname: null, // Not available at collection level
|
||||
collectionName
|
||||
};
|
||||
collectionHookManager = await getOrCreateHookManager(hookManagersMap, collectionHookManagerKey, collectionHooks, hookManagerOptions);
|
||||
} else {
|
||||
// Create empty HookManager for collection even if no hooks
|
||||
collectionHookManager = new HookManager();
|
||||
hookManagersMap.set(collectionHookManagerKey, collectionHookManager);
|
||||
}
|
||||
|
||||
// Call onBeforeCollectionRun hook before starting to run requests
|
||||
if (collectionHookManager) {
|
||||
try {
|
||||
await collectionHookManager.call(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection });
|
||||
} catch (error) {
|
||||
console.error('Error calling onBeforeCollectionRun hooks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const runSingleRequestByPathname = async (relativeItemPathname) => {
|
||||
const ext = FORMAT_CONFIG[collection.format].ext;
|
||||
@@ -632,7 +676,8 @@ const handler = async function (argv) {
|
||||
collectionRoot,
|
||||
runtime,
|
||||
collection,
|
||||
runSingleRequestByPathname
|
||||
runSingleRequestByPathname,
|
||||
hookManagersMap
|
||||
);
|
||||
resolve(res?.response);
|
||||
}
|
||||
@@ -657,7 +702,8 @@ const handler = async function (argv) {
|
||||
collectionRoot,
|
||||
runtime,
|
||||
collection,
|
||||
runSingleRequestByPathname
|
||||
runSingleRequestByPathname,
|
||||
hookManagersMap
|
||||
);
|
||||
|
||||
const isLastRun = currentRequestIndex === requestItems.length - 1;
|
||||
@@ -752,6 +798,18 @@ const handler = async function (argv) {
|
||||
const skippedFileResults = createSkippedFileResults(global.brunoSkippedFiles || [], collectionPath);
|
||||
results.push(...skippedFileResults);
|
||||
|
||||
// Call onAfterCollectionRun hook after all requests are done
|
||||
if (collectionHookManager) {
|
||||
try {
|
||||
await collectionHookManager.call(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection });
|
||||
} catch (error) {
|
||||
console.error('Error calling onAfterCollectionRun hooks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup: Clear hook managers map (will be garbage collected)
|
||||
hookManagersMap.clear();
|
||||
|
||||
const summary = printRunSummary(results);
|
||||
const runCompletionTime = new Date().toISOString();
|
||||
const totalTime = results.reduce((acc, res) => acc + res.response.responseTime, 0);
|
||||
|
||||
@@ -6,9 +6,13 @@ 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 } = require('@usebruno/js');
|
||||
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime, HooksRuntime } = require('@usebruno/js');
|
||||
const HookManager = require('@usebruno/js/src/hook-manager');
|
||||
const BrunoRequest = require('@usebruno/js/src/bruno-request');
|
||||
const BrunoResponse = require('@usebruno/js/src/bruno-response');
|
||||
const { stripExtension } = require('../utils/filesystem');
|
||||
const { getOptions } = require('../utils/bru');
|
||||
const { extractHooks, getTreePathFromCollectionToItem, HOOK_EVENTS, getOrCreateHookManager } = require('../utils/collection');
|
||||
const https = require('https');
|
||||
const { HttpProxyAgent } = require('http-proxy-agent');
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
@@ -93,7 +97,8 @@ const runSingleRequest = async function (
|
||||
collectionRoot,
|
||||
runtime,
|
||||
collection,
|
||||
runSingleRequestByPathname
|
||||
runSingleRequestByPathname,
|
||||
hookManagersMap
|
||||
) {
|
||||
const { pathname: itemPathname } = item;
|
||||
const relativeItemPathname = path.relative(collectionPath, itemPathname);
|
||||
@@ -164,9 +169,65 @@ const runSingleRequest = async function (
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = runtime;
|
||||
|
||||
// Get request tree path for hook extraction
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
const collectionName = collection?.brunoConfig?.name;
|
||||
|
||||
// Get or create HookManagers for each level using shared map
|
||||
let allHookManagers = [];
|
||||
if (hookManagersMap) {
|
||||
try {
|
||||
const { collectionHooks, folderHooks, requestHooks } = extractHooks(collection, request, requestTreePath);
|
||||
|
||||
const hookManagerOptions = {
|
||||
request,
|
||||
envVars: envVariables, // Will be mapped to envVariables in runHooks
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname: runSingleRequestByPathname,
|
||||
collectionName
|
||||
};
|
||||
|
||||
// Collection-level HookManager (shared across all requests)
|
||||
const collectionHookManagerKey = `collection:${collection.pathname}`;
|
||||
const collectionHookManager = await getOrCreateHookManager(hookManagersMap, collectionHookManagerKey, collectionHooks, hookManagerOptions);
|
||||
|
||||
// Folder-level HookManagers (in order from collection to request)
|
||||
const folderHookManagers = [];
|
||||
for (const folderHook of folderHooks) {
|
||||
// folderPathname is set by extractHooks (i.pathname)
|
||||
const folderHookManagerKey = `folder:${folderHook.folderPathname}`;
|
||||
const folderHookManager = await getOrCreateHookManager(hookManagersMap, folderHookManagerKey, folderHook.hooks, hookManagerOptions);
|
||||
folderHookManagers.push(folderHookManager);
|
||||
}
|
||||
|
||||
// Request-level HookManager (unique per request)
|
||||
const requestHookManagerKey = item.pathname;
|
||||
const requestHookManager = await getOrCreateHookManager(hookManagersMap, requestHookManagerKey, requestHooks, hookManagerOptions);
|
||||
|
||||
// Combine all HookManagers in order: collection -> folder(s) -> request
|
||||
allHookManagers = [collectionHookManager, ...folderHookManagers, requestHookManager];
|
||||
} catch (error) {
|
||||
console.error('Error getting/creating hook managers:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Call beforeRequest hooks before running pre-request scripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
for (const hookManager of allHookManagers) {
|
||||
try {
|
||||
const req = new BrunoRequest(request);
|
||||
await hookManager.call(HOOK_EVENTS.HTTP_BEFORE_REQUEST, { request, req, collection });
|
||||
} catch (error) {
|
||||
console.error('Error calling beforeRequest hooks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// run pre request script
|
||||
const requestScriptFile = get(request, 'script.req');
|
||||
const collectionName = collection?.brunoConfig?.name;
|
||||
if (requestScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await scriptRuntime.runRequestScript(
|
||||
@@ -190,6 +251,11 @@ const runSingleRequest = async function (
|
||||
}
|
||||
|
||||
if (result?.skipRequest) {
|
||||
// Clean up request-level hook manager if request is skipped
|
||||
if (hookManagersMap && allHookManagers.length > 0) {
|
||||
const requestHookManagerKey = item.pathname;
|
||||
hookManagersMap.delete(requestHookManagerKey);
|
||||
}
|
||||
return {
|
||||
test: {
|
||||
filename: relativeItemPathname
|
||||
@@ -546,6 +612,7 @@ const runSingleRequest = async function (
|
||||
|
||||
if (request.ntlmConfig) {
|
||||
axiosInstance = NtlmClient(request.ntlmConfig, axiosInstance.defaults);
|
||||
|
||||
delete request.ntlmConfig;
|
||||
}
|
||||
|
||||
@@ -639,6 +706,26 @@ const runSingleRequest = async function (
|
||||
// Log pre-request test results
|
||||
logResults(preRequestTestResults, 'Pre-Request Tests');
|
||||
|
||||
// Call afterResponse hooks after response is received but before post-response scripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
for (const hookManager of allHookManagers) {
|
||||
try {
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
await hookManager.call(HOOK_EVENTS.HTTP_AFTER_RESPONSE, { request, response, req, res, collection });
|
||||
} catch (error) {
|
||||
console.error('Error calling afterResponse hooks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up request-level hook manager after request completes
|
||||
// Requests are only run once, so we can safely remove the hook manager to free memory
|
||||
// TODO: we probably don't even have to store the request level hook manager in the first place
|
||||
if (hookManagersMap && allHookManagers.length > 0) {
|
||||
const requestHookManagerKey = item.pathname;
|
||||
hookManagersMap.delete(requestHookManagerKey);
|
||||
}
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(item, 'request.vars.res');
|
||||
if (postResponseVars?.length) {
|
||||
@@ -766,6 +853,11 @@ const runSingleRequest = async function (
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
} catch (err) {
|
||||
// Clean up request-level hook manager on error
|
||||
if (hookManagersMap) {
|
||||
const requestHookManagerKey = item.pathname;
|
||||
hookManagersMap.delete(requestHookManagerKey);
|
||||
}
|
||||
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));
|
||||
return {
|
||||
test: {
|
||||
|
||||
@@ -6,6 +6,9 @@ const { sanitizeName } = require('./filesystem');
|
||||
const { parseRequest, parseCollection, parseFolder, stringifyCollection, stringifyFolder, stringifyEnvironment, stringifyRequest } = require('@usebruno/filestore');
|
||||
const constants = require('../constants');
|
||||
const chalk = require('chalk');
|
||||
const { HooksRuntime } = require('@usebruno/js');
|
||||
const HookManager = require('@usebruno/js/src/hook-manager');
|
||||
const decomment = require('decomment');
|
||||
|
||||
const FORMAT_CONFIG = {
|
||||
yml: { ext: '.yml', collectionFile: 'opencollection.yml', folderFile: 'folder.yml' },
|
||||
@@ -369,6 +372,117 @@ 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.
|
||||
*/
|
||||
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'
|
||||
});
|
||||
|
||||
/**
|
||||
* 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>} 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 = [];
|
||||
|
||||
@@ -598,5 +712,8 @@ module.exports = {
|
||||
mergeAuth,
|
||||
getAllRequestsInFolder,
|
||||
getAllRequestsAtFolderRoot,
|
||||
getCallStack
|
||||
getCallStack,
|
||||
extractHooks,
|
||||
HOOK_EVENTS,
|
||||
getOrCreateHookManager
|
||||
};
|
||||
|
||||
@@ -8,7 +8,10 @@ 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 } = require('@usebruno/js');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime, HooksRuntime } = require('@usebruno/js');
|
||||
// BrunoRequest and BrunoResponse are not exported from main index, require directly
|
||||
const BrunoRequest = require('@usebruno/js/src/bruno-request');
|
||||
const BrunoResponse = require('@usebruno/js/src/bruno-response');
|
||||
const { encodeUrl } = require('@usebruno/common').utils;
|
||||
const { extractPromptVariables } = require('@usebruno/common').utils;
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
@@ -24,7 +27,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 } = require('../../utils/collection');
|
||||
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars, sortByNameThenSequence, extractHooks, HOOK_EVENTS, getOrCreateHookManager } = require('../../utils/collection');
|
||||
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, updateCollectionOauth2Credentials } = require('../../utils/oauth2');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
@@ -36,6 +39,7 @@ const registerGrpcEventHandlers = require('./grpc-event-handlers');
|
||||
const { registerWsEventHandlers } = require('./ws-event-handlers');
|
||||
const { getCertsAndProxyConfig } = require('./cert-utils');
|
||||
const { buildFormUrlEncodedPayload, isFormData } = require('@usebruno/common').utils;
|
||||
const HookManager = require('@usebruno/js/src/hook-manager');
|
||||
|
||||
const ERROR_OCCURRED_WHILE_EXECUTING_REQUEST = 'Error occurred while executing the request!';
|
||||
|
||||
@@ -443,8 +447,189 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
};
|
||||
|
||||
const runPreRequest = async (
|
||||
/**
|
||||
* Register all hooks along the request path tree to a new HookManager instance.
|
||||
* Hooks are registered in order: collection -> folder(s) -> request
|
||||
* The HookManager will be garbage collected after the request run completes.
|
||||
*
|
||||
* @param {object} options - Configuration options
|
||||
* @param {object} options.collection - Collection object
|
||||
* @param {object} options.request - Request object
|
||||
* @param {array} options.requestTreePath - Path from collection to request
|
||||
* @param {object} options.envVars - Environment variables
|
||||
* @param {object} options.runtimeVariables - Runtime variables
|
||||
* @param {string} options.collectionPath - Collection path
|
||||
* @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
|
||||
* @param {function} options.onConsoleLog - Console log callback
|
||||
* @returns {HookManager} HookManager instance with all hooks registered
|
||||
*/
|
||||
/**
|
||||
* Generic function to register hooks from a hooks file to a HookManager
|
||||
* @param {object} options - Configuration options
|
||||
* @param {string} options.hooksFile - The hooks script content
|
||||
* @param {HookManager} options.hookManager - HookManager instance to register hooks to
|
||||
* @param {object} options.request - Request object (used for variable extraction)
|
||||
* @param {object} options.envVars - Environment variables
|
||||
* @param {object} options.runtimeVariables - Runtime variables
|
||||
* @param {string} options.collectionPath - Collection path
|
||||
* @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
|
||||
* @param {function} options.onConsoleLog - Console log callback
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const registerHooks = async ({
|
||||
hooksFile,
|
||||
hookManager,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName,
|
||||
onConsoleLog
|
||||
}) => {
|
||||
if (!hooksFile || !hooksFile.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
|
||||
|
||||
await hooksRuntime.runHooks({
|
||||
hooksFile: decomment(hooksFile),
|
||||
request,
|
||||
envVariables: envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName,
|
||||
hookManager
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register all hooks for a standalone request run
|
||||
* Creates a single HookManager with all hooks (collection, folder, request)
|
||||
* @param {object} options - Configuration options
|
||||
* @returns {Promise<HookManager>} HookManager instance with all hooks registered
|
||||
*/
|
||||
const registerHooksForRequest = async ({
|
||||
collection,
|
||||
request,
|
||||
requestTreePath,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName,
|
||||
onConsoleLog,
|
||||
requestUid,
|
||||
itemUid,
|
||||
collectionUid,
|
||||
runInBackground,
|
||||
notifyScriptExecution
|
||||
}) => {
|
||||
// Create a new HookManager for this request run
|
||||
const hookManager = new HookManager();
|
||||
|
||||
// Extract hooks from different levels
|
||||
const { collectionHooks, folderHooks, requestHooks } = extractHooks(collection, request, requestTreePath);
|
||||
|
||||
// Register collection-level hooks
|
||||
try {
|
||||
await registerHooks({
|
||||
hooksFile: collectionHooks,
|
||||
hookManager,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName,
|
||||
onConsoleLog
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error registering collection hooks:', error);
|
||||
onConsoleLog?.('error', [`Error registering collection hooks: ${error.message}`]);
|
||||
!runInBackground && notifyScriptExecution && notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid },
|
||||
scriptType: 'hooks',
|
||||
error
|
||||
});
|
||||
}
|
||||
|
||||
// Register folder-level hooks (in order from collection to request)
|
||||
for (const folderHook of folderHooks) {
|
||||
try {
|
||||
await registerHooks({
|
||||
hooksFile: folderHook.hooks,
|
||||
hookManager,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName,
|
||||
onConsoleLog
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error registering folder hooks:', error);
|
||||
onConsoleLog?.('error', [`Error registering folder hooks: ${error.message}`]);
|
||||
!runInBackground && notifyScriptExecution && notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid },
|
||||
scriptType: 'hooks',
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Register request-level hooks
|
||||
try {
|
||||
await registerHooks({
|
||||
hooksFile: requestHooks,
|
||||
hookManager,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName,
|
||||
onConsoleLog
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error registering request hooks:', error);
|
||||
onConsoleLog?.('error', [`Error registering request hooks: ${error.message}`]);
|
||||
!runInBackground && notifyScriptExecution && notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid },
|
||||
scriptType: 'hooks',
|
||||
error
|
||||
});
|
||||
}
|
||||
|
||||
return hookManager;
|
||||
};
|
||||
|
||||
const runPreRequest = async (request,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
@@ -676,10 +861,53 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
|
||||
// Get request tree path for hooks registration
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
try {
|
||||
request.signal = abortController.signal;
|
||||
saveCancelToken(cancelTokenUid, abortController);
|
||||
|
||||
// Register all hooks along the request path tree to a new HookManager for this request run
|
||||
const hookManager = await registerHooksForRequest({
|
||||
collection,
|
||||
request,
|
||||
requestTreePath,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName: collection?.name,
|
||||
onConsoleLog,
|
||||
requestUid,
|
||||
itemUid: item.uid,
|
||||
collectionUid,
|
||||
runInBackground,
|
||||
notifyScriptExecution
|
||||
});
|
||||
|
||||
// Call pre-request hooks before running pre-request scripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
if (hookManager) {
|
||||
try {
|
||||
// Create req object for hook callbacks (res is not available yet)
|
||||
const req = new BrunoRequest(request);
|
||||
await hookManager.call(HOOK_EVENTS.HTTP_BEFORE_REQUEST, { request, req, collection, collectionUid });
|
||||
} catch (error) {
|
||||
console.error('Error calling pre-request hooks:', error);
|
||||
onConsoleLog?.('error', [`Error calling pre-request hooks: ${error.message}`]);
|
||||
|
||||
!runInBackground && notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid: item.uid },
|
||||
scriptType: 'hooks',
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let preRequestScriptResult = null;
|
||||
let preRequestError = null;
|
||||
try {
|
||||
@@ -844,6 +1072,27 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
cookiesStore.saveCookieJar();
|
||||
|
||||
// Call post-response hooks after response is received but before post-response scripts
|
||||
// Use the same HookManager that was created for this request run
|
||||
if (hookManager) {
|
||||
try {
|
||||
// Create req and res objects for hook callbacks
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
await hookManager.call(HOOK_EVENTS.HTTP_AFTER_RESPONSE, { request, response, req, res, collection, collectionUid });
|
||||
} catch (error) {
|
||||
console.error('Error calling post-response hooks:', error);
|
||||
onConsoleLog?.('error', [`Error calling post-response hooks: ${error.message}`]);
|
||||
|
||||
!runInBackground && notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
basePayload: { requestUid, collectionUid, itemUid: item.uid },
|
||||
scriptType: 'hooks',
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const runPostScripts = async () => {
|
||||
let postResponseScriptResult = null;
|
||||
let postResponseError = null;
|
||||
@@ -1163,6 +1412,57 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
folder = collection;
|
||||
}
|
||||
|
||||
// Create a map to store HookManagers for this collection/folder run
|
||||
// Key format: 'collection:<collectionUid>', 'folder:<folderUid>', 'request:<requestUid>'
|
||||
const hookManagersMap = new Map();
|
||||
|
||||
// Register collection-level hooks immediately
|
||||
const collectionHookManagerOptions = {
|
||||
request: {}, // Placeholder request for hook registration
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName: collection?.name,
|
||||
onConsoleLog: (type, args) => {
|
||||
console[type](...args);
|
||||
mainWindow.webContents.send('main:console-log', {
|
||||
type,
|
||||
args
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionHooks = get(collectionRoot, 'request.script.hooks', '');
|
||||
const collectionHookManagerKey = `collection:${collectionPath}`;
|
||||
await getOrCreateHookManager(hookManagersMap, collectionHookManagerKey, collectionHooks, collectionHookManagerOptions);
|
||||
const isCollectionRun = folder?.uid === collection?.uid;
|
||||
|
||||
// If a folder is being run (not the collection itself), register folder hooks along the folder path tree
|
||||
if (folder && !isCollectionRun) {
|
||||
const folderTreePath = getTreePathFromCollectionToItem(collection, folder);
|
||||
|
||||
// Extract folder hooks from the folder tree path
|
||||
for (const pathItem of folderTreePath) {
|
||||
if (pathItem.type === 'folder') {
|
||||
const folderRoot = pathItem?.draft || pathItem?.root;
|
||||
const folderHooks = get(folderRoot, 'request.script.hooks', '');
|
||||
if (folderHooks && folderHooks.trim() !== '') {
|
||||
const folderHookManagerKey = `folder:${pathItem.pathname}`;
|
||||
await getOrCreateHookManager(hookManagersMap, folderHookManagerKey, folderHooks, collectionHookManagerOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCollectionRun) {
|
||||
const collectionHookManager = hookManagersMap.get(collectionHookManagerKey);
|
||||
await collectionHookManager.call(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection, collectionUid });
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'testrun-started',
|
||||
isRecursive: recursive,
|
||||
@@ -1216,6 +1516,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
let currentRequestIndex = 0;
|
||||
let nJumps = 0; // count the number of jumps to avoid infinite loops
|
||||
|
||||
while (currentRequestIndex < folderRequests.length) {
|
||||
// user requested to cancel runner
|
||||
if (abortController.signal.aborted) {
|
||||
@@ -1287,7 +1588,69 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get request tree path for hooks registration
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
const { collectionHooks, folderHooks, requestHooks } = extractHooks(collection, request, requestTreePath);
|
||||
|
||||
// Get or create HookManagers for each level
|
||||
const hookManagerOptions = {
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName: collection?.name,
|
||||
onConsoleLog: (type, args) => {
|
||||
console[type](...args);
|
||||
mainWindow.webContents.send('main:console-log', {
|
||||
type,
|
||||
args
|
||||
});
|
||||
},
|
||||
notifyScriptExecution,
|
||||
eventData
|
||||
};
|
||||
|
||||
// Collection-level HookManager
|
||||
const collectionHookManagerKey = `collection:${collectionPath}`;
|
||||
const collectionHookManager = await getOrCreateHookManager(hookManagersMap, collectionHookManagerKey, collectionHooks, hookManagerOptions);
|
||||
|
||||
// Folder-level HookManagers (in order from collection to request)
|
||||
const folderHookManagers = [];
|
||||
for (const folderHook of folderHooks) {
|
||||
const folderHookManagerKey = `folder:${folderHook.folderPathname}`;
|
||||
const folderHookManager = await getOrCreateHookManager(hookManagersMap, folderHookManagerKey, folderHook.hooks, hookManagerOptions);
|
||||
folderHookManagers.push(folderHookManager);
|
||||
}
|
||||
|
||||
// Request-level HookManager
|
||||
const requestHookManagerKey = item.pathname;
|
||||
const requestHookManager = await getOrCreateHookManager(hookManagersMap, requestHookManagerKey, requestHooks, hookManagerOptions);
|
||||
|
||||
// Combine all HookManagers in order: collection -> folders -> request
|
||||
const allHookManagers = [collectionHookManager, ...folderHookManagers, requestHookManager];
|
||||
|
||||
try {
|
||||
// Call pre-request hooks before running pre-request scripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
for (const hookManager of allHookManagers) {
|
||||
try {
|
||||
const req = new BrunoRequest(request);
|
||||
await hookManager.call(HOOK_EVENTS.HTTP_BEFORE_REQUEST, { request, req, collection, collectionUid });
|
||||
} catch (error) {
|
||||
console.error('Error calling pre-request hooks:', error);
|
||||
|
||||
notifyScriptExecution({
|
||||
channel: 'main:run-folder-event',
|
||||
basePayload: eventData,
|
||||
scriptType: 'hooks',
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let preRequestScriptResult;
|
||||
let preRequestError = null;
|
||||
try {
|
||||
@@ -1339,6 +1702,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
if (preRequestScriptResult?.skipRequest) {
|
||||
// Clean up request-level hook manager if request is skipped
|
||||
if (hookManagersMap) {
|
||||
hookManagersMap.delete(item.pathname);
|
||||
}
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'runner-request-skipped',
|
||||
error: 'Request has been skipped from pre-request script',
|
||||
@@ -1509,6 +1876,32 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Call post-response hooks after response is received but before post-response scripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
for (const hookManager of allHookManagers) {
|
||||
try {
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
await hookManager.call(HOOK_EVENTS.HTTP_AFTER_RESPONSE, { request, response, req, res, collection, collectionUid });
|
||||
} catch (error) {
|
||||
console.error('Error calling post-response hooks:', error);
|
||||
|
||||
notifyScriptExecution({
|
||||
channel: 'main:run-folder-event',
|
||||
basePayload: eventData,
|
||||
scriptType: 'hooks',
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up request-level hook manager after request completes
|
||||
// Requests are only run once, so we can safely remove the hook manager to free memory
|
||||
// TODO: we probably don't even have to store the request level hook manager in the first place
|
||||
if (hookManagersMap) {
|
||||
hookManagersMap.delete(item.pathname);
|
||||
}
|
||||
|
||||
let postResponseScriptResult;
|
||||
let postResponseError = null;
|
||||
try {
|
||||
@@ -1689,6 +2082,17 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Call collection run end hooks
|
||||
if (isCollectionRun) {
|
||||
const collectionHookManager = hookManagersMap.get(collectionHookManagerKey);
|
||||
if (collectionHookManager) {
|
||||
await collectionHookManager.call(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection, collectionUid });
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup: Clear hook managers map (will be garbage collected)
|
||||
hookManagersMap.clear();
|
||||
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'testrun-ended',
|
||||
@@ -1698,6 +2102,18 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
|
||||
// Call collection run end hooks even on error
|
||||
if (isCollectionRun) {
|
||||
const collectionHookManager = hookManagersMap.get(collectionHookManagerKey);
|
||||
if (collectionHookManager) {
|
||||
await collectionHookManager.call(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection, collectionUid });
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup: Clear hook managers map even on error
|
||||
hookManagersMap.clear();
|
||||
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'testrun-ended',
|
||||
|
||||
@@ -4,6 +4,8 @@ const { getRequestUid, getExampleUid } = require('../cache/requestUids');
|
||||
const { uuid } = require('./common');
|
||||
const os = require('os');
|
||||
const { preferencesUtil } = require('../store/preferences');
|
||||
const { HooksRuntime, HookManager } = require('@usebruno/js');
|
||||
const decomment = require('decomment');
|
||||
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let headers = new Map();
|
||||
@@ -227,6 +229,126 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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'
|
||||
});
|
||||
|
||||
/**
|
||||
* Get or create HookManager for a specific level (collection, folder, or request)
|
||||
* @param {Map} hookManagersMap - Map storing HookManagers by key
|
||||
* @param {string} uid - Unique identifier (collectionUid, folderUid, or requestUid)
|
||||
* @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
|
||||
* @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>} HookManager instance for this level
|
||||
*/
|
||||
const getOrCreateHookManager = async (hookManagersMap, uid, hooksFile, options = {}) => {
|
||||
// Return existing HookManager if already created
|
||||
if (hookManagersMap.has(uid)) {
|
||||
return hookManagersMap.get(uid);
|
||||
}
|
||||
|
||||
// Create new HookManager and register hooks
|
||||
const hookManager = new HookManager();
|
||||
hookManagersMap.set(uid, 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 ${uid}:`, error);
|
||||
if (options.onConsoleLog) {
|
||||
options.onConsoleLog('error', [`Error registering hooks for ${uid}: ${error.message}`]);
|
||||
}
|
||||
// Notify frontend if notification parameters are provided (for collection runner)
|
||||
if (options.notifyScriptExecution && options.eventData) {
|
||||
options.notifyScriptExecution({
|
||||
channel: 'main:run-folder-event',
|
||||
basePayload: options.eventData,
|
||||
scriptType: 'hooks',
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hookManager;
|
||||
};
|
||||
|
||||
const flattenItems = (items = []) => {
|
||||
const flattenedItems = [];
|
||||
|
||||
@@ -752,5 +874,8 @@ module.exports = {
|
||||
getEnvVars,
|
||||
getFormattedCollectionOauth2Credentials,
|
||||
sortByNameThenSequence,
|
||||
resolveInheritedSettings
|
||||
resolveInheritedSettings,
|
||||
extractHooks,
|
||||
HOOK_EVENTS,
|
||||
getOrCreateHookManager
|
||||
};
|
||||
|
||||
@@ -279,7 +279,8 @@ export const stringifyBruCollection = (json: any, isFolder?: boolean): string =>
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
script: {
|
||||
req: _.get(json, 'request.script.req', ''),
|
||||
res: _.get(json, 'request.script.res', '')
|
||||
res: _.get(json, 'request.script.res', ''),
|
||||
hooks: _.get(json, 'request.script.hooks', '')
|
||||
},
|
||||
vars: {
|
||||
req: _.get(json, 'request.vars.req', []),
|
||||
|
||||
@@ -19,6 +19,12 @@ export const toOpenCollectionScripts = (request: BrunoFolderRequest | BrunoHttpR
|
||||
code: request.script.res.trim()
|
||||
});
|
||||
}
|
||||
if (request?.script?.hooks?.trim().length) {
|
||||
ocScripts.push({
|
||||
type: 'hooks',
|
||||
code: request.script.hooks.trim()
|
||||
});
|
||||
}
|
||||
if (request?.tests?.trim().length) {
|
||||
ocScripts.push({
|
||||
type: 'tests',
|
||||
@@ -30,7 +36,7 @@ export const toOpenCollectionScripts = (request: BrunoFolderRequest | BrunoHttpR
|
||||
};
|
||||
|
||||
export const toBrunoScripts = (scripts: Scripts | null | undefined): {
|
||||
script?: { req?: string; res?: string };
|
||||
script?: { req?: string; res?: string; hooks?: string };
|
||||
tests?: string;
|
||||
} | undefined => {
|
||||
if (!scripts || !Array.isArray(scripts) || scripts.length === 0) {
|
||||
@@ -38,7 +44,7 @@ export const toBrunoScripts = (scripts: Scripts | null | undefined): {
|
||||
}
|
||||
|
||||
const brunoScripts: {
|
||||
script?: { req?: string; res?: string };
|
||||
script?: { req?: string; res?: string; hooks?: string };
|
||||
tests?: string;
|
||||
} = {};
|
||||
|
||||
@@ -58,6 +64,12 @@ export const toBrunoScripts = (scripts: Scripts | null | undefined): {
|
||||
if (script.type === 'tests' && script.code) {
|
||||
brunoScripts.tests = script.code;
|
||||
}
|
||||
if (script.type === 'hooks' && script.code) {
|
||||
if (!brunoScripts.script) {
|
||||
brunoScripts.script = {};
|
||||
}
|
||||
brunoScripts.script.hooks = script.code;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(brunoScripts).length > 0 ? brunoScripts : undefined;
|
||||
|
||||
@@ -37,7 +37,8 @@ const parseGraphQLRequest = (ocRequest: GraphQLRequest): BrunoItem => {
|
||||
},
|
||||
script: {
|
||||
req: null,
|
||||
res: null
|
||||
res: null,
|
||||
hooks: null
|
||||
},
|
||||
vars: {
|
||||
req: [],
|
||||
@@ -57,6 +58,9 @@ const parseGraphQLRequest = (ocRequest: GraphQLRequest): BrunoItem => {
|
||||
if (scripts.script.res) {
|
||||
brunoRequest.script.res = scripts.script.res;
|
||||
}
|
||||
if (scripts.script.hooks) {
|
||||
brunoRequest.script.hooks = scripts.script.hooks;
|
||||
}
|
||||
}
|
||||
if (scripts?.tests) {
|
||||
brunoRequest.tests = scripts.tests;
|
||||
|
||||
@@ -35,7 +35,8 @@ const parseHttpRequest = (ocRequest: HttpRequest): BrunoItem => {
|
||||
},
|
||||
script: {
|
||||
req: null,
|
||||
res: null
|
||||
res: null,
|
||||
hooks: null
|
||||
},
|
||||
vars: {
|
||||
req: [],
|
||||
@@ -55,6 +56,9 @@ const parseHttpRequest = (ocRequest: HttpRequest): BrunoItem => {
|
||||
if (scripts.script.res) {
|
||||
brunoRequest.script.res = scripts.script.res;
|
||||
}
|
||||
if (scripts.script.hooks) {
|
||||
brunoRequest.script.hooks = scripts.script.hooks;
|
||||
}
|
||||
}
|
||||
if (scripts?.tests) {
|
||||
brunoRequest.tests = scripts.tests;
|
||||
|
||||
@@ -139,7 +139,8 @@ const parseCollection = (ymlString: string): ParsedCollection => {
|
||||
auth: null,
|
||||
script: {
|
||||
req: null,
|
||||
res: null
|
||||
res: null,
|
||||
hooks: null
|
||||
},
|
||||
vars: {
|
||||
req: [],
|
||||
@@ -171,6 +172,9 @@ const parseCollection = (ymlString: string): ParsedCollection => {
|
||||
if (scripts.script.res) {
|
||||
collectionRoot.request.script.res = scripts.script.res;
|
||||
}
|
||||
if (scripts.script.hooks) {
|
||||
collectionRoot.request.script.hooks = scripts.script.hooks;
|
||||
}
|
||||
}
|
||||
if (scripts?.tests) {
|
||||
collectionRoot.request.tests = scripts.tests;
|
||||
|
||||
@@ -29,7 +29,8 @@ const parseFolder = (ymlString: string): FolderRoot => {
|
||||
auth: null,
|
||||
script: {
|
||||
req: null,
|
||||
res: null
|
||||
res: null,
|
||||
hooks: null
|
||||
},
|
||||
vars: {
|
||||
req: [],
|
||||
@@ -63,6 +64,9 @@ const parseFolder = (ymlString: string): FolderRoot => {
|
||||
if (scripts.script.res) {
|
||||
folderRoot.request.script.res = scripts.script.res;
|
||||
}
|
||||
if (scripts.script.hooks) {
|
||||
folderRoot.request.script.hooks = scripts.script.hooks;
|
||||
}
|
||||
}
|
||||
if (scripts?.tests) {
|
||||
folderRoot.request.tests = scripts.tests;
|
||||
|
||||
@@ -50,6 +50,7 @@ const hasRequestAuth = (collectionRoot: any): boolean => {
|
||||
const hasRequestScripts = (collectionRoot: any): boolean => {
|
||||
return (collectionRoot.request?.script?.req)
|
||||
|| (collectionRoot.request?.script?.res)
|
||||
|| (collectionRoot.request?.script?.hooks)
|
||||
|| (collectionRoot.request?.tests);
|
||||
};
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ const hasRequestAuth = (folderRoot: FolderRoot): boolean => {
|
||||
const hasRequestScripts = (folderRoot: FolderRoot): boolean => {
|
||||
return Boolean((folderRoot.request?.script?.req)
|
||||
|| (folderRoot.request?.script?.res)
|
||||
|| (folderRoot.request?.script?.hooks)
|
||||
|| (folderRoot.request?.tests));
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ const { jar: createCookieJar } = require('@usebruno/requests').cookies;
|
||||
const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
class Bru {
|
||||
constructor(runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables) {
|
||||
// Private class field - truly private, not accessible from outside the class
|
||||
#hookManager;
|
||||
|
||||
constructor(runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, hookManager) {
|
||||
this.envVariables = envVariables || {};
|
||||
this.runtimeVariables = runtimeVariables || {};
|
||||
this.promptVariables = promptVariables || {};
|
||||
@@ -19,6 +22,9 @@ class Bru {
|
||||
this.oauth2CredentialVariables = oauth2CredentialVariables || {};
|
||||
this.collectionPath = collectionPath;
|
||||
this.collectionName = collectionName;
|
||||
// Store HookManager in private class field - truly private, not accessible from outside
|
||||
this.#hookManager = hookManager || null;
|
||||
this._initializeHooksConvenienceMethods();
|
||||
this.sendRequest = sendRequest;
|
||||
this.runtime = runtime;
|
||||
this.cookies = {
|
||||
@@ -284,6 +290,56 @@ class Bru {
|
||||
isSafeMode() {
|
||||
return this.runtime === 'quickjs';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hooks convenience methods if hookManager is available
|
||||
* This creates a namespaced hooks object with only the convenience methods
|
||||
* The HookManager itself is kept private using a private class field - truly inaccessible from outside
|
||||
*/
|
||||
_initializeHooksConvenienceMethods() {
|
||||
if (!this.#hookManager) {
|
||||
// Create empty hooks object if no hookManager
|
||||
this.hooks = {
|
||||
runner: {},
|
||||
http: {}
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Create namespaced hooks object with only convenience methods
|
||||
// Users cannot access the HookManager directly (no .on() or .call() methods)
|
||||
// The HookManager is stored in a private class field and is truly private
|
||||
this.hooks = {
|
||||
runner: {
|
||||
onBeforeCollectionRun: (handler) => {
|
||||
if (!this.#hookManager) {
|
||||
throw new Error('HookManager is not available');
|
||||
}
|
||||
return this.#hookManager.on('runner:beforeCollectionRun', handler);
|
||||
},
|
||||
onAfterCollectionRun: (handler) => {
|
||||
if (!this.#hookManager) {
|
||||
throw new Error('HookManager is not available');
|
||||
}
|
||||
return this.#hookManager.on('runner:afterCollectionRun', handler);
|
||||
}
|
||||
},
|
||||
http: {
|
||||
onBeforeRequest: (handler) => {
|
||||
if (!this.#hookManager) {
|
||||
throw new Error('HookManager is not available');
|
||||
}
|
||||
return this.#hookManager.on('http:beforeRequest', handler);
|
||||
},
|
||||
onAfterResponse: (handler) => {
|
||||
if (!this.#hookManager) {
|
||||
throw new Error('HookManager is not available');
|
||||
}
|
||||
return this.#hookManager.on('http:afterResponse', handler);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bru;
|
||||
|
||||
178
packages/bruno-js/src/hook-manager.js
Normal file
178
packages/bruno-js/src/hook-manager.js
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* HookManager provides a simple event system for registering and calling hooks (event listeners).
|
||||
*
|
||||
* Hooks can be registered for specific string patterns or arrays of patterns. The special pattern '*' acts as a wildcard.
|
||||
*
|
||||
* Usage examples:
|
||||
*
|
||||
* Note: The `on()` method is internal only. Users should use the namespaced convenience methods:
|
||||
* - bru.hooks.http.onBeforeRequest(handler)
|
||||
* - bru.hooks.http.onAfterResponse(handler)
|
||||
* - bru.hooks.runner.onBeforeCollectionRun(handler)
|
||||
* - bru.hooks.runner.onAfterCollectionRun(handler)
|
||||
*
|
||||
* Unregister handler by calling `unhook`
|
||||
* unhook()
|
||||
* or unregister for a specific pattern
|
||||
* unhook('beforeRequest')
|
||||
*
|
||||
* Call hooks for a single event (internal use)
|
||||
* hookManager.call('beforeRequest', { request, req, collection });
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
class HookManager {
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Call all registered handlers for the given pattern(s)
|
||||
* Supports both sync and async handlers - all handlers are awaited
|
||||
* Wildcard handlers ('*') are called for every pattern, in addition to specific pattern handlers
|
||||
* @param {string|string[]} pattern - Event pattern(s) to trigger
|
||||
* @param {*} data - Data to pass to handlers
|
||||
* @returns {Promise<void>} Promise that resolves when all handlers complete
|
||||
*/
|
||||
async call(pattern, data) {
|
||||
if (typeof pattern !== 'string' && !Array.isArray(pattern)) {
|
||||
throw new TypeError('Pattern must be a string or an array of strings.');
|
||||
}
|
||||
|
||||
const patternList = [].concat(pattern).map((d) => String(d).trim());
|
||||
const hasWildcard = patternList.includes('*');
|
||||
|
||||
if (hasWildcard) {
|
||||
for (const ptn of Object.keys(this.listeners)) {
|
||||
const handlers = this.listeners[ptn];
|
||||
for (const handler of handlers) {
|
||||
await callHandler(handler, data, ptn);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Call handlers for each specific pattern
|
||||
for (const ptn of patternList) {
|
||||
if (!this.listeners[ptn]) continue;
|
||||
for (const handler of this.listeners[ptn]) {
|
||||
await callHandler(handler, data, ptn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a handler for the given pattern(s)
|
||||
* @param {string|string[]} pattern - Event pattern(s) to listen to
|
||||
* @param {Function} handler - Handler function to call
|
||||
* @returns {Function} Unhook function to remove the handler
|
||||
*/
|
||||
on(pattern, handler) {
|
||||
if (typeof pattern !== 'string' && !Array.isArray(pattern)) {
|
||||
throw new TypeError('Pattern must be a string or an array of strings.');
|
||||
}
|
||||
|
||||
if (typeof handler !== 'function') {
|
||||
throw new TypeError('Handler must be a function.');
|
||||
}
|
||||
|
||||
const patternList = [].concat(pattern).map((d) => String(d).trim());
|
||||
const hasWildcard = patternList.includes('*');
|
||||
|
||||
if (hasWildcard) {
|
||||
(this.listeners['*'] ||= []).push(handler);
|
||||
return this._createUnhook(patternList, handler);
|
||||
}
|
||||
|
||||
for (const ptn of patternList) {
|
||||
this.listeners[ptn] ||= [];
|
||||
|
||||
// Check if handler is already registered
|
||||
const exists = this.listeners[ptn].some((d) => Object.is(d, handler));
|
||||
if (exists) {
|
||||
throw new Error(`${handler.name ?? 'anonymous'} handler was registered twice for hook pattern '${ptn}'`);
|
||||
}
|
||||
|
||||
this.listeners[ptn].push(handler);
|
||||
}
|
||||
|
||||
return this._createUnhook(patternList, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an unhook function for the given patterns and handler
|
||||
* @private
|
||||
*/
|
||||
_createUnhook(patternList, handler) {
|
||||
const self = this;
|
||||
return function unhook(specific) {
|
||||
let patterns = [];
|
||||
if (specific) {
|
||||
patterns = [].concat(specific).map((d) => String(d).trim());
|
||||
} else {
|
||||
patterns = patternList;
|
||||
}
|
||||
|
||||
const hasStar = patterns.includes('*');
|
||||
|
||||
if (hasStar && self.listeners['*']) {
|
||||
self.listeners['*'] = self.listeners['*'].filter((d) => !Object.is(d, handler));
|
||||
}
|
||||
|
||||
for (const ptn of patterns) {
|
||||
if (!self.listeners[ptn]) continue;
|
||||
self.listeners[ptn] = self.listeners[ptn].filter((d) => !Object.is(d, handler));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all handlers for the given pattern(s)
|
||||
* @param {string|string[]} pattern - Event pattern(s) to clear
|
||||
*/
|
||||
clear(pattern) {
|
||||
if (typeof pattern !== 'string' && !Array.isArray(pattern)) {
|
||||
throw new TypeError('Pattern must be a string or an array of strings.');
|
||||
}
|
||||
|
||||
const patternList = [].concat(pattern).map((d) => String(d).trim());
|
||||
|
||||
for (const ptn of patternList) {
|
||||
if (ptn === '*') {
|
||||
delete this.listeners['*'];
|
||||
} else if (this.listeners[ptn]) {
|
||||
delete this.listeners[ptn];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered handlers
|
||||
*/
|
||||
clearAll() {
|
||||
this.listeners = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely call a handler function with error handling
|
||||
* Supports both sync and async handlers
|
||||
* @private
|
||||
* @param {Function} handler - Handler function to call
|
||||
* @param {*} data - Data to pass to handler
|
||||
* @param {string} event - Event name for error reporting
|
||||
* @returns {Promise<void>} Promise that resolves when handler completes
|
||||
*/
|
||||
async function callHandler(handler, data, event) {
|
||||
try {
|
||||
const result = handler(data);
|
||||
// If handler returns a Promise, await it
|
||||
if (result && typeof result.then === 'function') {
|
||||
await result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to execute handler for event: '${event}' with handler: '${handler?.name ?? 'anonymous'}'`, error);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HookManager;
|
||||
@@ -2,6 +2,8 @@ const ScriptRuntime = require('./runtime/script-runtime');
|
||||
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 HookManager = require('./hook-manager');
|
||||
const { runScriptInNodeVm } = require('./sandbox/node-vm');
|
||||
|
||||
module.exports = {
|
||||
@@ -9,5 +11,7 @@ module.exports = {
|
||||
TestRuntime,
|
||||
VarsRuntime,
|
||||
AssertRuntime,
|
||||
HooksRuntime,
|
||||
HookManager,
|
||||
runScriptInNodeVm
|
||||
};
|
||||
|
||||
119
packages/bruno-js/src/runtime/hooks-runtime.js
Normal file
119
packages/bruno-js/src/runtime/hooks-runtime.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
||||
const Bru = require('../bru');
|
||||
const HookManager = require('../hook-manager');
|
||||
const { cleanJson } = require('../utils');
|
||||
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
||||
class HooksRuntime {
|
||||
constructor(props) {
|
||||
this.runtime = props?.runtime || 'quickjs';
|
||||
}
|
||||
|
||||
/**
|
||||
* Run hooks script to register event handlers
|
||||
* @param {object} options - Configuration options
|
||||
* @param {string} options.hooksFile - The hooks script content
|
||||
* @param {object} options.request - The request object (used for variable extraction only)
|
||||
* @param {object} options.envVariables - Environment variables
|
||||
* @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
|
||||
* @param {HookManager} [options.hookManager] - Existing HookManager instance to use (for shared hook registration)
|
||||
* @returns {object} Result containing the hookManager instance
|
||||
*/
|
||||
async runHooks(options) {
|
||||
const {
|
||||
hooksFile,
|
||||
request,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName,
|
||||
hookManager
|
||||
} = options;
|
||||
const activeHookManager = hookManager || new HookManager();
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
// Pass activeHookManager to Bru so it uses the same instance (whether provided or newly created)
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, activeHookManager);
|
||||
|
||||
const context = {
|
||||
bru
|
||||
};
|
||||
|
||||
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')
|
||||
};
|
||||
}
|
||||
|
||||
if (runRequestByItemPathname) {
|
||||
context.bru.runRequest = runRequestByItemPathname;
|
||||
}
|
||||
|
||||
// If no hooks file, return early with the hookManager
|
||||
if (!hooksFile || !hooksFile.length) {
|
||||
return {
|
||||
hookManager: activeHookManager,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
persistentEnvVariables: bru.persistentEnvVariables,
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables)
|
||||
};
|
||||
}
|
||||
|
||||
// Execute hooks script
|
||||
if (this.runtime === 'nodevm') {
|
||||
await runScriptInNodeVm({
|
||||
script: hooksFile,
|
||||
context,
|
||||
collectionPath,
|
||||
scriptingConfig
|
||||
});
|
||||
|
||||
return {
|
||||
hookManager: activeHookManager,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
persistentEnvVariables: bru.persistentEnvVariables,
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables)
|
||||
};
|
||||
}
|
||||
|
||||
await executeQuickJsVmAsync({
|
||||
script: hooksFile,
|
||||
context: context,
|
||||
collectionPath
|
||||
});
|
||||
|
||||
return {
|
||||
hookManager: activeHookManager,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
persistentEnvVariables: bru.persistentEnvVariables,
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HooksRuntime;
|
||||
@@ -1,5 +1,8 @@
|
||||
const { cleanJson, cleanCircularJson } = require('../../../utils');
|
||||
const { marshallToVm } = require('../utils');
|
||||
const { createBrunoRequestShim } = require('./bruno-request');
|
||||
const { createBrunoResponseShim } = require('./bruno-response');
|
||||
const uuid = require('uuid');
|
||||
|
||||
const addBruShimToContext = (vm, bru) => {
|
||||
const bruObject = vm.newObject();
|
||||
@@ -394,6 +397,289 @@ const addBruShimToContext = (vm, bru) => {
|
||||
vm.setProp(bruObject, 'cookies', bruCookiesObject);
|
||||
bruCookiesObject.dispose();
|
||||
|
||||
// Store handler handles - we need a Map (not WeakMap) because we need to look up by string ID
|
||||
// WeakMap only allows object keys, but we need string-based lookup for handlerId
|
||||
// Proper cleanup via unhook() and cleanupHandlerHandles() prevents memory leaks
|
||||
const handlerIdToHandle = new Map(); // handlerId (string) -> handle (for lookup and cleanup)
|
||||
|
||||
// Cleanup function to dispose handler handles and prevent memory leaks
|
||||
const cleanupHandlerHandles = () => {
|
||||
if (handlerIdToHandle.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Dispose all handler handles
|
||||
handlerIdToHandle.forEach((handle, handlerId) => {
|
||||
try {
|
||||
if (handle && typeof handle.dispose === 'function') {
|
||||
handle.dispose();
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore disposal errors for individual handles
|
||||
// Log only if it's not a UseAfterFree error
|
||||
const errorMsg = e?.message || String(e);
|
||||
if (!errorMsg.includes('UseAfterFree') && !errorMsg.includes('Lifetime not alive')) {
|
||||
console.warn(`Error disposing handler handle ${handlerId}:`, e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear the Map
|
||||
handlerIdToHandle.clear();
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
console.warn('Error during handler handles cleanup:', error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Add hooks shim if bru.hooks exists
|
||||
if (bru.hooks) {
|
||||
const hooksObject = vm.newObject();
|
||||
|
||||
// Execute handler using the original function handle from the VM
|
||||
// Returns a Promise that resolves when the handler completes (supports async handlers)
|
||||
const executeHandler = async (handlerHandle, vmInstance, data) => {
|
||||
if (!handlerHandle) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!vmInstance) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify handler is still a function in the VM
|
||||
const handlerType = vmInstance.typeof(handlerHandle);
|
||||
if (handlerType !== 'function') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Prepare data (clean circular refs) - use try-catch to prevent stack overflow
|
||||
let cleanedData;
|
||||
try {
|
||||
cleanedData = { ...cleanCircularJson(data) };
|
||||
} catch (e) {
|
||||
// If cleaning fails due to circular refs or stack overflow, use minimal data
|
||||
console.warn('Error cleaning hook data, using minimal data:', e.message);
|
||||
cleanedData = {};
|
||||
}
|
||||
|
||||
// Create data object in VM
|
||||
const dataHandle = vmInstance.newObject();
|
||||
|
||||
// Add all cleaned data properties
|
||||
Object.keys(cleanedData).forEach((key) => {
|
||||
if (key !== 'req' && key !== 'res') {
|
||||
const value = marshallToVm(cleanedData[key], vmInstance);
|
||||
vmInstance.setProp(dataHandle, key, value);
|
||||
value.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
// Add req/res shim objects to data if provided
|
||||
// In QuickJS, when you setProp, the parent object takes ownership
|
||||
// We dispose them after setting to avoid keeping extra references
|
||||
// but dataHandle will maintain the reference until it's disposed
|
||||
if (data.req) {
|
||||
const reqShim = createBrunoRequestShim(vmInstance, data.req);
|
||||
vmInstance.setProp(dataHandle, 'req', reqShim);
|
||||
// Dispose the original handle - dataHandle now owns the reference
|
||||
reqShim.dispose();
|
||||
}
|
||||
|
||||
if (data.res) {
|
||||
const resShim = createBrunoResponseShim(vmInstance, data.res);
|
||||
vmInstance.setProp(dataHandle, 'res', resShim);
|
||||
// Dispose the original handle - dataHandle now owns the reference
|
||||
resShim.dispose();
|
||||
}
|
||||
|
||||
// Call the original handler function
|
||||
// Use vmInstance.global as context to ensure proper scope access
|
||||
const result = vmInstance.callFunction(handlerHandle, vmInstance.global, dataHandle);
|
||||
// Dispose dataHandle - this will clean up all child references
|
||||
dataHandle.dispose();
|
||||
|
||||
if (result.error) {
|
||||
const error = vmInstance.dump(result.error);
|
||||
result.error.dispose();
|
||||
const errorMsg = error?.message || error?.toString() || String(error);
|
||||
if (!errorMsg.includes('UseAfterFree') && !errorMsg.includes('Lifetime not alive')) {
|
||||
console.error('Error in hook handler:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the result is a Promise (async handler) and await it
|
||||
// This is crucial for handlers that need to complete before the request proceeds
|
||||
const resultType = vmInstance.typeof(result.value);
|
||||
|
||||
// Only try to resolve as Promise if it's an object (Promises are objects in JS)
|
||||
// For non-object values (undefined, null, primitives), just dispose and return
|
||||
if (resultType !== 'object') {
|
||||
result.value.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the object has a .then property (duck-typing for Promise)
|
||||
let isPromise = false;
|
||||
try {
|
||||
const thenProp = vmInstance.getProp(result.value, 'then');
|
||||
isPromise = vmInstance.typeof(thenProp) === 'function';
|
||||
thenProp.dispose();
|
||||
} catch (e) {
|
||||
// If we can't check for .then, assume it's not a promise
|
||||
isPromise = false;
|
||||
}
|
||||
|
||||
if (!isPromise) {
|
||||
// Not a promise, just dispose and return
|
||||
result.value.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a Promise - await it using resolvePromise
|
||||
try {
|
||||
const resolvedResult = await vmInstance.resolvePromise(result.value);
|
||||
result.value.dispose();
|
||||
|
||||
if (resolvedResult.error) {
|
||||
const error = vmInstance.dump(resolvedResult.error);
|
||||
resolvedResult.error.dispose();
|
||||
const errorMsg = error?.message || error?.toString() || String(error);
|
||||
if (!errorMsg.includes('UseAfterFree') && !errorMsg.includes('Lifetime not alive')) {
|
||||
console.error('Error in async hook handler:', error);
|
||||
}
|
||||
} else {
|
||||
resolvedResult.value.dispose();
|
||||
}
|
||||
} catch (promiseError) {
|
||||
// If resolvePromise fails, just dispose the value
|
||||
try {
|
||||
result.value.dispose();
|
||||
} catch (e) {
|
||||
// Ignore disposal errors
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error?.message || error?.toString() || String(error);
|
||||
if (!errorMsg.includes('UseAfterFree') && !errorMsg.includes('Lifetime not alive')) {
|
||||
console.error('Error executing hook handler:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a hook function that registers a handler with the native hook system.
|
||||
* This helper eliminates code duplication across different hook types.
|
||||
*
|
||||
* @param {string} handlerIdPrefix - Prefix for the unique handler ID
|
||||
* @param {Function} nativeHookRegister - Function to register with native hooks (e.g., bru.hooks.http.onBeforeRequest)
|
||||
* @param {boolean} validateHandler - Whether to validate handler is a function (default: true)
|
||||
* @returns {Function} VM function that can be registered as a hook
|
||||
*/
|
||||
const createHookFunction = (handlerIdPrefix, nativeHookRegister, validateHandler = true) => {
|
||||
return vm.newFunction(handlerIdPrefix, function (handler) {
|
||||
// Validate handler if required
|
||||
if (validateHandler && vm.typeof(handler) !== 'function') {
|
||||
throw new Error('Handler must be a function');
|
||||
}
|
||||
|
||||
// Create unique handler ID
|
||||
const handlerId = `${handlerIdPrefix}-${uuid.v4()}`;
|
||||
|
||||
// Try to duplicate the handle to own a reference
|
||||
let handlerHandle;
|
||||
try {
|
||||
handlerHandle = handler.dup ? handler.dup() : handler;
|
||||
} catch (e) {
|
||||
handlerHandle = handler;
|
||||
}
|
||||
|
||||
// Store the handle - we need Map (not WeakMap) because we need string-based lookup
|
||||
handlerIdToHandle.set(handlerId, handlerHandle);
|
||||
|
||||
// Create native handler that executes the stored handle
|
||||
// Returns a Promise so HookManager can await async handlers
|
||||
const nativeHandler = (data) => {
|
||||
const storedHandle = handlerIdToHandle.get(handlerId);
|
||||
if (!storedHandle || !vm) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// Return the Promise from executeHandler so HookManager awaits it
|
||||
return executeHandler(storedHandle, vm, data);
|
||||
};
|
||||
|
||||
// Register with native hook system
|
||||
const unhook = nativeHookRegister(nativeHandler);
|
||||
|
||||
// Create unhook function
|
||||
const unhookFn = vm.newFunction('unhook', () => {
|
||||
unhook();
|
||||
|
||||
// Clean up handler handle
|
||||
if (handlerIdToHandle.has(handlerId)) {
|
||||
const storedHandle = handlerIdToHandle.get(handlerId);
|
||||
try {
|
||||
if (storedHandle && storedHandle.dispose) {
|
||||
storedHandle.dispose();
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore disposal errors
|
||||
}
|
||||
handlerIdToHandle.delete(handlerId);
|
||||
}
|
||||
});
|
||||
|
||||
return unhookFn;
|
||||
});
|
||||
};
|
||||
|
||||
// Add namespaced hooks structure
|
||||
if (bru.hooks) {
|
||||
const hooksNamespacedObject = vm.newObject();
|
||||
|
||||
// HTTP hooks namespace
|
||||
if (bru.hooks.http) {
|
||||
const httpHooksObject = vm.newObject();
|
||||
|
||||
if (typeof bru.hooks.http.onBeforeRequest === 'function') {
|
||||
const onBeforeRequest = createHookFunction('onBeforeRequest', (nativeHandler) => bru.hooks.http.onBeforeRequest(nativeHandler), false);
|
||||
onBeforeRequest.consume((handle) => vm.setProp(httpHooksObject, 'onBeforeRequest', handle));
|
||||
}
|
||||
|
||||
if (typeof bru.hooks.http.onAfterResponse === 'function') {
|
||||
const onAfterResponse = createHookFunction('onAfterResponse', (nativeHandler) => bru.hooks.http.onAfterResponse(nativeHandler), false);
|
||||
onAfterResponse.consume((handle) => vm.setProp(httpHooksObject, 'onAfterResponse', handle));
|
||||
}
|
||||
|
||||
vm.setProp(hooksNamespacedObject, 'http', httpHooksObject);
|
||||
httpHooksObject.dispose();
|
||||
}
|
||||
|
||||
// Runner hooks namespace
|
||||
if (bru.hooks.runner) {
|
||||
const runnerHooksObject = vm.newObject();
|
||||
|
||||
if (typeof bru.hooks.runner.onBeforeCollectionRun === 'function') {
|
||||
const onBeforeCollectionRun = createHookFunction('onBeforeCollectionRun', (nativeHandler) => bru.hooks.runner.onBeforeCollectionRun(nativeHandler), true);
|
||||
onBeforeCollectionRun.consume((handle) => vm.setProp(runnerHooksObject, 'onBeforeCollectionRun', handle));
|
||||
}
|
||||
|
||||
if (typeof bru.hooks.runner.onAfterCollectionRun === 'function') {
|
||||
const onAfterCollectionRun = createHookFunction('onAfterCollectionRun', (nativeHandler) => bru.hooks.runner.onAfterCollectionRun(nativeHandler), true);
|
||||
onAfterCollectionRun.consume((handle) => vm.setProp(runnerHooksObject, 'onAfterCollectionRun', handle));
|
||||
}
|
||||
|
||||
vm.setProp(hooksNamespacedObject, 'runner', runnerHooksObject);
|
||||
runnerHooksObject.dispose();
|
||||
}
|
||||
|
||||
vm.setProp(bruObject, 'hooks', hooksNamespacedObject);
|
||||
hooksNamespacedObject.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
vm.setProp(bruObject, 'runner', bruRunnerObject);
|
||||
vm.setProp(vm.global, 'bru', bruObject);
|
||||
bruObject.dispose();
|
||||
@@ -454,6 +740,9 @@ const addBruShimToContext = (vm, bru) => {
|
||||
};
|
||||
};
|
||||
`);
|
||||
|
||||
// Always return cleanup function; it is a no-op if no hooks were registered
|
||||
return cleanupHandlerHandles;
|
||||
};
|
||||
|
||||
module.exports = addBruShimToContext;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { marshallToVm } = require('../utils');
|
||||
|
||||
const addBrunoRequestShimToContext = (vm, req) => {
|
||||
const createBrunoRequestShim = (vm, req) => {
|
||||
const reqObject = vm.newObject();
|
||||
|
||||
const url = marshallToVm(req.getUrl(), vm);
|
||||
@@ -163,8 +163,14 @@ const addBrunoRequestShimToContext = (vm, req) => {
|
||||
vm.setProp(reqObject, 'getTags', getTags);
|
||||
getTags.dispose();
|
||||
|
||||
return reqObject;
|
||||
};
|
||||
|
||||
const addBrunoRequestShimToContext = (vm, req) => {
|
||||
const reqObject = createBrunoRequestShim(vm, req);
|
||||
vm.setProp(vm.global, 'req', reqObject);
|
||||
reqObject.dispose();
|
||||
};
|
||||
|
||||
module.exports = addBrunoRequestShimToContext;
|
||||
module.exports.createBrunoRequestShim = createBrunoRequestShim;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { marshallToVm } = require('../utils');
|
||||
|
||||
const addBrunoResponseShimToContext = (vm, res) => {
|
||||
const createBrunoResponseShim = (vm, res) => {
|
||||
let resFn = vm.newFunction('res', function (exprStr) {
|
||||
return marshallToVm(res(vm.dump(exprStr)), vm);
|
||||
});
|
||||
@@ -80,8 +80,14 @@ const addBrunoResponseShimToContext = (vm, res) => {
|
||||
vm.setProp(resFn, 'getSize', getSize);
|
||||
getSize.dispose();
|
||||
|
||||
return resFn;
|
||||
};
|
||||
|
||||
const addBrunoResponseShimToContext = (vm, res) => {
|
||||
const resFn = createBrunoResponseShim(vm, res);
|
||||
vm.setProp(vm.global, 'res', resFn);
|
||||
resFn.dispose();
|
||||
};
|
||||
|
||||
module.exports = addBrunoResponseShimToContext;
|
||||
module.exports.createBrunoResponseShim = createBrunoResponseShim;
|
||||
|
||||
@@ -153,9 +153,10 @@ const grammar = ohm.grammar(`Bru {
|
||||
example = "example" st* "{" nl* examplecontent tagend
|
||||
examplecontent = (~tagend any)*
|
||||
|
||||
script = scriptreq | scriptres
|
||||
script = scriptreq | scriptres | scripthooks
|
||||
scriptreq = "script:pre-request" st* "{" nl* textblock tagend
|
||||
scriptres = "script:post-response" st* "{" nl* textblock tagend
|
||||
scripthooks = "script:hooks" st* "{" nl* textblock tagend
|
||||
tests = "tests" st* "{" nl* textblock tagend
|
||||
docs = "docs" st* "{" nl* textblock tagend
|
||||
}`);
|
||||
@@ -1017,6 +1018,13 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
}
|
||||
};
|
||||
},
|
||||
scripthooks(_1, _2, _3, _4, textblock, _5) {
|
||||
return {
|
||||
script: {
|
||||
hooks: outdentString(textblock.sourceString)
|
||||
}
|
||||
};
|
||||
},
|
||||
tests(_1, _2, _3, _4, textblock, _5) {
|
||||
return {
|
||||
tests: outdentString(textblock.sourceString)
|
||||
|
||||
@@ -72,9 +72,10 @@ const grammar = ohm.grammar(`Bru {
|
||||
authwsse = "auth:wsse" dictionary
|
||||
authapikey = "auth:apikey" dictionary
|
||||
|
||||
script = scriptreq | scriptres
|
||||
script = scriptreq | scriptres | scripthooks
|
||||
scriptreq = "script:pre-request" st* "{" nl* textblock tagend
|
||||
scriptres = "script:post-response" st* "{" nl* textblock tagend
|
||||
scripthooks = "script:hooks" st* "{" nl* textblock tagend
|
||||
tests = "tests" st* "{" nl* textblock tagend
|
||||
docs = "docs" st* "{" nl* textblock tagend
|
||||
}`);
|
||||
@@ -546,6 +547,13 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
}
|
||||
};
|
||||
},
|
||||
scripthooks(_1, _2, _3, _4, textblock, _5) {
|
||||
return {
|
||||
script: {
|
||||
hooks: outdentString(textblock.sourceString)
|
||||
}
|
||||
};
|
||||
},
|
||||
tests(_1, _2, _3, _4, textblock, _5) {
|
||||
return {
|
||||
tests: outdentString(textblock.sourceString)
|
||||
|
||||
@@ -716,6 +716,14 @@ ${indentString(script.req)}
|
||||
${indentString(script.res)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
if (script && script.hooks && script.hooks.length) {
|
||||
bru += `script:hooks {
|
||||
${indentString(script.hooks)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -411,6 +411,14 @@ ${indentString(script.req)}
|
||||
${indentString(script.res)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
if (script && script.hooks && script.hooks.length) {
|
||||
bru += `script:hooks {
|
||||
${indentString(script.hooks)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface Script {
|
||||
req?: string | null;
|
||||
res?: string | null;
|
||||
hooks?: string | null;
|
||||
}
|
||||
|
||||
@@ -401,7 +401,8 @@ const requestSchema = Yup.object({
|
||||
body: requestBodySchema,
|
||||
script: Yup.object({
|
||||
req: Yup.string().nullable(),
|
||||
res: Yup.string().nullable()
|
||||
res: Yup.string().nullable(),
|
||||
hooks: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict(),
|
||||
@@ -437,7 +438,8 @@ const grpcRequestSchema = Yup.object({
|
||||
.required('body is required'),
|
||||
script: Yup.object({
|
||||
req: Yup.string().nullable(),
|
||||
res: Yup.string().nullable()
|
||||
res: Yup.string().nullable(),
|
||||
hooks: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict(),
|
||||
@@ -475,7 +477,8 @@ const wsRequestSchema = Yup.object({
|
||||
.required('body is required'),
|
||||
script: Yup.object({
|
||||
req: Yup.string().nullable(),
|
||||
res: Yup.string().nullable()
|
||||
res: Yup.string().nullable(),
|
||||
hooks: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict(),
|
||||
@@ -510,7 +513,8 @@ const folderRootSchema = Yup.object({
|
||||
auth: authSchema,
|
||||
script: Yup.object({
|
||||
req: Yup.string().nullable(),
|
||||
res: Yup.string().nullable()
|
||||
res: Yup.string().nullable(),
|
||||
hooks: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict()
|
||||
|
||||
2
packages/bruno-tests/hooks-comprehensive-tests/.gitignore
vendored
Normal file
2
packages/bruno-tests/hooks-comprehensive-tests/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
!.env
|
||||
junit*.xml
|
||||
@@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: async-collection-run-end
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("request should execute successfully", function() {
|
||||
// This request helps track request count for onAfterCollectionRun
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
meta {
|
||||
name: async-collection-run-start
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("async onBeforeCollectionRun hook should have executed", function() {
|
||||
const started = bru.getVar('async-collection-run-started');
|
||||
expect(started).to.equal('true');
|
||||
});
|
||||
|
||||
test("collection run should wait for async setup", function() {
|
||||
// If we get here, the collection run waited for the async hook
|
||||
const setupComplete = bru.getEnvVar('async-setup-complete');
|
||||
expect(setupComplete).to.equal('true');
|
||||
});
|
||||
|
||||
test("request should execute after async setup", function() {
|
||||
// Verify request executed after async hook completed
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "hooks-comprehensive-tests",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
meta {
|
||||
name: collection-run-end-cleanup
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("request should execute successfully", function() {
|
||||
// This request helps track request count for onAfterCollectionRun
|
||||
// Actual onAfterCollectionRun verification is done in verify-collection-run-end.bru
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
meta {
|
||||
name: collection-run-start-setup
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("setup should have set token", function() {
|
||||
const token = bru.getVar('setup-token');
|
||||
expect(token).to.equal('mock-token-12345');
|
||||
});
|
||||
|
||||
test("setup should have set env token", function() {
|
||||
const apiToken = bru.getEnvVar('api-token');
|
||||
expect(apiToken).to.equal('mock-token-12345');
|
||||
});
|
||||
|
||||
test("setup should have initialized counters", function() {
|
||||
const requestCounter = bru.getVar('request-counter');
|
||||
expect(requestCounter).to.equal('0');
|
||||
});
|
||||
|
||||
test("request should have access to setup vars", function() {
|
||||
const setupComplete = bru.getVar('setup-complete');
|
||||
expect(setupComplete).to.equal('true');
|
||||
});
|
||||
}
|
||||
|
||||
174
packages/bruno-tests/hooks-comprehensive-tests/collection.bru
Normal file
174
packages/bruno-tests/hooks-comprehensive-tests/collection.bru
Normal file
@@ -0,0 +1,174 @@
|
||||
script:hooks {
|
||||
// ============================================
|
||||
// Comprehensive Hooks Test Collection
|
||||
// This collection tests all available hooks:
|
||||
// - HTTP Hooks: onBeforeRequest, onAfterResponse (tested in individual request files)
|
||||
// - Runner Hooks: onBeforeCollectionRun, onAfterCollectionRun (tested here)
|
||||
// ============================================
|
||||
|
||||
// ============================================
|
||||
// Runner Hooks: onBeforeCollectionRun Tests
|
||||
// ============================================
|
||||
|
||||
// Test: Sync onBeforeCollectionRun hook
|
||||
bru.hooks.runner.onBeforeCollectionRun(() => {
|
||||
console.log('[onBeforeCollectionRun] Sync hook - Collection run starting...');
|
||||
|
||||
// Test: Can use bru APIs
|
||||
bru.setVar('collection-run-started', 'true');
|
||||
bru.setVar('start-time', Date.now().toString());
|
||||
|
||||
// Test: Can set collection-level vars
|
||||
bru.setEnvVar('collection-setup-complete', 'true');
|
||||
|
||||
// Test: Can use getEnvName
|
||||
const envName = bru.getEnvName();
|
||||
bru.setVar('env-name-at-start', envName);
|
||||
|
||||
// Test: Can use getCollectionName
|
||||
const collectionName = bru.getCollectionName();
|
||||
bru.setVar('collection-name-at-start', collectionName);
|
||||
|
||||
console.log('[onBeforeCollectionRun] Sync setup complete');
|
||||
});
|
||||
|
||||
// Test: Async onBeforeCollectionRun hook
|
||||
bru.hooks.runner.onBeforeCollectionRun(async() => {
|
||||
console.log('[onBeforeCollectionRun] Async hook - Starting async setup...');
|
||||
|
||||
// Test: Can use async/await
|
||||
await bru.sleep(1000);
|
||||
|
||||
// Test: Can use bru APIs in async context
|
||||
bru.setVar('async-collection-run-started', 'true');
|
||||
bru.setVar('async-start-time', Date.now().toString());
|
||||
|
||||
// Test: Can set env vars
|
||||
bru.setEnvVar('async-setup-complete', 'true');
|
||||
|
||||
await bru.sleep(500);
|
||||
console.log('[onBeforeCollectionRun] Async setup completed');
|
||||
});
|
||||
|
||||
// Test: onBeforeCollectionRun with setup operations
|
||||
bru.hooks.runner.onBeforeCollectionRun(async() => {
|
||||
console.log('[onBeforeCollectionRun] Setup hook - Performing setup operations...');
|
||||
|
||||
// Simulate setup operations like fetching tokens, initializing data, etc.
|
||||
await bru.sleep(500);
|
||||
|
||||
// Test: Setup - Set initial vars
|
||||
bru.setVar('setup-token', 'mock-token-12345');
|
||||
bru.setVar('setup-complete', 'true');
|
||||
|
||||
// Test: Setup - Set env vars
|
||||
bru.setEnvVar('api-token', 'mock-token-12345');
|
||||
bru.setEnvVar('setup-timestamp', Date.now().toString());
|
||||
|
||||
// Test: Setup - Initialize counters
|
||||
bru.setVar('request-counter', '0');
|
||||
bru.setVar('success-counter', '0');
|
||||
|
||||
console.log('[onBeforeCollectionRun] Setup complete - token and counters initialized');
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// onAfterCollectionRun Hook Tests
|
||||
// ============================================
|
||||
|
||||
// Test: Sync onAfterCollectionRun hook
|
||||
bru.hooks.runner.onAfterCollectionRun(() => {
|
||||
console.log('[onAfterCollectionRun] Sync hook - Collection run ending...');
|
||||
|
||||
// Test: Can use bru APIs
|
||||
// Test: Can read final state (read before other hooks delete)
|
||||
const requestCount = bru.getVar('request-count') || '0';
|
||||
console.log('[onAfterCollectionRun] Final request count:', requestCount);
|
||||
|
||||
// Test: Can set final env vars
|
||||
bru.setEnvVar('collection-run-complete', 'true');
|
||||
|
||||
console.log('[onAfterCollectionRun] Sync cleanup complete');
|
||||
});
|
||||
|
||||
// Test: Async onAfterCollectionRun hook
|
||||
bru.hooks.runner.onAfterCollectionRun(async() => {
|
||||
console.log('[onAfterCollectionRun] Async hook - Starting async cleanup...');
|
||||
|
||||
// Test: Can use async/await
|
||||
await bru.sleep(1000);
|
||||
|
||||
// Test: Can use bru APIs in async context
|
||||
// Test: Can read final state (read before cleanup hook deletes)
|
||||
const requestCount = bru.getVar('request-count') || '0';
|
||||
console.log('[onAfterCollectionRun] Async cleanup - Final request count:', requestCount);
|
||||
|
||||
// Test: Can set final env vars
|
||||
bru.setEnvVar('async-cleanup-complete', 'true');
|
||||
|
||||
await bru.sleep(500);
|
||||
console.log('[onAfterCollectionRun] Async cleanup completed');
|
||||
});
|
||||
|
||||
// Test: onAfterCollectionRun with cleanup operations
|
||||
bru.hooks.runner.onAfterCollectionRun(async() => {
|
||||
console.log('[onAfterCollectionRun] Cleanup hook - Performing cleanup operations...');
|
||||
|
||||
// Test: Cleanup - Read final statistics (read before cleanup)
|
||||
const requestCount = bru.getVar('request-count') || '0';
|
||||
const successCount = bru.getVar('success-count') || '0';
|
||||
|
||||
console.log('[onAfterCollectionRun] Final statistics:', {
|
||||
requests: parseInt(requestCount),
|
||||
successes: parseInt(successCount)
|
||||
});
|
||||
|
||||
// Test: Cleanup - Set final env vars
|
||||
bru.setEnvVar('collection-run-finished', 'true');
|
||||
bru.setEnvVar('final-request-count', requestCount);
|
||||
|
||||
// Cleanup: Remove all test variables created during the run
|
||||
bru.deleteVar('collection-run-started');
|
||||
bru.deleteVar('start-time');
|
||||
bru.deleteVar('env-name-at-start');
|
||||
bru.deleteVar('collection-name-at-start');
|
||||
bru.deleteVar('async-collection-run-started');
|
||||
bru.deleteVar('async-start-time');
|
||||
bru.deleteVar('setup-token');
|
||||
bru.deleteVar('setup-complete');
|
||||
bru.deleteVar('request-counter');
|
||||
bru.deleteVar('success-counter');
|
||||
bru.deleteVar('request-count');
|
||||
bru.deleteVar('success-count');
|
||||
bru.deleteVar('final-stats');
|
||||
bru.deleteVar('cleanup-performed');
|
||||
|
||||
await bru.sleep(300);
|
||||
console.log('[onAfterCollectionRun] Cleanup complete - all test variables removed');
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// HTTP Hooks: Collection-level HTTP hooks
|
||||
// Individual request files in hooks/ subdirectories test HTTP hooks at request level
|
||||
// This collection-level hook tracks requests for runner hook tests
|
||||
// ============================================
|
||||
|
||||
// Collection-level HTTP hook: Track requests for onAfterCollectionRun tests
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
const count = bru.getVar('request-count') || 0;
|
||||
bru.setVar('request-count', parseInt(count) + 1);
|
||||
if (res.getStatus() === 200) {
|
||||
const successCount = bru.getVar('success-count') || 0;
|
||||
bru.setVar('success-count', parseInt(successCount) + 1);
|
||||
} else {
|
||||
console.log('[onAfterResponse] Request failed:');
|
||||
}
|
||||
});
|
||||
|
||||
// Collection-level HTTP hook: Example onBeforeRequest hook
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
// This hook runs before every request in the collection
|
||||
// Individual request files in hooks/ subdirectories have their own request-level hooks
|
||||
console.log('[Collection-level HTTP Hook] Before request:', req.getName());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
vars {
|
||||
host: https://testbench-sanity.usebruno.com
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
meta {
|
||||
name: after-response-read-response
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
console.log("res.getSize()",res.getSize())
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
// Test: Read status code
|
||||
const status = res.getStatus();
|
||||
bru.setVar('response-status', status.toString());
|
||||
|
||||
// Test: Read status text
|
||||
const statusText = res.getStatusText();
|
||||
bru.setVar('response-status-text', statusText);
|
||||
|
||||
// Test: Read headers
|
||||
const headers = res.getHeaders();
|
||||
bru.setVar('response-has-headers', (Object.keys(headers).length > 0).toString());
|
||||
|
||||
// Test: Read specific header
|
||||
const contentType = res.getHeader('content-type');
|
||||
if (contentType) {
|
||||
bru.setVar('response-content-type', contentType);
|
||||
}
|
||||
|
||||
// Test: Read body
|
||||
const body = res.getBody();
|
||||
bru.setVar('response-body', body);
|
||||
|
||||
// Test: Read response time
|
||||
const responseTime = res.getResponseTime();
|
||||
bru.setVar('response-time', responseTime.toString());
|
||||
|
||||
// Test: Read URL
|
||||
const url = res.getUrl();
|
||||
bru.setVar('response-url', url);
|
||||
|
||||
// Test: Read size
|
||||
const size = res.getSize();
|
||||
bru.setVar('response-size', size.body.toString());
|
||||
|
||||
console.log('[afterResponse] Read all response properties');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should have read status code", function() {
|
||||
const status = bru.getVar('response-status');
|
||||
expect(status).to.equal('200');
|
||||
});
|
||||
|
||||
test("should have read status text", function() {
|
||||
const statusText = bru.getVar('response-status-text');
|
||||
expect(statusText).to.equal('OK');
|
||||
});
|
||||
|
||||
test("should have read headers", function() {
|
||||
const hasHeaders = bru.getVar('response-has-headers');
|
||||
expect(hasHeaders).to.equal('true');
|
||||
});
|
||||
|
||||
test("should have read body", function() {
|
||||
const body = bru.getVar('response-body');
|
||||
expect(body).to.equal('pong');
|
||||
});
|
||||
|
||||
test("should have read response time", function() {
|
||||
const responseTime = bru.getVar('response-time');
|
||||
expect(parseInt(responseTime)).to.be.a('number');
|
||||
});
|
||||
|
||||
test("should have read URL", function() {
|
||||
const url = bru.getVar('response-url');
|
||||
expect(url).to.include('/ping');
|
||||
});
|
||||
|
||||
test("should have read size", function() {
|
||||
const size = bru.getVar('response-size');
|
||||
expect(parseInt(size)).to.be.at.least(0);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
meta {
|
||||
name: after-response-with-bru-apis
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onAfterResponse(({ req, res }) => {
|
||||
// Test: Can use bru APIs based on response
|
||||
const status = res.getStatus();
|
||||
const body = res.getBody();
|
||||
|
||||
// Test: Set vars based on response
|
||||
if (status === 200) {
|
||||
bru.setVar('request-successful', 'true');
|
||||
bru.setVar('response-data', body);
|
||||
}
|
||||
|
||||
// Test: Can use setEnvVar
|
||||
bru.setEnvVar('last-response-status', status.toString());
|
||||
bru.setEnvVar('last-response-time', res.getResponseTime().toString());
|
||||
|
||||
// Test: Can use interpolate
|
||||
const interpolated = bru.interpolate('Status: {{last-response-status}}');
|
||||
bru.setVar('interpolated-status', interpolated);
|
||||
|
||||
// Test: Store response data for later use
|
||||
bru.setVar('last-response-body', body);
|
||||
bru.setVar('last-request-url', req.getUrl());
|
||||
|
||||
console.log('[afterResponse] Stored response data using bru APIs');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should have set success flag based on response", function() {
|
||||
const successful = bru.getVar('request-successful');
|
||||
expect(successful).to.equal('true');
|
||||
});
|
||||
|
||||
test("should have stored response data", function() {
|
||||
const responseData = bru.getVar('response-data');
|
||||
expect(responseData).to.equal('pong');
|
||||
});
|
||||
|
||||
test("should have set env vars from response", function() {
|
||||
const lastStatus = bru.getEnvVar('last-response-status');
|
||||
expect(lastStatus).to.equal('200');
|
||||
});
|
||||
|
||||
test("should have stored last response body", function() {
|
||||
const body = bru.getVar('last-response-body');
|
||||
expect(body).to.equal('pong');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
meta {
|
||||
name: async-after-response
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onAfterResponse(async({ req, res }) => {
|
||||
console.log('[afterResponse] Starting async hook...');
|
||||
|
||||
// Test: Can use async/await
|
||||
await bru.sleep(1000);
|
||||
|
||||
// Test: Can access req and res in async context
|
||||
const reqUrl = req.getUrl();
|
||||
const status = res.getStatus();
|
||||
const body = res.getBody();
|
||||
|
||||
console.log('[afterResponse] Request URL:', reqUrl);
|
||||
console.log('[afterResponse] Response:', status, body);
|
||||
|
||||
// Test: Can use bru APIs in async context
|
||||
bru.setVar('async-afterResponse-executed', 'true');
|
||||
bru.setVar('async-response-status', status.toString());
|
||||
bru.setVar('async-response-body', body);
|
||||
|
||||
// Test: Can read headers
|
||||
const headers = res.getHeaders();
|
||||
bru.setVar('response-header-count', Object.keys(headers).length.toString());
|
||||
|
||||
await bru.sleep(500);
|
||||
console.log('[afterResponse] Async hook completed');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("async afterResponse hook should have executed", function() {
|
||||
const executed = bru.getVar('async-afterResponse-executed');
|
||||
expect(executed).to.equal('true');
|
||||
});
|
||||
|
||||
test("should have captured response in async hook", function() {
|
||||
const status = bru.getVar('async-response-status');
|
||||
expect(status).to.equal('200');
|
||||
});
|
||||
|
||||
test("should have captured response body in async hook", function() {
|
||||
const body = bru.getVar('async-response-body');
|
||||
expect(body).to.equal('pong');
|
||||
});
|
||||
|
||||
test("should have read headers in async hook", function() {
|
||||
const headerCount = bru.getVar('response-header-count');
|
||||
expect(parseInt(headerCount)).to.be.at.least(0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
meta {
|
||||
name: multiple-after-response
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
// First hook
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
console.log('[afterResponse 1] First hook executing');
|
||||
bru.setVar('hook-order', '1');
|
||||
const status = res.getStatus();
|
||||
bru.setVar('status-from-hook-1', status.toString());
|
||||
});
|
||||
|
||||
// Second hook
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
console.log('[afterResponse 2] Second hook executing');
|
||||
const order = bru.getVar('hook-order') || '';
|
||||
bru.setVar('hook-order', order + ',2');
|
||||
|
||||
const status = res.getStatus();
|
||||
bru.setVar('status-from-hook-2', status.toString());
|
||||
});
|
||||
|
||||
// Third hook (async)
|
||||
bru.hooks.http.onAfterResponse(async({ res }) => {
|
||||
console.log('[afterResponse 3] Third hook (async) executing');
|
||||
await bru.sleep(200);
|
||||
const order = bru.getVar('hook-order') || '';
|
||||
bru.setVar('hook-order', order + ',3');
|
||||
|
||||
const status = res.getStatus();
|
||||
bru.setVar('status-from-hook-3', status.toString());
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("all afterResponse hooks should execute in order", function() {
|
||||
const order = bru.getVar('hook-order');
|
||||
expect(order).to.equal('1,2,3');
|
||||
});
|
||||
|
||||
test("all hooks should have access to response", function() {
|
||||
const status1 = bru.getVar('status-from-hook-1');
|
||||
const status2 = bru.getVar('status-from-hook-2');
|
||||
const status3 = bru.getVar('status-from-hook-3');
|
||||
|
||||
expect(status1).to.equal('200');
|
||||
expect(status2).to.equal('200');
|
||||
expect(status3).to.equal('200');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
meta {
|
||||
name: sync-after-response
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onAfterResponse(({ req, res }) => {
|
||||
// Test: Can access req object
|
||||
const reqUrl = req.getUrl();
|
||||
const reqName = req.getName();
|
||||
const reqMethod = req.getMethod();
|
||||
|
||||
console.log('[afterResponse] Request:', reqMethod, reqUrl, reqName);
|
||||
|
||||
// Test: Can access res object
|
||||
const status = res.getStatus();
|
||||
const statusText = res.getStatusText();
|
||||
const body = res.getBody();
|
||||
const responseTime = res.getResponseTime();
|
||||
|
||||
console.log('[afterResponse] Response:', status, statusText, 'Time:', responseTime);
|
||||
|
||||
// Test: Can use bru APIs
|
||||
bru.setVar('afterResponse-executed', 'true');
|
||||
bru.setVar('response-status', status.toString());
|
||||
bru.setVar('response-body', body);
|
||||
bru.setVar('response-time', responseTime.toString());
|
||||
|
||||
console.log('[afterResponse] Hook executed successfully');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("afterResponse hook should have executed", function() {
|
||||
const executed = bru.getVar('afterResponse-executed');
|
||||
expect(executed).to.equal('true');
|
||||
});
|
||||
|
||||
test("should have captured response status in hook", function() {
|
||||
const status = bru.getVar('response-status');
|
||||
expect(status).to.equal('200');
|
||||
});
|
||||
|
||||
test("should have captured response body in hook", function() {
|
||||
const body = bru.getVar('response-body');
|
||||
expect(body).to.equal('pong');
|
||||
});
|
||||
|
||||
test("should have captured response time in hook", function() {
|
||||
const responseTime = bru.getVar('response-time');
|
||||
const parsed = parseInt(responseTime);
|
||||
expect(parsed).to.be.a('number');
|
||||
expect(isNaN(parsed)).to.be.false;
|
||||
expect(parsed).to.be.greaterThan(0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
meta {
|
||||
name: collection-folder-vars
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
// These may return undefined if not set, which is valid
|
||||
bru.setVar('collection-var', bru.getCollectionVar('testCollectionVar') || 'not-set');
|
||||
bru.setVar('folder-var', bru.getFolderVar('testFolderVar') || 'not-set');
|
||||
bru.setVar('request-var', bru.getRequestVar('testRequestVar') || 'not-set');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("bru.getCollectionVar - callable in hook", function() {
|
||||
// Just verify the function is callable and returns expected type
|
||||
const val = bru.getVar('collection-var');
|
||||
expect(val).to.be.a('string');
|
||||
});
|
||||
|
||||
test("bru.getFolderVar - callable in hook", function() {
|
||||
const val = bru.getVar('folder-var');
|
||||
expect(val).to.be.a('string');
|
||||
});
|
||||
|
||||
test("bru.getRequestVar - callable in hook", function() {
|
||||
const val = bru.getVar('request-var');
|
||||
expect(val).to.be.a('string');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
meta {
|
||||
name: context
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
bru.setVar('hook-collection-name', bru.getCollectionName());
|
||||
bru.setVar('hook-env-name', bru.getEnvName());
|
||||
bru.setVar('hook-process-env', bru.getProcessEnv('PATH'));
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("bru.getCollectionName - returns collection name", function() {
|
||||
const name = bru.getVar('hook-collection-name');
|
||||
expect(name).to.be.a('string');
|
||||
expect(name.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
test("bru.getEnvName - returns environment name", function() {
|
||||
const envName = bru.getVar('hook-env-name');
|
||||
expect(envName).to.equal('Prod');
|
||||
});
|
||||
|
||||
test("bru.getProcessEnv - returns process env value", function() {
|
||||
// PATH is typically available in all environments
|
||||
const path = bru.getProcessEnv('PATH');
|
||||
const storedPath = bru.getVar('hook-process-env');
|
||||
expect(path).to.equal(storedPath);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
meta {
|
||||
name: env-variables
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
bru.setEnvVar('hook-env-test', 'env-value-from-hook');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("bru.getEnvVar - read existing env var", function() {
|
||||
const host = bru.getEnvVar('host');
|
||||
expect(host).to.include('http');
|
||||
});
|
||||
|
||||
test("bru.setEnvVar/getEnvVar - set in hook", function() {
|
||||
expect(bru.getEnvVar('hook-env-test')).to.equal('env-value-from-hook');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
meta {
|
||||
name: interpolate
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
bru.setVar('test-value', 'interpolated');
|
||||
bru.setVar('interpolate-result', bru.interpolate('Result: {{test-value}}'));
|
||||
bru.setVar('interpolate-env', bru.interpolate('Host: {{host}}'));
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("bru.interpolate - with runtime variable", function() {
|
||||
expect(bru.getVar('interpolate-result')).to.equal('Result: interpolated');
|
||||
});
|
||||
|
||||
test("bru.interpolate - with env variable", function() {
|
||||
const result = bru.getVar('interpolate-env');
|
||||
expect(result).to.include('Host:');
|
||||
expect(result).to.include('http');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
meta {
|
||||
name: sleep
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(async ({ req }) => {
|
||||
const start = Date.now();
|
||||
await bru.sleep(100);
|
||||
bru.setVar('sleep-elapsed', Date.now() - start);
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("bru.sleep - delays execution", function() {
|
||||
const elapsed = bru.getVar('sleep-elapsed');
|
||||
expect(elapsed).to.be.at.least(100);
|
||||
});
|
||||
|
||||
test("request succeeds after async hook", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
meta {
|
||||
name: variables
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
bru.setVar('hook-string', 'hello');
|
||||
bru.setVar('hook-number', 42);
|
||||
bru.setVar('hook-object', { key: 'value' });
|
||||
bru.setVar('hook-array', [1, 2, 3]);
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("bru.setVar/getVar - string", function() {
|
||||
expect(bru.getVar('hook-string')).to.equal('hello');
|
||||
});
|
||||
|
||||
test("bru.setVar/getVar - number", function() {
|
||||
expect(bru.getVar('hook-number')).to.equal(42);
|
||||
});
|
||||
|
||||
test("bru.setVar/getVar - object", function() {
|
||||
expect(bru.getVar('hook-object')).to.deep.equal({ key: 'value' });
|
||||
});
|
||||
|
||||
test("bru.setVar/getVar - array", function() {
|
||||
expect(bru.getVar('hook-array')).to.deep.equal([1, 2, 3]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
meta {
|
||||
name: auth-timeout
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
bru.setVar('auth-mode', req.getAuthMode());
|
||||
bru.setVar('exec-mode', req.getExecutionMode());
|
||||
|
||||
// Test timeout methods
|
||||
bru.setVar('timeout-before', req.getTimeout());
|
||||
req.setTimeout(10000);
|
||||
bru.setVar('timeout-after', req.getTimeout());
|
||||
|
||||
// Test max redirects
|
||||
req.setMaxRedirects(5);
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("req.getAuthMode - returns auth mode", function() {
|
||||
const mode = bru.getVar('auth-mode');
|
||||
expect(mode).to.equal('none');
|
||||
});
|
||||
|
||||
test("req.getExecutionMode - returns execution mode", function() {
|
||||
const mode = bru.getVar('exec-mode');
|
||||
expect(['standalone', 'runner', 'cli', undefined]).to.include(mode);
|
||||
});
|
||||
|
||||
test("req.getTimeout/setTimeout - manages timeout", function() {
|
||||
expect(bru.getVar('timeout-after')).to.equal(10000);
|
||||
});
|
||||
|
||||
test("request succeeds with configured options", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
meta {
|
||||
name: body
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"original": "value",
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
const body = req.getBody();
|
||||
bru.setVar('original-field', body.original);
|
||||
|
||||
// Modify body
|
||||
req.setBody({
|
||||
...body,
|
||||
modified: true,
|
||||
count: body.count + 1,
|
||||
added: 'new-field'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("req.getBody - reads original body", function() {
|
||||
expect(bru.getVar('original-field')).to.equal('value');
|
||||
});
|
||||
|
||||
test("req.setBody - preserves original fields", function() {
|
||||
expect(res.getBody().original).to.equal('value');
|
||||
});
|
||||
|
||||
test("req.setBody - adds new fields", function() {
|
||||
expect(res.getBody().modified).to.equal(true);
|
||||
expect(res.getBody().added).to.equal('new-field');
|
||||
});
|
||||
|
||||
test("req.setBody - modifies existing fields", function() {
|
||||
expect(res.getBody().count).to.equal(2);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
meta {
|
||||
name: headers
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/headers
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
X-Original: original-value
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
// Test getHeader
|
||||
bru.setVar('read-original', req.getHeader('X-Original'));
|
||||
|
||||
// Test setHeader - add new
|
||||
req.setHeader('X-Added-In-Hook', 'hook-value');
|
||||
|
||||
// Test setHeader - modify existing
|
||||
req.setHeader('X-Original', 'modified-value');
|
||||
|
||||
// Test getHeaders
|
||||
bru.setVar('headers-count', Object.keys(req.getHeaders()).length);
|
||||
|
||||
// Test setHeaders - bulk add
|
||||
const headers = req.getHeaders();
|
||||
req.setHeaders({ ...headers, 'X-Bulk-1': 'bulk-1', 'X-Bulk-2': 'bulk-2' });
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("req.getHeader - reads header value", function() {
|
||||
expect(bru.getVar('read-original')).to.equal('original-value');
|
||||
});
|
||||
|
||||
test("req.setHeader - adds new header", function() {
|
||||
const body = res.getBody();
|
||||
expect(body['x-added-in-hook']).to.equal('hook-value');
|
||||
});
|
||||
|
||||
test("req.setHeader - modifies existing header", function() {
|
||||
const body = res.getBody();
|
||||
expect(body['x-original']).to.equal('modified-value');
|
||||
});
|
||||
|
||||
test("req.setHeaders - bulk adds headers", function() {
|
||||
const body = res.getBody();
|
||||
expect(body['x-bulk-1']).to.equal('bulk-1');
|
||||
expect(body['x-bulk-2']).to.equal('bulk-2');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
meta {
|
||||
name: metadata
|
||||
type: http
|
||||
seq: 5
|
||||
tags: [
|
||||
hook-test
|
||||
api-test
|
||||
]
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
bru.setVar('req-name', req.getName());
|
||||
bru.setVar('req-tags', req.getTags());
|
||||
bru.setVar('req-timeout-before', req.getTimeout());
|
||||
|
||||
req.setTimeout(5000);
|
||||
bru.setVar('req-timeout-after', req.getTimeout());
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("req.getName - returns request name", function() {
|
||||
expect(bru.getVar('req-name')).to.equal('metadata');
|
||||
});
|
||||
|
||||
test("req.getTags - returns tags array", function() {
|
||||
const tags = bru.getVar('req-tags');
|
||||
expect(tags).to.include('hook-test');
|
||||
expect(tags).to.include('api-test');
|
||||
});
|
||||
|
||||
test("req.setTimeout - changes timeout", function() {
|
||||
expect(bru.getVar('req-timeout-after')).to.equal(5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
meta {
|
||||
name: method
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/headers
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
bru.setVar('original-method', req.getMethod());
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("req.getMethod - returns HTTP method", function() {
|
||||
expect(bru.getVar('original-method')).to.equal('GET');
|
||||
});
|
||||
|
||||
test("req.getMethod - available in tests", function() {
|
||||
expect(req.getMethod()).to.equal('GET');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
meta {
|
||||
name: url
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/headers
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
bru.setVar('original-url', req.getUrl());
|
||||
req.setUrl(req.getUrl().replace('/headers', '/ping'));
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("req.getUrl - returns original URL", function() {
|
||||
expect(bru.getVar('original-url')).to.include('/headers');
|
||||
});
|
||||
|
||||
test("req.setUrl - changes request endpoint", function() {
|
||||
// Response should be 'pong' from /ping endpoint
|
||||
expect(res.getBody()).to.equal('pong');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
meta {
|
||||
name: body
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"message": "original"
|
||||
}
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
const body = res.getBody();
|
||||
bru.setVar('original-message', body.message);
|
||||
|
||||
// Modify response body
|
||||
res.setBody({
|
||||
...body,
|
||||
message: 'modified-in-hook',
|
||||
addedInHook: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("res.getBody - reads response body", function() {
|
||||
expect(bru.getVar('original-message')).to.equal('original');
|
||||
});
|
||||
|
||||
test("res.setBody - modifies response", function() {
|
||||
expect(res.getBody().message).to.equal('modified-in-hook');
|
||||
});
|
||||
|
||||
test("res.setBody - adds fields", function() {
|
||||
expect(res.getBody().addedInHook).to.equal(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
meta {
|
||||
name: headers
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
bru.setVar('res-content-type', res.getHeader('content-type'));
|
||||
bru.setVar('res-headers-count', Object.keys(res.getHeaders()).length);
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("res.getHeader - returns header value", function() {
|
||||
const contentType = bru.getVar('res-content-type');
|
||||
expect(contentType).to.be.a('string');
|
||||
});
|
||||
|
||||
test("res.getHeaders - returns all headers", function() {
|
||||
const count = bru.getVar('res-headers-count');
|
||||
expect(count).to.be.greaterThan(0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
meta {
|
||||
name: metadata
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
bru.setVar('res-time', res.getResponseTime());
|
||||
bru.setVar('res-url', res.getUrl());
|
||||
bru.setVar('res-size', res.getSize());
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("res.getResponseTime - returns response time", function() {
|
||||
const time = bru.getVar('res-time');
|
||||
expect(time).to.be.a('number');
|
||||
expect(time).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
test("res.getUrl - returns request URL", function() {
|
||||
const url = bru.getVar('res-url');
|
||||
expect(url).to.include('/ping');
|
||||
});
|
||||
|
||||
test("res.getSize - returns size object", function() {
|
||||
const size = bru.getVar('res-size');
|
||||
expect(size).to.have.property('header');
|
||||
expect(size).to.have.property('body');
|
||||
expect(size).to.have.property('total');
|
||||
});
|
||||
|
||||
test("res.getSize - total equals header + body", function() {
|
||||
const size = bru.getVar('res-size');
|
||||
expect(size.total).to.equal(size.header + size.body);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
meta {
|
||||
name: status
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
bru.setVar('res-status', res.getStatus());
|
||||
bru.setVar('res-status-text', res.getStatusText());
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("res.getStatus - returns status code", function() {
|
||||
expect(bru.getVar('res-status')).to.equal(200);
|
||||
});
|
||||
|
||||
test("res.getStatusText - returns status text", function() {
|
||||
const statusText = bru.getVar('res-status-text');
|
||||
expect(statusText).to.be.a('string');
|
||||
});
|
||||
|
||||
test("res.getStatus - accessible in tests", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
meta {
|
||||
name: auth-injection
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/headers
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
req.setHeader('Authorization', 'Bearer mock-token-12345');
|
||||
req.setHeader('X-API-Key', 'api-key-67890');
|
||||
req.setHeader('X-Request-ID', 'req-' + Date.now());
|
||||
});
|
||||
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
const body = res.getBody();
|
||||
bru.setVar('auth-header-echoed', body['authorization']);
|
||||
bru.setVar('api-key-echoed', body['x-api-key']);
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("Authorization header is injected", function() {
|
||||
expect(bru.getVar('auth-header-echoed')).to.equal('Bearer mock-token-12345');
|
||||
});
|
||||
|
||||
test("API key header is injected", function() {
|
||||
expect(bru.getVar('api-key-echoed')).to.equal('api-key-67890');
|
||||
});
|
||||
|
||||
test("Request succeeds with injected auth", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
meta {
|
||||
name: both-hooks-req-res
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"test": "data"
|
||||
}
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
// beforeRequest: Only req is available
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
bru.setVar('before-req-url', req.getUrl());
|
||||
bru.setVar('before-req-method', req.getMethod());
|
||||
bru.setVar('before-req-name', req.getName());
|
||||
req.setHeader('X-Modified-In-Before', 'yes');
|
||||
});
|
||||
|
||||
// afterResponse: Both req and res are available
|
||||
bru.hooks.http.onAfterResponse(({ req, res }) => {
|
||||
// Can still access req in afterResponse
|
||||
bru.setVar('after-req-url', req.getUrl());
|
||||
bru.setVar('after-req-method', req.getMethod());
|
||||
bru.setVar('after-req-header', req.getHeader('X-Modified-In-Before'));
|
||||
|
||||
// Access res
|
||||
bru.setVar('after-res-status', res.getStatus());
|
||||
bru.setVar('after-res-body', res.getBody());
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("req is accessible in beforeRequest", function() {
|
||||
expect(bru.getVar('before-req-url')).to.include('/api/echo/json');
|
||||
expect(bru.getVar('before-req-method')).to.equal('POST');
|
||||
});
|
||||
|
||||
test("req is accessible in afterResponse", function() {
|
||||
expect(bru.getVar('after-req-url')).to.include('/api/echo/json');
|
||||
expect(bru.getVar('after-req-method')).to.equal('POST');
|
||||
});
|
||||
|
||||
test("modifications from beforeRequest visible in afterResponse", function() {
|
||||
expect(bru.getVar('after-req-header')).to.equal('yes');
|
||||
});
|
||||
|
||||
test("res is accessible in afterResponse", function() {
|
||||
expect(bru.getVar('after-res-status')).to.equal(200);
|
||||
expect(bru.getVar('after-res-body')).to.have.property('test');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
meta {
|
||||
name: request-transform
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"users": [
|
||||
{"id": 1, "name": "Alice"},
|
||||
{"id": 2, "name": "Bob"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
const body = req.getBody();
|
||||
// Transform: add timestamp to each user
|
||||
body.users = body.users.map(u => ({ ...u, timestamp: Date.now() }));
|
||||
body.requestTime = new Date().toISOString();
|
||||
req.setBody(body);
|
||||
});
|
||||
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
const body = res.getBody();
|
||||
// Transform: add summary
|
||||
res.setBody({
|
||||
data: body,
|
||||
summary: { userCount: body.users.length }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("Request body is transformed", function() {
|
||||
const body = res.getBody();
|
||||
expect(body.data.users[0]).to.have.property('timestamp');
|
||||
expect(body.data).to.have.property('requestTime');
|
||||
});
|
||||
|
||||
test("Response body is enriched", function() {
|
||||
const body = res.getBody();
|
||||
expect(body.summary.userCount).to.equal(2);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
meta {
|
||||
name: async-before-request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(async ({ req }) => {
|
||||
console.log('[beforeRequest] Starting async hook...');
|
||||
|
||||
// Test: Can use async/await
|
||||
await bru.sleep(1000);
|
||||
|
||||
// Test: Can access req object in async context
|
||||
const url = req.getUrl();
|
||||
const method = req.getMethod();
|
||||
|
||||
console.log('[beforeRequest] Request:', method, url);
|
||||
|
||||
// Test: Can use bru APIs in async context
|
||||
bru.setVar('async-beforeRequest-executed', 'true');
|
||||
bru.setVar('async-request-method', method);
|
||||
|
||||
// Test: Can modify request
|
||||
req.setHeader('X-Async-Header', 'async-before-request');
|
||||
|
||||
await bru.sleep(500);
|
||||
console.log('[beforeRequest] Async hook completed');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("async beforeRequest hook should have executed", function() {
|
||||
const executed = bru.getVar('async-beforeRequest-executed');
|
||||
expect(executed).to.equal('true');
|
||||
});
|
||||
|
||||
test("should have captured request method in async hook", function() {
|
||||
const method = bru.getVar('async-request-method');
|
||||
expect(method).to.equal('GET');
|
||||
});
|
||||
|
||||
test("request should execute after async hook completes", function() {
|
||||
// If we get here, the request executed after the hook
|
||||
// The hook had 1.5 seconds of sleep, so this test verifies waiting
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
meta {
|
||||
name: before-request-modify-request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] Original URL:', req.getUrl());
|
||||
console.log('[beforeRequest] Original method:', req.getMethod());
|
||||
|
||||
// Test: Modify URL
|
||||
const originalUrl = req.getUrl();
|
||||
req.setUrl(originalUrl + '?modified=true');
|
||||
|
||||
// Test: Modify headers
|
||||
req.setHeader('X-Modified-By', 'beforeRequest-hook');
|
||||
req.setHeader('X-Original-URL', originalUrl);
|
||||
|
||||
// Test: Get existing headers
|
||||
const existingHeaders = req.getHeaders();
|
||||
bru.setVar('header-count', Object.keys(existingHeaders).length.toString());
|
||||
|
||||
console.log('[beforeRequest] Modified URL:', req.getUrl());
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("request should have been modified by hook", function() {
|
||||
// Verify hook executed
|
||||
const headerCount = bru.getVar('header-count');
|
||||
expect(parseInt(headerCount)).to.be.at.least(2);
|
||||
});
|
||||
|
||||
test("response should be successful", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
meta {
|
||||
name: before-request-with-bru-apis
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
// Test: Can use getEnvVar
|
||||
const host = bru.getEnvVar('host');
|
||||
console.log('[beforeRequest] Host from env:', host);
|
||||
|
||||
// Test: Can use setVar/getVar
|
||||
bru.setVar('test-var', 'test-value');
|
||||
const testVar = bru.getVar('test-var');
|
||||
|
||||
// Test: Can use setEnvVar
|
||||
bru.setEnvVar('hook-set-env-var', 'env-value-from-hook');
|
||||
|
||||
// Test: Can use getCollectionVar
|
||||
const collectionVar = bru.getCollectionVar('collection-var');
|
||||
if (collectionVar) {
|
||||
bru.setVar('got-collection-var', collectionVar);
|
||||
}
|
||||
|
||||
// Test: Can use interpolate
|
||||
const interpolated = bru.interpolate('{{host}}/ping');
|
||||
bru.setVar('interpolated-url', interpolated);
|
||||
|
||||
// Test: Can use getEnvName
|
||||
const envName = bru.getEnvName();
|
||||
bru.setVar('env-name', envName);
|
||||
|
||||
// Test: Can use getCollectionName
|
||||
const collectionName = bru.getCollectionName();
|
||||
bru.setVar('collection-name', collectionName);
|
||||
|
||||
console.log('[beforeRequest] All bru APIs tested');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should have set and retrieved var in hook", function() {
|
||||
const testVar = bru.getVar('test-var');
|
||||
expect(testVar).to.equal('test-value');
|
||||
});
|
||||
|
||||
test("should have set env var in hook", function() {
|
||||
const envVar = bru.getEnvVar('hook-set-env-var');
|
||||
expect(envVar).to.equal('env-value-from-hook');
|
||||
});
|
||||
|
||||
test("should have interpolated URL", function() {
|
||||
const interpolated = bru.getVar('interpolated-url');
|
||||
expect(interpolated).to.include('/ping');
|
||||
});
|
||||
|
||||
test("should have captured env name", function() {
|
||||
const envName = bru.getVar('env-name');
|
||||
expect(envName).to.be.a('string');
|
||||
});
|
||||
|
||||
test("should have captured collection name", function() {
|
||||
const collectionName = bru.getVar('collection-name');
|
||||
expect(collectionName).to.be.a('string');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
meta {
|
||||
name: multiple-before-request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
// First hook
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest 1] First hook executing');
|
||||
bru.setVar('hook-order', '1');
|
||||
req.setHeader('X-Hook-Order', '1');
|
||||
});
|
||||
|
||||
// Second hook
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest 2] Second hook executing');
|
||||
const order = bru.getVar('hook-order') || '';
|
||||
bru.setVar('hook-order', order + ',2');
|
||||
|
||||
const existingOrder = req.getHeader('X-Hook-Order');
|
||||
req.setHeader('X-Hook-Order', existingOrder + ',2');
|
||||
});
|
||||
|
||||
// Third hook (async)
|
||||
bru.hooks.http.onBeforeRequest(async({ req }) => {
|
||||
console.log('[beforeRequest 3] Third hook (async) executing');
|
||||
await bru.sleep(200);
|
||||
const order = bru.getVar('hook-order') || '';
|
||||
bru.setVar('hook-order', order + ',3');
|
||||
|
||||
const existingOrder = req.getHeader('X-Hook-Order');
|
||||
req.setHeader('X-Hook-Order', existingOrder + ',3');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("all beforeRequest hooks should execute in order", function() {
|
||||
const order = bru.getVar('hook-order');
|
||||
expect(order).to.equal('1,2,3');
|
||||
});
|
||||
|
||||
test("request should have headers from all hooks", function() {
|
||||
// Verify hooks executed sequentially
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
meta {
|
||||
name: sync-before-request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
// Test: Can access req object
|
||||
const url = req.getUrl();
|
||||
console.log('[beforeRequest] Request URL:', url);
|
||||
|
||||
// Test: Can modify request headers
|
||||
req.setHeader('X-Test-Header', 'sync-before-request');
|
||||
|
||||
// Test: Can use bru APIs
|
||||
bru.setVar('beforeRequest-executed', 'true');
|
||||
bru.setVar('request-url', url);
|
||||
|
||||
console.log('[beforeRequest] Hook executed successfully');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("beforeRequest hook should have executed", function() {
|
||||
const executed = bru.getVar('beforeRequest-executed');
|
||||
expect(executed).to.equal('true');
|
||||
});
|
||||
|
||||
test("should have access to request URL in hook", function() {
|
||||
const url = bru.getVar('request-url');
|
||||
expect(url).to.include('/ping');
|
||||
});
|
||||
|
||||
test("request should have custom header set by hook", function() {
|
||||
// Note: This would need to be verified via response if server echoes headers
|
||||
// For now, we verify the hook executed
|
||||
expect(true).to.be.true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
meta {
|
||||
name: all-hooks-together
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
// beforeRequest hook
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] Before request');
|
||||
bru.setVar('hook-execution-order', 'beforeRequest');
|
||||
});
|
||||
|
||||
// afterResponse hook
|
||||
bru.hooks.http.onAfterResponse(({ req, res }) => {
|
||||
console.log('[afterResponse] After response');
|
||||
const order = bru.getVar('hook-execution-order') || '';
|
||||
bru.setVar('hook-execution-order', order + ',afterResponse');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("request-level hooks should execute in correct order", function() {
|
||||
const order = bru.getVar('hook-execution-order');
|
||||
// Verify beforeRequest and afterResponse executed
|
||||
expect(order).to.include('beforeRequest');
|
||||
expect(order).to.include('afterResponse');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
meta {
|
||||
name: hook-chaining
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
// beforeRequest uses data (simulating data from onBeforeCollectionRun)
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] Using setup data');
|
||||
// Simulate token from collection-level setup
|
||||
const token = bru.getVar('test-token') || 'default-token';
|
||||
req.setHeader('X-Auth-Token', token);
|
||||
bru.setVar('token-used-in-beforeRequest', 'true');
|
||||
});
|
||||
|
||||
// afterResponse reads and stores response data
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
console.log('[afterResponse] Storing response data');
|
||||
const status = res.getStatus();
|
||||
bru.setVar('last-response-status', status.toString());
|
||||
bru.setVar('response-stored', 'true');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("beforeRequest should have used token", function() {
|
||||
const tokenUsed = bru.getVar('token-used-in-beforeRequest');
|
||||
expect(tokenUsed).to.equal('true');
|
||||
});
|
||||
|
||||
test("afterResponse should have stored response data", function() {
|
||||
const responseStored = bru.getVar('response-stored');
|
||||
expect(responseStored).to.equal('true');
|
||||
});
|
||||
|
||||
test("should have stored last response status", function() {
|
||||
const lastStatus = bru.getVar('last-response-status');
|
||||
expect(lastStatus).to.equal('200');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
meta {
|
||||
name: empty-hook-callback
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
// Test: Empty hook callback
|
||||
bru.hooks.http.onBeforeRequest(() => {
|
||||
// Empty callback - should not cause errors
|
||||
});
|
||||
|
||||
bru.hooks.http.onAfterResponse(() => {
|
||||
// Empty callback - should not cause errors
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("empty hook callbacks should not cause errors", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
meta {
|
||||
name: hook-with-no-data
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
// Test: Hook that doesn't use data parameter
|
||||
bru.hooks.http.onBeforeRequest(() => {
|
||||
// Don't use the data parameter, just use bru APIs
|
||||
bru.setVar('hook-executed-without-data', 'true');
|
||||
console.log('[beforeRequest] Hook executed without using data parameter');
|
||||
});
|
||||
|
||||
bru.hooks.http.onAfterResponse(() => {
|
||||
// Don't use the data parameter
|
||||
bru.setVar('afterResponse-executed-without-data', 'true');
|
||||
console.log('[afterResponse] Hook executed without using data parameter');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("hook should execute without using data parameter", function() {
|
||||
const executed = bru.getVar('hook-executed-without-data');
|
||||
expect(executed).to.equal('true');
|
||||
});
|
||||
|
||||
test("afterResponse should execute without using data parameter", function() {
|
||||
const executed = bru.getVar('afterResponse-executed-without-data');
|
||||
expect(executed).to.equal('true');
|
||||
});
|
||||
|
||||
test("request should execute successfully", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
meta {
|
||||
name: error-in-async-hook
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(async () => {
|
||||
console.log('[beforeRequest] About to throw error in async hook');
|
||||
await bru.sleep(500);
|
||||
|
||||
// Intentionally throw an error to test error handling
|
||||
try {
|
||||
throw new Error('Test error in async hook');
|
||||
} catch (error) {
|
||||
bru.setVar('async-error-caught-in-hook', 'true');
|
||||
bru.setVar('async-error-message', error.message);
|
||||
console.log('[beforeRequest] Async error caught:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
bru.hooks.http.onAfterResponse(async () => {
|
||||
await bru.sleep(200);
|
||||
// This should still execute even if beforeRequest had an error
|
||||
bru.setVar('async-afterResponse-executed', 'true');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("async error should have been caught in hook", function() {
|
||||
const errorCaught = bru.getVar('async-error-caught-in-hook');
|
||||
expect(errorCaught).to.equal('true');
|
||||
});
|
||||
|
||||
test("async afterResponse should execute even if beforeRequest had error", function() {
|
||||
const afterResponseExecuted = bru.getVar('async-afterResponse-executed');
|
||||
expect(afterResponseExecuted).to.equal('true');
|
||||
});
|
||||
|
||||
test("request should still execute", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
meta {
|
||||
name: error-in-sync-hook
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(() => {
|
||||
console.log('[beforeRequest] About to throw error');
|
||||
// Intentionally throw an error to test error handling
|
||||
try {
|
||||
throw new Error('Test error in sync hook');
|
||||
} catch (error) {
|
||||
bru.setVar('error-caught-in-hook', 'true');
|
||||
bru.setVar('error-message', error.message);
|
||||
console.log('[beforeRequest] Error caught:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
bru.hooks.http.onAfterResponse(() => {
|
||||
// This should still execute even if beforeRequest had an error
|
||||
bru.setVar('afterResponse-executed', 'true');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("error should have been caught in hook", function() {
|
||||
const errorCaught = bru.getVar('error-caught-in-hook');
|
||||
expect(errorCaught).to.equal('true');
|
||||
});
|
||||
|
||||
test("afterResponse should execute even if beforeRequest had error", function() {
|
||||
const afterResponseExecuted = bru.getVar('afterResponse-executed');
|
||||
expect(afterResponseExecuted).to.equal('true');
|
||||
});
|
||||
|
||||
test("request should still execute", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: sync-collection-run-end
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("request should execute successfully", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
meta {
|
||||
name: sync-collection-run-start
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("onBeforeCollectionRun hook should have executed", function() {
|
||||
const started = bru.getVar('collection-run-started');
|
||||
expect(started).to.equal('true');
|
||||
});
|
||||
|
||||
test("should have set start time", function() {
|
||||
const startTime = bru.getVar('start-time');
|
||||
expect(parseInt(startTime)).to.be.a('number');
|
||||
});
|
||||
|
||||
test("should have set env var in onBeforeCollectionRun", function() {
|
||||
const setupComplete = bru.getEnvVar('collection-setup-complete');
|
||||
expect(setupComplete).to.equal('true');
|
||||
});
|
||||
|
||||
test("should have captured env name at start", function() {
|
||||
const envName = bru.getVar('env-name-at-start');
|
||||
expect(envName).to.be.a('string');
|
||||
});
|
||||
|
||||
test("should have captured collection name at start", function() {
|
||||
const collectionName = bru.getVar('collection-name-at-start');
|
||||
expect(collectionName).to.be.a('string');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"node_modules",
|
||||
".git"
|
||||
],
|
||||
"size": 0.001827239990234375,
|
||||
"filesCount": 10,
|
||||
"size": 0.0006170272827148438,
|
||||
"filesCount": 5,
|
||||
"protobuf": {
|
||||
"protoFiles": [
|
||||
{
|
||||
|
||||
36
tests/scripting/hooks/hooks.spec.ts
Normal file
36
tests/scripting/hooks/hooks.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { test } from '../../../playwright';
|
||||
import { setSandboxMode, runCollection, validateRunnerResults } from '../../utils/page';
|
||||
|
||||
test.describe.serial('Hooks feature', () => {
|
||||
test.describe('developer mode', () => {
|
||||
test('should execute all hooks comprehensively', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(5 * 60 * 1000);
|
||||
|
||||
await setSandboxMode(page, 'hooks-comprehensive-tests', 'developer');
|
||||
await runCollection(page, 'hooks-comprehensive-tests');
|
||||
|
||||
await validateRunnerResults(page, {
|
||||
totalRequests: 41,
|
||||
passed: 41,
|
||||
failed: 0,
|
||||
skipped: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('safe mode', () => {
|
||||
test('should execute all hooks comprehensively', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(5 * 60 * 1000);
|
||||
|
||||
await setSandboxMode(page, 'hooks-comprehensive-tests', 'safe');
|
||||
await runCollection(page, 'hooks-comprehensive-tests');
|
||||
|
||||
await validateRunnerResults(page, {
|
||||
totalRequests: 41,
|
||||
passed: 41,
|
||||
failed: 0,
|
||||
skipped: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
6
tests/scripting/hooks/init-user-data/preferences.json
Normal file
6
tests/scripting/hooks/init-user-data/preferences.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/packages/bruno-tests/hooks-comprehensive-tests"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"pathname": "{{projectRoot}}/packages/bruno-tests/hooks-comprehensive-tests",
|
||||
"selectedEnvironment": "Prod"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user