mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-04 18:08:32 +00:00
Compare commits
26 Commits
dependabot
...
feat/hooks
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc09a26fa6 | ||
|
|
521f7332fd | ||
|
|
3414c4fe2e | ||
|
|
5b9511c50d | ||
|
|
63e5fbab36 | ||
|
|
5e03846da6 | ||
|
|
2a585b0e62 | ||
|
|
68bd6d5303 | ||
|
|
60593575e3 | ||
|
|
5851693529 | ||
|
|
57351b74ec | ||
|
|
10408a344a | ||
|
|
9b40dd4551 | ||
|
|
b9d1b43042 | ||
|
|
b82f068c8c | ||
|
|
ef2498a898 | ||
|
|
3d319855bc | ||
|
|
2c474e8052 | ||
|
|
cc33299702 | ||
|
|
777707180e | ||
|
|
34fcc5bbfb | ||
|
|
de43ad00d8 | ||
|
|
60d5d5e98d | ||
|
|
f6e2279fe3 | ||
|
|
80b4ab0561 | ||
|
|
4bb01ca0ac |
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);
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="flex flex-row gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={shouldDisableCollectionRun || (configureMode && selectedRequestItems.length === 0) || isCollectionLoading}
|
||||
disabled={shouldDisableCollectionRun || (configureMode && selectedRequestItems.length === 0) || isCollectionLoading || runnerInfo.status === 'started'}
|
||||
onClick={runCollection}
|
||||
>
|
||||
{configureMode && selectedRequestItems.length > 0
|
||||
|
||||
@@ -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');
|
||||
@@ -18,6 +18,9 @@ const constants = require('../constants');
|
||||
const { findItemInCollection, createCollectionJsonFromPathname, getCallStack, FORMAT_CONFIG } = require('../utils/collection');
|
||||
const { hasExecutableTestInScript } = require('../utils/request');
|
||||
const { createSkippedFileResults } = require('../utils/run');
|
||||
const { HooksRuntime, HookManager } = require('@usebruno/js');
|
||||
const HOOK_EVENTS = HookManager.EVENTS;
|
||||
const decomment = require('decomment');
|
||||
const command = 'run [paths...]';
|
||||
const desc = 'Run one or more requests/folders';
|
||||
|
||||
@@ -315,7 +318,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,7 +615,15 @@ const handler = async function (argv) {
|
||||
});
|
||||
|
||||
const runtime = getJsSandboxRuntime(sandbox);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = runtime;
|
||||
|
||||
const collectionName = collection?.brunoConfig?.name;
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
};
|
||||
|
||||
// Define runSingleRequestByPathname before hooks initialization so it's available at all hook levels
|
||||
const runSingleRequestByPathname = async (relativeItemPathname) => {
|
||||
const ext = FORMAT_CONFIG[collection.format].ext;
|
||||
return new Promise(async (resolve, reject) => {
|
||||
@@ -640,6 +651,42 @@ const handler = async function (argv) {
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize collection-level hooks once (evaluated once, reused for before/after events)
|
||||
let collectionHooksCtx = null;
|
||||
{
|
||||
collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionHooks = get(collectionRoot, 'request.script.hooks', '');
|
||||
|
||||
if (collectionHooks && collectionHooks.trim()) {
|
||||
try {
|
||||
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
|
||||
collectionHooksCtx = await hooksRuntime.runHooks({
|
||||
hooksFile: decomment(collectionHooks),
|
||||
request: {}, // Placeholder request for collection-level hooks
|
||||
envVariables: envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname: runSingleRequestByPathname,
|
||||
collectionName
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initializing collection-level hooks:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call onBeforeCollectionRun hook before starting to run requests
|
||||
if (collectionHooksCtx?.hookManager) {
|
||||
try {
|
||||
await collectionHooksCtx.hookManager.call(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection });
|
||||
} catch (error) {
|
||||
console.error('Error executing beforeCollectionRun hook:', error);
|
||||
}
|
||||
}
|
||||
|
||||
let currentRequestIndex = 0;
|
||||
let nJumps = 0; // count the number of jumps to avoid infinite loops
|
||||
while (currentRequestIndex < requestItems.length) {
|
||||
@@ -752,6 +799,17 @@ const handler = async function (argv) {
|
||||
const skippedFileResults = createSkippedFileResults(global.brunoSkippedFiles || [], collectionPath);
|
||||
results.push(...skippedFileResults);
|
||||
|
||||
// Call onAfterCollectionRun hook after all requests are done
|
||||
if (collectionHooksCtx?.hookManager) {
|
||||
try {
|
||||
await collectionHooksCtx.hookManager.call(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection });
|
||||
} catch (error) {
|
||||
console.error('Error executing afterCollectionRun hook:', error);
|
||||
}
|
||||
collectionHooksCtx.hookManager.dispose();
|
||||
collectionHooksCtx = null;
|
||||
}
|
||||
|
||||
const summary = printRunSummary(results);
|
||||
const runCompletionTime = new Date().toISOString();
|
||||
const totalTime = results.reduce((acc, res) => acc + res.response.responseTime, 0);
|
||||
|
||||
@@ -6,9 +6,11 @@ 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, HookManager, BrunoResponse } = require('@usebruno/js');
|
||||
const HOOK_EVENTS = HookManager.EVENTS;
|
||||
const { stripExtension } = require('../utils/filesystem');
|
||||
const { getOptions } = require('../utils/bru');
|
||||
const { getTreePathFromCollectionToItem } = require('../utils/collection');
|
||||
const https = require('https');
|
||||
const { HttpProxyAgent } = require('http-proxy-agent');
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
@@ -34,6 +36,71 @@ const getCACertHostRegex = (domain) => {
|
||||
return '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply runner control signals from a script/hook result
|
||||
* @param {object} result - Result from script/hook execution
|
||||
* @param {object} state - Current runner state (modified in place)
|
||||
* @param {string|undefined} state.nextRequestName - Current next request name
|
||||
* @param {boolean} state.shouldStopRunnerExecution - Current stop flag
|
||||
* @returns {object} Updated state with nextRequestName and shouldStopRunnerExecution
|
||||
*/
|
||||
const applyRunnerControlFromResult = (result, state) => {
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
state.nextRequestName = result.nextRequestName;
|
||||
}
|
||||
if (result?.stopExecution) {
|
||||
state.shouldStopRunnerExecution = true;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a standardized skipped response object
|
||||
* @param {object} options - Options for creating the response
|
||||
* @param {string} options.filename - The relative file pathname
|
||||
* @param {object} options.request - The request object
|
||||
* @param {string} options.statusText - The reason for skipping
|
||||
* @param {array} [options.preRequestTestResults] - Pre-request test results
|
||||
* @param {array} [options.postResponseTestResults] - Post-response test results
|
||||
* @param {string} [options.nextRequestName] - Next request name if set
|
||||
* @param {boolean} [options.shouldStopRunnerExecution] - Stop execution flag
|
||||
* @returns {object} Standardized skipped response object
|
||||
*/
|
||||
const createSkippedResponse = ({
|
||||
filename,
|
||||
request,
|
||||
statusText,
|
||||
preRequestTestResults = [],
|
||||
postResponseTestResults = [],
|
||||
nextRequestName,
|
||||
shouldStopRunnerExecution = false
|
||||
}) => {
|
||||
return {
|
||||
test: { filename },
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
data: request.data
|
||||
},
|
||||
response: {
|
||||
status: 'skipped',
|
||||
statusText,
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: null,
|
||||
status: 'skipped',
|
||||
skipped: true,
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
preRequestTestResults,
|
||||
postResponseTestResults,
|
||||
nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract prompt variables from a request
|
||||
* Tries to respect the hierarchy of the variables and avoid unnecessary prompts as much as possible
|
||||
@@ -132,31 +199,12 @@ const runSingleRequest = async function (
|
||||
if (promptVars.length > 0) {
|
||||
const errorMsg = `Prompt variables detected in request. CLI execution is not supported for requests with prompt variables. \nPrompts: ${promptVars.join(', ')}`;
|
||||
console.log(chalk.yellow(stripExtension(relativeItemPathname) + ' Skipped:') + chalk.dim(` (${errorMsg})`));
|
||||
return {
|
||||
test: {
|
||||
filename: relativeItemPathname
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
data: request.data
|
||||
},
|
||||
response: {
|
||||
status: 'skipped',
|
||||
statusText: errorMsg,
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: null,
|
||||
status: 'skipped',
|
||||
skipped: true,
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
preRequestTestResults: [],
|
||||
postResponseTestResults: [],
|
||||
return createSkippedResponse({
|
||||
filename: relativeItemPathname,
|
||||
request,
|
||||
statusText: errorMsg,
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
request.__bruno__executionMode = 'cli';
|
||||
@@ -164,9 +212,90 @@ const runSingleRequest = async function (
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = runtime;
|
||||
|
||||
// Get request tree path for hook execution
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
const collectionName = collection?.brunoConfig?.name;
|
||||
|
||||
// Initialize hooks once for this request lifecycle (reused for beforeRequest + afterResponse)
|
||||
let hooksCtx = null;
|
||||
const hooksFile = request.script?.hooks;
|
||||
if (hooksFile && hooksFile.trim()) {
|
||||
try {
|
||||
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
|
||||
hooksCtx = await hooksRuntime.runHooks({
|
||||
hooksFile,
|
||||
request,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname: runSingleRequestByPathname,
|
||||
collectionName
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initializing hooks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a specific hook event using the initialized hooks context.
|
||||
* Re-reads runner control signals from bru instance after handlers execute.
|
||||
*/
|
||||
const triggerHookEvent = async (hookEvent, eventData) => {
|
||||
if (!hooksCtx?.hookManager) return null;
|
||||
|
||||
try {
|
||||
const enrichedEventData = {
|
||||
...eventData,
|
||||
req: hooksCtx.req || eventData.req,
|
||||
res: eventData.response ? new BrunoResponse(eventData.response) : (hooksCtx.res || eventData.res)
|
||||
};
|
||||
await hooksCtx.hookManager.call(hookEvent, enrichedEventData);
|
||||
|
||||
// Re-read runner control signals from bru instance AFTER handlers have executed
|
||||
const bru = hooksCtx.__bru;
|
||||
if (bru) {
|
||||
hooksCtx.nextRequestName = bru.nextRequest;
|
||||
hooksCtx.skipRequest = bru.skipRequest;
|
||||
hooksCtx.stopExecution = bru.stopExecution;
|
||||
}
|
||||
|
||||
return hooksCtx;
|
||||
} catch (error) {
|
||||
console.error(`Error executing hooks for ${hookEvent}:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Call beforeRequest hooks before running pre-request scripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
const beforeRequestEventData = { request, collection };
|
||||
|
||||
const beforeRequestHooksResult = await triggerHookEvent(
|
||||
HOOK_EVENTS.HTTP_BEFORE_REQUEST,
|
||||
beforeRequestEventData
|
||||
);
|
||||
|
||||
// Check runner control from hooks
|
||||
const runnerState = { nextRequestName, shouldStopRunnerExecution };
|
||||
applyRunnerControlFromResult(beforeRequestHooksResult, runnerState);
|
||||
nextRequestName = runnerState.nextRequestName;
|
||||
shouldStopRunnerExecution = runnerState.shouldStopRunnerExecution;
|
||||
|
||||
if (beforeRequestHooksResult?.skipRequest) {
|
||||
return createSkippedResponse({
|
||||
filename: relativeItemPathname,
|
||||
request,
|
||||
statusText: 'request skipped via beforeRequest hook',
|
||||
nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
});
|
||||
}
|
||||
|
||||
// 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(
|
||||
@@ -181,40 +310,18 @@ const runSingleRequest = async function (
|
||||
runSingleRequestByPathname,
|
||||
collectionName
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
applyRunnerControlFromResult(result, runnerState);
|
||||
nextRequestName = runnerState.nextRequestName;
|
||||
shouldStopRunnerExecution = runnerState.shouldStopRunnerExecution;
|
||||
|
||||
if (result?.skipRequest) {
|
||||
return {
|
||||
test: {
|
||||
filename: relativeItemPathname
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
data: request.data
|
||||
},
|
||||
response: {
|
||||
status: 'skipped',
|
||||
statusText: 'request skipped via pre-request script',
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: null,
|
||||
status: 'skipped',
|
||||
skipped: true,
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
return createSkippedResponse({
|
||||
filename: relativeItemPathname,
|
||||
request,
|
||||
statusText: 'request skipped via pre-request script',
|
||||
preRequestTestResults: result?.results || [],
|
||||
postResponseTestResults: [],
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
preRequestTestResults = result?.results || [];
|
||||
@@ -639,6 +746,25 @@ 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
|
||||
const afterResponseEventData = { request, response, collection };
|
||||
|
||||
const afterResponseHooksResult = await triggerHookEvent(
|
||||
HOOK_EVENTS.HTTP_AFTER_RESPONSE,
|
||||
afterResponseEventData
|
||||
);
|
||||
|
||||
// Dispose hooks context — done with all events for this request
|
||||
if (hooksCtx?.hookManager) {
|
||||
hooksCtx.hookManager.dispose();
|
||||
}
|
||||
|
||||
// Check runner control from hooks
|
||||
applyRunnerControlFromResult(afterResponseHooksResult, runnerState);
|
||||
nextRequestName = runnerState.nextRequestName;
|
||||
shouldStopRunnerExecution = runnerState.shouldStopRunnerExecution;
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(item, 'request.vars.res');
|
||||
if (postResponseVars?.length) {
|
||||
@@ -672,13 +798,9 @@ const runSingleRequest = async function (
|
||||
runSingleRequestByPathname,
|
||||
collectionName
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
applyRunnerControlFromResult(result, runnerState);
|
||||
nextRequestName = runnerState.nextRequestName;
|
||||
shouldStopRunnerExecution = runnerState.shouldStopRunnerExecution;
|
||||
|
||||
postResponseTestResults = result?.results || [];
|
||||
logResults(postResponseTestResults, 'Post-Response Tests');
|
||||
@@ -722,13 +844,9 @@ const runSingleRequest = async function (
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
applyRunnerControlFromResult(result, runnerState);
|
||||
nextRequestName = runnerState.nextRequestName;
|
||||
shouldStopRunnerExecution = runnerState.shouldStopRunnerExecution;
|
||||
|
||||
logResults(testResults, 'Tests');
|
||||
} catch (error) {
|
||||
|
||||
@@ -231,10 +231,12 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
let collectionPreReqScript = get(collectionRoot, 'request.script.req', '');
|
||||
let collectionPostResScript = get(collectionRoot, 'request.script.res', '');
|
||||
let collectionTests = get(collectionRoot, 'request.tests', '');
|
||||
let collectionHooks = get(collectionRoot, 'request.script.hooks', '');
|
||||
|
||||
let combinedPreReqScript = [];
|
||||
let combinedPostResScript = [];
|
||||
let combinedTests = [];
|
||||
let combinedHooks = [];
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
@@ -252,6 +254,11 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
if (tests && tests?.trim?.() !== '') {
|
||||
combinedTests.push(tests);
|
||||
}
|
||||
|
||||
let hooks = get(folderRoot, 'request.script.hooks', '');
|
||||
if (hooks && hooks.trim() !== '') {
|
||||
combinedHooks.push(hooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +308,21 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
];
|
||||
request.tests = compact(testScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
}
|
||||
|
||||
// Handle hooks - always merged sequentially: collection -> folders -> request
|
||||
let requestHooks = request?.script?.hooks || '';
|
||||
const hooksScripts = [
|
||||
collectionHooks,
|
||||
...combinedHooks,
|
||||
requestHooks
|
||||
];
|
||||
|
||||
// Ensure request.script exists
|
||||
if (!request.script) {
|
||||
request.script = {};
|
||||
}
|
||||
|
||||
request.script.hooks = compact(hooksScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
};
|
||||
|
||||
const findItem = (items = [], pathname) => {
|
||||
|
||||
@@ -8,7 +8,8 @@ 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, HookManager, BrunoResponse } = require('@usebruno/js');
|
||||
const HOOK_EVENTS = HookManager.EVENTS;
|
||||
const { encodeUrl } = require('@usebruno/common').utils;
|
||||
const { extractPromptVariables } = require('@usebruno/common').utils;
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
@@ -443,8 +444,141 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
};
|
||||
|
||||
const runPreRequest = async (
|
||||
request,
|
||||
/**
|
||||
* Send script environment updates to the renderer
|
||||
* This is a reusable helper for updating the UI after script/hook execution
|
||||
* @param {object} options - Update options
|
||||
* @param {object} options.scriptResult - The result from script/hook execution
|
||||
* @param {object} options.collection - The collection object
|
||||
* @param {string} options.collectionUid - The collection UID
|
||||
* @param {string} [options.requestUid] - The request UID (optional, not needed for collection-level hooks)
|
||||
* @param {boolean} [options.updateCookies=true] - Whether to update cookies
|
||||
*/
|
||||
const sendScriptEnvironmentUpdates = async ({
|
||||
scriptResult,
|
||||
collection,
|
||||
collectionUid,
|
||||
requestUid,
|
||||
updateCookies = true
|
||||
}) => {
|
||||
if (!scriptResult) return;
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: scriptResult.envVariables,
|
||||
runtimeVariables: scriptResult.runtimeVariables,
|
||||
persistentEnvVariables: scriptResult.persistentEnvVariables,
|
||||
...(requestUid && { requestUid }),
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:persistent-env-variables-update', {
|
||||
persistentEnvVariables: scriptResult.persistentEnvVariables,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
if (scriptResult.globalEnvironmentVariables) {
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
}
|
||||
|
||||
if (updateCookies) {
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize hooks by evaluating the hooks file once.
|
||||
* Returns context for subsequent event triggering via triggerHookEvent().
|
||||
* Caller must dispose hooksCtx.hookManager when done with all events.
|
||||
* @param {object} options - Configuration options
|
||||
* @returns {Promise<object|null>} Hooks context or null if no hooks/error
|
||||
*/
|
||||
const initHooks = async (options) => {
|
||||
const hooksFile = options.request?.script?.hooks;
|
||||
if (!hooksFile || !hooksFile.trim()) return null;
|
||||
|
||||
try {
|
||||
const hooksRuntime = new HooksRuntime({ runtime: options.scriptingConfig?.runtime });
|
||||
const result = await hooksRuntime.runHooks({
|
||||
hooksFile,
|
||||
request: options.request || {},
|
||||
envVariables: options.envVars,
|
||||
runtimeVariables: options.runtimeVariables,
|
||||
collectionPath: options.collectionPath,
|
||||
onConsoleLog: options.onConsoleLog,
|
||||
processEnvVars: options.processEnvVars,
|
||||
scriptingConfig: options.scriptingConfig,
|
||||
runRequestByItemPathname: options.runRequestByItemPathname,
|
||||
collectionName: options.collectionName
|
||||
});
|
||||
|
||||
if (result) {
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: result,
|
||||
collection: options.collection,
|
||||
collectionUid: options.collectionUid,
|
||||
requestUid: options.requestUid,
|
||||
updateCookies: true
|
||||
});
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error initializing hooks:', error);
|
||||
options.onConsoleLog?.('error', [`Error initializing hooks: ${error.message}`]);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger a specific hook event using an already-initialized hooks context.
|
||||
* Re-reads runner control signals from bru instance after handlers execute.
|
||||
* @param {object} hooksCtx - Context from initHooks()
|
||||
* @param {string} hookEvent - Hook event to trigger
|
||||
* @param {object} eventData - Data to pass to hook handlers
|
||||
* @param {object} options - Configuration options
|
||||
* @returns {Promise<object|null>} Updated hooks context or null if error
|
||||
*/
|
||||
const triggerHookEvent = async (hooksCtx, hookEvent, eventData, options) => {
|
||||
if (!hooksCtx?.hookManager) return null;
|
||||
|
||||
try {
|
||||
const enrichedEventData = {
|
||||
...eventData,
|
||||
req: hooksCtx.req || eventData.req,
|
||||
res: eventData.response ? new BrunoResponse(eventData.response) : (hooksCtx.res || eventData.res)
|
||||
};
|
||||
await hooksCtx.hookManager.call(hookEvent, enrichedEventData);
|
||||
|
||||
// Re-read runner control signals from bru instance AFTER handlers have executed
|
||||
// Handlers may have called bru.runner.setNextRequest(), skipRequest(), or stopExecution()
|
||||
const bru = hooksCtx.__bru;
|
||||
if (bru) {
|
||||
hooksCtx.nextRequestName = bru.nextRequest;
|
||||
hooksCtx.skipRequest = bru.skipRequest;
|
||||
hooksCtx.stopExecution = bru.stopExecution;
|
||||
}
|
||||
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: hooksCtx,
|
||||
collection: options.collection,
|
||||
collectionUid: options.collectionUid,
|
||||
requestUid: options.requestUid,
|
||||
updateCookies: true
|
||||
});
|
||||
|
||||
return hooksCtx;
|
||||
} catch (error) {
|
||||
console.error(`Error executing hooks for ${hookEvent}:`, error);
|
||||
options.onConsoleLog?.('error', [`Error executing hooks for ${hookEvent}: ${error.message}`]);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const runPreRequest = async (request,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
@@ -475,27 +609,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionName
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: scriptResult.envVariables,
|
||||
runtimeVariables: scriptResult.runtimeVariables,
|
||||
persistentEnvVariables: scriptResult.persistentEnvVariables,
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult,
|
||||
collection,
|
||||
collectionUid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
updateCookies: true
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:persistent-env-variables-update', {
|
||||
persistentEnvVariables: scriptResult.persistentEnvVariables,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
@@ -564,24 +684,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
);
|
||||
|
||||
if (result) {
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: result.envVariables,
|
||||
runtimeVariables: result.runtimeVariables,
|
||||
persistentEnvVariables: result.persistentEnvVariables,
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: result,
|
||||
collection,
|
||||
collectionUid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
updateCookies: false
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:persistent-env-variables-update', {
|
||||
persistentEnvVariables: result.persistentEnvVariables,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: result.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = result.globalEnvironmentVariables;
|
||||
}
|
||||
|
||||
if (result?.error) {
|
||||
@@ -609,27 +718,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionName
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: scriptResult.envVariables,
|
||||
runtimeVariables: scriptResult.runtimeVariables,
|
||||
persistentEnvVariables: scriptResult.persistentEnvVariables,
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult,
|
||||
collection,
|
||||
collectionUid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
updateCookies: true
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:persistent-env-variables-update', {
|
||||
persistentEnvVariables: scriptResult.persistentEnvVariables,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
|
||||
const domainsWithCookiesPost = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPost)));
|
||||
}
|
||||
return scriptResult;
|
||||
};
|
||||
@@ -676,10 +771,40 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
|
||||
// Get request tree path for hooks execution
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
try {
|
||||
request.signal = abortController.signal;
|
||||
saveCancelToken(cancelTokenUid, abortController);
|
||||
|
||||
// Initialize hooks once for this request lifecycle
|
||||
// Hooks are merged in prepareRequest via mergeScripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
const hookOptions = {
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName: collection?.name,
|
||||
collection,
|
||||
requestUid,
|
||||
itemUid: item.uid,
|
||||
collectionUid,
|
||||
runInBackground,
|
||||
notifyScriptExecution
|
||||
};
|
||||
|
||||
const hooksCtx = await initHooks(hookOptions);
|
||||
|
||||
// Call beforeRequest hooks using initialized hooks context
|
||||
const beforeRequestEventData = { request, collection, collectionUid };
|
||||
await triggerHookEvent(hooksCtx, HOOK_EVENTS.HTTP_BEFORE_REQUEST, beforeRequestEventData, hookOptions);
|
||||
|
||||
let preRequestScriptResult = null;
|
||||
let preRequestError = null;
|
||||
try {
|
||||
@@ -844,6 +969,18 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
cookiesStore.saveCookieJar();
|
||||
|
||||
// Call afterResponse hooks after response is received but before post-response scripts
|
||||
// Hooks are called in registration order: collection -> folder(s) -> request
|
||||
const afterResponseEventData = { request, response, collection, collectionUid };
|
||||
|
||||
// Call afterResponse hooks using already-initialized hooks context
|
||||
await triggerHookEvent(hooksCtx, HOOK_EVENTS.HTTP_AFTER_RESPONSE, afterResponseEventData, hookOptions);
|
||||
|
||||
// Dispose hooks context — done with all events for this request
|
||||
if (hooksCtx?.hookManager) {
|
||||
hooksCtx.hookManager.dispose();
|
||||
}
|
||||
|
||||
const runPostScripts = async () => {
|
||||
let postResponseScriptResult = null;
|
||||
let postResponseError = null;
|
||||
@@ -945,23 +1082,14 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
runtimeVariables: testResults.runtimeVariables,
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: testResults,
|
||||
collection,
|
||||
collectionUid,
|
||||
requestUid,
|
||||
collectionUid
|
||||
updateCookies: true
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:persistent-env-variables-update', {
|
||||
persistentEnvVariables: testResults.persistentEnvVariables,
|
||||
collectionUid
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: testResults.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
cookiesStore.saveCookieJar();
|
||||
|
||||
!runInBackground && notifyScriptExecution({
|
||||
channel: 'main:run-request-event',
|
||||
@@ -969,10 +1097,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
scriptType: 'test',
|
||||
error: testError
|
||||
});
|
||||
|
||||
const domainsWithCookiesTest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest)));
|
||||
cookiesStore.saveCookieJar();
|
||||
}
|
||||
};
|
||||
if (isResponseStream) {
|
||||
@@ -1171,6 +1295,65 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
const isCollectionRun = folder?.uid === collection?.uid;
|
||||
|
||||
// Initialize collection-level hooks once (evaluated once, reused for before/after events)
|
||||
let collectionHooksCtx = null;
|
||||
if (isCollectionRun) {
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionHooks = get(collectionRoot, 'request.script.hooks', '');
|
||||
|
||||
if (collectionHooks && collectionHooks.trim()) {
|
||||
try {
|
||||
const hooksRuntime = new HooksRuntime({ runtime: scriptingConfig?.runtime });
|
||||
collectionHooksCtx = await hooksRuntime.runHooks({
|
||||
hooksFile: decomment(collectionHooks),
|
||||
request: {}, // Placeholder request for collection-level hooks
|
||||
envVariables: envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
onConsoleLog: (type, args) => {
|
||||
console[type](...args);
|
||||
mainWindow.webContents.send('main:console-log', {
|
||||
type,
|
||||
args
|
||||
});
|
||||
},
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName: collection?.name
|
||||
});
|
||||
|
||||
if (collectionHooksCtx) {
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: collectionHooksCtx,
|
||||
collection,
|
||||
collectionUid,
|
||||
updateCookies: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing collection-level hooks:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call onBeforeCollectionRun hook before starting to run requests
|
||||
if (collectionHooksCtx?.hookManager) {
|
||||
try {
|
||||
await collectionHooksCtx.hookManager.call(HOOK_EVENTS.RUNNER_BEFORE_COLLECTION_RUN, { collection, collectionUid });
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: collectionHooksCtx,
|
||||
collection,
|
||||
collectionUid,
|
||||
updateCookies: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error executing beforeCollectionRun hook:', error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let folderRequests = [];
|
||||
|
||||
@@ -1216,6 +1399,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 +1471,77 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get request tree path for hooks execution (hooks are merged in prepareRequest via mergeScripts)
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
// Hook execution options
|
||||
const hookOptions = {
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname,
|
||||
collectionName: collection?.name,
|
||||
collection,
|
||||
onConsoleLog: (type, args) => {
|
||||
console[type](...args);
|
||||
mainWindow.webContents.send('main:console-log', {
|
||||
type,
|
||||
args
|
||||
});
|
||||
},
|
||||
requestUid,
|
||||
itemUid: item.uid,
|
||||
collectionUid,
|
||||
runInBackground: false,
|
||||
notifyScriptExecution: (notification) => {
|
||||
notifyScriptExecution({
|
||||
...notification,
|
||||
basePayload: eventData
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize hooks once per request iteration (reused for beforeRequest + afterResponse)
|
||||
const hooksCtx = await initHooks(hookOptions);
|
||||
|
||||
try {
|
||||
// Call beforeRequest hooks using initialized hooks context
|
||||
const beforeRequestEventData = { request, collection, collectionUid };
|
||||
|
||||
const beforeRequestHooksResult = await triggerHookEvent(
|
||||
hooksCtx,
|
||||
HOOK_EVENTS.HTTP_BEFORE_REQUEST,
|
||||
beforeRequestEventData,
|
||||
hookOptions
|
||||
);
|
||||
|
||||
// Check runner control from hooks
|
||||
if (beforeRequestHooksResult?.nextRequestName !== undefined) {
|
||||
nextRequestName = beforeRequestHooksResult.nextRequestName;
|
||||
}
|
||||
if (beforeRequestHooksResult?.stopExecution) {
|
||||
stopRunnerExecution = true;
|
||||
}
|
||||
if (beforeRequestHooksResult?.skipRequest) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'runner-request-skipped',
|
||||
error: 'Request has been skipped from beforeRequest hook',
|
||||
responseReceived: {
|
||||
status: 'skipped',
|
||||
statusText: 'request skipped via beforeRequest hook',
|
||||
data: null,
|
||||
responseTime: 0,
|
||||
headers: null
|
||||
},
|
||||
...eventData
|
||||
});
|
||||
currentRequestIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
let preRequestScriptResult;
|
||||
let preRequestError = null;
|
||||
try {
|
||||
@@ -1509,6 +1763,29 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Call afterResponse hooks using already-initialized hooks context
|
||||
const afterResponseEventData = { request, response, collection, collectionUid };
|
||||
|
||||
const afterResponseHooksResult = await triggerHookEvent(
|
||||
hooksCtx,
|
||||
HOOK_EVENTS.HTTP_AFTER_RESPONSE,
|
||||
afterResponseEventData,
|
||||
hookOptions
|
||||
);
|
||||
|
||||
// Dispose hooks context — done with all events for this request
|
||||
if (hooksCtx?.hookManager) {
|
||||
hooksCtx.hookManager.dispose();
|
||||
}
|
||||
|
||||
// Check runner control from hooks
|
||||
if (afterResponseHooksResult?.nextRequestName !== undefined) {
|
||||
nextRequestName = afterResponseHooksResult.nextRequestName;
|
||||
}
|
||||
if (afterResponseHooksResult?.stopExecution) {
|
||||
stopRunnerExecution = true;
|
||||
}
|
||||
|
||||
let postResponseScriptResult;
|
||||
let postResponseError = null;
|
||||
try {
|
||||
@@ -1626,27 +1903,20 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
...eventData
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
envVariables: testResults.envVariables,
|
||||
runtimeVariables: testResults.runtimeVariables,
|
||||
collectionUid
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: testResults,
|
||||
collection,
|
||||
collectionUid,
|
||||
requestUid: undefined,
|
||||
updateCookies: true
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: testResults.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
|
||||
|
||||
notifyScriptExecution({
|
||||
channel: 'main:run-folder-event',
|
||||
basePayload: eventData,
|
||||
scriptType: 'test',
|
||||
error: testError
|
||||
});
|
||||
|
||||
const domainsWithCookiesTest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest)));
|
||||
}
|
||||
} catch (error) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
@@ -1689,6 +1959,23 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Call onAfterCollectionRun hook after all requests are done
|
||||
if (collectionHooksCtx?.hookManager) {
|
||||
try {
|
||||
await collectionHooksCtx.hookManager.call(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection, collectionUid });
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: collectionHooksCtx,
|
||||
collection,
|
||||
collectionUid,
|
||||
updateCookies: true
|
||||
});
|
||||
} catch (hookError) {
|
||||
console.error('Error executing afterCollectionRun hook:', hookError);
|
||||
}
|
||||
collectionHooksCtx.hookManager.dispose();
|
||||
collectionHooksCtx = null;
|
||||
}
|
||||
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'testrun-ended',
|
||||
@@ -1698,6 +1985,24 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
|
||||
// Call onAfterCollectionRun hook even on error
|
||||
if (collectionHooksCtx?.hookManager) {
|
||||
try {
|
||||
await collectionHooksCtx.hookManager.call(HOOK_EVENTS.RUNNER_AFTER_COLLECTION_RUN, { collection, collectionUid });
|
||||
await sendScriptEnvironmentUpdates({
|
||||
scriptResult: collectionHooksCtx,
|
||||
collection,
|
||||
collectionUid,
|
||||
updateCookies: true
|
||||
});
|
||||
} catch (hookError) {
|
||||
console.error('Error executing afterCollectionRun hook:', hookError);
|
||||
}
|
||||
collectionHooksCtx.hookManager.dispose();
|
||||
collectionHooksCtx = null;
|
||||
}
|
||||
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'testrun-ended',
|
||||
|
||||
@@ -155,10 +155,12 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
let collectionPreReqScript = get(collectionRoot, 'request.script.req', '');
|
||||
let collectionPostResScript = get(collectionRoot, 'request.script.res', '');
|
||||
let collectionTests = get(collectionRoot, 'request.tests', '');
|
||||
let collectionHooks = get(collectionRoot, 'request.script.hooks', '');
|
||||
|
||||
let combinedPreReqScript = [];
|
||||
let combinedPostResScript = [];
|
||||
let combinedTests = [];
|
||||
let combinedHooks = [];
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
@@ -176,6 +178,11 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
if (tests && tests?.trim?.() !== '') {
|
||||
combinedTests.push(tests);
|
||||
}
|
||||
|
||||
let hooks = get(folderRoot, 'request.script.hooks', '');
|
||||
if (hooks && hooks.trim() !== '') {
|
||||
combinedHooks.push(hooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +232,21 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
];
|
||||
request.tests = compact(testScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
}
|
||||
|
||||
// Handle hooks - always merged sequentially: collection -> folders -> request
|
||||
let requestHooks = request?.script?.hooks || '';
|
||||
const hooksScripts = [
|
||||
collectionHooks,
|
||||
...combinedHooks,
|
||||
requestHooks
|
||||
];
|
||||
|
||||
// Ensure request.script exists
|
||||
if (!request.script) {
|
||||
request.script = {};
|
||||
}
|
||||
|
||||
request.script.hooks = compact(hooksScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
};
|
||||
|
||||
const flattenItems = (items = []) => {
|
||||
|
||||
@@ -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,44 @@ 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) => {
|
||||
return this.#hookManager.on('runner:beforeCollectionRun', handler);
|
||||
},
|
||||
onAfterCollectionRun: (handler) => {
|
||||
return this.#hookManager.on('runner:afterCollectionRun', handler);
|
||||
}
|
||||
},
|
||||
http: {
|
||||
onBeforeRequest: (handler) => {
|
||||
return this.#hookManager.on('http:beforeRequest', handler);
|
||||
},
|
||||
onAfterResponse: (handler) => {
|
||||
return this.#hookManager.on('http:afterResponse', handler);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bru;
|
||||
|
||||
237
packages/bruno-js/src/hook-manager.js
Normal file
237
packages/bruno-js/src/hook-manager.js
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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 });
|
||||
*
|
||||
* Error Handling:
|
||||
* - By default, errors in one handler don't stop other handlers from running
|
||||
* - Errors are logged to console for debugging
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
class HookManager {
|
||||
/**
|
||||
* HookManager lifecycle states
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
static State = Object.freeze({
|
||||
ACTIVE: 'active',
|
||||
DISPOSED: 'disposed'
|
||||
});
|
||||
|
||||
/**
|
||||
* Standard hook event names used throughout the application.
|
||||
* @readonly
|
||||
*/
|
||||
static 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'
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
this._state = HookManager.State.ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current state of the HookManager
|
||||
* @returns {string} Current state ('active' or 'disposed')
|
||||
*/
|
||||
get state() {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the HookManager is disposed
|
||||
* @returns {boolean} True if disposed
|
||||
*/
|
||||
get isDisposed() {
|
||||
return this._state === HookManager.State.DISPOSED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of all resources held by this HookManager
|
||||
* Clears all handlers and marks the manager as disposed
|
||||
* Should be called when the HookManager is no longer needed
|
||||
*/
|
||||
dispose() {
|
||||
if (this._state === HookManager.State.DISPOSED) {
|
||||
return;
|
||||
}
|
||||
this._state = HookManager.State.DISPOSED;
|
||||
|
||||
// Clear all listeners
|
||||
this.clearAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the HookManager is in a valid state for the operation
|
||||
* @private
|
||||
* @param {string} operation - Name of the operation being performed
|
||||
* @throws {Error} If HookManager is disposed
|
||||
*/
|
||||
_validateState(operation) {
|
||||
if (this._state === HookManager.State.DISPOSED) {
|
||||
throw new Error(`Cannot ${operation}: HookManager has been disposed`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call all registered handlers for the given pattern(s)
|
||||
* Supports both sync and async handlers - all handlers are awaited
|
||||
* Error Isolation: Errors in one handler don't stop other handlers from running.
|
||||
* Errors are logged to console but don't affect execution flow.
|
||||
*
|
||||
* @param {string|string[]} pattern - Event pattern(s) to trigger
|
||||
* @param {*} data - Data to pass to handlers
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async call(pattern, data) {
|
||||
// Validate state - but allow calls on disposed manager to fail gracefully
|
||||
if (this._state === HookManager.State.DISPOSED) {
|
||||
console.warn('HookManager.call() called on disposed instance');
|
||||
return;
|
||||
}
|
||||
|
||||
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 (!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
|
||||
* @throws {Error} If HookManager is disposed
|
||||
*/
|
||||
on(pattern, handler) {
|
||||
this._validateState('register 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());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 (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
|
||||
*/
|
||||
async function callHandler(handler, data, event) {
|
||||
const handlerName = handler?.name || 'anonymous';
|
||||
|
||||
try {
|
||||
const result = handler(data);
|
||||
// If handler returns a Promise, await it
|
||||
if (result && typeof result.then === 'function') {
|
||||
await result;
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the error with context
|
||||
console.error(
|
||||
`[Hook Error] Event: '${event}', Handler: '${handlerName}'`,
|
||||
error?.message || error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HookManager;
|
||||
@@ -2,6 +2,9 @@ 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 BrunoResponse = require('./bruno-response');
|
||||
const { runScriptInNodeVm } = require('./sandbox/node-vm');
|
||||
|
||||
module.exports = {
|
||||
@@ -9,5 +12,8 @@ module.exports = {
|
||||
TestRuntime,
|
||||
VarsRuntime,
|
||||
AssertRuntime,
|
||||
HooksRuntime,
|
||||
HookManager,
|
||||
BrunoResponse,
|
||||
runScriptInNodeVm
|
||||
};
|
||||
|
||||
155
packages/bruno-js/src/runtime/hooks-runtime.js
Normal file
155
packages/bruno-js/src/runtime/hooks-runtime.js
Normal file
@@ -0,0 +1,155 @@
|
||||
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
||||
const Bru = require('../bru');
|
||||
const BrunoRequest = require('../bruno-request');
|
||||
const BrunoResponse = require('../bruno-response');
|
||||
const HookManager = require('../hook-manager');
|
||||
const { cleanJson } = require('../utils');
|
||||
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
||||
|
||||
/**
|
||||
* HooksRuntime manages the execution of hook scripts in a sandboxed environment.
|
||||
*
|
||||
* Hooks are now merged into a single script using mergeScripts() in collection.js,
|
||||
* following the same pattern as pre-request, post-response, and test scripts.
|
||||
*
|
||||
* Features:
|
||||
* - Lazy VM creation: VMs are only created when hooks are present
|
||||
* - Shared HookManager: Can reuse an existing HookManager for handler registration
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
class HooksRuntime {
|
||||
/**
|
||||
* Creates a new HooksRuntime instance
|
||||
* @param {object} [props] - Configuration options
|
||||
* @param {string} [props.runtime='quickjs'] - Runtime to use ('quickjs' or 'nodevm')
|
||||
*/
|
||||
constructor(props) {
|
||||
this.runtime = props?.runtime || 'quickjs';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if hooks content is empty/whitespace
|
||||
* @private
|
||||
* @param {string} content - Content to check
|
||||
* @returns {boolean} True if empty
|
||||
*/
|
||||
_isEmptyContent(content) {
|
||||
return !content || !content.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run hooks script to register event handlers
|
||||
* @param {object} options - Configuration options
|
||||
* @param {string} [options.hooksFile] - The merged hooks script content
|
||||
* @param {object} options.request - The request object (used for variable extraction and BrunoRequest creation)
|
||||
* @param {object} [options.response] - The response object (used for BrunoResponse creation, only for afterResponse hooks)
|
||||
* @param {object} options.envVariables - Environment variables
|
||||
* @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, and req/res wrapper objects
|
||||
*/
|
||||
async runHooks(options) {
|
||||
const {
|
||||
hooksFile,
|
||||
request,
|
||||
response,
|
||||
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(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, activeHookManager);
|
||||
|
||||
// Create BrunoRequest and BrunoResponse wrappers (similar to ScriptRuntime)
|
||||
const req = request ? new BrunoRequest(request) : null;
|
||||
const res = response ? new BrunoResponse(response) : null;
|
||||
|
||||
const context = {
|
||||
bru,
|
||||
req,
|
||||
res
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Build result from current state — shared across all return paths
|
||||
const buildResult = () => ({
|
||||
hookManager: activeHookManager,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
persistentEnvVariables: bru.persistentEnvVariables,
|
||||
globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
|
||||
nextRequestName: bru.nextRequest,
|
||||
skipRequest: bru.skipRequest,
|
||||
stopExecution: bru.stopExecution,
|
||||
__bru: bru,
|
||||
req,
|
||||
res
|
||||
});
|
||||
|
||||
// Lazy VM creation: If no hooks file, return early without creating a VM
|
||||
if (this._isEmptyContent(hooksFile)) {
|
||||
return buildResult();
|
||||
}
|
||||
|
||||
// Execute hooks script
|
||||
// Note: Hooks need the VM to persist so registered handlers can be called later
|
||||
if (this.runtime === 'nodevm') {
|
||||
await runScriptInNodeVm({
|
||||
script: hooksFile,
|
||||
context,
|
||||
collectionPath,
|
||||
scriptingConfig
|
||||
});
|
||||
} else {
|
||||
// QuickJS: persist the VM so hook handlers can be called later during the collection run
|
||||
await executeQuickJsVmAsync({
|
||||
script: hooksFile,
|
||||
context: context,
|
||||
collectionPath
|
||||
});
|
||||
}
|
||||
|
||||
return buildResult();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HooksRuntime;
|
||||
@@ -1,5 +1,7 @@
|
||||
const { cleanJson, cleanCircularJson } = require('../../../utils');
|
||||
const { marshallToVm } = require('../utils');
|
||||
const { createBrunoRequestShim } = require('./bruno-request');
|
||||
const { createBrunoResponseShim } = require('./bruno-response');
|
||||
|
||||
const addBruShimToContext = (vm, bru) => {
|
||||
const bruObject = vm.newObject();
|
||||
@@ -394,6 +396,232 @@ const addBruShimToContext = (vm, bru) => {
|
||||
vm.setProp(bruObject, 'cookies', bruCookiesObject);
|
||||
bruCookiesObject.dispose();
|
||||
|
||||
// Add hooks shim if bru.hooks exists
|
||||
if (bru.hooks) {
|
||||
// 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 logging/debugging (not used for lookup)
|
||||
* @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');
|
||||
}
|
||||
|
||||
// Try to duplicate the handle to own a reference
|
||||
let handlerHandle;
|
||||
try {
|
||||
handlerHandle = handler.dup ? handler.dup() : handler;
|
||||
} catch (e) {
|
||||
handlerHandle = handler;
|
||||
}
|
||||
|
||||
// Create native handler that executes the stored handle
|
||||
// Returns a Promise so HookManager can await async handlers
|
||||
// Capture handlerHandle directly in closure - no lookup needed
|
||||
const nativeHandler = (data) => {
|
||||
if (!handlerHandle || !vm) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// Return the Promise from executeHandler so HookManager awaits it
|
||||
return executeHandler(handlerHandle, vm, data);
|
||||
};
|
||||
|
||||
// Register with native hook system
|
||||
const unhook = nativeHookRegister(nativeHandler);
|
||||
|
||||
// Create unhook function (just calls native unhook - no handle cleanup needed)
|
||||
const unhookFn = vm.newFunction('unhook', () => {
|
||||
unhook();
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
180
packages/bruno-js/tests/hook-manager.spec.js
Normal file
180
packages/bruno-js/tests/hook-manager.spec.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Unit tests for HookManager
|
||||
*/
|
||||
const HookManager = require('../src/hook-manager');
|
||||
|
||||
describe('HookManager', () => {
|
||||
let hookManager;
|
||||
|
||||
beforeEach(() => {
|
||||
hookManager = new HookManager();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (hookManager && !hookManager.isDisposed) {
|
||||
hookManager.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with empty listeners', () => {
|
||||
expect(hookManager.listeners).toEqual({});
|
||||
});
|
||||
|
||||
it('should initialize in active state', () => {
|
||||
expect(hookManager.state).toBe(HookManager.State.ACTIVE);
|
||||
expect(hookManager.isDisposed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on()', () => {
|
||||
it('should register a handler for a pattern', () => {
|
||||
const handler = jest.fn();
|
||||
hookManager.on('test:event', handler);
|
||||
expect(hookManager.listeners['test:event']).toContain(handler);
|
||||
});
|
||||
|
||||
it('should return an unhook function', () => {
|
||||
const handler = jest.fn();
|
||||
const unhook = hookManager.on('test:event', handler);
|
||||
expect(typeof unhook).toBe('function');
|
||||
});
|
||||
|
||||
it('should register handlers for multiple patterns', () => {
|
||||
const handler = jest.fn();
|
||||
hookManager.on(['event1', 'event2'], handler);
|
||||
expect(hookManager.listeners['event1']).toContain(handler);
|
||||
expect(hookManager.listeners['event2']).toContain(handler);
|
||||
});
|
||||
|
||||
it('should throw if handler is not a function', () => {
|
||||
expect(() => hookManager.on('test', 'not a function')).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it('should throw if pattern is not string or array', () => {
|
||||
expect(() => hookManager.on(123, jest.fn())).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it('should throw if same handler is registered twice', () => {
|
||||
const handler = jest.fn();
|
||||
hookManager.on('test', handler);
|
||||
expect(() => hookManager.on('test', handler)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw if HookManager is disposed', () => {
|
||||
hookManager.dispose();
|
||||
expect(() => hookManager.on('test', jest.fn())).toThrow(/disposed/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unhook', () => {
|
||||
it('should remove handler when unhook is called', () => {
|
||||
const handler = jest.fn();
|
||||
const unhook = hookManager.on('test', handler);
|
||||
unhook();
|
||||
expect(hookManager.listeners['test']).not.toContain(handler);
|
||||
});
|
||||
|
||||
it('should remove handler only for specified pattern', () => {
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
hookManager.on('event1', handler1);
|
||||
hookManager.on('event2', handler2);
|
||||
const unhook = hookManager.on('event3', handler1);
|
||||
unhook('event3');
|
||||
expect(hookManager.listeners['event3']).not.toContain(handler1);
|
||||
// handler1 should still be registered for event1
|
||||
expect(hookManager.listeners['event1']).toContain(handler1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('call()', () => {
|
||||
it('should call registered handler', async () => {
|
||||
const handler = jest.fn();
|
||||
hookManager.on('test', handler);
|
||||
await hookManager.call('test', { value: 1 });
|
||||
expect(handler).toHaveBeenCalledWith({ value: 1 });
|
||||
});
|
||||
|
||||
it('should call async handlers', async () => {
|
||||
const handler = jest.fn().mockResolvedValue(undefined);
|
||||
hookManager.on('test', handler);
|
||||
await hookManager.call('test', { value: 1 });
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call multiple handlers in order', async () => {
|
||||
const results = [];
|
||||
const handler1 = jest.fn(() => results.push(1));
|
||||
const handler2 = jest.fn(() => results.push(2));
|
||||
hookManager.on('test', handler1);
|
||||
hookManager.on('test', handler2);
|
||||
await hookManager.call('test', {});
|
||||
expect(results).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('should handle errors without stopping execution', async () => {
|
||||
const errorHandler = jest.fn(() => { throw new Error('Test error'); });
|
||||
const normalHandler = jest.fn();
|
||||
hookManager.on('test', errorHandler);
|
||||
hookManager.on('test', normalHandler);
|
||||
await hookManager.call('test', {});
|
||||
expect(normalHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should warn when called on disposed instance', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
hookManager.dispose();
|
||||
await hookManager.call('test', {});
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('disposed'));
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear()', () => {
|
||||
it('should clear handlers for a pattern', () => {
|
||||
hookManager.on('test', jest.fn());
|
||||
hookManager.clear('test');
|
||||
expect(hookManager.listeners['test']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should clear handlers for multiple patterns', () => {
|
||||
hookManager.on('event1', jest.fn());
|
||||
hookManager.on('event2', jest.fn());
|
||||
hookManager.clear(['event1', 'event2']);
|
||||
expect(hookManager.listeners['event1']).toBeUndefined();
|
||||
expect(hookManager.listeners['event2']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAll()', () => {
|
||||
it('should clear all handlers', () => {
|
||||
hookManager.on('event1', jest.fn());
|
||||
hookManager.on('event2', jest.fn());
|
||||
hookManager.clearAll();
|
||||
expect(hookManager.listeners).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispose()', () => {
|
||||
it('should mark as disposed', () => {
|
||||
hookManager.dispose();
|
||||
expect(hookManager.isDisposed).toBe(true);
|
||||
expect(hookManager.state).toBe(HookManager.State.DISPOSED);
|
||||
});
|
||||
|
||||
it('should clear all listeners', () => {
|
||||
hookManager.on('test', jest.fn());
|
||||
hookManager.dispose();
|
||||
expect(hookManager.listeners).toEqual({});
|
||||
});
|
||||
|
||||
it('should be idempotent', () => {
|
||||
hookManager.on('test', jest.fn());
|
||||
hookManager.dispose();
|
||||
hookManager.dispose();
|
||||
expect(hookManager.isDisposed).toBe(true);
|
||||
expect(hookManager.listeners).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,28 @@
|
||||
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("request should have access to setup vars", function() {
|
||||
const setupComplete = bru.getVar('setup-complete');
|
||||
expect(setupComplete).to.equal('true');
|
||||
});
|
||||
}
|
||||
198
packages/bruno-tests/hooks-comprehensive-tests/collection.bru
Normal file
198
packages/bruno-tests/hooks-comprehensive-tests/collection.bru
Normal file
@@ -0,0 +1,198 @@
|
||||
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());
|
||||
|
||||
console.log('[onBeforeCollectionRun] Setup complete - token and counters initialized');
|
||||
});
|
||||
|
||||
// Test: bru.runRequest in onBeforeCollectionRun hook
|
||||
bru.hooks.runner.onBeforeCollectionRun(async() => {
|
||||
console.log('[onBeforeCollectionRun] Testing bru.runRequest in collection-level hook...');
|
||||
|
||||
try {
|
||||
// Call another request using bru.runRequest
|
||||
const response = await bru.runRequest('hooks/bru-run-request/helper-request');
|
||||
|
||||
// Store the response for verification in tests
|
||||
if (response) {
|
||||
bru.setVar('collection-run-request-response', response.data);
|
||||
bru.setVar('collection-run-request-status', response.status);
|
||||
bru.setVar('collection-run-request-tested', 'true');
|
||||
console.log('[onBeforeCollectionRun] bru.runRequest succeeded:', response.data);
|
||||
} else {
|
||||
bru.setVar('collection-run-request-error', 'No response received');
|
||||
console.log('[onBeforeCollectionRun] bru.runRequest failed - no response');
|
||||
}
|
||||
} catch (error) {
|
||||
bru.setVar('collection-run-request-error', error.message || String(error));
|
||||
console.log('[onBeforeCollectionRun] bru.runRequest error:', error);
|
||||
}
|
||||
|
||||
console.log('[onBeforeCollectionRun] bru.runRequest test completed');
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// onAfterCollectionRun Hook Tests
|
||||
// ============================================
|
||||
|
||||
// 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-count');
|
||||
bru.deleteVar('success-count');
|
||||
bru.deleteVar('final-stats');
|
||||
bru.deleteVar('cleanup-performed');
|
||||
bru.deleteVar('collection-run-request-response');
|
||||
bru.deleteVar('collection-run-request-status');
|
||||
bru.deleteVar('collection-run-request-tested');
|
||||
bru.deleteVar('collection-run-request-error');
|
||||
|
||||
await bru.sleep(300);
|
||||
console.log('[onAfterCollectionRun] Cleanup complete - all test variables removed');
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 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,49 @@
|
||||
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() {
|
||||
const before = bru.getVar('timeout-before');
|
||||
expect(before).to.not.equal(10000);
|
||||
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,17 @@
|
||||
meta {
|
||||
name: helper-request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("helper request returns pong", function() {
|
||||
expect(res.getBody()).to.equal('pong');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
meta {
|
||||
name: run-request-in-after-hook
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onAfterResponse(async ({ res }) => {
|
||||
console.log('[afterResponse] Testing bru.runRequest API');
|
||||
console.log('[afterResponse] Main request status:', res.getStatus());
|
||||
|
||||
// Call another request using bru.runRequest
|
||||
const response = await bru.runRequest('hooks/bru-run-request/helper-request');
|
||||
|
||||
// Store the response body for verification in tests
|
||||
if (response) {
|
||||
bru.setVar('after-helper-response-body', response.data);
|
||||
bru.setVar('after-helper-response-status', response.status);
|
||||
console.log('[afterResponse] Helper request response:', response.data);
|
||||
} else {
|
||||
bru.setVar('after-helper-response-error', 'No response received');
|
||||
console.log('[afterResponse] Helper request failed - no response');
|
||||
}
|
||||
|
||||
console.log('[afterResponse] bru.runRequest test completed');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("bru.runRequest in afterResponse hook should return helper response", function() {
|
||||
const helperBody = bru.getVar('after-helper-response-body');
|
||||
expect(helperBody).to.equal('pong');
|
||||
});
|
||||
|
||||
test("bru.runRequest in afterResponse hook should return status 200", function() {
|
||||
const helperStatus = bru.getVar('after-helper-response-status');
|
||||
expect(helperStatus).to.equal(200);
|
||||
});
|
||||
|
||||
test("main request should succeed", function() {
|
||||
expect(res.getBody()).to.equal('pong');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
meta {
|
||||
name: run-request-in-before-hook
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(async ({ req }) => {
|
||||
console.log('[beforeRequest] Testing bru.runRequest API');
|
||||
|
||||
// Call another request using bru.runRequest
|
||||
const response = await bru.runRequest('hooks/bru-run-request/helper-request');
|
||||
|
||||
// Store the response body for verification in tests
|
||||
if (response) {
|
||||
bru.setVar('helper-response-body', response.data);
|
||||
bru.setVar('helper-response-status', response.status);
|
||||
console.log('[beforeRequest] Helper request response:', response.data);
|
||||
} else {
|
||||
bru.setVar('helper-response-error', 'No response received');
|
||||
console.log('[beforeRequest] Helper request failed - no response');
|
||||
}
|
||||
|
||||
console.log('[beforeRequest] bru.runRequest test completed');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("bru.runRequest in beforeRequest hook should return helper response", function() {
|
||||
const helperBody = bru.getVar('helper-response-body');
|
||||
expect(helperBody).to.equal('pong');
|
||||
});
|
||||
|
||||
test("bru.runRequest in beforeRequest hook should return status 200", function() {
|
||||
const helperStatus = bru.getVar('helper-response-status');
|
||||
expect(helperStatus).to.equal(200);
|
||||
});
|
||||
|
||||
test("main request should still succeed", function() {
|
||||
expect(res.getBody()).to.equal('pong');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
meta {
|
||||
name: verify-collection-run-request
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("collection-level bru.runRequest should have set response body", function() {
|
||||
const responseBody = bru.getVar('collection-run-request-response');
|
||||
expect(responseBody).to.equal('pong');
|
||||
});
|
||||
|
||||
test("collection-level bru.runRequest should have set response status", function() {
|
||||
const responseStatus = bru.getVar('collection-run-request-status');
|
||||
expect(responseStatus).to.equal(200);
|
||||
});
|
||||
|
||||
test("collection-level bru.runRequest flag should be set", function() {
|
||||
const flag = bru.getVar('collection-run-request-tested');
|
||||
expect(flag).to.equal('true');
|
||||
});
|
||||
}
|
||||
@@ -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,45 @@
|
||||
meta {
|
||||
name: legacy-set-next-request-first
|
||||
type: http
|
||||
seq: 300
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] Testing legacy bru.setNextRequest() API');
|
||||
|
||||
// Clear any previous markers
|
||||
bru.deleteVar('legacy-set-next-skipped-ran');
|
||||
|
||||
// Mark that this request started
|
||||
bru.setVar('legacy-set-next-first-ran', 'true');
|
||||
|
||||
console.log('[beforeRequest] Legacy first request starting');
|
||||
});
|
||||
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
console.log('[afterResponse] Using legacy bru.setNextRequest() to jump to target');
|
||||
|
||||
// Use the legacy API to jump to the target request
|
||||
bru.setNextRequest('legacy-set-next-request-target');
|
||||
|
||||
console.log('[afterResponse] Called bru.setNextRequest("legacy-set-next-request-target")');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("first request should execute successfully", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
|
||||
test("first request marker should be set", function() {
|
||||
const ran = bru.getVar('legacy-set-next-first-ran');
|
||||
expect(ran).to.equal('true');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
meta {
|
||||
name: legacy-set-next-request-skipped
|
||||
type: http
|
||||
seq: 301
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] ERROR: This request should have been skipped by legacy setNextRequest!');
|
||||
|
||||
// If this request runs, set an error marker
|
||||
bru.setVar('legacy-set-next-skipped-ran', 'true');
|
||||
|
||||
console.log('[beforeRequest] Setting error marker - legacy setNextRequest jump failed');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("this request should be skipped due to legacy setNextRequest jump", function() {
|
||||
// If we get here, the request ran when it should have been skipped
|
||||
const ran = bru.getVar('legacy-set-next-skipped-ran');
|
||||
expect(ran).to.be.undefined;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
meta {
|
||||
name: legacy-set-next-request-target
|
||||
type: http
|
||||
seq: 302
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] Target request reached via legacy bru.setNextRequest() jump');
|
||||
|
||||
// Mark that target was reached
|
||||
bru.setVar('legacy-set-next-target-reached', 'true');
|
||||
|
||||
console.log('[beforeRequest] Legacy target request starting');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("target request should execute successfully", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
|
||||
test("target should have been reached via legacy API", function() {
|
||||
const reached = bru.getVar('legacy-set-next-target-reached');
|
||||
expect(reached).to.equal('true');
|
||||
});
|
||||
|
||||
test("first request should have run before jump", function() {
|
||||
const firstRan = bru.getVar('legacy-set-next-first-ran');
|
||||
expect(firstRan).to.equal('true');
|
||||
});
|
||||
|
||||
test("skipped request should NOT have run", function() {
|
||||
const skippedRan = bru.getVar('legacy-set-next-skipped-ran');
|
||||
expect(skippedRan).to.be.undefined;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
meta {
|
||||
name: set-next-null-should-not-run
|
||||
type: http
|
||||
seq: 901
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] ERROR: This request should NOT have run after setNextRequest(null)!');
|
||||
|
||||
// If this request runs, set an error marker
|
||||
bru.setVar('set-next-null-error', 'Request ran when setNextRequest(null) should have stopped the runner');
|
||||
|
||||
console.log('[beforeRequest] Setting error marker - setNextRequest(null) failed');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("this request should not run - setNextRequest(null) should have stopped the runner", function() {
|
||||
// If we get here, the request ran when it should have been stopped
|
||||
const error = bru.getVar('set-next-null-error');
|
||||
expect(error).to.be.undefined;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
meta {
|
||||
name: set-next-null-trigger
|
||||
type: http
|
||||
seq: 900
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] Testing bru.runner.setNextRequest(null) API');
|
||||
|
||||
// Clear any previous error markers
|
||||
bru.deleteVar('set-next-null-error');
|
||||
|
||||
// Mark that this request started
|
||||
bru.setVar('set-next-null-trigger-ran', 'true');
|
||||
|
||||
console.log('[beforeRequest] setNextRequest(null) trigger starting');
|
||||
});
|
||||
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
console.log('[afterResponse] Setting next request to null to stop runner gracefully');
|
||||
|
||||
// Setting nextRequest to null should stop the runner gracefully
|
||||
bru.runner.setNextRequest(null);
|
||||
|
||||
console.log('[afterResponse] Called bru.runner.setNextRequest(null) - runner should stop');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("trigger request should execute successfully", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
|
||||
test("trigger marker should be set", function() {
|
||||
const ran = bru.getVar('set-next-null-trigger-ran');
|
||||
expect(ran).to.equal('true');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
meta {
|
||||
name: set-next-request-first
|
||||
type: http
|
||||
seq: 200
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] Testing bru.runner.setNextRequest() API');
|
||||
|
||||
// Clear any previous markers
|
||||
bru.deleteVar('set-next-request-skipped-ran');
|
||||
|
||||
// Mark that this request started
|
||||
bru.setVar('set-next-request-first-ran', 'true');
|
||||
|
||||
console.log('[beforeRequest] First request starting');
|
||||
});
|
||||
|
||||
bru.hooks.http.onAfterResponse(({ res }) => {
|
||||
console.log('[afterResponse] Setting next request to jump to target');
|
||||
|
||||
// Jump to the target request, skipping the intermediate request
|
||||
bru.runner.setNextRequest('set-next-request-target');
|
||||
|
||||
console.log('[afterResponse] Called bru.runner.setNextRequest("set-next-request-target")');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("first request should execute successfully", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
|
||||
test("first request marker should be set", function() {
|
||||
const ran = bru.getVar('set-next-request-first-ran');
|
||||
expect(ran).to.equal('true');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
meta {
|
||||
name: set-next-request-skipped
|
||||
type: http
|
||||
seq: 201
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] ERROR: This request should have been skipped by setNextRequest!');
|
||||
|
||||
// If this request runs, set an error marker
|
||||
bru.setVar('set-next-request-skipped-ran', 'true');
|
||||
|
||||
console.log('[beforeRequest] Setting error marker - setNextRequest jump failed');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("this request should be skipped due to setNextRequest jump", function() {
|
||||
// If we get here, the request ran when it should have been skipped
|
||||
const ran = bru.getVar('set-next-request-skipped-ran');
|
||||
expect(ran).to.be.undefined;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
meta {
|
||||
name: set-next-request-target
|
||||
type: http
|
||||
seq: 202
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] Target request reached via setNextRequest jump');
|
||||
|
||||
// Mark that target was reached
|
||||
bru.setVar('set-next-request-target-reached', 'true');
|
||||
|
||||
console.log('[beforeRequest] Target request starting');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("target request should execute successfully", function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
|
||||
test("target should have been reached", function() {
|
||||
const reached = bru.getVar('set-next-request-target-reached');
|
||||
expect(reached).to.equal('true');
|
||||
});
|
||||
|
||||
test("first request should have run before jump", function() {
|
||||
const firstRan = bru.getVar('set-next-request-first-ran');
|
||||
expect(firstRan).to.equal('true');
|
||||
});
|
||||
|
||||
test("skipped request should NOT have run", function() {
|
||||
const skippedRan = bru.getVar('set-next-request-skipped-ran');
|
||||
expect(skippedRan).to.be.undefined;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
meta {
|
||||
name: skip-request-in-before-hook
|
||||
type: http
|
||||
seq: 100
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:hooks {
|
||||
bru.hooks.http.onBeforeRequest(({ req }) => {
|
||||
console.log('[beforeRequest] Testing bru.runner.skipRequest() API');
|
||||
|
||||
// Set a marker to confirm the hook executed
|
||||
bru.setVar('skip-request-hook-executed', 'true');
|
||||
|
||||
// Skip this request - the main request should not execute
|
||||
bru.runner.skipRequest();
|
||||
|
||||
console.log('[beforeRequest] Called bru.runner.skipRequest() - request should be skipped');
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("hook should have executed before skip", function() {
|
||||
const hookExecuted = bru.getVar('skip-request-hook-executed');
|
||||
expect(hookExecuted).to.equal('true');
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
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: 51,
|
||||
passed: 50,
|
||||
failed: 0,
|
||||
skipped: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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: 51,
|
||||
passed: 50,
|
||||
failed: 0,
|
||||
skipped: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user