mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 09:28:33 +00:00
Compare commits
71 Commits
v2.3.0
...
feature/pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a006fe8230 | ||
|
|
577d54b432 | ||
|
|
afaebf6b3d | ||
|
|
6e89001825 | ||
|
|
e7dd78ea53 | ||
|
|
9ad0f2d169 | ||
|
|
bf19645282 | ||
|
|
bb01199877 | ||
|
|
5627c5624f | ||
|
|
8e23a7054f | ||
|
|
d820069371 | ||
|
|
2de9b87c6f | ||
|
|
178773d63a | ||
|
|
7994946c85 | ||
|
|
b020255269 | ||
|
|
73b0f0919d | ||
|
|
8975b9eef6 | ||
|
|
865e813b42 | ||
|
|
51f36b1903 | ||
|
|
6b122d7262 | ||
|
|
a8e5ce9c13 | ||
|
|
8ac916b0ff | ||
|
|
8d860a051c | ||
|
|
4ac2c4ac34 | ||
|
|
7c27193983 | ||
|
|
2c3d2ff6a7 | ||
|
|
a4fff01647 | ||
|
|
2cd985faf7 | ||
|
|
9a35302d4b | ||
|
|
553f7675f2 | ||
|
|
b299879b82 | ||
|
|
3696562414 | ||
|
|
e02c6c274b | ||
|
|
ab0a4b8140 | ||
|
|
1b268ae9db | ||
|
|
8debb9fd11 | ||
|
|
7c07488e16 | ||
|
|
6073a9e2c3 | ||
|
|
9c652f86c9 | ||
|
|
3c0090d86f | ||
|
|
9132755d49 | ||
|
|
2a44691cb3 | ||
|
|
0d8a696498 | ||
|
|
bfa2706598 | ||
|
|
5fdb52388a | ||
|
|
799dc9a1ca | ||
|
|
2bb56e8a4b | ||
|
|
084d2bf692 | ||
|
|
10640c7561 | ||
|
|
9f044c48fe | ||
|
|
79f4e69a05 | ||
|
|
f13148af3d | ||
|
|
d2eb2d2941 | ||
|
|
fbd3a38587 | ||
|
|
45b660985e | ||
|
|
0888125899 | ||
|
|
c85d9bcd84 | ||
|
|
c14d3f4274 | ||
|
|
5a4e33e503 | ||
|
|
5649799167 | ||
|
|
0f6da35c0b | ||
|
|
0d7c94e7e9 | ||
|
|
9e29821012 | ||
|
|
e0fb379511 | ||
|
|
ba9362ccb2 | ||
|
|
261a36c435 | ||
|
|
cb92e46f8d | ||
|
|
f6ab59ceda | ||
|
|
f1004e2e36 | ||
|
|
26eaec4c72 | ||
|
|
d0419edb92 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno
|
||||
44
.github/workflows/playwright.yml
vendored
44
.github/workflows/playwright.yml
vendored
@@ -1,44 +0,0 @@
|
||||
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
|
||||
38
.github/workflows/tests.yml
vendored
38
.github/workflows/tests.yml
vendored
@@ -91,5 +91,43 @@ jobs:
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
check_name: CLI Test Results
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
comment_mode: always
|
||||
e2e-test:
|
||||
name: Playwright E2E Tests
|
||||
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
|
||||
|
||||
@@ -98,6 +98,15 @@ npm run dev:electron
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### Customize Electron `userData` path
|
||||
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
|
||||
|
||||
e.g.
|
||||
```sh
|
||||
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
|
||||
```
|
||||
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
|
||||
|
||||
5
e2e-tests/001-sanity-tests/001-home-screen.spec.ts
Normal file
5
e2e-tests/001-sanity-tests/001-home-screen.spec.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Check if the logo on top left is visible', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
|
||||
});
|
||||
31
e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts
Normal file
31
e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Create new collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').click();
|
||||
await page.getByLabel('Name').fill('test-collection');
|
||||
await page.getByLabel('Name').press('Tab');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
await page.getByText('test-collection').click();
|
||||
await page.getByLabel('Safe ModeBETA').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByPlaceholder('Request Name').fill('r1');
|
||||
await page.getByPlaceholder('Request URL').click();
|
||||
await page.getByPlaceholder('Request URL').fill('http://localhost:8081');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.locator('pre').filter({ hasText: 'http://localhost:' }).click();
|
||||
await page.locator('textarea').fill('/ping');
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
|
||||
await page.getByRole('tab', { name: 'GET r1' }).locator('circle').click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await page.getByText('GETr1').click();
|
||||
await page.getByRole('button', { name: 'Clear response' }).click();
|
||||
await page.locator('body').press('ControlOrMeta+Enter');
|
||||
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": ["{{projectRoot}}/packages/bruno-tests/collection"]
|
||||
}
|
||||
49
e2e-tests/bruno-testbench/run-testbench-requests.spec.ts
Normal file
49
e2e-tests/bruno-testbench/run-testbench-requests.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test.describe.parallel('Run Testbench Requests', () => {
|
||||
test('Run bruno-testbench in Developer Mode', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(2 * 60 * 1000);
|
||||
|
||||
await page.getByText('bruno-testbench').click();
|
||||
await page.getByLabel('Developer Mode(use only if').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('.environment-selector').nth(1).click();
|
||||
await page.locator('.dropdown-item').getByText('Prod').click();
|
||||
await page.locator('.collection-actions').hover();
|
||||
await page.locator('.collection-actions .icon').click();
|
||||
await page.getByText('Run', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Run Collection' }).click();
|
||||
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
|
||||
|
||||
const result = await page.getByText('Total Requests: ').innerText();
|
||||
const [totalRequests, passed, failed, skipped] = result
|
||||
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
|
||||
});
|
||||
|
||||
test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(2 * 60 * 1000);
|
||||
|
||||
await page.getByText('bruno-testbench').click();
|
||||
await page.getByLabel('Safe ModeBETA').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('.environment-selector').nth(1).click();
|
||||
await page.locator('.dropdown-item').getByText('Prod').click();
|
||||
await page.locator('.collection-actions').hover();
|
||||
await page.locator('.collection-actions .icon').click();
|
||||
await page.getByText('Run', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Run Collection' }).click();
|
||||
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
|
||||
|
||||
const result = await page.getByText('Total Requests: ').innerText();
|
||||
const [totalRequests, passed, failed, skipped] = result
|
||||
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Should verify all support links with correct URL in preference > Support tab', async ({ page }) => {
|
||||
|
||||
// Open Preferences
|
||||
await page.getByLabel('Open Preferences').click();
|
||||
|
||||
// Verify Support tab
|
||||
await page.getByRole('tab', { name: 'Support' }).click();
|
||||
|
||||
const locator_twitter = page.getByRole('link', { name: 'Twitter' });
|
||||
expect(await locator_twitter.getAttribute('href')).toEqual('https://twitter.com/use_bruno');
|
||||
|
||||
const locator_github = page.getByRole('link', { name: 'GitHub', exact: true });
|
||||
expect(await locator_github.getAttribute('href')).toEqual('https://github.com/usebruno/bruno');
|
||||
|
||||
const locator_discord = page.getByRole('link', { name: 'Discord', exact: true });
|
||||
expect(await locator_discord.getAttribute('href')).toEqual('https://discord.com/invite/KgcZUncpjq');
|
||||
|
||||
const locator_reportissues = page.getByRole('link', { name: 'Report Issues', exact: true });
|
||||
expect(await locator_reportissues.getAttribute('href')).toEqual('https://github.com/usebruno/bruno/issues');
|
||||
|
||||
const locator_documentation = page.getByRole('link', { name: 'Documentation', exact: true });
|
||||
expect(await locator_documentation.getAttribute('href')).toEqual('https://docs.usebruno.com');
|
||||
|
||||
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { test, expect } from '../playwright';
|
||||
|
||||
test('test-app-start', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
|
||||
});
|
||||
@@ -38,4 +38,4 @@ module.exports = defineConfig([
|
||||
"no-undef": "error",
|
||||
},
|
||||
}
|
||||
]);
|
||||
]);
|
||||
@@ -71,4 +71,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,7 @@ if (!SERVER_RENDERED) {
|
||||
'req.getTimeout()',
|
||||
'req.setTimeout(timeout)',
|
||||
'req.getExecutionMode()',
|
||||
'req.getName()',
|
||||
'bru',
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName()',
|
||||
@@ -80,12 +81,14 @@ if (!SERVER_RENDERED) {
|
||||
'bru.getAssertionResults()',
|
||||
'bru.getTestResults()',
|
||||
'bru.sleep(ms)',
|
||||
'bru.getCollectionName()',
|
||||
'bru.getGlobalEnvVar(key)',
|
||||
'bru.setGlobalEnvVar(key, value)',
|
||||
'bru.runner',
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()'
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.interpolate(str)'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
@@ -363,7 +366,7 @@ export default class CodeEditor extends React.Component {
|
||||
let variables = getAllVariables(this.props.collection, this.props.item);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode);
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const auth = get(collection, 'root.request.auth', {}).mode;
|
||||
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
@@ -155,7 +155,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{auth !== 'none' && <ContentIndicator />}
|
||||
{authMode !== 'none' && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
|
||||
Script
|
||||
|
||||
@@ -11,6 +11,12 @@ const Wrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
.inherit-mode-text {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -9,6 +9,14 @@ import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/Passwo
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
import AuthMode from '../AuthMode';
|
||||
import BasicAuth from 'components/RequestPane/Auth/BasicAuth';
|
||||
import BearerAuth from 'components/RequestPane/Auth/BearerAuth';
|
||||
import DigestAuth from 'components/RequestPane/Auth/DigestAuth';
|
||||
import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
|
||||
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
|
||||
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
|
||||
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
|
||||
import { findItemInCollection, findParentItemInCollection, humanizeRequestAuthMode } from 'utils/collections/index';
|
||||
|
||||
const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -37,12 +45,132 @@ const Auth = ({ collection, folder }) => {
|
||||
let request = get(folder, 'root.request', {});
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
|
||||
const getTreePathFromCollectionToFolder = (collection, _folder) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _folder?.uid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Get path from collection to current folder
|
||||
const folderTreePath = getTreePathFromCollectionToFolder(collection, folder);
|
||||
|
||||
// Check parent folders to find closest auth configuration
|
||||
// Skip the last item which is the current folder
|
||||
for (let i = 0; i < folderTreePath.length - 1; i++) {
|
||||
const parentFolder = folderTreePath[i];
|
||||
if (parentFolder.type === 'folder') {
|
||||
const folderAuth = get(parentFolder, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: parentFolder.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'basic': {
|
||||
return (
|
||||
<BasicAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'bearer': {
|
||||
return (
|
||||
<BearerAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'digest': {
|
||||
return (
|
||||
<DigestAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'ntlm': {
|
||||
return (
|
||||
<NTLMAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'wsse': {
|
||||
return (
|
||||
<WsseAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'apikey': {
|
||||
return (
|
||||
<ApiKeyAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'awsv4': {
|
||||
return (
|
||||
<AwsV4Auth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'oauth2': {
|
||||
return (
|
||||
<>
|
||||
@@ -56,6 +184,17 @@ const Auth = ({ collection, folder }) => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'none': {
|
||||
return null;
|
||||
}
|
||||
@@ -64,6 +203,7 @@ const Auth = ({ collection, folder }) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
|
||||
@@ -35,6 +35,51 @@ const AuthMode = ({ collection, folder }) => {
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('awsv4');
|
||||
}}
|
||||
>
|
||||
AWS Sig v4
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('basic');
|
||||
}}
|
||||
>
|
||||
Basic Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('bearer');
|
||||
}}
|
||||
>
|
||||
Bearer Token
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('digest');
|
||||
}}
|
||||
>
|
||||
Digest Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('ntlm');
|
||||
}}
|
||||
>
|
||||
NTLM Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
@@ -44,6 +89,33 @@ const AuthMode = ({ collection, folder }) => {
|
||||
>
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('wsse');
|
||||
}}
|
||||
>
|
||||
WSSE Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('apikey');
|
||||
}}
|
||||
>
|
||||
API Key
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('inherit');
|
||||
}}
|
||||
>
|
||||
Inherit
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
||||
@@ -130,7 +130,7 @@ class MultiLineEditor extends Component {
|
||||
|
||||
addOverlay = (variables) => {
|
||||
this.variables = variables;
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', false, true);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
|
||||
@@ -5,21 +5,23 @@ import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
|
||||
|
||||
const ApiKeyAuth = ({ item, collection }) => {
|
||||
const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const apikeyAuth = item.draft ? get(item, 'draft.request.auth.apikey', {}) : get(item, 'request.auth.apikey', {});
|
||||
const apikeyAuth = get(request, 'auth.apikey', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
@@ -90,7 +92,7 @@ const ApiKeyAuth = ({ item, collection }) => {
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
dropdownTippyRef?.current?.hide();
|
||||
handleAuthChange('placement', 'header');
|
||||
}}
|
||||
>
|
||||
@@ -99,11 +101,11 @@ const ApiKeyAuth = ({ item, collection }) => {
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
dropdownTippyRef?.current?.hide();
|
||||
handleAuthChange('placement', 'queryparams');
|
||||
}}
|
||||
>
|
||||
Query Params
|
||||
Query Param
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -8,14 +8,17 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { update } from 'lodash';
|
||||
|
||||
const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = item.draft ? get(item, 'draft.request.auth.awsv4', {}) : get(item, 'request.auth.awsv4', {});
|
||||
const awsv4Auth = get(request, 'auth.awsv4', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleAccessKeyIdChange = (accessKeyId) => {
|
||||
dispatch(
|
||||
|
||||
@@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BasicAuth = ({ item, collection }) => {
|
||||
const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = item.draft ? get(item, 'draft.request.auth.basic', {}) : get(item, 'request.auth.basic', {});
|
||||
const basicAuth = get(request, 'auth.basic', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -7,16 +7,18 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BearerAuth = ({ item, collection }) => {
|
||||
const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const bearerToken = item.draft
|
||||
? get(item, 'draft.request.auth.bearer.token', '')
|
||||
: get(item, 'request.auth.bearer.token', '');
|
||||
// Use the request prop directly like OAuth2ClientCredentials does
|
||||
const bearerToken = get(request, 'auth.bearer.token', '');
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleTokenChange = (token) => {
|
||||
dispatch(
|
||||
|
||||
@@ -3,18 +3,20 @@ import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DigestAuth = ({ item, collection }) => {
|
||||
const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = item.draft ? get(item, 'draft.request.auth.digest', {}) : get(item, 'request.auth.digest', {});
|
||||
const digestAuth = get(request, 'auth.digest', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NTLMAuth = ({ item, collection }) => {
|
||||
const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = item.draft ? get(item, 'draft.request.auth.ntlm', {}) : get(item, 'request.auth.ntlm', {});
|
||||
const ntlmAuth = get(request, 'auth.ntlm', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
@@ -26,7 +29,6 @@ const NTLMAuth = ({ item, collection }) => {
|
||||
username: username,
|
||||
password: ntlmAuth.password,
|
||||
domain: ntlmAuth.domain
|
||||
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -7,14 +7,17 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WsseAuth = ({ item, collection }) => {
|
||||
const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = item.draft ? get(item, 'draft.request.auth.wsse', {}) : get(item, 'request.auth.wsse', {});
|
||||
const wsseAuth = get(request, 'auth.wsse', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUserChange = (username) => {
|
||||
dispatch(
|
||||
@@ -55,6 +58,7 @@ const WsseAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handleUserChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +71,8 @@ const WsseAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -7,6 +7,8 @@ import BasicAuth from './BasicAuth';
|
||||
import DigestAuth from './DigestAuth';
|
||||
import WsseAuth from './WsseAuth';
|
||||
import NTLMAuth from './NTLMAuth';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import ApiKeyAuth from './ApiKeyAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -27,6 +29,16 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
const Auth = ({ item, collection }) => {
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
// Create a request object to pass to the auth components
|
||||
const request = item.draft
|
||||
? get(item, 'draft.request', {})
|
||||
: get(item, 'request', {});
|
||||
|
||||
// Save function for request level
|
||||
const save = () => {
|
||||
return saveRequest(item.uid, collection.uid);
|
||||
};
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
@@ -59,28 +71,28 @@ const Auth = ({ item, collection }) => {
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'awsv4': {
|
||||
return <AwsV4Auth collection={collection} item={item} />;
|
||||
return <AwsV4Auth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'basic': {
|
||||
return <BasicAuth collection={collection} item={item} />;
|
||||
return <BasicAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'bearer': {
|
||||
return <BearerAuth collection={collection} item={item} />;
|
||||
return <BearerAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'digest': {
|
||||
return <DigestAuth collection={collection} item={item} />;
|
||||
return <DigestAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'ntlm': {
|
||||
return <NTLMAuth collection={collection} item={item} />;
|
||||
return <NTLMAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} item={item} />;
|
||||
return <OAuth2 collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'wsse': {
|
||||
return <WsseAuth collection={collection} item={item} />;
|
||||
return <WsseAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'apikey': {
|
||||
return <ApiKeyAuth collection={collection} item={item} />;
|
||||
return <ApiKeyAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
|
||||
@@ -64,9 +64,10 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
mode="application/json"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
item={item}
|
||||
item={item}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
@@ -58,13 +58,14 @@ const RequestBody = ({ item, collection }) => {
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
mode={codeMirrorMode[bodyMode]}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (bodyMode === 'file') {
|
||||
return <FileBody item={item} collection={collection}/>
|
||||
return <FileBody item={item} collection={collection} />;
|
||||
}
|
||||
|
||||
if (bodyMode === 'formUrlEncoded') {
|
||||
@@ -77,4 +78,4 @@ const RequestBody = ({ item, collection }) => {
|
||||
|
||||
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
|
||||
};
|
||||
export default RequestBody;
|
||||
export default RequestBody;
|
||||
|
||||
@@ -17,7 +17,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
|
||||
<div className="overlay">
|
||||
<div style={{ marginBottom: 15, fontSize: 26 }}>
|
||||
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>
|
||||
<StopWatch requestTimestamp={item?.requestSent?.timestamp} />
|
||||
<StopWatch startTime={item?.requestStartTime} />
|
||||
</div>
|
||||
</div>
|
||||
<IconRefresh size={24} className="loading-icon" />
|
||||
|
||||
@@ -4,12 +4,60 @@ import CodeView from './CodeView';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isValidUrl } from 'utils/url';
|
||||
import { get } from 'lodash';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import { findEnvironmentInCollection, findItemInCollection, findParentItemInCollection } from 'utils/collections';
|
||||
import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _itemUid) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _itemUid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
// Function to resolve inherited auth
|
||||
const resolveInheritedAuth = (item, collection) => {
|
||||
const request = item.draft?.request || item.request;
|
||||
const authMode = request?.auth?.mode;
|
||||
|
||||
// If auth is not inherit or no auth defined, return the request as is
|
||||
if (!authMode || authMode !== 'inherit') {
|
||||
return {
|
||||
...request
|
||||
};
|
||||
}
|
||||
|
||||
// Get the tree path from collection to item
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item.uid);
|
||||
|
||||
// Default to collection auth
|
||||
const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
|
||||
let effectiveAuth = collectionAuth;
|
||||
let source = 'collection';
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveAuth = folderAuth;
|
||||
source = 'folder';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
auth: effectiveAuth
|
||||
};
|
||||
};
|
||||
|
||||
const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
const languages = getLanguages();
|
||||
|
||||
@@ -46,6 +94,9 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params')
|
||||
);
|
||||
|
||||
// Resolve auth inheritance
|
||||
const resolvedRequest = resolveInheritedAuth(item, collection);
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
|
||||
return (
|
||||
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
|
||||
@@ -94,16 +145,10 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
language={selectedLanguage}
|
||||
item={{
|
||||
...item,
|
||||
request:
|
||||
item.request.url !== ''
|
||||
? {
|
||||
...item.request,
|
||||
url: finalUrl
|
||||
}
|
||||
: {
|
||||
...item.draft.request,
|
||||
url: finalUrl
|
||||
}
|
||||
request: {
|
||||
...resolvedRequest,
|
||||
url: finalUrl
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -146,7 +146,7 @@ class SingleLineEditor extends Component {
|
||||
|
||||
addOverlay = (variables) => {
|
||||
this.variables = variables;
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams);
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams, true);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const StopWatch = () => {
|
||||
const [milliseconds, setMilliseconds] = useState(0);
|
||||
|
||||
const tickInterval = 100;
|
||||
const tick = () => {
|
||||
setMilliseconds(_milliseconds => _milliseconds + tickInterval);
|
||||
};
|
||||
|
||||
const StopWatch = ({ startTime }) => {
|
||||
const [currentTime, setCurrentTime] = useState(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
let timerID = setInterval(() => {
|
||||
tick()
|
||||
}, tickInterval);
|
||||
return () => {
|
||||
clearTimeout(timerID);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (milliseconds < 250) {
|
||||
return 'Loading...';
|
||||
}
|
||||
|
||||
let seconds = milliseconds / 1000;
|
||||
if (!startTime) return;
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
setCurrentTime(Date.now());
|
||||
}, 100);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [startTime]);
|
||||
|
||||
if (!startTime) return <span>Loading...</span>;
|
||||
|
||||
const elapsedTime = currentTime - startTime;
|
||||
if (elapsedTime < 250) return <span>Loading...</span>;
|
||||
|
||||
const seconds = elapsedTime / 1000;
|
||||
return <span>{seconds.toFixed(1)}s</span>;
|
||||
};
|
||||
|
||||
|
||||
@@ -211,13 +211,15 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [activeTabUid, tabs, collections, dispatch]);
|
||||
|
||||
const currentCollection = getCurrentCollection();
|
||||
|
||||
return (
|
||||
<HotkeysContext.Provider {...props} value="hotkey">
|
||||
{showEnvSettingsModal && (
|
||||
<EnvironmentSettings collection={getCurrentCollection()} onClose={() => setShowEnvSettingsModal(false)} />
|
||||
<EnvironmentSettings collection={currentCollection} onClose={() => setShowEnvSettingsModal(false)} />
|
||||
)}
|
||||
{showNewRequestModal && (
|
||||
<NewRequest collection={getCurrentCollection()} onClose={() => setShowNewRequestModal(false)} />
|
||||
<NewRequest collectionUid={currentCollection?.uid} onClose={() => setShowNewRequestModal(false)} />
|
||||
)}
|
||||
<div>{props.children}</div>
|
||||
</HotkeysContext.Provider>
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
responseReceived,
|
||||
updateLastAction,
|
||||
setCollectionSecurityConfig,
|
||||
setRequestStartTime,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl
|
||||
} from './index';
|
||||
@@ -221,6 +222,12 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
dispatch(setRequestStartTime({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
@@ -381,7 +388,12 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) =>
|
||||
meta: {
|
||||
name: folderName,
|
||||
seq: items?.length + 1
|
||||
}
|
||||
},
|
||||
request: {
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ipcRenderer
|
||||
@@ -417,7 +429,12 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) =>
|
||||
meta: {
|
||||
name: folderName,
|
||||
seq: items?.length + 1
|
||||
}
|
||||
},
|
||||
request: {
|
||||
auth: {
|
||||
mode: 'inherit'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ipcRenderer
|
||||
|
||||
@@ -1593,6 +1593,27 @@ export const collectionsSlice = createSlice({
|
||||
case 'oauth2':
|
||||
set(folder, 'root.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
case 'basic':
|
||||
set(folder, 'root.request.auth.basic', action.payload.content);
|
||||
break;
|
||||
case 'bearer':
|
||||
set(folder, 'root.request.auth.bearer', action.payload.content);
|
||||
break;
|
||||
case 'digest':
|
||||
set(folder, 'root.request.auth.digest', action.payload.content);
|
||||
break;
|
||||
case 'ntlm':
|
||||
set(folder, 'root.request.auth.ntlm', action.payload.content);
|
||||
break;
|
||||
case 'apikey':
|
||||
set(folder, 'root.request.auth.apikey', action.payload.content);
|
||||
break;
|
||||
case 'awsv4':
|
||||
set(folder, 'root.request.auth.awsv4', action.payload.content);
|
||||
break;
|
||||
case 'wsse':
|
||||
set(folder, 'root.request.auth.wsse', action.payload.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2084,6 +2105,17 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
setRequestStartTime: (state, action) => {
|
||||
const { itemUid, collectionUid, timestamp } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
item.requestStartTime = timestamp;
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionAddOauth2CredentialsByUrl: (state, action) => {
|
||||
const { collectionUid, folderUid, itemUid, url, credentials, credentialsId, debugInfo } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -2165,6 +2197,7 @@ export const collectionsSlice = createSlice({
|
||||
);
|
||||
return oauth2Credential;
|
||||
},
|
||||
|
||||
updateFolderAuthMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
@@ -2173,8 +2206,9 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'root.request.auth', {});
|
||||
set(folder, 'root.request.auth.mode', action.payload.mode);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export const {
|
||||
@@ -2280,12 +2314,13 @@ export const {
|
||||
resetCollectionRunner,
|
||||
updateRequestDocs,
|
||||
updateFolderDocs,
|
||||
moveCollection,
|
||||
setRequestStartTime,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
collectionGetOauth2CredentialsByUrl,
|
||||
updateFolderAuth,
|
||||
updateFolderAuthMode,
|
||||
moveCollection
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
||||
@@ -74,11 +74,11 @@ export class MaskedEditor {
|
||||
} else {
|
||||
for (let line = 0; line < lineCount; line++) {
|
||||
const lineLength = this.editor.getLine(line).length;
|
||||
const maskedNode = document.createTextNode('*'.repeat(lineLength));
|
||||
const maskedNode = document.createTextNode('*'.repeat(lineLength));
|
||||
this.editor.markText(
|
||||
{ line, ch: 0 },
|
||||
{ line, ch: lineLength },
|
||||
{ replacedWith: maskedNode, handleMouseEvents: false }
|
||||
{ replacedWith: maskedNode, handleMouseEvents: false }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,18 @@ export class MaskedEditor {
|
||||
};
|
||||
}
|
||||
|
||||
export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams) => {
|
||||
/**
|
||||
* Defines a custom CodeMirror mode for Bruno variables highlighting.
|
||||
* This function creates a specialized mode that can highlight both Bruno template
|
||||
* variables (in the format {{variable}}) and URL path parameters (in the format /:param).
|
||||
*
|
||||
* @param {Object} _variables - The variables object containing data to validate against
|
||||
* @param {string} mode - The base CodeMirror mode to extend (e.g., 'javascript', 'application/json')
|
||||
* @param {boolean} highlightPathParams - Whether to highlight URL path parameters
|
||||
* @param {boolean} highlightVariables - Whether to highlight template variables
|
||||
* @returns {void} - Registers the mode with CodeMirror for later use
|
||||
*/
|
||||
export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams, highlightVariables) => {
|
||||
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
|
||||
const { pathParams = {}, ...variables } = _variables || {};
|
||||
const variablesOverlay = {
|
||||
@@ -139,13 +150,15 @@ export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPa
|
||||
}
|
||||
};
|
||||
|
||||
let baseMode = CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
|
||||
let baseMode = CodeMirror.getMode(config, parserConfig.backdrop || mode);
|
||||
|
||||
if (highlightPathParams) {
|
||||
return CodeMirror.overlayMode(baseMode, urlPathParamsOverlay);
|
||||
} else {
|
||||
return baseMode;
|
||||
if (highlightVariables) {
|
||||
baseMode = CodeMirror.overlayMode(baseMode, variablesOverlay);
|
||||
}
|
||||
if (highlightPathParams) {
|
||||
baseMode = CodeMirror.overlayMode(baseMode, urlPathParamsOverlay);
|
||||
}
|
||||
return baseMode;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -83,29 +83,40 @@ export const normalizeFileName = (name) => {
|
||||
};
|
||||
|
||||
export const getContentType = (headers) => {
|
||||
const headersArray = typeof headers === 'object' ? Object.entries(headers) : [];
|
||||
|
||||
if (headersArray.length > 0) {
|
||||
let contentType = headersArray
|
||||
.filter((header) => header[0].toLowerCase() === 'content-type')
|
||||
.map((header) => {
|
||||
return header[1];
|
||||
});
|
||||
if (contentType && contentType.length) {
|
||||
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
|
||||
return 'application/ld+json';
|
||||
} else if (typeof contentType[0] === 'string' && /^image\/svg\+xml/i.test(contentType[0])) {
|
||||
return 'image/svg+xml';
|
||||
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
|
||||
return 'application/xml';
|
||||
}
|
||||
|
||||
return contentType[0];
|
||||
}
|
||||
// Return empty string for invalid headers
|
||||
if (!headers || typeof headers !== 'object' || Object.keys(headers).length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
// Get content-type header value
|
||||
const contentTypeHeader = Object.entries(headers)
|
||||
.find(([key]) => key.toLowerCase() === 'content-type');
|
||||
|
||||
const contentType = contentTypeHeader && contentTypeHeader[1];
|
||||
|
||||
// Return empty string if no content-type or not a string
|
||||
if (!contentType || typeof contentType !== 'string') {
|
||||
return '';
|
||||
}
|
||||
// This pattern matches content types like application/json, application/ld+json, text/json, etc.
|
||||
const JSON_PATTERN = /^[\w\-]+\/([\w\-]+\+)?json/;
|
||||
// This pattern matches content types like image/svg.
|
||||
const SVG_PATTERN = /^image\/svg/i;
|
||||
// This pattern matches content types like application/xml, text/xml, application/atom+xml, etc.
|
||||
const XML_PATTERN = /^[\w\-]+\/([\w\-]+\+)?xml/;
|
||||
|
||||
if (JSON_PATTERN.test(contentType)) {
|
||||
return 'application/ld+json';
|
||||
} else if (SVG_PATTERN.test(contentType)) {
|
||||
return 'image/svg+xml';
|
||||
} else if (XML_PATTERN.test(contentType)) {
|
||||
return 'application/xml';
|
||||
}
|
||||
|
||||
return contentType;
|
||||
}
|
||||
|
||||
|
||||
export const startsWith = (str, search) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
import { normalizeFileName, startsWith, humanizeDate, relativeDate } from './index';
|
||||
import { normalizeFileName, startsWith, humanizeDate, relativeDate, getContentType } from './index';
|
||||
|
||||
describe('common utils', () => {
|
||||
describe('normalizeFileName', () => {
|
||||
@@ -107,4 +107,45 @@ describe('common utils', () => {
|
||||
expect(relativeDate(date)).toBe('2 months ago');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContentType', () => {
|
||||
it('should handle JSON content types correctly', () => {
|
||||
expect(getContentType({ 'content-type': 'application/json' })).toBe('application/ld+json');
|
||||
expect(getContentType({ 'content-type': 'text/json' })).toBe('application/ld+json');
|
||||
expect(getContentType({ 'content-type': 'application/ld+json' })).toBe('application/ld+json');
|
||||
});
|
||||
|
||||
it('should handle XML content types correctly', () => {
|
||||
expect(getContentType({ 'content-type': 'text/xml' })).toBe('application/xml');
|
||||
expect(getContentType({ 'content-type': 'application/xml' })).toBe('application/xml');
|
||||
expect(getContentType({ 'content-type': 'application/atom+xml' })).toBe('application/xml');
|
||||
});
|
||||
|
||||
it('should handle image content types correctly', () => {
|
||||
expect(getContentType({ 'content-type': 'image/svg+xml;charset=utf-8' })).toBe('image/svg+xml');
|
||||
expect(getContentType({ 'content-type': 'IMAGE/SVG+xml' })).toBe('image/svg+xml');
|
||||
});
|
||||
|
||||
it('should return original content type when no pattern matches', () => {
|
||||
expect(getContentType({ 'content-type': 'image/jpeg' })).toBe('image/jpeg');
|
||||
expect(getContentType({ 'content-type': 'application/pdf' })).toBe('application/pdf');
|
||||
});
|
||||
|
||||
it('should not be case sensitive', () => {
|
||||
expect(getContentType({ 'content-type': 'text/json' })).toBe('application/ld+json');
|
||||
expect(getContentType({ 'Content-Type': 'text/json' })).toBe('application/ld+json');
|
||||
});
|
||||
|
||||
it('should handle empty content type', () => {
|
||||
expect(getContentType({ 'content-type': '' })).toBe('');
|
||||
expect(getContentType({ 'content-type': null })).toBe('');
|
||||
expect(getContentType({ 'content-type': undefined })).toBe('');
|
||||
});
|
||||
|
||||
it('should handle empty or invalid inputs', () => {
|
||||
expect(getContentType({})).toBe('');
|
||||
expect(getContentType(null)).toBe('');
|
||||
expect(getContentType(undefined)).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,7 +183,13 @@ const curlToJson = (curlCommand) => {
|
||||
|
||||
if (request.query) {
|
||||
requestJson.queries = getQueries(request);
|
||||
} else if (request.multipartUploads || request.isDataBinary) {
|
||||
} else if (request.multipartUploads) {
|
||||
requestJson.data = request.multipartUploads;
|
||||
if (!requestJson.headers) {
|
||||
requestJson.headers = {};
|
||||
}
|
||||
requestJson.headers['Content-Type'] = 'multipart/form-data';
|
||||
} else if (request.isDataBinary) {
|
||||
Object.assign(requestJson, getFilesString(request));
|
||||
} else if (typeof request.data === 'string' || typeof request.data === 'number') {
|
||||
Object.assign(requestJson, getDataString(request));
|
||||
|
||||
@@ -37,7 +37,8 @@ const parseCurlCommand = (curlCommand) => {
|
||||
alias: {
|
||||
H: 'header',
|
||||
A: 'user-agent',
|
||||
u: 'user'
|
||||
u: 'user',
|
||||
F: 'form'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -95,17 +96,31 @@ const parseCurlCommand = (curlCommand) => {
|
||||
cookieString = parsedArguments.cookie;
|
||||
}
|
||||
let multipartUploads;
|
||||
if (parsedArguments.F) {
|
||||
multipartUploads = {};
|
||||
if (!Array.isArray(parsedArguments.F)) {
|
||||
parsedArguments.F = [parsedArguments.F];
|
||||
}
|
||||
parsedArguments.F.forEach((multipartArgument) => {
|
||||
// input looks like key=value. value could be json or a file path prepended with an @
|
||||
const splitArguments = multipartArgument.split('=', 2);
|
||||
const key = splitArguments[0];
|
||||
const value = splitArguments[1];
|
||||
multipartUploads[key] = value;
|
||||
// Handle multipart form data specified via -F or --form flags
|
||||
// Example: curl -F 'id=123' -F 'file=@/path/to/file.txt'
|
||||
if (parsedArguments.F || parsedArguments.form) {
|
||||
multipartUploads = [];
|
||||
const formArgs = parsedArguments.F || parsedArguments.form;
|
||||
const formArray = Array.isArray(formArgs) ? formArgs : [formArgs];
|
||||
|
||||
formArray.forEach((multipartArgument) => {
|
||||
// Parse each form field using regex:
|
||||
// - Group 1: Field name before =
|
||||
// - Group 2: Value in quotes after = (for text fields)
|
||||
// - Group 3: Value after @ (for file fields)
|
||||
const match = multipartArgument.match(/^([^=]+)=(?:@?"([^"]*)"|([^@]*))?$/);
|
||||
if (match) {
|
||||
const key = match[1];
|
||||
const value = match[2] || match[3] || '';
|
||||
const isFile = multipartArgument.includes('@');
|
||||
|
||||
multipartUploads.push({
|
||||
name: key,
|
||||
value: value,
|
||||
type: isFile ? 'file' : 'text',
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (cookieString) {
|
||||
|
||||
145
packages/bruno-app/src/utils/curl/parse-curl.spec.js
Normal file
145
packages/bruno-app/src/utils/curl/parse-curl.spec.js
Normal file
@@ -0,0 +1,145 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
import parseCurlCommand from './parse-curl';
|
||||
|
||||
describe('parseCurlCommand', () => {
|
||||
describe('basic functionality', () => {
|
||||
it('should handle basic GET request', () => {
|
||||
const result = parseCurlCommand('curl https://api.example.com/users');
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com/users',
|
||||
urlWithoutQuery: 'https://api.example.com/users',
|
||||
method: 'get'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse explicit POST method', () => {
|
||||
const result = parseCurlCommand('curl -X POST https://api.example.com/users');
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com/users',
|
||||
urlWithoutQuery: 'https://api.example.com/users',
|
||||
method: 'post'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('headers handling', () => {
|
||||
it('should parse multiple headers', () => {
|
||||
const result = parseCurlCommand(
|
||||
`curl -H 'Content-Type: application/json' -H 'Authorization: Bearer token' https://api.example.com`
|
||||
);
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com',
|
||||
urlWithoutQuery: 'https://api.example.com',
|
||||
method: 'get',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer token'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse user-agent', () => {
|
||||
const result = parseCurlCommand(`curl -A 'Custom Agent' https://api.example.com`);
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com',
|
||||
urlWithoutQuery: 'https://api.example.com',
|
||||
method: 'get',
|
||||
headers: {
|
||||
'User-Agent': 'Custom Agent'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('auth handling', () => {
|
||||
it('should parse basic auth', () => {
|
||||
const result = parseCurlCommand(`curl -u user:pass https://api.example.com`);
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com',
|
||||
urlWithoutQuery: 'https://api.example.com',
|
||||
method: 'get',
|
||||
auth: {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: 'user',
|
||||
password: 'pass'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data handling', () => {
|
||||
it('should parse POST data', () => {
|
||||
const result = parseCurlCommand(`curl -d 'foo=bar&baz=qux' https://api.example.com`);
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com',
|
||||
urlWithoutQuery: 'https://api.example.com',
|
||||
method: 'post',
|
||||
data: 'foo=bar&baz=qux'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle data-binary', () => {
|
||||
const result = parseCurlCommand(`curl --data-binary '@file.json' https://api.example.com`);
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com',
|
||||
urlWithoutQuery: 'https://api.example.com',
|
||||
method: 'post',
|
||||
data: '@file.json',
|
||||
isDataBinary: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('form data handling', () => {
|
||||
it('should parse complex form data with multiple fields and file upload', () => {
|
||||
const curlCommand = `curl --location 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d' \
|
||||
--form 'id="1"' \
|
||||
--form 'documentid="ADMINN_ID"' \
|
||||
--form 'appoinID="12376"' \
|
||||
--form 'autoclose="false"' \
|
||||
--form 'fileData=@"/path/to/file"'`;
|
||||
|
||||
const result = parseCurlCommand(curlCommand);
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d',
|
||||
urlWithoutQuery: 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d',
|
||||
method: 'post',
|
||||
multipartUploads: [
|
||||
{
|
||||
name: 'id',
|
||||
value: '1',
|
||||
type: 'text',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: 'documentid',
|
||||
value: 'ADMINN_ID',
|
||||
type: 'text',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: 'appoinID',
|
||||
value: '12376',
|
||||
type: 'text',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: 'autoclose',
|
||||
value: 'false',
|
||||
type: 'text',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
name: 'fileData',
|
||||
value: '/path/to/file',
|
||||
type: 'file',
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -164,6 +164,11 @@ const builder = async (yargs) => {
|
||||
type: 'string',
|
||||
description: 'Path to the Client certificate config file used for securing the connection in the request'
|
||||
})
|
||||
.option('--noproxy', {
|
||||
type: 'boolean',
|
||||
description: 'Disable all proxy settings (both collection-defined and system proxies)',
|
||||
default: false
|
||||
})
|
||||
.option('delay', {
|
||||
type:"number",
|
||||
description: "Delay between each requests (in miliseconds)"
|
||||
@@ -197,7 +202,6 @@ const builder = async (yargs) => {
|
||||
'$0 run request.bru --reporter-junit results.xml --reporter-html results.html',
|
||||
'Run a request and write the results to results.html in html format and results.xml in junit format in the current directory'
|
||||
)
|
||||
|
||||
.example('$0 run request.bru --tests-only', 'Run all requests that have a test')
|
||||
.example(
|
||||
'$0 run request.bru --cacert myCustomCA.pem',
|
||||
@@ -208,7 +212,8 @@ const builder = async (yargs) => {
|
||||
'Use a custom CA certificate exclusively when validating the peers of the requests in the specified folder.'
|
||||
)
|
||||
.example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations')
|
||||
.example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.');
|
||||
.example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.')
|
||||
.example('$0 run --noproxy', 'Run requests with system proxy disabled');
|
||||
};
|
||||
|
||||
const handler = async function (argv) {
|
||||
@@ -233,6 +238,7 @@ const handler = async function (argv) {
|
||||
reporterSkipAllHeaders,
|
||||
reporterSkipHeaders,
|
||||
clientCertConfig,
|
||||
noproxy,
|
||||
delay
|
||||
} = argv;
|
||||
const collectionPath = process.cwd();
|
||||
@@ -339,6 +345,9 @@ const handler = async function (argv) {
|
||||
if (disableCookies) {
|
||||
options['disableCookies'] = true;
|
||||
}
|
||||
if (noproxy) {
|
||||
options['noproxy'] = true;
|
||||
}
|
||||
if (cacert && cacert.length) {
|
||||
if (insecure) {
|
||||
console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`));
|
||||
@@ -608,7 +617,7 @@ const handler = async function (argv) {
|
||||
}
|
||||
}
|
||||
|
||||
if (summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) {
|
||||
if ((summary.failedAssertions + summary.failedTests + summary.failedRequests > 0) || (summary?.errorRequests > 0)) {
|
||||
process.exit(constants.EXIT_STATUS.ERROR_FAILED_COLLECTION);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -32,6 +32,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: headers,
|
||||
name: item.name,
|
||||
pathParams: request?.params?.filter((param) => param.type === 'path'),
|
||||
responseType: 'arraybuffer'
|
||||
};
|
||||
|
||||
@@ -58,6 +58,7 @@ const runSingleRequest = async function (
|
||||
|
||||
// run pre request script
|
||||
const requestScriptFile = get(request, 'script.req');
|
||||
const collectionName = collection?.brunoConfig?.name
|
||||
if (requestScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await scriptRuntime.runRequestScript(
|
||||
@@ -69,7 +70,8 @@ const runSingleRequest = async function (
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
runSingleRequestByPathname,
|
||||
collectionName
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
@@ -115,6 +117,7 @@ const runSingleRequest = async function (
|
||||
|
||||
const options = getOptions();
|
||||
const insecure = get(options, 'insecure', false);
|
||||
const noproxy = get(options, 'noproxy', false);
|
||||
const httpsAgentRequestFields = {};
|
||||
if (insecure) {
|
||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
||||
@@ -179,15 +182,22 @@ const runSingleRequest = async function (
|
||||
|
||||
const collectionProxyConfig = get(brunoConfig, 'proxy', {});
|
||||
const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', false);
|
||||
if (collectionProxyEnabled === true) {
|
||||
|
||||
if (noproxy) {
|
||||
// If noproxy flag is set, don't use any proxy
|
||||
proxyMode = 'off';
|
||||
} else if (collectionProxyEnabled === true) {
|
||||
// If collection proxy is enabled, use it
|
||||
proxyConfig = collectionProxyConfig;
|
||||
proxyMode = 'on';
|
||||
} else {
|
||||
// if the collection level proxy is not set, pick the system level proxy by default, to maintain backward compatibility
|
||||
} else if (collectionProxyEnabled === 'global') {
|
||||
// If collection proxy is set to 'global', use system proxy
|
||||
const { http_proxy, https_proxy } = getSystemProxyEnvVariables();
|
||||
if (http_proxy?.length || https_proxy?.length) {
|
||||
proxyMode = 'system';
|
||||
}
|
||||
} else {
|
||||
proxyMode = 'off';
|
||||
}
|
||||
|
||||
if (proxyMode === 'on') {
|
||||
@@ -304,6 +314,14 @@ const runSingleRequest = async function (
|
||||
}
|
||||
}
|
||||
|
||||
let requestMaxRedirects = request.maxRedirects
|
||||
request.maxRedirects = 0
|
||||
|
||||
// Set default value for requestMaxRedirects if not explicitly set
|
||||
if (requestMaxRedirects === undefined) {
|
||||
requestMaxRedirects = 5; // Default to 5 redirects
|
||||
}
|
||||
|
||||
// Handle OAuth2 authentication
|
||||
if (request.oauth2) {
|
||||
try {
|
||||
@@ -334,7 +352,7 @@ const runSingleRequest = async function (
|
||||
let response, responseTime;
|
||||
try {
|
||||
|
||||
let axiosInstance = makeAxiosInstance();
|
||||
let axiosInstance = makeAxiosInstance({ requestMaxRedirects: requestMaxRedirects, disableCookies: options.disableCookies });
|
||||
if (request.ntlmConfig) {
|
||||
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults)
|
||||
delete request.ntlmConfig;
|
||||
@@ -452,7 +470,8 @@ const runSingleRequest = async function (
|
||||
null,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
runSingleRequestByPathname,
|
||||
collectionName
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
@@ -502,7 +521,8 @@ const runSingleRequest = async function (
|
||||
null,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
runSingleRequestByPathname,
|
||||
collectionName
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
const axios = require('axios');
|
||||
const { CLI_VERSION } = require('../constants');
|
||||
const { addCookieToJar, getCookieStringForUrl } = require('./cookies');
|
||||
|
||||
const redirectResponseCodes = [301, 302, 303, 307, 308];
|
||||
const METHOD_CHANGING_REDIRECTS = [301, 302, 303];
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
if (headers['set-cookie']) {
|
||||
let setCookieHeaders = Array.isArray(headers['set-cookie'])
|
||||
? headers['set-cookie']
|
||||
: [headers['set-cookie']];
|
||||
for (let setCookieHeader of setCookieHeaders) {
|
||||
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
|
||||
addCookieToJar(setCookieHeader, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createRedirectConfig = (error, redirectUrl) => {
|
||||
const requestConfig = {
|
||||
...error.config,
|
||||
url: redirectUrl,
|
||||
headers: { ...error.config.headers }
|
||||
};
|
||||
|
||||
const statusCode = error.response.status;
|
||||
const originalMethod = (error.config.method || 'get').toLowerCase();
|
||||
|
||||
// For 301, 302, 303: change method to GET unless it was HEAD
|
||||
if (METHOD_CHANGING_REDIRECTS.includes(statusCode) && originalMethod !== 'head') {
|
||||
requestConfig.method = 'get';
|
||||
requestConfig.data = undefined;
|
||||
|
||||
// Clean up headers that are no longer relevant
|
||||
delete requestConfig.headers['content-length'];
|
||||
delete requestConfig.headers['Content-Length'];
|
||||
delete requestConfig.headers['content-type'];
|
||||
delete requestConfig.headers['Content-Type'];
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that configures axios with timing interceptors
|
||||
@@ -7,10 +49,13 @@ const { CLI_VERSION } = require('../constants');
|
||||
* @see https://github.com/axios/axios/issues/695
|
||||
* @returns {axios.AxiosInstance}
|
||||
*/
|
||||
function makeAxiosInstance() {
|
||||
function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies } = {}) {
|
||||
let redirectCount = 0;
|
||||
|
||||
/** @type {axios.AxiosInstance} */
|
||||
const instance = axios.create({
|
||||
proxy: false,
|
||||
maxRedirects: 0,
|
||||
headers: {
|
||||
"User-Agent": `bruno-runtime/${CLI_VERSION}`
|
||||
}
|
||||
@@ -18,6 +63,15 @@ function makeAxiosInstance() {
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
config.headers['request-start-time'] = Date.now();
|
||||
|
||||
// Add cookies to request if available and not disabled
|
||||
if (!disableCookies) {
|
||||
const cookieString = getCookieStringForUrl(config.url);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
config.headers['cookie'] = cookieString;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
@@ -26,6 +80,8 @@ function makeAxiosInstance() {
|
||||
const end = Date.now();
|
||||
const start = response.config.headers['request-start-time'];
|
||||
response.headers['request-duration'] = end - start;
|
||||
redirectCount = 0;
|
||||
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
@@ -33,6 +89,42 @@ function makeAxiosInstance() {
|
||||
const end = Date.now();
|
||||
const start = error.config.headers['request-start-time'];
|
||||
error.response.headers['request-duration'] = end - start;
|
||||
|
||||
if (redirectResponseCodes.includes(error.response.status)) {
|
||||
if (redirectCount >= requestMaxRedirects) {
|
||||
// todo: needs to be discussed whether the original error response message should be modified or not
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const locationHeader = error.response.headers.location;
|
||||
if (!locationHeader) {
|
||||
// todo: needs to be discussed whether the original error response message should be modified or not
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
redirectCount++;
|
||||
let redirectUrl = locationHeader;
|
||||
|
||||
if (!locationHeader.match(/^https?:\/\//i)) {
|
||||
const URL = require('url');
|
||||
redirectUrl = URL.resolve(error.config.url, locationHeader);
|
||||
}
|
||||
|
||||
if (!disableCookies){
|
||||
saveCookies(redirectUrl, error.response.headers);
|
||||
}
|
||||
|
||||
const requestConfig = createRedirectConfig(error, redirectUrl);
|
||||
|
||||
if (!disableCookies) {
|
||||
const cookieString = getCookieStringForUrl(redirectUrl);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
requestConfig.headers['cookie'] = cookieString;
|
||||
}
|
||||
}
|
||||
|
||||
return instance(requestConfig);
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { Cookie, CookieJar } = require('tough-cookie');
|
||||
const each = require('lodash/each');
|
||||
const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils;
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
@@ -11,7 +12,9 @@ const addCookieToJar = (setCookieHeader, requestUrl) => {
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (url) => {
|
||||
return cookieJar.getCookiesSync(url);
|
||||
return cookieJar.getCookiesSync(url, {
|
||||
secure: isPotentiallyTrustworthyOrigin(url)
|
||||
});
|
||||
};
|
||||
|
||||
const getCookieStringForUrl = (url) => {
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
import { mockDataFunctions } from "./faker-functions";
|
||||
|
||||
describe("mockDataFunctions Regex Validation", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-01T00:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test("timestamp and isoTimestamp should return mocked time values", () => {
|
||||
const expectedTimestamp = '1704067200';
|
||||
const expectedIsoTimestamp = '2024-01-01T00:00:00.000Z';
|
||||
|
||||
expect(mockDataFunctions.timestamp()).toBe(expectedTimestamp);
|
||||
expect(mockDataFunctions.isoTimestamp()).toBe(expectedIsoTimestamp);
|
||||
});
|
||||
|
||||
test("all values should match their expected patterns", () => {
|
||||
const patterns: Record<string, RegExp> = {
|
||||
guid: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/,
|
||||
timestamp: /^\d{13,}$/,
|
||||
isoTimestamp: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
|
||||
randomUUID: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/,
|
||||
randomAlphaNumeric: /^[\w]$/,
|
||||
randomBoolean: /^(true|false)$/,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { faker } from '@faker-js/faker';
|
||||
|
||||
export const mockDataFunctions = {
|
||||
guid: () => faker.string.uuid(),
|
||||
timestamp: () => faker.date.anytime().getTime().toString(),
|
||||
isoTimestamp: () => faker.date.anytime().toISOString(),
|
||||
timestamp: () => Math.floor(Date.now() / 1000).toString(),
|
||||
isoTimestamp: () => new Date().toISOString(),
|
||||
randomUUID: () => faker.string.uuid(),
|
||||
randomAlphaNumeric: () => faker.string.alphanumeric(),
|
||||
randomBoolean: () => faker.datatype.boolean(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
|
||||
|
||||
const parseGraphQL = (text) => {
|
||||
@@ -288,6 +289,9 @@ const parseInsomniaCollection = (data) => {
|
||||
|
||||
export const insomniaToBruno = (insomniaCollection) => {
|
||||
try {
|
||||
if(typeof insomniaCollection !== 'object') {
|
||||
insomniaCollection = jsyaml.load(insomniaCollection);
|
||||
}
|
||||
let collection;
|
||||
if (isInsomniaV5Export(insomniaCollection)) {
|
||||
collection = parseInsomniaV5Collection(insomniaCollection);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
|
||||
|
||||
const ensureUrl = (url) => {
|
||||
@@ -7,14 +8,22 @@ const ensureUrl = (url) => {
|
||||
return url.replace(/([^:])\/{2,}/g, '$1/');
|
||||
};
|
||||
|
||||
const buildEmptyJsonBody = (bodySchema) => {
|
||||
const buildEmptyJsonBody = (bodySchema, visited = new Map()) => {
|
||||
// Check for circular references
|
||||
if (visited.has(bodySchema)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Add this schema to visited map
|
||||
visited.set(bodySchema, true);
|
||||
|
||||
let _jsonBody = {};
|
||||
each(bodySchema.properties || {}, (prop, name) => {
|
||||
if (prop.type === 'object') {
|
||||
_jsonBody[name] = buildEmptyJsonBody(prop);
|
||||
_jsonBody[name] = buildEmptyJsonBody(prop, visited);
|
||||
} else if (prop.type === 'array') {
|
||||
if (prop.items && prop.items.type === 'object') {
|
||||
_jsonBody[name] = [buildEmptyJsonBody(prop.items)];
|
||||
_jsonBody[name] = [buildEmptyJsonBody(prop.items, visited)];
|
||||
} else {
|
||||
_jsonBody[name] = [];
|
||||
}
|
||||
@@ -422,6 +431,10 @@ export const parseOpenApiCollection = (data) => {
|
||||
|
||||
export const openApiToBruno = (openApiSpecification) => {
|
||||
try {
|
||||
if(typeof openApiSpecification !== 'object') {
|
||||
openApiSpecification = jsyaml.load(openApiSpecification);
|
||||
}
|
||||
|
||||
const collection = parseOpenApiCollection(openApiSpecification);
|
||||
const transformedCollection = transformItemsInCollection(collection);
|
||||
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
|
||||
|
||||
@@ -5,6 +5,7 @@ const replacements = {
|
||||
'pm\\.environment\\.set\\(': 'bru.setEnvVar(',
|
||||
'pm\\.variables\\.get\\(': 'bru.getVar(',
|
||||
'pm\\.variables\\.set\\(': 'bru.setVar(',
|
||||
'pm\\.variables\\.replaceIn\\(': 'bru.interpolate(',
|
||||
'pm\\.collectionVariables\\.get\\(': 'bru.getVar(',
|
||||
'pm\\.collectionVariables\\.set\\(': 'bru.setVar(',
|
||||
'pm\\.collectionVariables\\.has\\(': 'bru.hasVar(',
|
||||
@@ -33,6 +34,7 @@ const replacements = {
|
||||
'pm\\.request\\.method': 'req.getMethod()',
|
||||
'pm\\.request\\.headers': 'req.getHeaders()',
|
||||
'pm\\.request\\.body': 'req.getBody()',
|
||||
'pm\\.info\\.requestName': 'req.getName()',
|
||||
// deprecated translations
|
||||
'postman\\.setEnvironmentVariable\\(': 'bru.setEnvVar(',
|
||||
'postman\\.getEnvironmentVariable\\(': 'bru.getEnvVar(',
|
||||
|
||||
@@ -52,7 +52,7 @@ const simpleTranslations = {
|
||||
'pm.variables.get': 'bru.getVar',
|
||||
'pm.variables.set': 'bru.setVar',
|
||||
'pm.variables.has': 'bru.hasVar',
|
||||
|
||||
'pm.variables.replaceIn': 'bru.interpolate',
|
||||
// Collection variables
|
||||
'pm.collectionVariables.get': 'bru.getVar',
|
||||
'pm.collectionVariables.set': 'bru.setVar',
|
||||
@@ -67,6 +67,9 @@ const simpleTranslations = {
|
||||
'pm.expect': 'expect',
|
||||
'pm.expect.fail': 'expect.fail',
|
||||
|
||||
// Info
|
||||
'pm.info.requestName': 'req.getName()',
|
||||
|
||||
// Request properties
|
||||
'pm.request.url': 'req.getUrl()',
|
||||
'pm.request.method': 'req.getMethod()',
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import insomniaToBruno from '../../src/insomnia/insomnia-to-bruno';
|
||||
import jsyaml from 'js-yaml';
|
||||
|
||||
describe('insomnia-collection', () => {
|
||||
it('should correctly import a valid Insomnia v5 collection file', async () => {
|
||||
const brunoCollection = insomniaToBruno(jsyaml.load(insomniaCollection));
|
||||
const brunoCollection = insomniaToBruno(insomniaCollection);
|
||||
|
||||
expect(brunoCollection).toMatchObject(expectedOutput)
|
||||
});
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import openApiToBruno from '../../../src/openapi/openapi-to-bruno';
|
||||
|
||||
describe('openapi-circular-references', () => {
|
||||
it('should handle simple circular references in schema correctly', async () => {
|
||||
const brunoCollection = openApiToBruno(circularRefsData);
|
||||
|
||||
expect(brunoCollection).toMatchObject(circularRefsOutput);
|
||||
});
|
||||
|
||||
it('should handle complex circular reference chains correctly', async () => {
|
||||
const brunoCollection = openApiToBruno(complexCircularRefsData);
|
||||
|
||||
expect(brunoCollection).toMatchObject(circularRefsOutput);
|
||||
});
|
||||
});
|
||||
|
||||
const circularRefsData = {
|
||||
"components": {
|
||||
"schemas": {
|
||||
"schema_1": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_1",
|
||||
"properties": {
|
||||
"conditions": {
|
||||
"$ref": "#/components/schemas/schema_1"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_2": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_2",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_1",
|
||||
"items": { "$ref": "#/components/schemas/schema_1" },
|
||||
"type": "array"
|
||||
},
|
||||
"operation": {
|
||||
"description": "operation",
|
||||
"enum": ["ANY", "ALL"],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"description": "circular reference openapi sample json spec",
|
||||
"title": "circular reference openapi sample json spec",
|
||||
"version": "0.1"
|
||||
},
|
||||
"openapi": "3.0.1",
|
||||
"paths": {
|
||||
"/": {
|
||||
"post": {
|
||||
"deprecated": false,
|
||||
"description": "echo ping api",
|
||||
"operationId": "echo ping",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/schema_1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "echo ping api",
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": "ping"
|
||||
}
|
||||
},
|
||||
"description": "Returned if the request is successful."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [{ "url": "https://echo.usebruno.com" }]
|
||||
};
|
||||
|
||||
// More complex circular reference test with a longer chain
|
||||
const complexCircularRefsData = {
|
||||
"components": {
|
||||
"schemas": {
|
||||
"schema_1": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_1",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_1",
|
||||
"items": { "$ref": "#/components/schemas/schema_2" },
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_2": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_2",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_2",
|
||||
"items": { "$ref": "#/components/schemas/schema_3" },
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_3": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_3",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_3",
|
||||
"items": { "$ref": "#/components/schemas/schema_4" },
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_4": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_4",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_4",
|
||||
"items": { "$ref": "#/components/schemas/schema_5" },
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_5": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_4",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_5",
|
||||
"items": { "$ref": "#/components/schemas/schema_1" },
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema_6": {
|
||||
"additionalProperties": false,
|
||||
"description": "schema_3",
|
||||
"properties": {
|
||||
"conditionGroup": {
|
||||
"description": "nested schema_3",
|
||||
"items": { "$ref": "#/components/schemas/schema_1" },
|
||||
"type": "array"
|
||||
},
|
||||
"operation": {
|
||||
"description": "operation",
|
||||
"enum": ["ANY", "ALL"],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"description": "circular reference openapi sample json spec",
|
||||
"title": "circular reference openapi sample json spec",
|
||||
"version": "0.1"
|
||||
},
|
||||
"openapi": "3.0.1",
|
||||
"paths": {
|
||||
"/": {
|
||||
"post": {
|
||||
"deprecated": false,
|
||||
"description": "echo ping api",
|
||||
"operationId": "echo ping",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/schema_1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "echo ping api",
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": "ping"
|
||||
}
|
||||
},
|
||||
"description": "Returned if the request is successful."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [{ "url": "https://echo.usebruno.com" }]
|
||||
};
|
||||
|
||||
const circularRefsOutput = {
|
||||
"environments": [
|
||||
{
|
||||
"name": "Environment 1",
|
||||
"variables": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "baseUrl",
|
||||
"secret": false,
|
||||
"type": "text",
|
||||
"value": "https://echo.usebruno.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"name": "echo ping",
|
||||
"type": "http-request",
|
||||
"request": {
|
||||
"url": "{{baseUrl}}/",
|
||||
"method": "POST",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "json",
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
"name": "circular reference openapi sample json spec",
|
||||
"version": "1",
|
||||
};
|
||||
@@ -1,11 +1,9 @@
|
||||
import jsyaml from 'js-yaml';
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import openApiToBruno from '../../src/openapi/openapi-to-bruno';
|
||||
import openApiToBruno from '../../../src/openapi/openapi-to-bruno';
|
||||
|
||||
describe('openapi-collection', () => {
|
||||
it('should correctly import a valid OpenAPI file', async () => {
|
||||
const openApiSpecification = jsyaml.load(openApiCollectionString);
|
||||
const brunoCollection = openApiToBruno(openApiSpecification);
|
||||
const brunoCollection = openApiToBruno(openApiCollectionString);
|
||||
|
||||
expect(brunoCollection).toMatchObject(expectedOutput);
|
||||
});
|
||||
@@ -7,6 +7,7 @@ describe('postmanTranslations - request commands', () => {
|
||||
const requestMethod = pm.request.method;
|
||||
const requestHeaders = pm.request.headers;
|
||||
const requestBody = pm.request.body;
|
||||
const requestName = pm.info.requestName;
|
||||
|
||||
pm.test('Request method is POST', function() {
|
||||
pm.expect(pm.request.method).to.equal('POST');
|
||||
@@ -17,6 +18,7 @@ describe('postmanTranslations - request commands', () => {
|
||||
const requestMethod = req.getMethod();
|
||||
const requestHeaders = req.getHeaders();
|
||||
const requestBody = req.getBody();
|
||||
const requestName = req.getName();
|
||||
|
||||
test('Request method is POST', function() {
|
||||
expect(req.getMethod()).to.equal('POST');
|
||||
|
||||
@@ -16,8 +16,8 @@ describe('postmanTranslations - comment handling', () => {
|
||||
});
|
||||
|
||||
test('should comment non-translated pm commands', () => {
|
||||
const inputScript = "pm.test('random test', () => postman.variables.replaceIn('{{$guid}}'));";
|
||||
const expectedOutput = "// test('random test', () => pm.variables.replaceIn('{{$guid}}'));";
|
||||
const inputScript = "pm.test('random test', () => pm.cookies.get('cookieName'));";
|
||||
const expectedOutput = "// test('random test', () => pm.cookies.get('cookieName'));";
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,55 +5,104 @@ describe('Variables Translation', () => {
|
||||
it('should translate pm.variables.get', () => {
|
||||
const code = 'pm.variables.get("test");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.getVar("test");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.set', () => {
|
||||
const code = 'pm.variables.set("test", "value");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.setVar("test", "value");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.has', () => {
|
||||
const code = 'pm.variables.has("userId");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.hasVar("userId");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.replaceIn', () => {
|
||||
const code = 'pm.variables.replaceIn("Hello {{name}}");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.interpolate("Hello {{name}}");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.replaceIn with variables and expressions', () => {
|
||||
const code = 'const greeting = pm.variables.replaceIn("Hello {{name}}, your user id is {{userId}}");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('const greeting = bru.interpolate("Hello {{name}}, your user id is {{userId}}");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.replaceIn within complex expressions', () => {
|
||||
const code = 'const url = baseUrl + pm.variables.replaceIn("/users/{{userId}}/profile");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('const url = baseUrl + bru.interpolate("/users/{{userId}}/profile");');
|
||||
});
|
||||
|
||||
it('should translate pm.variables.replaceIn with multiple nested variable references', () => {
|
||||
const code = 'const template = pm.variables.replaceIn("{{prefix}}-{{env}}-{{suffix}}");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('const template = bru.interpolate("{{prefix}}-{{env}}-{{suffix}}");');
|
||||
});
|
||||
|
||||
it('should translate aliased variables.replaceIn', () => {
|
||||
const code = `
|
||||
const variables = pm.variables;
|
||||
const message = variables.replaceIn("Welcome, {{username}}!");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe(`
|
||||
const message = bru.interpolate("Welcome, {{username}}!");
|
||||
`);
|
||||
});
|
||||
|
||||
// Collection variables tests
|
||||
it('should translate pm.collectionVariables.get', () => {
|
||||
const code = 'pm.collectionVariables.get("apiUrl");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.getVar("apiUrl");');
|
||||
});
|
||||
|
||||
it('should translate pm.collectionVariables.set', () => {
|
||||
const code = 'pm.collectionVariables.set("token", jsonData.token);';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.setVar("token", jsonData.token);');
|
||||
});
|
||||
|
||||
it('should translate pm.collectionVariables.has', () => {
|
||||
const code = 'pm.collectionVariables.has("authToken");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.hasVar("authToken");');
|
||||
});
|
||||
|
||||
it('should translate pm.collectionVariables.unset', () => {
|
||||
const code = 'pm.collectionVariables.unset("tempVar");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.deleteVar("tempVar");');
|
||||
});
|
||||
|
||||
it('should handle pm.globals.get', () => {
|
||||
const code = 'pm.globals.get("test");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.getGlobalEnvVar("test");');
|
||||
});
|
||||
|
||||
it('should handle pm.globals.set', () => {
|
||||
const code = 'pm.globals.set("test", "value");';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.setGlobalEnvVar("test", "value");');
|
||||
});
|
||||
|
||||
@@ -66,6 +115,7 @@ describe('Variables Translation', () => {
|
||||
const get = vars.get("test");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe(`
|
||||
const has = bru.hasVar("test");
|
||||
const set = bru.setVar("test", "value");
|
||||
@@ -83,6 +133,7 @@ describe('Variables Translation', () => {
|
||||
const unset = collVars.unset("test");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe(`
|
||||
const has = bru.hasVar("test");
|
||||
const set = bru.setVar("test", "value");
|
||||
@@ -98,6 +149,7 @@ describe('Variables Translation', () => {
|
||||
const set = globals.set("test", "value");
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe(`
|
||||
const get = bru.getGlobalEnvVar("test");
|
||||
const set = bru.setGlobalEnvVar("test", "value");
|
||||
@@ -108,6 +160,7 @@ describe('Variables Translation', () => {
|
||||
it('should handle conditional expressions with variable calls', () => {
|
||||
const code = 'const userStatus = pm.variables.has("userId") ? "logged-in" : "guest";';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('const userStatus = bru.hasVar("userId") ? "logged-in" : "guest";');
|
||||
});
|
||||
|
||||
@@ -148,6 +201,7 @@ describe('Variables Translation', () => {
|
||||
it('should handle more complex nested expressions with variables', () => {
|
||||
const code = 'pm.collectionVariables.set("fullPath", pm.environment.get("baseUrl") + pm.variables.get("endpoint"));';
|
||||
const translatedCode = translateCode(code);
|
||||
|
||||
expect(translatedCode).toBe('bru.setVar("fullPath", bru.getEnvVar("baseUrl") + bru.getVar("endpoint"));');
|
||||
});
|
||||
});
|
||||
@@ -36,9 +36,22 @@ const config = {
|
||||
},
|
||||
win: {
|
||||
artifactName: '${name}_${version}_${arch}_win.${ext}',
|
||||
icon: 'resources/icons/png',
|
||||
certificateFile: `${process.env.WIN_CERT_FILEPATH}`,
|
||||
certificatePassword: `${process.env.WIN_CERT_PASSWORD}`
|
||||
icon: 'resources/icons/win/icon.ico',
|
||||
target: [
|
||||
{
|
||||
target: 'nsis',
|
||||
arch: ['x64']
|
||||
}
|
||||
],
|
||||
sign: null,
|
||||
publisherName: 'Bruno Software Inc'
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
allowElevation: true,
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/usebruno/bruno.git"
|
||||
},
|
||||
"private": true,
|
||||
"main": "src/index.js",
|
||||
"author": "Anoop M D <anoop.md1421@gmail.com> (https://helloanoop.com/)",
|
||||
|
||||
@@ -14,6 +14,13 @@ const { format } = require('url');
|
||||
const { BrowserWindow, app, session, Menu, ipcMain } = require('electron');
|
||||
const { setContentSecurityPolicy } = require('electron-util');
|
||||
|
||||
if (isDev && process.env.ELECTRON_USER_DATA_PATH) {
|
||||
console.debug("`ELECTRON_USER_DATA_PATH` found, modifying `userData` path: \n"
|
||||
+ `\t${app.getPath("userData")} -> ${process.env.ELECTRON_USER_DATA_PATH}`);
|
||||
|
||||
app.setPath('userData', process.env.ELECTRON_USER_DATA_PATH);
|
||||
}
|
||||
|
||||
const menuTemplate = require('./app/menu-template');
|
||||
const { openCollection } = require('./app/collections');
|
||||
const LastOpenedCollections = require('./store/last-opened-collections');
|
||||
|
||||
@@ -309,6 +309,27 @@ function makeAxiosInstance({
|
||||
},
|
||||
};
|
||||
|
||||
// Apply proper HTTP redirect behavior based on status code
|
||||
const statusCode = error.response.status;
|
||||
const originalMethod = (error.config.method || 'get').toLowerCase();
|
||||
|
||||
// For 301, 302, 303: change method to GET unless it was HEAD
|
||||
if ([301, 302, 303].includes(statusCode) && originalMethod !== 'head') {
|
||||
requestConfig.method = 'get';
|
||||
requestConfig.data = undefined;
|
||||
delete requestConfig.headers['content-length'];
|
||||
delete requestConfig.headers['Content-Length'];
|
||||
|
||||
delete requestConfig.headers['content-type'];
|
||||
delete requestConfig.headers['Content-Type'];
|
||||
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Changed method from ${originalMethod.toUpperCase()} to GET for ${statusCode} redirect and removed request body`,
|
||||
});
|
||||
}
|
||||
|
||||
if (preferencesUtil.shouldSendCookies()) {
|
||||
const cookieString = getCookieStringForUrl(redirectUrl);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
@@ -316,7 +337,7 @@ function makeAxiosInstance({
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
setupProxyAgents({
|
||||
requestConfig,
|
||||
proxyMode,
|
||||
|
||||
@@ -341,6 +341,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
) => {
|
||||
// run pre-request script
|
||||
let scriptResult;
|
||||
const collectionName = collection?.name
|
||||
const requestScript = get(request, 'script.req');
|
||||
if (requestScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
@@ -353,7 +354,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
@@ -447,6 +449,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// run post-response script
|
||||
const responseScript = get(request, 'script.res');
|
||||
let scriptResult;
|
||||
const collectionName = collection?.name
|
||||
if (responseScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
scriptResult = await scriptRuntime.runResponseScript(
|
||||
@@ -459,7 +462,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
|
||||
mainWindow.webContents.send('main:script-environment-update', {
|
||||
@@ -706,6 +710,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
const testFile = get(request, 'tests');
|
||||
const collectionName = collection?.name
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
@@ -718,7 +723,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
@@ -1171,6 +1177,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
const testFile = get(request, 'tests');
|
||||
const collectionName = collection?.name
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
@@ -1183,7 +1190,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
);
|
||||
|
||||
if (testResults?.nextRequestName !== undefined) {
|
||||
|
||||
@@ -301,6 +301,7 @@ const prepareRequest = async (item, collection = {}, abortController) => {
|
||||
method: request.method,
|
||||
url,
|
||||
headers,
|
||||
name: item.name,
|
||||
pathParams: request?.params?.filter((param) => param.type === 'path'),
|
||||
responseType: 'arraybuffer'
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { Cookie, CookieJar } = require('tough-cookie');
|
||||
const each = require('lodash/each');
|
||||
const moment = require('moment');
|
||||
const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils;
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
@@ -12,7 +13,9 @@ const addCookieToJar = (setCookieHeader, requestUrl) => {
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (url) => {
|
||||
return cookieJar.getCookiesSync(url);
|
||||
return cookieJar.getCookiesSync(url, {
|
||||
secure: isPotentiallyTrustworthyOrigin(url)
|
||||
});
|
||||
};
|
||||
|
||||
const getCookieStringForUrl = (url) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const { cloneDeep } = require('lodash');
|
||||
const { interpolate } = require('@usebruno/common');
|
||||
const { interpolate: _interpolate } = require('@usebruno/common');
|
||||
|
||||
const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
class Bru {
|
||||
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables) {
|
||||
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName) {
|
||||
this.envVariables = envVariables || {};
|
||||
this.runtimeVariables = runtimeVariables || {};
|
||||
this.processEnvVars = cloneDeep(processEnvVars || {});
|
||||
@@ -14,6 +14,7 @@ class Bru {
|
||||
this.globalEnvironmentVariables = globalEnvironmentVariables || {};
|
||||
this.oauth2CredentialVariables = oauth2CredentialVariables || {};
|
||||
this.collectionPath = collectionPath;
|
||||
this.collectionName = collectionName;
|
||||
this.runner = {
|
||||
skipRequest: () => {
|
||||
this.skipRequest = true;
|
||||
@@ -27,10 +28,10 @@ class Bru {
|
||||
};
|
||||
}
|
||||
|
||||
_interpolate = (str) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
interpolate = (strOrObj) => {
|
||||
if (!strOrObj) return strOrObj;
|
||||
const isObj = typeof strOrObj === 'object';
|
||||
const strToInterpolate = isObj ? JSON.stringify(strOrObj) : strOrObj;
|
||||
|
||||
const combinedVars = {
|
||||
...this.globalEnvironmentVariables,
|
||||
@@ -47,7 +48,8 @@ class Bru {
|
||||
}
|
||||
};
|
||||
|
||||
return interpolate(str, combinedVars);
|
||||
const interpolatedStr = _interpolate(strToInterpolate, combinedVars);
|
||||
return isObj ? JSON.parse(interpolatedStr) : interpolatedStr;
|
||||
};
|
||||
|
||||
cwd() {
|
||||
@@ -67,7 +69,7 @@ class Bru {
|
||||
}
|
||||
|
||||
getEnvVar(key) {
|
||||
return this._interpolate(this.envVariables[key]);
|
||||
return this.interpolate(this.envVariables[key]);
|
||||
}
|
||||
|
||||
setEnvVar(key, value) {
|
||||
@@ -83,7 +85,7 @@ class Bru {
|
||||
}
|
||||
|
||||
getGlobalEnvVar(key) {
|
||||
return this._interpolate(this.globalEnvironmentVariables[key]);
|
||||
return this.interpolate(this.globalEnvironmentVariables[key]);
|
||||
}
|
||||
|
||||
setGlobalEnvVar(key, value) {
|
||||
@@ -95,7 +97,7 @@ class Bru {
|
||||
}
|
||||
|
||||
getOauth2CredentialVar(key) {
|
||||
return this._interpolate(this.oauth2CredentialVariables[key]);
|
||||
return this.interpolate(this.oauth2CredentialVariables[key]);
|
||||
}
|
||||
|
||||
hasVar(key) {
|
||||
@@ -110,7 +112,7 @@ class Bru {
|
||||
if (variableNameRegex.test(key) === false) {
|
||||
throw new Error(
|
||||
`Variable name: "${key}" contains invalid characters!` +
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,11 +123,11 @@ class Bru {
|
||||
if (variableNameRegex.test(key) === false) {
|
||||
throw new Error(
|
||||
`Variable name: "${key}" contains invalid characters!` +
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
}
|
||||
|
||||
return this._interpolate(this.runtimeVariables[key]);
|
||||
return this.interpolate(this.runtimeVariables[key]);
|
||||
}
|
||||
|
||||
deleteVar(key) {
|
||||
@@ -141,15 +143,15 @@ class Bru {
|
||||
}
|
||||
|
||||
getCollectionVar(key) {
|
||||
return this._interpolate(this.collectionVariables[key]);
|
||||
return this.interpolate(this.collectionVariables[key]);
|
||||
}
|
||||
|
||||
getFolderVar(key) {
|
||||
return this._interpolate(this.folderVariables[key]);
|
||||
return this.interpolate(this.folderVariables[key]);
|
||||
}
|
||||
|
||||
getRequestVar(key) {
|
||||
return this._interpolate(this.requestVariables[key]);
|
||||
return this.interpolate(this.requestVariables[key]);
|
||||
}
|
||||
|
||||
setNextRequest(nextRequest) {
|
||||
@@ -159,6 +161,10 @@ class Bru {
|
||||
sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
getCollectionName() {
|
||||
return this.collectionName;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bru;
|
||||
|
||||
@@ -17,7 +17,7 @@ class BrunoRequest {
|
||||
this.method = req.method;
|
||||
this.headers = req.headers;
|
||||
this.timeout = req.timeout;
|
||||
|
||||
this.name = req.name;
|
||||
/**
|
||||
* We automatically parse the JSON body if the content type is JSON
|
||||
* This is to make it easier for the user to access the body directly
|
||||
@@ -177,6 +177,10 @@ class BrunoRequest {
|
||||
getExecutionMode() {
|
||||
return this.req.__bruno__executionMode;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.req.name;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BrunoRequest;
|
||||
|
||||
@@ -49,14 +49,15 @@ class ScriptRuntime {
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
) {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName);
|
||||
const req = new BrunoRequest(request);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
|
||||
@@ -97,7 +98,7 @@ class ScriptRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
if(runRequestByItemPathname) {
|
||||
if (runRequestByItemPathname) {
|
||||
context.bru.runRequest = runRequestByItemPathname;
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ class ScriptRuntime {
|
||||
chai,
|
||||
'node-fetch': fetch,
|
||||
'crypto-js': CryptoJS,
|
||||
'xml2js': xml2js,
|
||||
xml2js: xml2js,
|
||||
cheerio,
|
||||
tv4,
|
||||
...whitelistedModules,
|
||||
@@ -183,14 +184,15 @@ class ScriptRuntime {
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
) {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
@@ -233,7 +235,7 @@ class ScriptRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
if(runRequestByItemPathname) {
|
||||
if (runRequestByItemPathname) {
|
||||
context.bru.runRequest = runRequestByItemPathname;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,14 +69,15 @@ class TestRuntime {
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
runRequestByItemPathname,
|
||||
collectionName
|
||||
) {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const assertionResults = request?.assertionResults || [];
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, {}, collectionName);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
|
||||
@@ -17,12 +17,24 @@ const addBruShimToContext = (vm, bru) => {
|
||||
vm.setProp(bruObject, 'getEnvName', getEnvName);
|
||||
getEnvName.dispose();
|
||||
|
||||
let getCollectionName = vm.newFunction('getCollectionName', function () {
|
||||
return marshallToVm(bru.getCollectionName(), vm);
|
||||
});
|
||||
vm.setProp(bruObject, 'getCollectionName', getCollectionName);
|
||||
getCollectionName.dispose();
|
||||
|
||||
let getProcessEnv = vm.newFunction('getProcessEnv', function (key) {
|
||||
return marshallToVm(bru.getProcessEnv(vm.dump(key)), vm);
|
||||
});
|
||||
vm.setProp(bruObject, 'getProcessEnv', getProcessEnv);
|
||||
getProcessEnv.dispose();
|
||||
|
||||
let interpolate = vm.newFunction('interpolate', function (str) {
|
||||
return marshallToVm(bru.interpolate(vm.dump(str)), vm);
|
||||
});
|
||||
vm.setProp(bruObject, 'interpolate', interpolate);
|
||||
interpolate.dispose();
|
||||
|
||||
let hasEnvVar = vm.newFunction('hasEnvVar', function (key) {
|
||||
return marshallToVm(bru.hasEnvVar(vm.dump(key)), vm);
|
||||
});
|
||||
@@ -151,7 +163,8 @@ const addBruShimToContext = (vm, bru) => {
|
||||
|
||||
let getTestResults = vm.newFunction('getTestResults', () => {
|
||||
const promise = vm.newPromise();
|
||||
bru.getTestResults()
|
||||
bru
|
||||
.getTestResults()
|
||||
.then((results) => {
|
||||
promise.resolve(marshallToVm(cleanJson(results), vm));
|
||||
})
|
||||
@@ -172,7 +185,8 @@ const addBruShimToContext = (vm, bru) => {
|
||||
|
||||
let getAssertionResults = vm.newFunction('getAssertionResults', () => {
|
||||
const promise = vm.newPromise();
|
||||
bru.getAssertionResults()
|
||||
bru
|
||||
.getAssertionResults()
|
||||
.then((results) => {
|
||||
promise.resolve(marshallToVm(cleanJson(results), vm));
|
||||
})
|
||||
@@ -193,7 +207,8 @@ const addBruShimToContext = (vm, bru) => {
|
||||
|
||||
let runRequestHandle = vm.newFunction('runRequest', (args) => {
|
||||
const promise = vm.newPromise();
|
||||
bru.runRequest(vm.dump(args))
|
||||
bru
|
||||
.runRequest(vm.dump(args))
|
||||
.then((response) => {
|
||||
const { status, headers, data, dataBuffer, size, statusText } = response || {};
|
||||
promise.resolve(marshallToVm(cleanJson({ status, statusText, headers, data, dataBuffer, size }), vm));
|
||||
|
||||
@@ -8,18 +8,21 @@ const addBrunoRequestShimToContext = (vm, req) => {
|
||||
const headers = marshallToVm(req.getHeaders(), vm);
|
||||
const body = marshallToVm(req.getBody(), vm);
|
||||
const timeout = marshallToVm(req.getTimeout(), vm);
|
||||
const name = marshallToVm(req.getName(), vm);
|
||||
|
||||
vm.setProp(reqObject, 'url', url);
|
||||
vm.setProp(reqObject, 'method', method);
|
||||
vm.setProp(reqObject, 'headers', headers);
|
||||
vm.setProp(reqObject, 'body', body);
|
||||
vm.setProp(reqObject, 'timeout', timeout);
|
||||
vm.setProp(reqObject, 'name', name);
|
||||
|
||||
url.dispose();
|
||||
method.dispose();
|
||||
headers.dispose();
|
||||
body.dispose();
|
||||
timeout.dispose();
|
||||
name.dispose();
|
||||
|
||||
let getUrl = vm.newFunction('getUrl', function () {
|
||||
return marshallToVm(req.getUrl(), vm);
|
||||
@@ -45,6 +48,12 @@ const addBrunoRequestShimToContext = (vm, req) => {
|
||||
vm.setProp(reqObject, 'getAuthMode', getAuthMode);
|
||||
getAuthMode.dispose();
|
||||
|
||||
let getName = vm.newFunction('getName', function () {
|
||||
return marshallToVm(req.getName(), vm);
|
||||
});
|
||||
vm.setProp(reqObject, 'getName', getName);
|
||||
getName.dispose();
|
||||
|
||||
let setMethod = vm.newFunction('setMethod', function (method) {
|
||||
req.setMethod(vm.dump(method));
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
|
||||
return {
|
||||
auth: {
|
||||
mode: auth ? auth.mode : 'none'
|
||||
mode: auth?.mode || 'none'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -330,8 +330,9 @@ ${indentString(body.sparql)}
|
||||
}
|
||||
|
||||
if (item.type === 'file') {
|
||||
let filepaths = item.value || [];
|
||||
let filestr = filepaths.join('|');
|
||||
const filepaths = Array.isArray(item.value) ? item.value : [];
|
||||
const filestr = filepaths.join('|');
|
||||
|
||||
const value = `@file(${filestr})`;
|
||||
return `${enabled}${item.name}: ${value}${contentType}`;
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { addDigestInterceptor, getOAuth2Token } from './auth';
|
||||
|
||||
export * as utils from './utils';
|
||||
|
||||
105
packages/bruno-requests/src/utils/cookie-utils.js
Normal file
105
packages/bruno-requests/src/utils/cookie-utils.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const { URL } = require('node:url');
|
||||
const net = require('node:net');
|
||||
|
||||
const isLoopbackV4 = (address) => {
|
||||
// 127.0.0.0/8: first octet = 127
|
||||
const octets = address.split('.');
|
||||
return (
|
||||
octets.length === 4
|
||||
) && parseInt(octets[0], 10) === 127;
|
||||
}
|
||||
|
||||
const isLoopbackV6 = (address) => {
|
||||
// new URL(...) follows the WHATWG URL Standard
|
||||
// which compresses IPv6 addresses, therefore the IPv6
|
||||
// loopback address will always be compressed to '[::1]':
|
||||
// https://url.spec.whatwg.org/#concept-ipv6-serializer
|
||||
return (address === '::1');
|
||||
}
|
||||
|
||||
const isIpLoopback = (address) => {
|
||||
if (net.isIPv4(address)) {
|
||||
return isLoopbackV4(address);
|
||||
}
|
||||
|
||||
if (net.isIPv6(address)) {
|
||||
return isLoopbackV6(address);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const isNormalizedLocalhostTLD = (host) => {
|
||||
return host.toLowerCase().endsWith('.localhost');
|
||||
}
|
||||
|
||||
const isLocalHostname = (host) => {
|
||||
return host.toLowerCase() === 'localhost' ||
|
||||
isNormalizedLocalhostTLD(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes leading and trailing square brackets if present.
|
||||
* Adapted from https://github.com/chromium/chromium/blob/main/url/gurl.cc#L440-L448
|
||||
*
|
||||
* @param {string} host
|
||||
* @returns {string}
|
||||
*/
|
||||
const hostNoBrackets = (host) => {
|
||||
if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) {
|
||||
return host.substring(1, host.length - 1);
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a URL string represents a potentially trustworthy origin.
|
||||
*
|
||||
* A URL is considered potentially trustworthy if it:
|
||||
* - Uses HTTPS, WSS or file schemes
|
||||
* - Points to a loopback address (IPv4 127.0.0.0/8 or IPv6 ::1)
|
||||
* - Uses localhost or *.localhost hostnames
|
||||
*
|
||||
* @param {string} urlString - The URL to check
|
||||
* @returns {boolean}
|
||||
* @see {@link https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin W3C Spec}
|
||||
*/
|
||||
const isPotentiallyTrustworthyOrigin = (urlString) => {
|
||||
let url;
|
||||
|
||||
// try ... catch doubles as an opaque origin check
|
||||
try {
|
||||
url = new URL(urlString);
|
||||
} catch (e) {
|
||||
if (e instanceof TypeError && e.code === 'ERR_INVALID_URL') {
|
||||
return false;
|
||||
} else throw e;
|
||||
}
|
||||
|
||||
const scheme = url.protocol.replace(':', '').toLowerCase();
|
||||
const hostname = hostNoBrackets(
|
||||
url.hostname
|
||||
).replace(/\.+$/, '');
|
||||
|
||||
if (
|
||||
scheme === 'https' ||
|
||||
scheme === 'wss' ||
|
||||
scheme === 'file' // https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's already an IP literal, check if it's a loopback address
|
||||
if (net.isIP(hostname)) {
|
||||
return isIpLoopback(hostname);
|
||||
}
|
||||
|
||||
// RFC 6761 states that localhost names will always resolve
|
||||
// to the respective IP loopback address:
|
||||
// https://datatracker.ietf.org/doc/html/rfc6761#section-6.3
|
||||
return isLocalHostname(hostname);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isPotentiallyTrustworthyOrigin
|
||||
};
|
||||
1
packages/bruno-requests/src/utils/index.ts
Normal file
1
packages/bruno-requests/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './cookie-utils';
|
||||
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: getCollectionName
|
||||
type: http
|
||||
seq: 13
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
test("Check if collection name is bruno-testbench", function () {
|
||||
expect(bru.getCollectionName()).to.eql("bruno-testbench");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
meta {
|
||||
name: interpolate
|
||||
type: http
|
||||
seq: 13
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should interpolate envs", function() {
|
||||
const interpolated = bru.interpolate("url: {{host}}")
|
||||
expect(interpolated).to.equal("url: https://testbench-sanity.usebruno.com");
|
||||
});
|
||||
|
||||
test("should interpolate random variables", function() {
|
||||
const a = bru.interpolate("{{$randomInt}}")
|
||||
const b = bru.interpolate("{{$randomInt}}")
|
||||
expect(a).to.not.equal(b)
|
||||
});
|
||||
|
||||
const randomObj = {
|
||||
host: "{{host}}",
|
||||
int: "{{$randomInt}}",
|
||||
timestamp: "{{$timestamp}}"
|
||||
}
|
||||
|
||||
test("should interpolate objects with vars, random vars", function() {
|
||||
const objA = bru.interpolate(randomObj)
|
||||
const objB = bru.interpolate(randomObj)
|
||||
|
||||
expect(objA).to.be.an("object")
|
||||
expect(objB).to.be.an("object")
|
||||
expect(objA).to.not.deep.eql(objB)
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: getName
|
||||
type: http
|
||||
seq: 11
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
test("Check if request name is getName", function () {
|
||||
expect(req.getName()).to.eql("getName");
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
const reporter: string[][string] = [['list'], ['html']];
|
||||
const reporter: any[] = [['list'], ['html']];
|
||||
|
||||
if (process.env.CI) {
|
||||
reporter.push(["github"]);
|
||||
reporter.push(['github']);
|
||||
}
|
||||
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e-tests',
|
||||
fullyParallel: false,
|
||||
@@ -14,8 +13,9 @@ export default defineConfig({
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: process.env.CI ? undefined : 1,
|
||||
reporter,
|
||||
|
||||
use: {
|
||||
trace: 'on-first-retry'
|
||||
trace: process.env.CI ? 'on-first-retry' : 'on'
|
||||
},
|
||||
|
||||
projects: [
|
||||
@@ -24,9 +24,16 @@ export default defineConfig({
|
||||
}
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'npm run dev:web',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI
|
||||
}
|
||||
webServer: [
|
||||
{
|
||||
command: 'npm run dev:web',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
{
|
||||
command: 'npm start --workspace=packages/bruno-tests',
|
||||
url: 'http://localhost:8081/ping',
|
||||
reuseExistingServer: !process.env.CI
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -7,7 +7,11 @@ 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()));
|
||||
app.process().stdout.on('data', (data) => {
|
||||
process.stdout.write(data.toString().replace(/^(?=.)/gm, '[Electron] |'));
|
||||
});
|
||||
app.process().stderr.on('data', (error) => {
|
||||
process.stderr.write(error.toString().replace(/^(?=.)/gm, '[Electron] |'));
|
||||
});
|
||||
return { app, context };
|
||||
};
|
||||
|
||||
@@ -1,23 +1,179 @@
|
||||
import { test as baseTest, ElectronApplication, Page } from '@playwright/test';
|
||||
import { test as baseTest, BrowserContext, ElectronApplication, Page } from '@playwright/test';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const { startApp } = require('./electron.ts');
|
||||
const electronAppPath = path.join(__dirname, '../packages/bruno-electron');
|
||||
|
||||
export const test = baseTest.extend<{ page: Page }, { electronApp: ElectronApplication }>({
|
||||
electronApp: [
|
||||
export const test = baseTest.extend<
|
||||
{
|
||||
context: BrowserContext;
|
||||
page: Page;
|
||||
newPage: Page;
|
||||
pageWithUserData: Page;
|
||||
},
|
||||
{
|
||||
createTmpDir: (tag?: string) => Promise<string>;
|
||||
launchElectronApp: (options?: { initUserDataPath?: string }) => Promise<ElectronApplication>;
|
||||
electronApp: ElectronApplication;
|
||||
reuseOrLaunchElectronApp: (options?: { initUserDataPath?: string }) => Promise<ElectronApplication>;
|
||||
}
|
||||
>({
|
||||
createTmpDir: [
|
||||
async ({}, use) => {
|
||||
const { app: electronApp, context } = await startApp();
|
||||
|
||||
await use(electronApp);
|
||||
await context.close();
|
||||
await electronApp.close();
|
||||
const dirs: string[] = [];
|
||||
await use(async (tag?: string) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), `pw-${tag || ''}-`));
|
||||
dirs.push(dir);
|
||||
return dir;
|
||||
});
|
||||
await Promise.all(
|
||||
dirs.map((dir) => fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch((e) => e))
|
||||
);
|
||||
},
|
||||
{ scope: 'worker' }
|
||||
],
|
||||
page: async ({ electronApp }, use) => {
|
||||
|
||||
launchElectronApp: [
|
||||
async ({ playwright, createTmpDir }, use, workerInfo) => {
|
||||
const apps: ElectronApplication[] = [];
|
||||
await use(async ({ initUserDataPath } = {}) => {
|
||||
const userDataPath = await createTmpDir('electron-userdata');
|
||||
|
||||
if (initUserDataPath) {
|
||||
const replacements = {
|
||||
projectRoot: path.join(__dirname, '..')
|
||||
};
|
||||
|
||||
for (const file of await fs.promises.readdir(initUserDataPath)) {
|
||||
let content = await fs.promises.readFile(path.join(initUserDataPath, file), 'utf-8');
|
||||
content = content.replace(/{{(\w+)}}/g, (_, key) => {
|
||||
if (replacements[key]) {
|
||||
return replacements[key];
|
||||
} else {
|
||||
throw new Error(`\tNo replacement for {{${key}}} in ${path.join(initUserDataPath, file)}`);
|
||||
}
|
||||
});
|
||||
await fs.promises.writeFile(path.join(userDataPath, file), content, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
const app = await playwright._electron.launch({
|
||||
args: [electronAppPath],
|
||||
env: {
|
||||
...process.env,
|
||||
ELECTRON_USER_DATA_PATH: userDataPath,
|
||||
}
|
||||
});
|
||||
|
||||
const { workerIndex } = workerInfo;
|
||||
app.process().stdout.on('data', (data) => {
|
||||
process.stdout.write(data.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`));
|
||||
});
|
||||
app.process().stderr.on('data', (error) => {
|
||||
process.stderr.write(error.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`));
|
||||
});
|
||||
|
||||
apps.push(app);
|
||||
return app;
|
||||
});
|
||||
for (const app of apps) {
|
||||
await app.context().close();
|
||||
await app.close();
|
||||
}
|
||||
},
|
||||
{ scope: 'worker' }
|
||||
],
|
||||
|
||||
electronApp: [
|
||||
async ({ launchElectronApp }, use) => {
|
||||
const app = await launchElectronApp();
|
||||
await use(app);
|
||||
},
|
||||
{ scope: 'worker' }
|
||||
],
|
||||
|
||||
context: async ({ electronApp }, use, testInfo) => {
|
||||
const context = await electronApp.context();
|
||||
const tracingOptions = (testInfo as any)._tracing.traceOptions();
|
||||
if (tracingOptions) {
|
||||
try {
|
||||
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
||||
} catch (e) {}
|
||||
}
|
||||
await use(context);
|
||||
},
|
||||
|
||||
page: async ({ electronApp, context }, use, testInfo) => {
|
||||
const page = await electronApp.firstWindow();
|
||||
await use(page);
|
||||
await page.reload();
|
||||
const tracingOptions = (testInfo as any)._tracing.traceOptions();
|
||||
if (tracingOptions) {
|
||||
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
|
||||
await context.tracing.startChunk();
|
||||
await use(page);
|
||||
await context.tracing.stopChunk({ path: tracePath });
|
||||
await testInfo.attach('trace', { path: tracePath });
|
||||
} else {
|
||||
await use(page);
|
||||
}
|
||||
},
|
||||
|
||||
newPage: async ({ launchElectronApp }, use, testInfo) => {
|
||||
const app = await launchElectronApp();
|
||||
const context = await app.context();
|
||||
const page = await app.firstWindow();
|
||||
const tracingOptions = (testInfo as any)._tracing.traceOptions();
|
||||
if (tracingOptions) {
|
||||
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
|
||||
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
||||
await use(page);
|
||||
await context.tracing.stop({ path: tracePath });
|
||||
await testInfo.attach('trace', { path: tracePath });
|
||||
} else {
|
||||
await use(page);
|
||||
}
|
||||
},
|
||||
|
||||
reuseOrLaunchElectronApp: [
|
||||
async ({ launchElectronApp }, use, testInfo) => {
|
||||
const apps: Record<string, ElectronApplication> = {};
|
||||
await use(async ({ initUserDataPath } = {}) => {
|
||||
const key = initUserDataPath;
|
||||
if (key && apps[key]) {
|
||||
return apps[key];
|
||||
}
|
||||
const app = await launchElectronApp({ initUserDataPath });
|
||||
apps[key] = app;
|
||||
return app;
|
||||
});
|
||||
},
|
||||
{ scope: 'worker' }
|
||||
],
|
||||
|
||||
pageWithUserData: async ({ reuseOrLaunchElectronApp }, use, testInfo) => {
|
||||
const testDir = path.dirname(testInfo.file);
|
||||
const initUserDataPath = path.join(testDir, 'init-user-data');
|
||||
|
||||
const app = await reuseOrLaunchElectronApp(
|
||||
(await fs.promises.stat(initUserDataPath).catch(() => false)) ? { initUserDataPath } : {}
|
||||
);
|
||||
|
||||
const context = await app.context();
|
||||
const page = await app.firstWindow();
|
||||
const tracingOptions = (testInfo as any)._tracing.traceOptions();
|
||||
if (tracingOptions) {
|
||||
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
|
||||
try {
|
||||
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
||||
} catch (e) {}
|
||||
await context.tracing.startChunk();
|
||||
await use(page);
|
||||
await context.tracing.stopChunk({ path: tracePath });
|
||||
await testInfo.attach('trace', { path: tracePath });
|
||||
} else {
|
||||
await use(page);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export * from '@playwright/test'
|
||||
export * from '@playwright/test';
|
||||
|
||||
Reference in New Issue
Block a user