mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 20:01:28 +00:00
Merge branch 'main' into fix/cli-not-following-redirects
This commit is contained in:
44
.github/workflows/playwright.yml
vendored
Normal file
44
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Playwright E2E Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
e2e-test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: v22.11.x
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get --no-install-recommends install -y \
|
||||
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
|
||||
xvfb
|
||||
npm ci --legacy-peer-deps
|
||||
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
|
||||
- name: Build libraries
|
||||
run: |
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
xvfb-run npm run test:e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -47,4 +47,7 @@ yarn-error.log*
|
||||
#dev editor
|
||||
bruno.iml
|
||||
.idea
|
||||
.vscode
|
||||
.vscode
|
||||
|
||||
# Playwright
|
||||
/blob-report/
|
||||
|
||||
@@ -48,7 +48,7 @@ Bruno is being developed as a desktop app. You need to load the app by running t
|
||||
## Install Dependencies
|
||||
|
||||
```bash
|
||||
# use nodejs 20 version
|
||||
# use nodejs 22 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
|
||||
5
e2e-tests/test-app-start.spec.ts
Normal file
5
e2e-tests/test-app-start.spec.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { test, expect } from '../playwright';
|
||||
|
||||
test('test-app-start', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
|
||||
});
|
||||
54
package-lock.json
generated
54
package-lock.json
generated
@@ -23,9 +23,10 @@
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.26.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
@@ -34,6 +35,7 @@
|
||||
"jest": "^29.2.0",
|
||||
"lint-staged": "^15.5.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"playwright": "^1.51.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
@@ -6105,13 +6107,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
|
||||
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz",
|
||||
"integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.49.1"
|
||||
"playwright": "1.52.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -8290,6 +8292,7 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
@@ -8312,6 +8315,7 @@
|
||||
"version": "12.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
|
||||
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "*",
|
||||
@@ -8322,6 +8326,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
@@ -8332,13 +8337,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
|
||||
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
|
||||
"version": "22.15.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
|
||||
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/plist": {
|
||||
@@ -8358,6 +8363,11 @@
|
||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
||||
"integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
||||
@@ -13263,6 +13273,7 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -21300,13 +21311,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
|
||||
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
|
||||
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.1"
|
||||
"playwright-core": "1.52.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -21319,9 +21330,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
|
||||
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
|
||||
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -26474,7 +26485,7 @@
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -26500,9 +26511,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -30570,6 +30581,9 @@
|
||||
"name": "@usebruno/requests",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/qs": "^6.9.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.26.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
@@ -31,6 +32,7 @@
|
||||
"jest": "^29.2.0",
|
||||
"lint-staged": "^15.5.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"playwright": "^1.51.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
@@ -58,9 +60,8 @@
|
||||
"build:electron:rpm": "./scripts/build-electron.sh rpm",
|
||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||
"watch:common": "npm run watch --workspace=packages/bruno-common",
|
||||
"test:codegen": "npm run dev:web & node ./scripts/playwright-codegen.js",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report",
|
||||
"test:codegen": "node playwright/codegen.ts",
|
||||
"test:e2e": "playwright test",
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"prepare": "husky install",
|
||||
"lint": "npx eslint ./"
|
||||
|
||||
@@ -13,6 +13,7 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||
const isCollectionRunInProgress = collection?.runnerResult?.info?.status && (collection?.runnerResult?.info?.status !== 'ended');
|
||||
|
||||
const onSubmit = (recursive) => {
|
||||
dispatch(
|
||||
@@ -22,10 +23,24 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
type: 'collection-runner'
|
||||
})
|
||||
);
|
||||
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));
|
||||
if (!isCollectionRunInProgress) {
|
||||
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleViewRunner = (e) => {
|
||||
e.preventDefault();
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-runner'
|
||||
})
|
||||
);
|
||||
onClose();
|
||||
}
|
||||
|
||||
const getRequestsCount = (items) => {
|
||||
const requestTypes = ['http-request', 'graphql-request']
|
||||
return items.filter(req => requestTypes.includes(req.type)).length;
|
||||
@@ -55,22 +70,34 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
</div>
|
||||
<div className={isFolderLoading ? "mb-2" : "mb-8"}>This will run all the requests in this folder and all its subfolders.</div>
|
||||
{isFolderLoading ? <div className='mb-8 warning'>Requests in this folder are still loading.</div> : null}
|
||||
{isCollectionRunInProgress ? <div className='mb-6 warning'>A Collection Run is already in progress.</div> : null}
|
||||
<div className="flex justify-end bruno-modal-footer">
|
||||
<span className="mr-3">
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" disabled={!recursiveRunLength} className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
|
||||
Recursive Run
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" disabled={!runLength} className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
|
||||
Run
|
||||
</button>
|
||||
</span>
|
||||
{
|
||||
isCollectionRunInProgress ?
|
||||
<span>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary mr-3" onClick={handleViewRunner}>
|
||||
View Run
|
||||
</button>
|
||||
</span>
|
||||
:
|
||||
<>
|
||||
<span>
|
||||
<button type="submit" disabled={!recursiveRunLength} className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
|
||||
Recursive Run
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" disabled={!runLength} className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
|
||||
Run
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,6 +11,8 @@ import PathDisplay from 'components/PathDisplay/index';
|
||||
import { useState } from 'react';
|
||||
import { IconArrowBackUp, IconEdit } from '@tabler/icons';
|
||||
import Help from 'components/Help';
|
||||
import { multiLineMsg } from "utils/common";
|
||||
import { formatIpcError } from "utils/common/error";
|
||||
|
||||
const CreateCollection = ({ onClose }) => {
|
||||
const inputRef = useRef();
|
||||
@@ -45,7 +47,7 @@ const CreateCollection = ({ onClose }) => {
|
||||
toast.success('Collection created!');
|
||||
onClose();
|
||||
})
|
||||
.catch((e) => toast.error('An error occurred while creating the collection - ' + e));
|
||||
.catch((e) => toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e))));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -113,7 +115,6 @@ const CreateCollection = ({ onClose }) => {
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
@@ -121,6 +122,9 @@ const CreateCollection = ({ onClose }) => {
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
onChange={e => {
|
||||
formik.setFieldValue('collectionLocation', e.target.value);
|
||||
}}
|
||||
/>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
|
||||
@@ -61,7 +61,6 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
@@ -69,6 +68,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
onChange={e => {
|
||||
formik.setFieldValue('collectionLocation', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
|
||||
@@ -11,6 +11,8 @@ import { useDispatch } from 'react-redux';
|
||||
import { showHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { multiLineMsg } from "utils/common";
|
||||
import { formatIpcError } from "utils/common/error";
|
||||
|
||||
const TitleBar = () => {
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
@@ -34,9 +36,8 @@ const TitleBar = () => {
|
||||
toast.success('Collection imported successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
console.error(err);
|
||||
toast.error('An error occurred while importing the collection. Check the logs for more information.');
|
||||
toast.error(multiLineMsg('An error occurred while importing the collection.', formatIpcError(err)));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -7,9 +7,17 @@ export const ToastContext = React.createContext();
|
||||
export const ToastProvider = (props) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const toastOptions = { duration: 2000 };
|
||||
const toastOptions = {
|
||||
duration: 2000,
|
||||
style: {
|
||||
// Break long word like file-path, URL etc. to prevent overflow
|
||||
overflowWrap: 'anywhere'
|
||||
}
|
||||
};
|
||||
|
||||
if (storedTheme === 'dark') {
|
||||
toastOptions.style = {
|
||||
...toastOptions.style,
|
||||
borderRadius: '10px',
|
||||
background: '#3d3d3d',
|
||||
color: '#fff'
|
||||
|
||||
@@ -34,3 +34,11 @@ export const toastError = (error, defaultErrorMsg = 'An error occurred') => {
|
||||
|
||||
return toast.error(errorMsg);
|
||||
};
|
||||
|
||||
export function formatIpcError(error) {
|
||||
if (!(error instanceof Error)) return error;
|
||||
if (!error?.message) return ''; // Avoid returning `null` or `undefined`
|
||||
// https://github.com/electron/electron/blob/659e79fc08c6ffc2f7506dd1358918d97d240147/lib/renderer/api/ipc-renderer.ts#L24-L30
|
||||
// There is no other way to get rid of this error prefix as of now.
|
||||
return error.message.replace(/^Error invoking remote method '.+?': (Error: )?/, '');
|
||||
}
|
||||
|
||||
@@ -181,4 +181,8 @@ export const getEncoding = (headers) => {
|
||||
// Parse the charset from content type: https://stackoverflow.com/a/33192813
|
||||
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(headers?.['content-type'] || '');
|
||||
return charsetMatch?.[1];
|
||||
}
|
||||
|
||||
export const multiLineMsg = (...messages) => {
|
||||
return messages.filter(m => m !== undefined && m !== null && m !== '').join('\n');
|
||||
}
|
||||
@@ -12,7 +12,7 @@ const { rpad } = require('../utils/common');
|
||||
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
const constants = require('../constants');
|
||||
const { findItemInCollection } = require('../utils/collection');
|
||||
const { findItemInCollection, getAllRequestsInFolder, createCollectionJsonFromPathname } = require('../utils/collection');
|
||||
const command = 'run [filename]';
|
||||
const desc = 'Run a request';
|
||||
|
||||
@@ -22,6 +22,7 @@ const printRunSummary = (results) => {
|
||||
passedRequests,
|
||||
failedRequests,
|
||||
skippedRequests,
|
||||
errorRequests,
|
||||
totalAssertions,
|
||||
passedAssertions,
|
||||
failedAssertions,
|
||||
@@ -36,6 +37,9 @@ const printRunSummary = (results) => {
|
||||
if (failedRequests > 0) {
|
||||
requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`;
|
||||
}
|
||||
if (errorRequests > 0) {
|
||||
requestSummary += `, ${chalk.red(`${errorRequests} error`)}`;
|
||||
}
|
||||
if (skippedRequests > 0) {
|
||||
requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`;
|
||||
}
|
||||
@@ -62,6 +66,7 @@ const printRunSummary = (results) => {
|
||||
passedRequests,
|
||||
failedRequests,
|
||||
skippedRequests,
|
||||
errorRequests,
|
||||
totalAssertions,
|
||||
passedAssertions,
|
||||
failedAssertions,
|
||||
@@ -71,163 +76,6 @@ const printRunSummary = (results) => {
|
||||
}
|
||||
};
|
||||
|
||||
const createCollectionFromPath = (collectionPath) => {
|
||||
const environmentsPath = path.join(collectionPath, `environments`);
|
||||
const getFilesInOrder = (collectionPath) => {
|
||||
let collection = {
|
||||
pathname: collectionPath
|
||||
};
|
||||
const traverse = (currentPath) => {
|
||||
const filesInCurrentDir = fs.readdirSync(currentPath);
|
||||
|
||||
if (currentPath.includes('node_modules')) {
|
||||
return;
|
||||
}
|
||||
const currentDirItems = [];
|
||||
for (const file of filesInCurrentDir) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
if (
|
||||
stats.isDirectory() &&
|
||||
filePath !== environmentsPath &&
|
||||
!filePath.startsWith('.git') &&
|
||||
!filePath.startsWith('node_modules')
|
||||
) {
|
||||
let folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) }
|
||||
const folderBruFilePath = path.join(filePath, 'folder.bru');
|
||||
const folderBruFileExists = fs.existsSync(folderBruFilePath);
|
||||
if(folderBruFileExists) {
|
||||
const folderBruContent = fs.readFileSync(folderBruFilePath, 'utf8');
|
||||
let folderBruJson = collectionBruToJson(folderBruContent);
|
||||
folderItem.root = folderBruJson;
|
||||
}
|
||||
currentDirItems.push(folderItem);
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of filesInCurrentDir) {
|
||||
if (['collection.bru', 'folder.bru'].includes(file)) {
|
||||
continue;
|
||||
}
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
|
||||
if (!stats.isDirectory() && path.extname(filePath) === '.bru') {
|
||||
const bruContent = fs.readFileSync(filePath, 'utf8');
|
||||
const bruJson = bruToJson(bruContent);
|
||||
currentDirItems.push({
|
||||
name: file,
|
||||
pathname: filePath,
|
||||
...bruJson
|
||||
});
|
||||
}
|
||||
}
|
||||
return currentDirItems;
|
||||
};
|
||||
collection.items = traverse(collectionPath);
|
||||
return collection;
|
||||
};
|
||||
return getFilesInOrder(collectionPath);
|
||||
};
|
||||
|
||||
const getBruFilesRecursively = (dir, testsOnly) => {
|
||||
const environmentsPath = 'environments';
|
||||
const collection = {};
|
||||
|
||||
const getFilesInOrder = (dir) => {
|
||||
let bruJsons = [];
|
||||
|
||||
const traverse = (currentPath) => {
|
||||
const filesInCurrentDir = fs.readdirSync(currentPath);
|
||||
|
||||
if (currentPath.includes('node_modules')) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of filesInCurrentDir) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
// todo: we might need a ignore config inside bruno.json
|
||||
if (
|
||||
stats.isDirectory() &&
|
||||
filePath !== environmentsPath &&
|
||||
!filePath.startsWith('.git') &&
|
||||
!filePath.startsWith('node_modules')
|
||||
) {
|
||||
traverse(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
const currentDirBruJsons = [];
|
||||
for (const file of filesInCurrentDir) {
|
||||
if (['collection.bru', 'folder.bru'].includes(file)) {
|
||||
continue;
|
||||
}
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
|
||||
if (!stats.isDirectory() && path.extname(filePath) === '.bru') {
|
||||
const bruContent = fs.readFileSync(filePath, 'utf8');
|
||||
const bruJson = bruToJson(bruContent);
|
||||
const requestHasTests = bruJson.request?.tests;
|
||||
const requestHasActiveAsserts = bruJson.request?.assertions.some((x) => x.enabled) || false;
|
||||
|
||||
if (testsOnly) {
|
||||
if (requestHasTests || requestHasActiveAsserts) {
|
||||
currentDirBruJsons.push({
|
||||
bruFilepath: filePath,
|
||||
bruJson
|
||||
});
|
||||
}
|
||||
} else {
|
||||
currentDirBruJsons.push({
|
||||
bruFilepath: filePath,
|
||||
bruJson
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// order requests by sequence
|
||||
currentDirBruJsons.sort((a, b) => {
|
||||
const aSequence = a.bruJson.seq || 0;
|
||||
const bSequence = b.bruJson.seq || 0;
|
||||
return aSequence - bSequence;
|
||||
});
|
||||
|
||||
bruJsons = bruJsons.concat(currentDirBruJsons);
|
||||
};
|
||||
|
||||
traverse(dir);
|
||||
return bruJsons;
|
||||
};
|
||||
|
||||
return getFilesInOrder(dir);
|
||||
};
|
||||
|
||||
const getCollectionRoot = (dir) => {
|
||||
const collectionRootPath = path.join(dir, 'collection.bru');
|
||||
const exists = fs.existsSync(collectionRootPath);
|
||||
if (!exists) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(collectionRootPath, 'utf8');
|
||||
return collectionBruToJson(content);
|
||||
};
|
||||
|
||||
const getFolderRoot = (dir) => {
|
||||
const folderRootPath = path.join(dir, 'folder.bru');
|
||||
const exists = fs.existsSync(folderRootPath);
|
||||
if (!exists) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(folderRootPath, 'utf8');
|
||||
return collectionBruToJson(content);
|
||||
};
|
||||
|
||||
const getJsSandboxRuntime = (sandbox) => {
|
||||
return sandbox === 'safe' ? 'quickjs' : 'vm2';
|
||||
};
|
||||
@@ -320,7 +168,6 @@ const builder = async (yargs) => {
|
||||
type:"number",
|
||||
description: "Delay between each requests (in miliseconds)"
|
||||
})
|
||||
|
||||
.example('$0 run request.bru', 'Run a request')
|
||||
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
|
||||
.example('$0 run folder', 'Run all requests in a folder')
|
||||
@@ -390,25 +237,8 @@ const handler = async function (argv) {
|
||||
} = argv;
|
||||
const collectionPath = process.cwd();
|
||||
|
||||
// todo
|
||||
// right now, bru must be run from the root of the collection
|
||||
// will add support in the future to run it from anywhere inside the collection
|
||||
const brunoJsonPath = path.join(collectionPath, 'bruno.json');
|
||||
const brunoJsonExists = await exists(brunoJsonPath);
|
||||
if (!brunoJsonExists) {
|
||||
console.error(chalk.red(`You can run only at the root of a collection`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION);
|
||||
}
|
||||
|
||||
const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8');
|
||||
const brunoConfig = JSON.parse(brunoConfigFile);
|
||||
const collectionRoot = getCollectionRoot(collectionPath);
|
||||
let collection = createCollectionFromPath(collectionPath);
|
||||
collection = {
|
||||
brunoConfig,
|
||||
root: collectionRoot,
|
||||
...collection
|
||||
}
|
||||
let collection = createCollectionJsonFromPathname(collectionPath);
|
||||
const { root: collectionRoot, brunoConfig } = collection;
|
||||
|
||||
if (clientCertConfig) {
|
||||
try {
|
||||
@@ -444,7 +274,6 @@ const handler = async function (argv) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (filename && filename.length) {
|
||||
const pathExists = await exists(filename);
|
||||
if (!pathExists) {
|
||||
@@ -566,54 +395,39 @@ const handler = async function (argv) {
|
||||
const _isFile = isFile(filename);
|
||||
let results = [];
|
||||
|
||||
let bruJsons = [];
|
||||
let requestItems = [];
|
||||
|
||||
if (_isFile) {
|
||||
console.log(chalk.yellow('Running Request \n'));
|
||||
const bruContent = fs.readFileSync(filename, 'utf8');
|
||||
const bruJson = bruToJson(bruContent);
|
||||
bruJsons.push({
|
||||
bruFilepath: filename,
|
||||
bruJson
|
||||
});
|
||||
const requestItem = bruToJson(bruContent);
|
||||
requestItem.pathname = path.resolve(collectionPath, filename);
|
||||
requestItems.push(requestItem);
|
||||
}
|
||||
|
||||
const _isDirectory = isDirectory(filename);
|
||||
if (_isDirectory) {
|
||||
if (!recursive) {
|
||||
console.log(chalk.yellow('Running Folder \n'));
|
||||
const files = fs.readdirSync(filename);
|
||||
const bruFiles = files.filter((file) => !['folder.bru'].includes(file) && file.endsWith('.bru'));
|
||||
|
||||
for (const bruFile of bruFiles) {
|
||||
const bruFilepath = path.join(filename, bruFile);
|
||||
const bruContent = fs.readFileSync(bruFilepath, 'utf8');
|
||||
const bruJson = bruToJson(bruContent);
|
||||
const requestHasTests = bruJson.request?.tests;
|
||||
const requestHasActiveAsserts = bruJson.request?.assertions.some((x) => x.enabled) || false;
|
||||
if (testsOnly) {
|
||||
if (requestHasTests || requestHasActiveAsserts) {
|
||||
bruJsons.push({
|
||||
bruFilepath,
|
||||
bruJson
|
||||
});
|
||||
}
|
||||
} else {
|
||||
bruJsons.push({
|
||||
bruFilepath,
|
||||
bruJson
|
||||
});
|
||||
}
|
||||
}
|
||||
bruJsons.sort((a, b) => {
|
||||
const aSequence = a.bruJson.seq || 0;
|
||||
const bSequence = b.bruJson.seq || 0;
|
||||
return aSequence - bSequence;
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.yellow('Running Folder Recursively \n'));
|
||||
}
|
||||
const resolvedFilepath = path.resolve(filename);
|
||||
if (resolvedFilepath === collectionPath) {
|
||||
requestItems = getAllRequestsInFolder(collection?.items, recursive);
|
||||
} else {
|
||||
const folderItem = findItemInCollection(collection, resolvedFilepath);
|
||||
if (folderItem) {
|
||||
requestItems = getAllRequestsInFolder(folderItem.items, recursive);
|
||||
}
|
||||
}
|
||||
|
||||
bruJsons = getBruFilesRecursively(filename, testsOnly);
|
||||
if (testsOnly) {
|
||||
requestItems = requestItems.filter((iter) => {
|
||||
const requestHasTests = iter.request?.tests;
|
||||
const requestHasActiveAsserts = iter.request?.assertions.some((x) => x.enabled) || false;
|
||||
return requestHasTests || requestHasActiveAsserts;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,11 +439,10 @@ const handler = async function (argv) {
|
||||
if (itemPathname && !itemPathname?.endsWith('.bru')) {
|
||||
itemPathname = `${itemPathname}.bru`;
|
||||
}
|
||||
const bruJson = cloneDeep(findItemInCollection(collection, itemPathname));
|
||||
if (bruJson) {
|
||||
const requestItem = cloneDeep(findItemInCollection(collection, itemPathname));
|
||||
if (requestItem) {
|
||||
const res = await runSingleRequest(
|
||||
itemPathname,
|
||||
bruJson,
|
||||
requestItem,
|
||||
collectionPath,
|
||||
runtimeVariables,
|
||||
envVars,
|
||||
@@ -648,14 +461,13 @@ const handler = async function (argv) {
|
||||
|
||||
let currentRequestIndex = 0;
|
||||
let nJumps = 0; // count the number of jumps to avoid infinite loops
|
||||
while (currentRequestIndex < bruJsons.length) {
|
||||
const iter = cloneDeep(bruJsons[currentRequestIndex]);
|
||||
const { bruFilepath, bruJson } = iter;
|
||||
while (currentRequestIndex < requestItems.length) {
|
||||
const requestItem = cloneDeep(requestItems[currentRequestIndex]);
|
||||
const { pathname } = requestItem;
|
||||
|
||||
const start = process.hrtime();
|
||||
const result = await runSingleRequest(
|
||||
bruFilepath,
|
||||
bruJson,
|
||||
requestItem,
|
||||
collectionPath,
|
||||
runtimeVariables,
|
||||
envVars,
|
||||
@@ -667,7 +479,7 @@ const handler = async function (argv) {
|
||||
runSingleRequestByPathname
|
||||
);
|
||||
|
||||
const isLastRun = currentRequestIndex === bruJsons.length - 1;
|
||||
const isLastRun = currentRequestIndex === requestItems.length - 1;
|
||||
const isValidDelay = !Number.isNaN(delay) && delay > 0;
|
||||
if(isValidDelay && !isLastRun){
|
||||
console.log(chalk.yellow(`Waiting for ${delay}ms or ${(delay/1000).toFixed(3)}s before next request.`));
|
||||
@@ -681,7 +493,7 @@ const handler = async function (argv) {
|
||||
results.push({
|
||||
...result,
|
||||
runtime: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9,
|
||||
suitename: bruFilepath.replace('.bru', '')
|
||||
suitename: pathname.replace('.bru', '')
|
||||
});
|
||||
|
||||
if (reporterSkipAllHeaders) {
|
||||
@@ -739,7 +551,7 @@ const handler = async function (argv) {
|
||||
if (nextRequestName === null) {
|
||||
break;
|
||||
}
|
||||
const nextRequestIdx = bruJsons.findIndex((iter) => iter.bruJson.name === nextRequestName);
|
||||
const nextRequestIdx = requestItems.findIndex((iter) => iter.name === nextRequestName);
|
||||
if (nextRequestIdx >= 0) {
|
||||
currentRequestIndex = nextRequestIdx;
|
||||
} else {
|
||||
|
||||
@@ -156,6 +156,37 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
delete request.basicAuth;
|
||||
}
|
||||
|
||||
if (request?.oauth2?.grantType) {
|
||||
switch (request.oauth2.grantType) {
|
||||
case 'password':
|
||||
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
|
||||
request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';
|
||||
request.oauth2.username = _interpolate(request.oauth2.username) || '';
|
||||
request.oauth2.password = _interpolate(request.oauth2.password) || '';
|
||||
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
|
||||
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
|
||||
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
|
||||
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
|
||||
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
|
||||
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
|
||||
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
|
||||
break;
|
||||
case 'client_credentials':
|
||||
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
|
||||
request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';
|
||||
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
|
||||
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
|
||||
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
|
||||
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
|
||||
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
|
||||
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
|
||||
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (request.awsv4config) {
|
||||
request.awsv4config.accessKeyId = _interpolate(request.awsv4config.accessKeyId) || '';
|
||||
request.awsv4config.secretAccessKey = _interpolate(request.awsv4config.secretAccessKey) || '';
|
||||
|
||||
6
packages/bruno-cli/src/runner/oauth2.js
Normal file
6
packages/bruno-cli/src/runner/oauth2.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { getOAuth2Token } = require('@usebruno/requests');
|
||||
const tokenStore = require('./tokenStore');
|
||||
|
||||
module.exports = {
|
||||
getOAuth2Token: (oauth2Config) => getOAuth2Token(oauth2Config, tokenStore)
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
const { get, each, filter } = require('lodash');
|
||||
const decomment = require('decomment');
|
||||
const crypto = require('node:crypto');
|
||||
const { mergeHeaders, mergeScripts, mergeVars, getTreePathFromCollectionToItem } = require('../utils/collection');
|
||||
const { mergeHeaders, mergeScripts, mergeVars, mergeAuth, getTreePathFromCollectionToItem } = require('../utils/collection');
|
||||
const { createFormData } = require('../utils/form-data');
|
||||
|
||||
const prepareRequest = (item = {}, collection = {}) => {
|
||||
@@ -16,6 +16,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
mergeHeaders(collection, request, requestTreePath);
|
||||
mergeScripts(collection, request, requestTreePath, scriptFlow);
|
||||
mergeVars(collection, request, requestTreePath);
|
||||
mergeAuth(collection, request, requestTreePath);
|
||||
}
|
||||
|
||||
each(get(request, 'headers', []), (h) => {
|
||||
@@ -72,6 +73,76 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
password: get(collectionAuth, 'digest.password')
|
||||
};
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'oauth2') {
|
||||
const grantType = get(collectionAuth, 'oauth2.grantType');
|
||||
|
||||
if (grantType === 'client_credentials') {
|
||||
axiosRequest.oauth2 = {
|
||||
grantType,
|
||||
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
|
||||
clientId: get(collectionAuth, 'oauth2.clientId'),
|
||||
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
|
||||
scope: get(collectionAuth, 'oauth2.scope'),
|
||||
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
|
||||
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey')
|
||||
};
|
||||
} else if (grantType === 'password') {
|
||||
axiosRequest.oauth2 = {
|
||||
grantType,
|
||||
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
|
||||
username: get(collectionAuth, 'oauth2.username'),
|
||||
password: get(collectionAuth, 'oauth2.password'),
|
||||
clientId: get(collectionAuth, 'oauth2.clientId'),
|
||||
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
|
||||
scope: get(collectionAuth, 'oauth2.scope'),
|
||||
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
|
||||
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey')
|
||||
};
|
||||
}
|
||||
}
|
||||
if (collectionAuth.mode === 'awsv4') {
|
||||
axiosRequest.awsv4config = {
|
||||
accessKeyId: get(collectionAuth, 'awsv4.accessKeyId'),
|
||||
secretAccessKey: get(collectionAuth, 'awsv4.secretAccessKey'),
|
||||
sessionToken: get(collectionAuth, 'awsv4.sessionToken'),
|
||||
service: get(collectionAuth, 'awsv4.service'),
|
||||
region: get(collectionAuth, 'awsv4.region'),
|
||||
profileName: get(collectionAuth, 'awsv4.profileName')
|
||||
};
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'ntlm') {
|
||||
axiosRequest.ntlmConfig = {
|
||||
username: get(collectionAuth, 'ntlm.username'),
|
||||
password: get(collectionAuth, 'ntlm.password'),
|
||||
domain: get(collectionAuth, 'ntlm.domain')
|
||||
};
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'wsse') {
|
||||
const username = get(collectionAuth, 'wsse.username', '');
|
||||
const password = get(collectionAuth, 'wsse.password', '');
|
||||
|
||||
const ts = new Date().toISOString();
|
||||
const nonce = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
// Create the password digest using SHA-1 as required for WSSE
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(nonce + ts + password);
|
||||
const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
|
||||
|
||||
// Construct the WSSE header
|
||||
axiosRequest.headers[
|
||||
'X-WSSE'
|
||||
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
|
||||
}
|
||||
|
||||
console.log('axiosRequest', axiosRequest);
|
||||
}
|
||||
|
||||
if (request.auth && request.auth.mode !== 'inherit') {
|
||||
@@ -129,6 +200,56 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
password: get(request, 'auth.digest.password')
|
||||
};
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'oauth2') {
|
||||
const grantType = get(request, 'auth.oauth2.grantType');
|
||||
|
||||
if (grantType === 'client_credentials') {
|
||||
axiosRequest.oauth2 = {
|
||||
grantType,
|
||||
clientId: get(request, 'auth.oauth2.clientId'),
|
||||
clientSecret: get(request, 'auth.oauth2.clientSecret'),
|
||||
scope: get(request, 'auth.oauth2.scope'),
|
||||
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
|
||||
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
|
||||
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
|
||||
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey')
|
||||
};
|
||||
} else if (grantType === 'password') {
|
||||
axiosRequest.oauth2 = {
|
||||
grantType,
|
||||
username: get(request, 'auth.oauth2.username'),
|
||||
password: get(request, 'auth.oauth2.password'),
|
||||
clientId: get(request, 'auth.oauth2.clientId'),
|
||||
clientSecret: get(request, 'auth.oauth2.clientSecret'),
|
||||
scope: get(request, 'auth.oauth2.scope'),
|
||||
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
|
||||
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
|
||||
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
|
||||
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'apikey') {
|
||||
if (request.auth.apikey?.placement === 'header') {
|
||||
axiosRequest.headers[request.auth.apikey?.key] = request.auth.apikey?.value;
|
||||
}
|
||||
|
||||
if (request.auth.apikey?.placement === 'queryparams') {
|
||||
if (axiosRequest.url && request.auth.apikey?.key) {
|
||||
try {
|
||||
const urlObj = new URL(request.url);
|
||||
urlObj.searchParams.set(request.auth.apikey?.key, request.auth.apikey?.value);
|
||||
axiosRequest.url = urlObj.toString();
|
||||
} catch (error) {
|
||||
console.error('Invalid URL:', request.url, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request.body = request.body || {};
|
||||
|
||||
@@ -22,6 +22,7 @@ const path = require('path');
|
||||
const { parseDataFromResponse } = require('../utils/common');
|
||||
const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies');
|
||||
const { createFormData } = require('../utils/form-data');
|
||||
const { getOAuth2Token } = require('./oauth2');
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { addDigestInterceptor } = require('@usebruno/requests');
|
||||
@@ -31,8 +32,7 @@ const onConsoleLog = (type, args) => {
|
||||
};
|
||||
|
||||
const runSingleRequest = async function (
|
||||
filename,
|
||||
bruJson,
|
||||
item,
|
||||
collectionPath,
|
||||
runtimeVariables,
|
||||
envVariables,
|
||||
@@ -43,14 +43,12 @@ const runSingleRequest = async function (
|
||||
collection,
|
||||
runSingleRequestByPathname
|
||||
) {
|
||||
const { pathname: itemPathname } = item;
|
||||
const relativeItemPathname = path.relative(collectionPath, itemPathname);
|
||||
try {
|
||||
let request;
|
||||
let nextRequestName;
|
||||
let shouldStopRunnerExecution = false;
|
||||
let item = {
|
||||
pathname: path.join(collectionPath, filename),
|
||||
...bruJson
|
||||
}
|
||||
request = prepareRequest(item, collection);
|
||||
|
||||
request.__bruno__executionMode = 'cli';
|
||||
@@ -84,7 +82,7 @@ const runSingleRequest = async function (
|
||||
if (result?.skipRequest) {
|
||||
return {
|
||||
test: {
|
||||
filename: filename
|
||||
filename: relativeItemPathname
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
@@ -98,7 +96,8 @@ const runSingleRequest = async function (
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: 'Request has been skipped from pre-request script',
|
||||
error: null,
|
||||
status: 'skipped',
|
||||
skipped: true,
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
@@ -313,6 +312,33 @@ const runSingleRequest = async function (
|
||||
requestMaxRedirects = 5; // Default to 5 redirects
|
||||
}
|
||||
|
||||
// Handle OAuth2 authentication
|
||||
if (request.oauth2) {
|
||||
try {
|
||||
const token = await getOAuth2Token(request.oauth2);
|
||||
if (token) {
|
||||
const { tokenPlacement = 'header', tokenHeaderPrefix = 'Bearer', tokenQueryKey = 'access_token' } = request.oauth2;
|
||||
|
||||
if (tokenPlacement === 'header') {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${token}`;
|
||||
} else if (tokenPlacement === 'url') {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url.searchParams.set(tokenQueryKey, token);
|
||||
request.url = url.toString();
|
||||
} catch (error) {
|
||||
console.error('Error applying OAuth2 token to URL:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('OAuth2 token fetch error:', error.message);
|
||||
}
|
||||
|
||||
// Remove oauth2 config from request to prevent it from being sent
|
||||
delete request.oauth2;
|
||||
}
|
||||
|
||||
let response, responseTime;
|
||||
try {
|
||||
|
||||
@@ -370,10 +396,10 @@ const runSingleRequest = async function (
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
} else {
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));
|
||||
return {
|
||||
test: {
|
||||
filename: filename
|
||||
filename: relativeItemPathname
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
@@ -382,13 +408,14 @@ const runSingleRequest = async function (
|
||||
data: request.data
|
||||
},
|
||||
response: {
|
||||
status: null,
|
||||
status: 'error',
|
||||
statusText: null,
|
||||
headers: null,
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
|
||||
status: 'error',
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
nextRequestName: nextRequestName,
|
||||
@@ -400,12 +427,12 @@ const runSingleRequest = async function (
|
||||
response.responseTime = responseTime;
|
||||
|
||||
console.log(
|
||||
chalk.green(stripExtension(filename)) +
|
||||
chalk.green(stripExtension(relativeItemPathname)) +
|
||||
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
||||
);
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(bruJson, 'request.vars.res');
|
||||
const postResponseVars = get(item, 'request.vars.res');
|
||||
if (postResponseVars?.length) {
|
||||
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
|
||||
varsRuntime.runPostResponseVars(
|
||||
@@ -446,7 +473,7 @@ const runSingleRequest = async function (
|
||||
|
||||
// run assertions
|
||||
let assertionResults = [];
|
||||
const assertions = get(bruJson, 'request.assertions');
|
||||
const assertions = get(item, 'request.assertions');
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
|
||||
assertionResults = assertRuntime.runAssertions(
|
||||
@@ -508,7 +535,7 @@ const runSingleRequest = async function (
|
||||
|
||||
return {
|
||||
test: {
|
||||
filename: filename
|
||||
filename: relativeItemPathname
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
@@ -524,16 +551,17 @@ const runSingleRequest = async function (
|
||||
responseTime
|
||||
},
|
||||
error: null,
|
||||
status: 'pass',
|
||||
assertionResults,
|
||||
testResults,
|
||||
nextRequestName: nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));
|
||||
return {
|
||||
test: {
|
||||
filename: filename
|
||||
filename: relativeItemPathname
|
||||
},
|
||||
request: {
|
||||
method: null,
|
||||
@@ -542,12 +570,13 @@ const runSingleRequest = async function (
|
||||
data: null
|
||||
},
|
||||
response: {
|
||||
status: null,
|
||||
status: 'error',
|
||||
statusText: null,
|
||||
headers: null,
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
status: 'error',
|
||||
error: err.message,
|
||||
assertionResults: [],
|
||||
testResults: []
|
||||
|
||||
22
packages/bruno-cli/src/runner/tokenStore.js
Normal file
22
packages/bruno-cli/src/runner/tokenStore.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// In-memory token store implementation for OAuth2 tokens
|
||||
const tokenStore = {
|
||||
tokens: new Map(),
|
||||
|
||||
// Save a token with optional expiry information
|
||||
async saveToken(serviceId, account, token) {
|
||||
this.tokens.set(`${serviceId}:${account}`, token);
|
||||
return true;
|
||||
},
|
||||
|
||||
// Get a token
|
||||
async getToken(serviceId, account) {
|
||||
return this.tokens.get(`${serviceId}:${account}`);
|
||||
},
|
||||
|
||||
// Delete a token
|
||||
async deleteToken(serviceId, account) {
|
||||
return this.tokens.delete(`${serviceId}:${account}`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = tokenStore;
|
||||
@@ -15,6 +15,17 @@ const collectionBruToJson = (bru) => {
|
||||
}
|
||||
};
|
||||
|
||||
// add meta if it exists
|
||||
// this is only for folder bru file
|
||||
// in the future, all of this will be replaced by standard bru lang
|
||||
const sequence = _.get(json, 'meta.seq');
|
||||
if (json?.meta) {
|
||||
transformedJson.meta = {
|
||||
name: json.meta.name,
|
||||
seq: !isNaN(sequence) ? Number(sequence) : 1
|
||||
};
|
||||
}
|
||||
|
||||
return transformedJson;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -4,6 +4,112 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { jsonToBruV2, envJsonToBruV2, jsonToCollectionBru } = require('@usebruno/lang');
|
||||
const { sanitizeName } = require('./filesystem');
|
||||
const { bruToJson, collectionBruToJson } = require('./bru');
|
||||
const constants = require('../constants');
|
||||
const chalk = require('chalk');
|
||||
|
||||
const createCollectionJsonFromPathname = (collectionPath) => {
|
||||
const environmentsPath = path.join(collectionPath, `environments`);
|
||||
|
||||
// get the collection bruno json config [<collection-path>/bruno.json]
|
||||
const brunoConfig = getCollectionBrunoJsonConfig(collectionPath);
|
||||
|
||||
// get the collection root [<collection-path>/collection.bru]
|
||||
const collectionRoot = getCollectionRoot(collectionPath);
|
||||
|
||||
// get the collection items recursively
|
||||
const traverse = (currentPath) => {
|
||||
const filesInCurrentDir = fs.readdirSync(currentPath);
|
||||
if (currentPath.includes('node_modules')) {
|
||||
return;
|
||||
}
|
||||
const currentDirItems = [];
|
||||
for (const file of filesInCurrentDir) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
if (filePath === environmentsPath) continue;
|
||||
if (filePath.startsWith('.git') || filePath.startsWith('node_modules')) continue;
|
||||
|
||||
// get the folder root
|
||||
let folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) }
|
||||
const folderBruJson = getFolderRoot(filePath);
|
||||
if (folderBruJson) {
|
||||
folderItem.root = folderBruJson;
|
||||
folderItem.seq = folderBruJson.meta.seq;
|
||||
}
|
||||
currentDirItems.push(folderItem);
|
||||
}
|
||||
else {
|
||||
if (['collection.bru', 'folder.bru'].includes(file)) continue;
|
||||
if (path.extname(filePath) !== '.bru') continue;
|
||||
|
||||
// get the request item
|
||||
const bruContent = fs.readFileSync(filePath, 'utf8');
|
||||
const requestItem = bruToJson(bruContent);
|
||||
currentDirItems.push({
|
||||
name: file,
|
||||
pathname: filePath,
|
||||
...requestItem
|
||||
});
|
||||
}
|
||||
}
|
||||
let currentDirFolderItems = currentDirItems?.filter((iter) => iter.type === 'folder');
|
||||
let sortedFolderItems = currentDirFolderItems?.sort((a, b) => a.seq - b.seq);
|
||||
|
||||
let currentDirRequestItems = currentDirItems?.filter((iter) => iter.type !== 'folder');
|
||||
let sortedRequestItems = currentDirRequestItems?.sort((a, b) => a.seq - b.seq);
|
||||
|
||||
return sortedFolderItems?.concat(sortedRequestItems);
|
||||
};
|
||||
let collectionItems = traverse(collectionPath);
|
||||
|
||||
let collection = {
|
||||
brunoConfig,
|
||||
root: collectionRoot,
|
||||
pathname: collectionPath,
|
||||
items: collectionItems
|
||||
}
|
||||
|
||||
return collection;
|
||||
};
|
||||
|
||||
const getCollectionBrunoJsonConfig = (dir) => {
|
||||
// right now, bru must be run from the root of the collection
|
||||
// will add support in the future to run it from anywhere inside the collection
|
||||
const brunoJsonPath = path.join(dir, 'bruno.json');
|
||||
const brunoJsonExists = fs.existsSync(brunoJsonPath);
|
||||
if (!brunoJsonExists) {
|
||||
console.error(chalk.red(`You can run only at the root of a collection`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION);
|
||||
}
|
||||
|
||||
const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8');
|
||||
const brunoConfig = JSON.parse(brunoConfigFile);
|
||||
return brunoConfig;
|
||||
}
|
||||
|
||||
const getCollectionRoot = (dir) => {
|
||||
const collectionRootPath = path.join(dir, 'collection.bru');
|
||||
const exists = fs.existsSync(collectionRootPath);
|
||||
if (!exists) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(collectionRootPath, 'utf8');
|
||||
return collectionBruToJson(content);
|
||||
};
|
||||
|
||||
const getFolderRoot = (dir) => {
|
||||
const folderRootPath = path.join(dir, 'folder.bru');
|
||||
const exists = fs.existsSync(folderRootPath);
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(folderRootPath, 'utf8');
|
||||
return collectionBruToJson(content);
|
||||
};
|
||||
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let headers = new Map();
|
||||
@@ -204,6 +310,45 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
return path;
|
||||
};
|
||||
|
||||
const mergeAuth = (collection, request, requestTreePath) => {
|
||||
let collectionAuth = collection?.root?.request?.auth || { mode: 'none' };
|
||||
let effectiveAuth = collectionAuth;
|
||||
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = i?.root?.request?.auth;
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveAuth = folderAuth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request.auth && request.auth.mode === 'inherit') {
|
||||
request.auth = effectiveAuth;
|
||||
}
|
||||
}
|
||||
|
||||
const getAllRequestsInFolder = (folderItems = [], recursive = true) => {
|
||||
let requests = [];
|
||||
|
||||
if (folderItems && folderItems.length) {
|
||||
folderItems.forEach((item) => {
|
||||
if (item.type !== 'folder') {
|
||||
requests.push(item);
|
||||
} else {
|
||||
if (recursive) {
|
||||
requests = requests.concat(getAllRequestsInFolder(item.items, recursive));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return requests;
|
||||
};
|
||||
|
||||
const getAllRequestsAtFolderRoot = (folderItems = []) => {
|
||||
return getAllRequestsInFolder(folderItems, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe write file implementation to handle errors
|
||||
* @param {string} filePath - Path to write file
|
||||
@@ -335,10 +480,14 @@ const processCollectionItems = async (items = [], currentPath) => {
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createCollectionJsonFromPathname,
|
||||
mergeHeaders,
|
||||
mergeVars,
|
||||
mergeScripts,
|
||||
findItemInCollection,
|
||||
getTreePathFromCollectionToItem,
|
||||
createCollectionFromBrunoObject
|
||||
createCollectionFromBrunoObject,
|
||||
mergeAuth,
|
||||
getAllRequestsInFolder,
|
||||
getAllRequestsAtFolderRoot
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
const path = require("node:path");
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
const constants = require('../../src/constants');
|
||||
const { createCollectionJsonFromPathname } = require('../../src/utils/collection');
|
||||
|
||||
describe('create collection json from pathname', () => {
|
||||
it("should throw an error when the pathname is not a valid bruno collection root", () => {
|
||||
const invalidCollectionPathname = path.join(__dirname, './fixtures/collection-invalid');
|
||||
jest.spyOn(console, 'error').mockImplementation(() => { });
|
||||
let mockProcessExit = jest.spyOn(process, 'exit').mockImplementation((code) => { throw new Error(code); });
|
||||
try { createCollectionJsonFromPathname(invalidCollectionPathname); } catch { }
|
||||
expect(mockProcessExit).toHaveBeenCalledWith(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION);
|
||||
jest.restoreAllMocks();
|
||||
})
|
||||
|
||||
it("creates a bruno collection json from the collection bru files", () => {
|
||||
const collectionPathname = path.join(__dirname, './fixtures/collection-json-from-pathname/collection');
|
||||
const outputCollectionJson = createCollectionJsonFromPathname(collectionPathname);
|
||||
|
||||
let c = outputCollectionJson;
|
||||
expect(c).toBeDefined();
|
||||
|
||||
/* collection bruno.json */
|
||||
expect(c).toHaveProperty('brunoConfig.version', "1");
|
||||
expect(c).toHaveProperty('brunoConfig.name', 'collection');
|
||||
expect(c).toHaveProperty('brunoConfig.type', 'collection');
|
||||
expect(c).toHaveProperty('brunoConfig.ignore', ["node_modules", ".git"]);
|
||||
expect(c).toHaveProperty('brunoConfig.proxy.enabled', false);
|
||||
expect(c).toHaveProperty('brunoConfig.proxy.protocol', 'http');
|
||||
expect(c).toHaveProperty('brunoConfig.proxy.hostname', '<proxy-hostname>');
|
||||
expect(c).toHaveProperty('brunoConfig.proxy.port', 3000);
|
||||
expect(c).toHaveProperty('brunoConfig.proxy.auth.enabled', false);
|
||||
expect(c).toHaveProperty('brunoConfig.proxy.auth.username', '<user-name>');
|
||||
expect(c).toHaveProperty('brunoConfig.proxy.auth.password', '<password>');
|
||||
expect(c).toHaveProperty('brunoConfig.proxy.bypassProxy', '');
|
||||
expect(c).toHaveProperty('brunoConfig.scripts.moduleWhitelist', ['crypto', 'buffer']);
|
||||
expect(c).toHaveProperty('brunoConfig.scripts.filesystemAccess.allow', true);
|
||||
expect(c).toHaveProperty('brunoConfig.clientCertificates.enabled', true);
|
||||
expect(c).toHaveProperty('brunoConfig.clientCertificates.certs', []);
|
||||
|
||||
/* collection pathname */
|
||||
expect(c).toHaveProperty('pathname', collectionPathname);
|
||||
|
||||
/* collection root */
|
||||
// headers
|
||||
expect(c).toHaveProperty('root.request.headers[0].name', 'collection_header');
|
||||
expect(c).toHaveProperty('root.request.headers[0].value', 'collection_header_value');
|
||||
expect(c).toHaveProperty('root.request.headers[0].enabled', true);
|
||||
// auth
|
||||
expect(c).toHaveProperty('root.request.auth.mode', 'basic');
|
||||
expect(c).toHaveProperty('root.request.auth.basic.username', 'username');
|
||||
expect(c).toHaveProperty('root.request.auth.basic.password', 'password');
|
||||
// pre-request scripts
|
||||
expect(c).toHaveProperty('root.request.script.req', 'const collectionPreRequestScript = true;');
|
||||
// collection root - post-response scripts
|
||||
expect(c).toHaveProperty('root.request.script.res', 'const collectionPostResponseScript = true;');
|
||||
// pre-request vars
|
||||
expect(c).toHaveProperty('root.request.vars.req[0].name', 'collection_pre_var');
|
||||
expect(c).toHaveProperty('root.request.vars.req[0].value', 'collection_pre_var_value');
|
||||
expect(c).toHaveProperty('root.request.vars.req[0].enabled', true);
|
||||
// post-response vars
|
||||
expect(c).toHaveProperty('root.request.vars.res[0].name', 'collection_post_var');
|
||||
expect(c).toHaveProperty('root.request.vars.res[0].value', 'collection_post_var_value');
|
||||
expect(c).toHaveProperty('root.request.vars.res[0].enabled', true);
|
||||
// tests
|
||||
expect(c).toHaveProperty('root.request.tests', 'test(\"collection level script\", function() {\n expect(\"test\").to.equal(\"test\");\n});');
|
||||
|
||||
/* collection items names and sequences */
|
||||
// <collection-root>/folder_2
|
||||
expect(c).toHaveProperty('items[0].type', 'folder');
|
||||
expect(c).toHaveProperty('items[0].name', 'folder_2');
|
||||
expect(c).toHaveProperty('items[0].seq', 1);
|
||||
|
||||
// <collection-root>/folder_2/request_1
|
||||
expect(c).toHaveProperty('items[0].items[0].name', 'request_1');
|
||||
expect(c).toHaveProperty('items[0].items[0].seq', 1);
|
||||
|
||||
// <collection-root>/folder_2/request_3
|
||||
expect(c).toHaveProperty('items[0].items[1].name', 'request_3');
|
||||
expect(c).toHaveProperty('items[0].items[1].seq', 2);
|
||||
|
||||
// <collection-root>/folder_2/request_2
|
||||
expect(c).toHaveProperty('items[0].items[2].name', 'request_2');
|
||||
expect(c).toHaveProperty('items[0].items[2].seq', 3);
|
||||
|
||||
// <collection-root>/folder_1
|
||||
expect(c).toHaveProperty('items[1].type', 'folder');
|
||||
expect(c).toHaveProperty('items[1].name', 'folder_1');
|
||||
expect(c).toHaveProperty('items[1].seq', 5);
|
||||
|
||||
// <collection-root>/folder_1/folder_2
|
||||
expect(c).toHaveProperty('items[1].items[0].name', 'folder_2');
|
||||
expect(c).toHaveProperty('items[1].items[0].seq', 1);
|
||||
|
||||
// <collection-root>/folder_1/folder_2/request_3
|
||||
expect(c).toHaveProperty('items[1].items[0].items[0].name', 'request_3');
|
||||
expect(c).toHaveProperty('items[1].items[0].items[0].seq', 1);
|
||||
|
||||
// <collection-root>/folder_1/folder_2/request_1
|
||||
expect(c).toHaveProperty('items[1].items[0].items[1].name', 'request_1');
|
||||
expect(c).toHaveProperty('items[1].items[0].items[1].seq', 2);
|
||||
|
||||
// <collection-root>/folder_1/folder_2/request_2
|
||||
expect(c).toHaveProperty('items[1].items[0].items[2].name', 'request_2');
|
||||
expect(c).toHaveProperty('items[1].items[0].items[2].seq', 3);
|
||||
|
||||
// <collection-root>/folder_1/folder_1
|
||||
expect(c).toHaveProperty('items[1].items[1].name', 'folder_1');
|
||||
expect(c).toHaveProperty('items[1].items[1].seq', 2);
|
||||
|
||||
// <collection-root>/folder_1/folder_1/request_3
|
||||
expect(c).toHaveProperty('items[1].items[1].items[0].name', 'request_3');
|
||||
expect(c).toHaveProperty('items[1].items[1].items[0].seq', 1);
|
||||
|
||||
// <collection-root>/folder_1/folder_1/request_2
|
||||
expect(c).toHaveProperty('items[1].items[1].items[1].name', 'request_2');
|
||||
expect(c).toHaveProperty('items[1].items[1].items[1].seq', 2);
|
||||
|
||||
// <collection-root>/folder_1/folder_1/request_1
|
||||
expect(c).toHaveProperty('items[1].items[1].items[2].name', 'request_1');
|
||||
expect(c).toHaveProperty('items[1].items[1].items[2].seq', 3);
|
||||
|
||||
// <collection-root>/folder_1/request_1
|
||||
expect(c).toHaveProperty('items[1].items[2].name', 'request_1');
|
||||
expect(c).toHaveProperty('items[1].items[2].seq', 3);
|
||||
|
||||
// <collection-root>/folder_1/request_3
|
||||
expect(c).toHaveProperty('items[1].items[3].name', 'request_3');
|
||||
expect(c).toHaveProperty('items[1].items[3].seq', 4);
|
||||
|
||||
// <collection-root>/folder_1/request_2
|
||||
expect(c).toHaveProperty('items[1].items[4].name', 'request_2');
|
||||
expect(c).toHaveProperty('items[1].items[4].seq', 5);
|
||||
|
||||
// <collection-root>/request_2
|
||||
expect(c).toHaveProperty('items[2].name', 'request_3');
|
||||
expect(c).toHaveProperty('items[2].seq', 2);
|
||||
|
||||
// <collection-root>/request_3
|
||||
expect(c).toHaveProperty('items[3].name', 'request_1');
|
||||
expect(c).toHaveProperty('items[3].seq', 3);
|
||||
|
||||
// <collection-root>/request_4
|
||||
expect(c).toHaveProperty('items[4].name', 'request_2');
|
||||
expect(c).toHaveProperty('items[4].seq', 4);
|
||||
|
||||
/* collection request item - <collection-root>/request_4 */
|
||||
// <collection-root>/request_4
|
||||
// headers
|
||||
expect(c).toHaveProperty('items[4].request.headers[0].name', 'request_header');
|
||||
expect(c).toHaveProperty('items[4].request.headers[0].value', 'request_header_value');
|
||||
expect(c).toHaveProperty('items[4].request.headers[0].enabled', true);
|
||||
// auth
|
||||
expect(c).toHaveProperty('items[4].request.auth.mode', 'basic');
|
||||
expect(c).toHaveProperty('items[4].request.auth.basic.username', 'username');
|
||||
expect(c).toHaveProperty('items[4].request.auth.basic.password', 'password');
|
||||
// pre-request scripts
|
||||
expect(c).toHaveProperty('items[4].request.script.req', 'const requestPreRequestScript = true;');
|
||||
// request items[4] - post-response scripts
|
||||
expect(c).toHaveProperty('items[4].request.script.res', 'const requestPostResponseScript = true;');
|
||||
// pre-request vars
|
||||
expect(c).toHaveProperty('items[4].request.vars.req[0].name', 'request_pre_var');
|
||||
expect(c).toHaveProperty('items[4].request.vars.req[0].value', 'request_pre_var_value');
|
||||
expect(c).toHaveProperty('items[4].request.vars.req[0].enabled', true);
|
||||
// post-response vars
|
||||
expect(c).toHaveProperty('items[4].request.vars.res[0].name', 'request_post_var');
|
||||
expect(c).toHaveProperty('items[4].request.vars.res[0].value', 'request_post_var_value');
|
||||
expect(c).toHaveProperty('items[4].request.vars.res[0].enabled', true);
|
||||
// tests
|
||||
expect(c).toHaveProperty('items[4].request.tests', 'test(\"request level script\", function() {\n expect(\"test\").to.equal(\"test\");\n});');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "collection",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
],
|
||||
"proxy": {
|
||||
"enabled": false,
|
||||
"protocol": "http",
|
||||
"hostname": "<proxy-hostname>",
|
||||
"port": 3000,
|
||||
"auth": {
|
||||
"enabled": false,
|
||||
"username": "<user-name>",
|
||||
"password": "<password>"
|
||||
},
|
||||
"bypassProxy": ""
|
||||
},
|
||||
"scripts": {
|
||||
"moduleWhitelist": ["crypto", "buffer"],
|
||||
"filesystemAccess": {
|
||||
"allow": true
|
||||
}
|
||||
},
|
||||
"clientCertificates": {
|
||||
"enabled": true,
|
||||
"certs": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
headers {
|
||||
collection_header: collection_header_value
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: username
|
||||
password: password
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
collection_pre_var: collection_pre_var_value
|
||||
}
|
||||
|
||||
vars:post-response {
|
||||
collection_post_var: collection_post_var_value
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const collectionPreRequestScript = true;
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
const collectionPostResponseScript = true;
|
||||
}
|
||||
|
||||
tests {
|
||||
test("collection level script", function() {
|
||||
expect("test").to.equal("test");
|
||||
});
|
||||
}
|
||||
|
||||
docs {
|
||||
# docs
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
meta {
|
||||
name: folder_1
|
||||
seq: 5
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
meta {
|
||||
name: folder_1
|
||||
seq: 2
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_1
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_2
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_3
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
meta {
|
||||
name: folder_2
|
||||
seq: 1
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_1
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_2
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_3
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_1
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_2
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_3
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
meta {
|
||||
name: folder_2
|
||||
seq: 1
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_1
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_2
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_3
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_1
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
meta {
|
||||
name: request_2
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com/:request_path_param?request_query_param=request_query_param_value
|
||||
body: text
|
||||
auth: basic
|
||||
}
|
||||
|
||||
params:query {
|
||||
request_query_param: request_query_param_value
|
||||
}
|
||||
|
||||
params:path {
|
||||
request_path_param: request_path_param_value
|
||||
}
|
||||
|
||||
headers {
|
||||
request_header: request_header_value
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: username
|
||||
password: password
|
||||
}
|
||||
|
||||
body:text {
|
||||
ping
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
request_pre_var: request_pre_var_value
|
||||
}
|
||||
|
||||
vars:post-response {
|
||||
request_post_var: request_post_var_value
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const requestPreRequestScript = true;
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
const requestPostResponseScript = true;
|
||||
}
|
||||
|
||||
tests {
|
||||
test("request level script", function() {
|
||||
expect("test").to.equal("test");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: request_3
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
@@ -150,6 +150,159 @@ describe('prepare-request: prepareRequest', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth2 Authentication', () => {
|
||||
it('If collection auth is OAuth2 with client credentials grant type', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: 'oauth2',
|
||||
oauth2: {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl: 'https://auth.example.com/token',
|
||||
clientId: 'test_client_id',
|
||||
clientSecret: 'test_client_secret',
|
||||
scope: 'read write',
|
||||
credentialsPlacement: 'header',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
tokenQueryKey: 'access_token'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
|
||||
expect(result.oauth2).toBeDefined();
|
||||
expect(result.oauth2.grantType).toBe('client_credentials');
|
||||
expect(result.oauth2.accessTokenUrl).toBe('https://auth.example.com/token');
|
||||
expect(result.oauth2.clientId).toBe('test_client_id');
|
||||
expect(result.oauth2.clientSecret).toBe('test_client_secret');
|
||||
expect(result.oauth2.scope).toBe('read write');
|
||||
expect(result.oauth2.credentialsPlacement).toBe('header');
|
||||
expect(result.oauth2.tokenPlacement).toBe('header');
|
||||
expect(result.oauth2.tokenHeaderPrefix).toBe('Bearer');
|
||||
expect(result.oauth2.tokenQueryKey).toBe('access_token');
|
||||
});
|
||||
|
||||
it('If collection auth is OAuth2 with password grant type', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: 'oauth2',
|
||||
oauth2: {
|
||||
grantType: 'password',
|
||||
accessTokenUrl: 'https://auth.example.com/token',
|
||||
username: 'test_user',
|
||||
password: 'test_password',
|
||||
clientId: 'test_client_id',
|
||||
clientSecret: 'test_client_secret',
|
||||
scope: 'read write',
|
||||
credentialsPlacement: 'body',
|
||||
tokenPlacement: 'url',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
tokenQueryKey: 'access_token'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
|
||||
expect(result.oauth2).toBeDefined();
|
||||
expect(result.oauth2.grantType).toBe('password');
|
||||
expect(result.oauth2.accessTokenUrl).toBe('https://auth.example.com/token');
|
||||
expect(result.oauth2.username).toBe('test_user');
|
||||
expect(result.oauth2.password).toBe('test_password');
|
||||
expect(result.oauth2.clientId).toBe('test_client_id');
|
||||
expect(result.oauth2.clientSecret).toBe('test_client_secret');
|
||||
expect(result.oauth2.scope).toBe('read write');
|
||||
expect(result.oauth2.credentialsPlacement).toBe('body');
|
||||
expect(result.oauth2.tokenPlacement).toBe('url');
|
||||
expect(result.oauth2.tokenHeaderPrefix).toBe('Bearer');
|
||||
expect(result.oauth2.tokenQueryKey).toBe('access_token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AWS v4 Authentication', () => {
|
||||
it('If collection auth is AWS v4', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: 'awsv4',
|
||||
awsv4: {
|
||||
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
||||
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||
sessionToken: 'session-token',
|
||||
service: 's3',
|
||||
region: 'us-west-2',
|
||||
profileName: 'default'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
const expected = {
|
||||
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
||||
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||
sessionToken: 'session-token',
|
||||
service: 's3',
|
||||
region: 'us-west-2',
|
||||
profileName: 'default'
|
||||
};
|
||||
expect(result.awsv4config).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NTLM Authentication', () => {
|
||||
it('If collection auth is NTLM', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: 'ntlm',
|
||||
ntlm: {
|
||||
username: 'testUser',
|
||||
password: 'testPass123',
|
||||
domain: 'testDomain'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
const expected = {
|
||||
username: 'testUser',
|
||||
password: 'testPass123',
|
||||
domain: 'testDomain'
|
||||
};
|
||||
expect(result.ntlmConfig).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WSSE Authentication', () => {
|
||||
it('If collection auth is WSSE', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: 'wsse',
|
||||
wsse: {
|
||||
username: 'testUser',
|
||||
password: 'testPass123'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
expect(result.headers).toHaveProperty('X-WSSE');
|
||||
expect(result.headers['X-WSSE']).toContain('UsernameToken Username="testUser"');
|
||||
expect(result.headers['X-WSSE']).toContain('PasswordDigest="');
|
||||
expect(result.headers['X-WSSE']).toContain('Nonce="');
|
||||
expect(result.headers['X-WSSE']).toContain('Created="');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Digest Authentication', () => {
|
||||
it('If collection auth is digest auth', () => {
|
||||
collection.root.request.auth = {
|
||||
mode: 'digest',
|
||||
digest: {
|
||||
username: 'testUser',
|
||||
password: 'testPass123'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
|
||||
const expected = {
|
||||
username: 'testUser',
|
||||
password: 'testPass123'
|
||||
};
|
||||
expect(result.digestConfig).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('No Authentication', () => {
|
||||
it('If request does not have auth configured', () => {
|
||||
delete item.request.auth;
|
||||
@@ -161,4 +314,209 @@ describe('prepare-request: prepareRequest', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Properly maps request-level auth', () => {
|
||||
let item;
|
||||
|
||||
beforeEach(() => {
|
||||
item = {
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
headers: [],
|
||||
params: [],
|
||||
url: 'https://usebruno.com',
|
||||
auth: {
|
||||
mode: 'basic' // Will be overridden in each test
|
||||
},
|
||||
script: {
|
||||
req: 'console.log("Pre Request")',
|
||||
res: 'console.log("Post Response")'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
describe('API Key Authentication', () => {
|
||||
it('If request auth is apikey in header', () => {
|
||||
item.request.auth = {
|
||||
mode: "apikey",
|
||||
apikey: {
|
||||
key: "x-api-key",
|
||||
value: "{{apiKey}}",
|
||||
placement: "header"
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item);
|
||||
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
|
||||
});
|
||||
|
||||
it('If request auth is apikey in header and request has existing headers', () => {
|
||||
item.request.auth = {
|
||||
mode: "apikey",
|
||||
apikey: {
|
||||
key: "x-api-key",
|
||||
value: "{{apiKey}}",
|
||||
placement: "header"
|
||||
}
|
||||
};
|
||||
|
||||
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
|
||||
const result = prepareRequest(item);
|
||||
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
|
||||
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
|
||||
});
|
||||
|
||||
it('If request auth is apikey in query parameters', () => {
|
||||
item.request.auth = {
|
||||
mode: "apikey",
|
||||
apikey: {
|
||||
key: "x-api-key",
|
||||
value: "{{apiKey}}",
|
||||
placement: "queryparams"
|
||||
}
|
||||
};
|
||||
|
||||
const urlObj = new URL(item.request.url);
|
||||
urlObj.searchParams.set(item.request.auth.apikey.key, item.request.auth.apikey.value);
|
||||
|
||||
const expected = urlObj.toString();
|
||||
const result = prepareRequest(item);
|
||||
expect(result.url).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Basic Authentication', () => {
|
||||
it('If request auth is basic auth', () => {
|
||||
item.request.auth = {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: 'testUser',
|
||||
password: 'testPass123'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item);
|
||||
const expected = { username: 'testUser', password: 'testPass123' };
|
||||
expect(result.basicAuth).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bearer Token Authentication', () => {
|
||||
it('If request auth is bearer token', () => {
|
||||
item.request.auth = {
|
||||
mode: 'bearer',
|
||||
bearer: {
|
||||
token: 'token123'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item);
|
||||
expect(result.headers).toHaveProperty('Authorization', 'Bearer token123');
|
||||
});
|
||||
|
||||
it('If request auth is bearer token and request has existing headers', () => {
|
||||
item.request.auth = {
|
||||
mode: 'bearer',
|
||||
bearer: {
|
||||
token: 'token123'
|
||||
}
|
||||
};
|
||||
|
||||
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
|
||||
|
||||
const result = prepareRequest(item);
|
||||
expect(result.headers).toHaveProperty('Authorization', 'Bearer token123');
|
||||
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AWS v4 Authentication', () => {
|
||||
it('If request auth is AWS v4', () => {
|
||||
item.request.auth = {
|
||||
mode: 'awsv4',
|
||||
awsv4: {
|
||||
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
||||
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||
sessionToken: 'request-session-token',
|
||||
service: 'dynamodb',
|
||||
region: 'us-east-1',
|
||||
profileName: 'dev'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item);
|
||||
const expected = {
|
||||
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
||||
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||
sessionToken: 'request-session-token',
|
||||
service: 'dynamodb',
|
||||
region: 'us-east-1',
|
||||
profileName: 'dev'
|
||||
};
|
||||
expect(result.awsv4config).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NTLM Authentication', () => {
|
||||
it('If request auth is NTLM', () => {
|
||||
item.request.auth = {
|
||||
mode: 'ntlm',
|
||||
ntlm: {
|
||||
username: 'testUser',
|
||||
password: 'testPass123',
|
||||
domain: 'testDomain'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item);
|
||||
const expected = {
|
||||
username: 'testUser',
|
||||
password: 'testPass123',
|
||||
domain: 'testDomain'
|
||||
};
|
||||
expect(result.ntlmConfig).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WSSE Authentication', () => {
|
||||
it('If request auth is WSSE', () => {
|
||||
item.request.auth = {
|
||||
mode: 'wsse',
|
||||
wsse: {
|
||||
username: 'requestUser',
|
||||
password: 'requestPass'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item);
|
||||
expect(result.headers).toHaveProperty('X-WSSE');
|
||||
expect(result.headers['X-WSSE']).toContain('UsernameToken Username="requestUser"');
|
||||
expect(result.headers['X-WSSE']).toContain('PasswordDigest="');
|
||||
expect(result.headers['X-WSSE']).toContain('Nonce="');
|
||||
expect(result.headers['X-WSSE']).toContain('Created="');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Digest Authentication', () => {
|
||||
it('If request auth is digest auth', () => {
|
||||
item.request.auth = {
|
||||
mode: 'digest',
|
||||
digest: {
|
||||
username: 'requestUser',
|
||||
password: 'requestPass123'
|
||||
}
|
||||
};
|
||||
|
||||
const result = prepareRequest(item);
|
||||
const expected = {
|
||||
username: 'requestUser',
|
||||
password: 'requestPass123'
|
||||
};
|
||||
expect(result.digestConfig).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,5 +28,8 @@
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/qs": "^6.9.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ module.exports = [
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({ tsconfig: './tsconfig.json' }),
|
||||
terser()
|
||||
]
|
||||
terser(),
|
||||
],
|
||||
external: ['axios', 'qs']
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { addDigestInterceptor } from './digestauth-helper';
|
||||
export { addDigestInterceptor } from './digestauth-helper';
|
||||
export { getOAuth2Token } from './oauth2-helper';
|
||||
199
packages/bruno-requests/src/auth/oauth2-helper.ts
Normal file
199
packages/bruno-requests/src/auth/oauth2-helper.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import qs from 'qs';
|
||||
|
||||
export interface TokenStore {
|
||||
saveToken(serviceId: string, account: string, token: any): Promise<boolean>;
|
||||
getToken(serviceId: string, account: string): Promise<any>;
|
||||
deleteToken(serviceId: string, account: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface OAuth2Config {
|
||||
grantType: 'client_credentials' | 'password';
|
||||
accessTokenUrl: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
scope?: string;
|
||||
credentialsPlacement?: 'header' | 'body';
|
||||
}
|
||||
|
||||
interface RequestConfig {
|
||||
headers: {
|
||||
'Content-Type': string;
|
||||
'Authorization'?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ClientCredentialsData {
|
||||
grant_type: string;
|
||||
scope: string;
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
interface PasswordGrantData {
|
||||
grant_type: string;
|
||||
username: string;
|
||||
password: string;
|
||||
scope: string;
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an OAuth2 token using client credentials grant
|
||||
*/
|
||||
const fetchTokenClientCredentials = async (oauth2Config: OAuth2Config) => {
|
||||
const {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement = 'header'
|
||||
} = oauth2Config;
|
||||
|
||||
if (!accessTokenUrl || !clientId) {
|
||||
throw new Error('Missing required OAuth2 parameters');
|
||||
}
|
||||
|
||||
const data: ClientCredentialsData = {
|
||||
grant_type: 'client_credentials',
|
||||
scope: scope || ''
|
||||
};
|
||||
|
||||
const config: RequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
};
|
||||
|
||||
// Handle credentials placement
|
||||
if (credentialsPlacement === 'header') {
|
||||
config.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret || ''}`).toString('base64')}`;
|
||||
} else {
|
||||
// Credentials in body
|
||||
data.client_id = clientId;
|
||||
if (clientSecret) {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(accessTokenUrl, qs.stringify(data), config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('CLIENT_CREDENTIALS: Error fetching OAuth2 token:', error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches an OAuth2 token using password grant
|
||||
*/
|
||||
const fetchTokenPassword = async (oauth2Config: OAuth2Config) => {
|
||||
const {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
username,
|
||||
password,
|
||||
scope,
|
||||
credentialsPlacement = 'header'
|
||||
} = oauth2Config;
|
||||
|
||||
if (!accessTokenUrl || !username || !password) {
|
||||
throw new Error('Missing required OAuth2 parameters for password grant');
|
||||
}
|
||||
|
||||
const data: PasswordGrantData = {
|
||||
grant_type: 'password',
|
||||
username,
|
||||
password,
|
||||
scope: scope || ''
|
||||
};
|
||||
|
||||
const config: RequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
};
|
||||
|
||||
// Handle credentials placement
|
||||
if (credentialsPlacement === 'header' && clientId) {
|
||||
config.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret || ''}`).toString('base64')}`;
|
||||
} else if (clientId) {
|
||||
// Credentials in body
|
||||
data.client_id = clientId;
|
||||
if (clientSecret) {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(accessTokenUrl, qs.stringify(data), config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError && error.response) {
|
||||
console.error('PASSWORD_GRANT: Error fetching OAuth2 token:', error.message);
|
||||
console.error('Status:', error.response.status, 'Response:', error.response.data);
|
||||
} else if (error instanceof Error) {
|
||||
console.error('PASSWORD_GRANT: Error fetching OAuth2 token:', error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages OAuth2 token retrieval and storage
|
||||
*/
|
||||
export const getOAuth2Token = async (oauth2Config: OAuth2Config, tokenStore: TokenStore): Promise<string | null> => {
|
||||
const { grantType, clientId, accessTokenUrl } = oauth2Config;
|
||||
|
||||
if (!grantType || !accessTokenUrl) {
|
||||
throw new Error('Missing required OAuth2 parameters: grantType or accessTokenUrl');
|
||||
}
|
||||
|
||||
const serviceId = accessTokenUrl;
|
||||
const account = clientId || oauth2Config.username || 'default';
|
||||
|
||||
// Check if we already have a token stored
|
||||
const existingToken = await tokenStore.getToken(serviceId, account);
|
||||
|
||||
if (existingToken) {
|
||||
// Check if token is expired
|
||||
if (existingToken.expires_at && existingToken.expires_at > Date.now()) {
|
||||
return existingToken.access_token;
|
||||
}
|
||||
}
|
||||
|
||||
// No valid token found, fetch a new one
|
||||
try {
|
||||
let tokenResponse;
|
||||
|
||||
if (grantType === 'client_credentials') {
|
||||
tokenResponse = await fetchTokenClientCredentials(oauth2Config);
|
||||
} else if (grantType === 'password') {
|
||||
tokenResponse = await fetchTokenPassword(oauth2Config);
|
||||
} else {
|
||||
throw new Error(`Unsupported grant type: ${grantType}`);
|
||||
}
|
||||
|
||||
// Calculate expiry time if expires_in is provided
|
||||
if (tokenResponse.expires_in) {
|
||||
tokenResponse.expires_at = Date.now() + tokenResponse.expires_in * 1000;
|
||||
}
|
||||
|
||||
// Store the token
|
||||
await tokenStore.saveToken(serviceId, account, tokenResponse);
|
||||
|
||||
return tokenResponse.access_token;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Failed to get OAuth2 token:', error.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
export { addDigestInterceptor } from './auth';
|
||||
export { addDigestInterceptor, getOAuth2Token } from './auth';
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
// @ts-check
|
||||
const { devices } = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
process.env.PLAYWRIGHT = "1";
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
* @type {import('@playwright/test').PlaywrightTestConfig}
|
||||
*/
|
||||
const config = {
|
||||
testDir: './tests',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: {
|
||||
...devices['Desktop Safari'],
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run dev:web',
|
||||
port: 3000,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
25
playwright.config.ts
Normal file
25
playwright.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e-tests',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? undefined : 1,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'Bruno Electron App'
|
||||
}
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'npm run dev:web',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI
|
||||
}
|
||||
});
|
||||
13
playwright/codegen.ts
Normal file
13
playwright/codegen.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
const path = require('path');
|
||||
const { startApp } = require('./electron.ts');
|
||||
|
||||
async function main() {
|
||||
const { app, context } = await startApp();
|
||||
let outputFile = process.argv[2]?.trim();
|
||||
if (outputFile && !/\.(ts|js)$/.test(outputFile)) {
|
||||
outputFile = path.join(__dirname, '../e2e-tests/', outputFile + '.spec.ts');
|
||||
}
|
||||
await context._enableRecorder({ language: 'playwright-test', mode: 'recording', outputFile });
|
||||
}
|
||||
|
||||
main();
|
||||
13
playwright/electron.ts
Normal file
13
playwright/electron.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
const path = require('path');
|
||||
const { _electron: electron } = require('playwright');
|
||||
|
||||
const electronAppPath = path.join(__dirname, '../packages/bruno-electron');
|
||||
|
||||
exports.startApp = async () => {
|
||||
const app = await electron.launch({ args: [electronAppPath] });
|
||||
const context = await app.context();
|
||||
|
||||
app.process().stdout.on('data', (data) => console.log(data.toString()));
|
||||
app.process().stderr.on('data', (error) => console.error(error.toString()));
|
||||
return { app, context };
|
||||
};
|
||||
23
playwright/index.ts
Normal file
23
playwright/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { test as baseTest, ElectronApplication, Page } from '@playwright/test';
|
||||
|
||||
const { startApp } = require('./electron.ts');
|
||||
|
||||
export const test = baseTest.extend<{ page: Page }, { electronApp: ElectronApplication }>({
|
||||
electronApp: [
|
||||
async ({}, use) => {
|
||||
const { app: electronApp, context } = await startApp();
|
||||
|
||||
await use(electronApp);
|
||||
await context.close();
|
||||
await electronApp.close();
|
||||
},
|
||||
{ scope: 'worker' }
|
||||
],
|
||||
page: async ({ electronApp }, use) => {
|
||||
const page = await electronApp.firstWindow();
|
||||
await use(page);
|
||||
await page.reload();
|
||||
}
|
||||
});
|
||||
|
||||
export * from '@playwright/test'
|
||||
@@ -1,17 +0,0 @@
|
||||
const path = require('path');
|
||||
const timer = require('node:timers/promises');
|
||||
const { _electron: electron } = require('playwright');
|
||||
|
||||
const electronAppPath = path.join(__dirname, '../packages/bruno-electron');
|
||||
|
||||
(async () => {
|
||||
const browser = await electron.launch({ args: [electronAppPath] });
|
||||
const context = await browser.context();
|
||||
await context.route('**/*', (route) => route.continue());
|
||||
|
||||
while (true) {
|
||||
if(browser.windows().length) break;
|
||||
await timer.setTimeout(200);
|
||||
}
|
||||
await browser.windows()[0].pause();
|
||||
})();
|
||||
@@ -1,48 +0,0 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { HomePage } = require('../tests/pages/home.page');
|
||||
const { faker } = require('./utils/data-faker');
|
||||
|
||||
test.describe('bruno e2e test', () => {
|
||||
let homePage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
homePage = new HomePage(page);
|
||||
|
||||
await homePage.open();
|
||||
await expect(page).toHaveURL('/');
|
||||
await expect(page).toHaveTitle(/bruno/);
|
||||
});
|
||||
|
||||
test('user should be able to create new collection & new request', async () => {
|
||||
await homePage.createNewCollection(faker.randomWords);
|
||||
await expect(homePage.createNewCollectionSuccessToast).toBeVisible();
|
||||
|
||||
// using fake data to simulate negative case
|
||||
await homePage.createNewRequest(faker.randomVerb, faker.randomHttpMethod, faker.randomUrl);
|
||||
await expect(homePage.networkErrorToast).toBeVisible();
|
||||
|
||||
// using real data to simulate positive case
|
||||
await homePage.createNewRequest('Single User', 'GET', 'https://reqres.in/api/users/2');
|
||||
await expect(homePage.statusRequestSuccess).toBeVisible();
|
||||
});
|
||||
|
||||
test('user should be able to load & use sample collection', async () => {
|
||||
await homePage.loadSampleCollection();
|
||||
await expect(homePage.loadSampleCollectionSuccessToast).toBeVisible();
|
||||
|
||||
await homePage.getUsers();
|
||||
await expect(homePage.statusRequestSuccess).toBeVisible();
|
||||
|
||||
await homePage.getSingleUser();
|
||||
await expect(homePage.statusRequestSuccess).toBeVisible();
|
||||
|
||||
await homePage.getUserNotFound();
|
||||
await expect(homePage.statusRequestNotFound).toBeVisible();
|
||||
|
||||
await homePage.createUser();
|
||||
await expect(homePage.statusRequestCreated).toBeVisible();
|
||||
|
||||
await homePage.updateUser();
|
||||
await expect(homePage.statusRequestSuccess).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
exports.HomePage = class HomePage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
|
||||
// welcome
|
||||
this.createCollectionSelector = page.locator('#create-collection');
|
||||
this.addCollectionSelector = page.locator('#add-collection');
|
||||
this.importCollectionSelector = page.locator('#import-collection');
|
||||
this.loadSampleCollectionSelector = page.locator('#load-sample-collection');
|
||||
|
||||
// sample collection
|
||||
this.loadSampleCollectionSuccessToast = page.getByText('Sample Collection loaded successfully');
|
||||
this.sampleCollectionSelector = page.locator('#sidebar-collection-name');
|
||||
this.getUsersSelector = page.getByText('Users');
|
||||
this.getSingleUserSelector = page.getByText('Single User');
|
||||
this.getUserNotFoundSelector = page.getByText('User Not Found');
|
||||
this.postCreateSelector = page.getByText('Create');
|
||||
this.putUpdateSelector = page.getByText('Update');
|
||||
|
||||
// request panel
|
||||
this.sendRequestButton = page.locator('#send-request');
|
||||
this.statusRequestSuccess = page.getByText('200 OK');
|
||||
this.statusRequestNotFound = page.getByText('404 Not Found');
|
||||
this.statusRequestCreated = page.getByText('201 Created');
|
||||
|
||||
// create collection
|
||||
this.collectionNameField = page.locator('#collection-name');
|
||||
this.submitButton = page.locator(`button[type='submit']`);
|
||||
this.createNewCollectionSuccessToast = page.getByText('Collection created');
|
||||
this.createNewTab = page.locator('#create-new-tab');
|
||||
this.requestNameField = page.locator('input[name="requestName"]');
|
||||
this.methodName = page.locator('#create-new-request-method').first();
|
||||
this.requestUrlField = page.locator('#request-url');
|
||||
this.networkErrorToast = page.getByText('Network Error');
|
||||
}
|
||||
|
||||
async open() {
|
||||
await this.page.goto('/');
|
||||
}
|
||||
|
||||
async loadSampleCollection() {
|
||||
await this.loadSampleCollectionSelector.click();
|
||||
}
|
||||
|
||||
async getUsers() {
|
||||
await this.sampleCollectionSelector.click();
|
||||
await this.getUsersSelector.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
async getSingleUser() {
|
||||
await this.getSingleUserSelector.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
async getUserNotFound() {
|
||||
await this.getUserNotFoundSelector.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
async createUser() {
|
||||
await this.postCreateSelector.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
async updateUser() {
|
||||
await this.putUpdateSelector.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
|
||||
async createNewCollection(collectionName) {
|
||||
await this.createCollectionSelector.click();
|
||||
await this.collectionNameField.fill(collectionName);
|
||||
await this.submitButton.click();
|
||||
}
|
||||
|
||||
async createNewRequest(name, method, endpoint) {
|
||||
await this.createNewTab.click();
|
||||
await this.requestNameField.fill(name);
|
||||
await this.methodName.click();
|
||||
await this.page.click(`text=${method}`);
|
||||
await this.requestUrlField.fill(endpoint);
|
||||
await this.submitButton.click();
|
||||
await this.sendRequestButton.click();
|
||||
}
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
const { faker } = require('@faker-js/faker');
|
||||
|
||||
export let randomWords = faker.random.words();
|
||||
export let randomVerb = faker.hacker.verb();
|
||||
export let randomHttpMethod = faker.internet.httpMethod();
|
||||
export let randomUrl = faker.internet.url();
|
||||
Reference in New Issue
Block a user