mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
28 Commits
feature/pl
...
v2.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d0dd60f56 | ||
|
|
9bb9a914ac | ||
|
|
44cef9999c | ||
|
|
3a792a021c | ||
|
|
2e5c63cfb9 | ||
|
|
9845363349 | ||
|
|
1a6fa7a799 | ||
|
|
6cd44662a8 | ||
|
|
9daf418886 | ||
|
|
37ee13353d | ||
|
|
8439e8871f | ||
|
|
4c1d3b4f3a | ||
|
|
cd3c66cb14 | ||
|
|
265b0114e4 | ||
|
|
17a63d599d | ||
|
|
d9e87fcd82 | ||
|
|
78c4cb11eb | ||
|
|
6feea75e45 | ||
|
|
2d1f7d0f33 | ||
|
|
841facc853 | ||
|
|
0e60bd3da7 | ||
|
|
5dc7f1ae2f | ||
|
|
6862cb4e58 | ||
|
|
0591530d44 | ||
|
|
592679538b | ||
|
|
9ef2699372 | ||
|
|
e4c37b916a | ||
|
|
7a8a0ae37e |
4
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
4
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
@@ -27,7 +27,9 @@ body:
|
||||
required: false
|
||||
- label: annoying
|
||||
required: false
|
||||
|
||||
- label: this feature was working in a previous version but is broken in the current release.
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Bruno version
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -28,6 +28,7 @@ jobs:
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
|
||||
# tests
|
||||
- name: Test Package bruno-js
|
||||
@@ -71,6 +72,7 @@ jobs:
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
|
||||
@@ -37,15 +37,15 @@ Libraries we use
|
||||
- Filesystem Watcher - chokidar
|
||||
- i18n - i18next
|
||||
|
||||
### Dependencies
|
||||
|
||||
You would need [Node v20.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
||||
> [!IMPORTANT]
|
||||
> You would need [Node v22.x or the latest LTS version](https://nodejs.org/en/). We use npm workspaces in the project
|
||||
|
||||
## Development
|
||||
|
||||
Bruno is being developed as a desktop app. You need to load the app by running the Next.js app in one terminal and then run the electron app in another terminal.
|
||||
|
||||
### Local Development
|
||||
|
||||
## Install Dependencies
|
||||
|
||||
```bash
|
||||
# use nodejs 20 version
|
||||
@@ -53,7 +53,11 @@ nvm use
|
||||
|
||||
# install deps
|
||||
npm i --legacy-peer-deps
|
||||
```
|
||||
|
||||
### Local Development (Option 1)
|
||||
|
||||
```bash
|
||||
# build packages
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
@@ -62,13 +66,23 @@ npm run build:bruno-common
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
|
||||
# run next app (terminal 1)
|
||||
# run react app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Local Development (Option 2)
|
||||
|
||||
```bash
|
||||
# install dependencies and setup
|
||||
npm run setup
|
||||
|
||||
# run electron and react app concurrently
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
171
package-lock.json
generated
171
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-common",
|
||||
"packages/bruno-converters",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
@@ -6343,6 +6344,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-alias": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz",
|
||||
"integrity": "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs": {
|
||||
"version": "23.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-23.0.7.tgz",
|
||||
@@ -7937,6 +7956,10 @@
|
||||
"resolved": "packages/bruno-common",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@usebruno/converters": {
|
||||
"resolved": "packages/bruno-converters",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@usebruno/crypto-js": {
|
||||
"version": "3.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@usebruno/crypto-js/-/crypto-js-3.1.9.tgz",
|
||||
@@ -16507,9 +16530,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-typed": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz",
|
||||
"integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.1.tgz",
|
||||
"integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
@@ -26274,11 +26297,136 @@
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-converters": {
|
||||
"name": "@usebruno/converters",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@usebruno/schema": "^0.7.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"nanoid": "3.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"babel-jest": "^29.7.0",
|
||||
"rimraf": "^5.0.7",
|
||||
"rollup": "3.2.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-converters/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/bruno-converters/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/bruno-converters/node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"packages/bruno-converters/node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-converters/node_modules/rimraf": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
|
||||
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^10.3.7"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/bruno-converters/node_modules/rollup": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.2.5.tgz",
|
||||
"integrity": "sha512-/Ha7HhVVofduy+RKWOQJrxe4Qb3xyZo+chcpYiD8SoQa4AG7llhupUtyfKSSrdBM2mWJjhM8wZwmbY23NmlIYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron": {
|
||||
"name": "bruno",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
@@ -26786,6 +26934,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/@faker-js/faker": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.5.1.tgz",
|
||||
"integrity": "sha512-0fzMEDxkExR2cn731kpDaCCnBGBUOIXEi2S1N5l8Hltp6aPf4soTMJ+g4k8r2sI5oB+rpwIW8Uy/6jkwGpnWPg==",
|
||||
"deprecated": "Please update to a newer version",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/@smithy/abort-controller": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.1.tgz",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-common",
|
||||
"packages/bruno-converters",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
@@ -38,6 +39,7 @@
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
|
||||
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
|
||||
"build:bruno-converters": "npm run build --workspace=packages/bruno-converters",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:electron": "node ./scripts/build-electron.js",
|
||||
@@ -54,6 +56,11 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5"
|
||||
"rollup": "3.29.5",
|
||||
"electron-store": {
|
||||
"conf": {
|
||||
"json-schema-typed": "8.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,13 @@ const StyledWrapper = styled.div`
|
||||
.cm-s-default span.cm-variable {
|
||||
color: #397d13 !important;
|
||||
}
|
||||
|
||||
//matching bracket fix
|
||||
.CodeMirror-matchingbracket {
|
||||
background: #5cc0b48c !important;
|
||||
text-decoration:unset;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -35,6 +35,7 @@ if (!SERVER_RENDERED) {
|
||||
'res.getHeader(name)',
|
||||
'res.getHeaders()',
|
||||
'res.getBody()',
|
||||
'res.setBody(data)',
|
||||
'res.getResponseTime()',
|
||||
'req',
|
||||
'req.url',
|
||||
|
||||
@@ -8,7 +8,7 @@ const DotIcon = ({ width }) => {
|
||||
className='inline-block'
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor" />
|
||||
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" strokeWidth="0" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -125,7 +125,7 @@ const General = ({ close }) => {
|
||||
className="mousetrap mr-0"
|
||||
/>
|
||||
<label className="block ml-2 select-none" htmlFor="customCaCertificateEnabled">
|
||||
Use custom CA Certificate
|
||||
Use Custom CA Certificate
|
||||
</label>
|
||||
</div>
|
||||
{formik.values.customCaCertificate.filePath ? (
|
||||
@@ -183,7 +183,7 @@ const General = ({ close }) => {
|
||||
className={`block ml-2 select-none ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
|
||||
htmlFor="keepDefaultCaCertificatesEnabled"
|
||||
>
|
||||
Keep default CA Certificates
|
||||
Keep Default CA Certificates
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
|
||||
@@ -239,17 +239,6 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Auto-refresh token</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer w-4 h-4 accent-indigo-600"
|
||||
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
|
||||
@@ -35,14 +35,14 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
toast.success('token fetched successfully!');
|
||||
}
|
||||
else {
|
||||
toast.error('An error occured while fetching token!');
|
||||
toast.error('An error occurred while fetching token!');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('could not fetch the token!');
|
||||
console.error(error);
|
||||
toggleFetchingToken(false);
|
||||
toast.error('An error occured while fetching token!');
|
||||
toast.error('An error occurred while fetching token!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,13 +58,13 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
toast.success('token refreshed successfully!');
|
||||
}
|
||||
else {
|
||||
toast.error('An error occured while refreshing token!');
|
||||
toast.error('An error occurred while refreshing token!');
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
toggleRefreshingToken(false);
|
||||
toast.error('An error occured while refreshing token!');
|
||||
toast.error('An error occurred while refreshing token!');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -242,17 +242,6 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Auto-refresh token</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer w-4 h-4 accent-indigo-600"
|
||||
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
|
||||
@@ -2,9 +2,17 @@ const Network = ({ logs }) => {
|
||||
return (
|
||||
<div className="bg-black/5 text-white network-logs rounded overflow-auto h-96">
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{logs.map((entry, index) => (
|
||||
<NetworkLogsEntry key={index} entry={entry} />
|
||||
))}
|
||||
{logs.map((currentLog, index) => {
|
||||
if (index > 0 && currentLog?.type === 'separator') {
|
||||
return <div className="border-t-2 border-gray-500 w-full my-2" key={index} />;
|
||||
}
|
||||
const nextLog = logs[index + 1];
|
||||
const isSameLogType = nextLog?.type === currentLog?.type;
|
||||
return <>
|
||||
<NetworkLogsEntry key={index} entry={currentLog} />
|
||||
{!isSameLogType && <div className="mt-4"/>}
|
||||
</>;
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -141,7 +141,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
)}
|
||||
{focusedTab?.responsePaneTab === "timeline" ? (
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
) : item?.response ? (
|
||||
) : (item?.response && !item?.response?.error) ? (
|
||||
<>
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import exportBrunoCollection from 'utils/collections/export';
|
||||
import exportPostmanCollection from 'utils/exporters/postman-collection';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import Modal from 'components/Modal';
|
||||
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
||||
@@ -7,14 +7,6 @@ import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const [options, setOptions] = useState({
|
||||
enablePostmanTranslations: {
|
||||
enabled: true,
|
||||
label: 'Auto translate postman scripts',
|
||||
subLabel:
|
||||
"When enabled, Bruno will try as best to translate the scripts from the imported collection to Bruno's format."
|
||||
}
|
||||
});
|
||||
const handleImportBrunoCollection = () => {
|
||||
importBrunoCollection()
|
||||
.then(({ collection }) => {
|
||||
@@ -24,9 +16,9 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
};
|
||||
|
||||
const handleImportPostmanCollection = () => {
|
||||
importPostmanCollection(options)
|
||||
.then(({ collection, translationLog }) => {
|
||||
handleSubmit({ collection, translationLog });
|
||||
importPostmanCollection()
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'));
|
||||
};
|
||||
@@ -46,15 +38,6 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
})
|
||||
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'));
|
||||
};
|
||||
const toggleOptions = (event, optionKey) => {
|
||||
setOptions({
|
||||
...options,
|
||||
[optionKey]: {
|
||||
...options[optionKey],
|
||||
enabled: !options[optionKey].enabled
|
||||
}
|
||||
});
|
||||
};
|
||||
const CollectionButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
@@ -77,31 +60,6 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
<CollectionButton onClick={handleImportInsomniaCollection}>Insomnia Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportOpenapiCollection}>OpenAPI V3 Spec</CollectionButton>
|
||||
</div>
|
||||
<div className="flex justify-start w-full mt-4 max-w-[450px]">
|
||||
{Object.entries(options || {}).map(([key, option]) => (
|
||||
<div key={key} className="relative flex items-start">
|
||||
<div className="flex h-6 items-center">
|
||||
<input
|
||||
id="comments"
|
||||
aria-describedby="comments-description"
|
||||
name="comments"
|
||||
type="checkbox"
|
||||
checked={option.enabled}
|
||||
onChange={(e) => toggleOptions(e, key)}
|
||||
className="h-3.5 w-3.5 rounded border-zinc-300 dark:ring-offset-zinc-800 bg-transparent text-indigo-600 dark:text-indigo-500 focus:ring-indigo-600 dark:focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 text-sm leading-6">
|
||||
<label htmlFor="comments" className="font-medium text-gray-900 dark:text-zinc-50">
|
||||
{option.label}
|
||||
</label>
|
||||
<p id="comments-description" className="text-zinc-500 text-xs dark:text-zinc-400">
|
||||
{option.subLabel}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -4,105 +4,8 @@ import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Modal from 'components/Modal';
|
||||
import { IconAlertTriangle, IconArrowRight, IconCaretDown, IconCaretRight, IconCopy } from '@tabler/icons';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const TranslationLog = ({ translationLog }) => {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const preventSetShowDetails = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowDetails(!showDetails);
|
||||
};
|
||||
const copyClipboard = (e, value) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
navigator.clipboard.writeText(value);
|
||||
toast.success('Copied to clipboard');
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col mt-2">
|
||||
<div className="border-l-2 border-amber-500 dark:border-amber-300 bg-amber-50 dark:bg-amber-50/10 p-1.5 rounded-r">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<IconAlertTriangle className="h-4 w-4 text-amber-500 dark:text-amber-300" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<p className="text-xs text-amber-700 dark:text-amber-300">
|
||||
<span className="font-semibold">Warning:</span> Some commands were not translated.{' '}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => preventSetShowDetails(e)}
|
||||
className="flex w-fit items-center rounded px-2.5 py-1 mt-2 text-xs font-semibold ring-1 ring-inset bg-slate-50 dark:bg-slate-400/10 text-slate-700 dark:text-slate-300 ring-slate-600/10 dark:ring-slate-400/20"
|
||||
>
|
||||
See details
|
||||
{showDetails ? <IconCaretDown size={16} className="ml-1" /> : <IconCaretRight size={16} className="ml-1" />}
|
||||
</button>
|
||||
{showDetails && (
|
||||
<div className="flex relative flex-col text-xs max-w-[364px] max-h-[300px] overflow-scroll mt-2 p-2 bg-slate-50 dark:bg-slate-400/10 ring-1 ring-inset rounded text-slate-700 dark:text-slate-300 ring-slate-600/20 dark:ring-slate-400/20">
|
||||
<span className="font-semibold flex items-center">
|
||||
Impacted Collections: {Object.keys(translationLog || {}).length}
|
||||
</span>
|
||||
<span className="font-semibold flex items-center">
|
||||
Impacted Lines:{' '}
|
||||
{Object.values(translationLog || {}).reduce(
|
||||
(acc, curr) => acc + (curr.script?.length || 0) + (curr.test?.length || 0),
|
||||
0
|
||||
)}
|
||||
</span>
|
||||
<span className="my-1">
|
||||
The numbers after 'script' and 'test' indicate the line numbers of incomplete translations.
|
||||
</span>
|
||||
<ul>
|
||||
{Object.entries(translationLog || {}).map(([name, value]) => (
|
||||
<li key={name} className="list-none text-xs font-semibold">
|
||||
<div className="font-semibold flex items-center text-xs whitespace-nowrap">
|
||||
<IconCaretRight className="min-w-4 max-w-4 -ml-1" />
|
||||
{name}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{value.script && (
|
||||
<div className="flex items-center text-xs font-light mb-1 flex-wrap">
|
||||
<span className="mr-2">script :</span>
|
||||
{value.script.map((scriptValue, index) => (
|
||||
<span className="flex items-center" key={`script_${name}_${index}`}>
|
||||
<span className="text-xs font-light">{scriptValue}</span>
|
||||
{index < value.script.length - 1 && <> - </>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{value.test && (
|
||||
<div className="flex items-center text-xs font-light mb-1 flex-wrap">
|
||||
<span className="mr-2">test :</span>
|
||||
{value.test.map((testValue, index) => (
|
||||
<div className="flex items-center" key={`test_${name}_${index}`}>
|
||||
<span className="text-xs font-light">{testValue}</span>
|
||||
{index < value.test.length - 1 && <> - </>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button
|
||||
className="absolute top-1 right-1 flex w-fit items-center rounded p-2 text-xs font-semibold ring-1 ring-inset bg-slate-50 dark:bg-slate-400/10 text-slate-700 dark:text-slate-300 ring-slate-600/10 dark:ring-slate-400/20"
|
||||
onClick={(e) => copyClipboard(e, JSON.stringify(translationLog))}
|
||||
>
|
||||
<IconCopy size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, translationLog }) => {
|
||||
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -150,9 +53,6 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
|
||||
Name
|
||||
</label>
|
||||
<div className="mt-2">{collectionName}</div>
|
||||
{translationLog && Object.keys(translationLog).length > 0 && (
|
||||
<TranslationLog translationLog={translationLog} />
|
||||
)}
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||
Location
|
||||
|
||||
@@ -14,18 +14,14 @@ import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TitleBar = () => {
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [importedTranslationLog, setImportedTranslationLog] = useState({});
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const handleImportCollection = ({ collection, translationLog }) => {
|
||||
const handleImportCollection = ({ collection }) => {
|
||||
setImportedCollection(collection);
|
||||
if (translationLog) {
|
||||
setImportedTranslationLog(translationLog);
|
||||
}
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
@@ -75,7 +71,6 @@ const TitleBar = () => {
|
||||
{importCollectionLocationModalOpen ? (
|
||||
<ImportCollectionLocation
|
||||
collectionName={importedCollection.name}
|
||||
translationLog={importedTranslationLog}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
|
||||
@@ -15,7 +15,6 @@ const Welcome = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [importedTranslationLog, setImportedTranslationLog] = useState({});
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
@@ -24,11 +23,8 @@ const Welcome = () => {
|
||||
dispatch(openCollection()).catch((err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR')));
|
||||
};
|
||||
|
||||
const handleImportCollection = ({ collection, translationLog }) => {
|
||||
const handleImportCollection = ({ collection }) => {
|
||||
setImportedCollection(collection);
|
||||
if (translationLog) {
|
||||
setImportedTranslationLog(translationLog);
|
||||
}
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
@@ -55,7 +51,6 @@ const Welcome = () => {
|
||||
) : null}
|
||||
{importCollectionLocationModalOpen ? (
|
||||
<ImportCollectionLocation
|
||||
translationLog={importedTranslationLog}
|
||||
collectionName={importedCollection.name}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
|
||||
@@ -1272,6 +1272,10 @@ export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getS
|
||||
|
||||
export const fetchOauth2Credentials = (payload) => async (dispatch, getState) => {
|
||||
const { request, collection, itemUid, folderUid } = payload;
|
||||
const state = getState();
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
request.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
return new Promise((resolve, reject) => {
|
||||
window.ipcRenderer
|
||||
.invoke('renderer:fetch-oauth2-credentials', { itemUid, request, collection })
|
||||
@@ -1295,6 +1299,10 @@ export const fetchOauth2Credentials = (payload) => async (dispatch, getState) =>
|
||||
|
||||
export const refreshOauth2Credentials = (payload) => async (dispatch, getState) => {
|
||||
const { request, collection, folderUid, itemUid } = payload;
|
||||
const state = getState();
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
request.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
return new Promise((resolve, reject) => {
|
||||
window.ipcRenderer
|
||||
.invoke('renderer:refresh-oauth2-credentials', { request, collection })
|
||||
|
||||
@@ -1,340 +1,10 @@
|
||||
import map from 'lodash/map';
|
||||
import * as FileSaver from 'file-saver';
|
||||
import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from '../collections/export';
|
||||
|
||||
/**
|
||||
* Transforms a given URL string into an object representing the protocol, host, path, query, and variables.
|
||||
*
|
||||
* @param {string} url - The raw URL to be transformed.
|
||||
* @param {Object} params - The params object.
|
||||
* @returns {Object|null} An object containing the URL's protocol, host, path, query, and variables, or {} if an error occurs.
|
||||
*/
|
||||
export const transformUrl = (url, params) => {
|
||||
if (typeof url !== 'string' || !url.trim()) {
|
||||
url = "";
|
||||
console.error("Invalid URL input:", url);
|
||||
}
|
||||
|
||||
const urlRegexPatterns = {
|
||||
protocolAndRestSeparator: /:\/\//,
|
||||
hostAndPathSeparator: /\/(.+)/,
|
||||
domainSegmentSeparator: /\./,
|
||||
pathSegmentSeparator: /\//,
|
||||
queryStringSeparator: /\?/
|
||||
};
|
||||
|
||||
const postmanUrl = { raw: url };
|
||||
|
||||
/**
|
||||
* Splits a URL into its protocol, host and path.
|
||||
*
|
||||
* @param {string} url - The URL to be split.
|
||||
* @returns {Object} An object containing the protocol and the raw host/path string.
|
||||
*/
|
||||
const splitUrl = (url) => {
|
||||
const urlParts = url.split(urlRegexPatterns.protocolAndRestSeparator);
|
||||
if (urlParts.length === 1) {
|
||||
return { protocol: '', rawHostAndPath: urlParts[0] };
|
||||
} else if (urlParts.length === 2) {
|
||||
const [hostAndPath, _] = urlParts[1].split(urlRegexPatterns.queryStringSeparator);
|
||||
return { protocol: urlParts[0], rawHostAndPath: hostAndPath };
|
||||
} else {
|
||||
throw new Error(`Invalid URL format: ${url}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits the host and path from a raw host/path string.
|
||||
*
|
||||
* @param {string} rawHostAndPath - The raw host and path string to be split.
|
||||
* @returns {Object} An object containing the host and path.
|
||||
*/
|
||||
const splitHostAndPath = (rawHostAndPath) => {
|
||||
const [host, path = ''] = rawHostAndPath.split(urlRegexPatterns.hostAndPathSeparator);
|
||||
return { host, path };
|
||||
};
|
||||
|
||||
try {
|
||||
const { protocol, rawHostAndPath } = splitUrl(url);
|
||||
postmanUrl.protocol = protocol;
|
||||
|
||||
const { host, path } = splitHostAndPath(rawHostAndPath);
|
||||
postmanUrl.host = host ? host.split(urlRegexPatterns.domainSegmentSeparator) : [];
|
||||
postmanUrl.path = path ? path.split(urlRegexPatterns.pathSegmentSeparator) : [];
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Construct query params.
|
||||
postmanUrl.query = params
|
||||
.filter((param) => param.type === 'query')
|
||||
.map(({ name, value, description }) => ({ key: name, value, description }));
|
||||
|
||||
// Construct path params.
|
||||
postmanUrl.variable = params
|
||||
.filter((param) => param.type === 'path')
|
||||
.map(({ name, value, description }) => ({ key: name, value, description }));
|
||||
|
||||
return postmanUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Collapses multiple consecutive slashes (`//`) into a single slash, while skipping the protocol (e.g., `http://` or `https://`).
|
||||
*
|
||||
* @param {String} url - A URL string
|
||||
* @returns {String} The sanitized URL
|
||||
*
|
||||
*/
|
||||
const collapseDuplicateSlashes = (url) => {
|
||||
return url.replace(/(?<!:)\/{2,}/g, '/');
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces all `\\` (backslashes) with `//` (forward slashes) and collapses multiple slashes into one.
|
||||
*
|
||||
* @param {string} url - The URL to sanitize.
|
||||
* @returns {string} The sanitized URL.
|
||||
*
|
||||
*/
|
||||
export const sanitizeUrl = (url) => {
|
||||
let sanitizedUrl = collapseDuplicateSlashes(url.replace(/\\/g, '//'));
|
||||
return sanitizedUrl;
|
||||
};
|
||||
import brunoConverters from '@usebruno/converters';
|
||||
const { brunoToPostman } = brunoConverters;
|
||||
|
||||
export const exportCollection = (collection) => {
|
||||
delete collection.uid;
|
||||
delete collection.processEnvVariables;
|
||||
deleteUidsInItems(collection.items);
|
||||
deleteUidsInEnvs(collection.environments);
|
||||
deleteSecretsInEnvs(collection.environments);
|
||||
|
||||
const generateInfoSection = () => {
|
||||
return {
|
||||
name: collection.name,
|
||||
description: collection.root?.docs,
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
};
|
||||
};
|
||||
|
||||
const generateCollectionVars = (collection) => {
|
||||
const pattern = /{{[^{}]+}}/g;
|
||||
let listOfVars = [];
|
||||
|
||||
const findOccurrences = (obj, results) => {
|
||||
if (typeof obj === 'object') {
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach((item) => findOccurrences(item, results));
|
||||
} else {
|
||||
for (const key in obj) {
|
||||
findOccurrences(obj[key], results);
|
||||
}
|
||||
}
|
||||
} else if (typeof obj === 'string') {
|
||||
obj.replace(pattern, (match) => {
|
||||
results.push(match.replace(/{{|}}/g, ''));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
findOccurrences(collection, listOfVars);
|
||||
|
||||
const finalArrayOfVars = [...new Set(listOfVars)];
|
||||
|
||||
return finalArrayOfVars.map((variable) => ({
|
||||
key: variable,
|
||||
value: '',
|
||||
type: 'default'
|
||||
}));
|
||||
};
|
||||
|
||||
const generateEventSection = (item) => {
|
||||
const eventArray = [];
|
||||
if (item?.request?.tests?.length) {
|
||||
eventArray.push({
|
||||
listen: 'test',
|
||||
script: {
|
||||
exec: item.request.tests.split('\n')
|
||||
// type: 'text/javascript'
|
||||
}
|
||||
});
|
||||
}
|
||||
if (item?.request?.script?.req) {
|
||||
eventArray.push({
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
exec: item.request.script.req.split('\n')
|
||||
// type: 'text/javascript'
|
||||
}
|
||||
});
|
||||
}
|
||||
return eventArray;
|
||||
};
|
||||
|
||||
const generateHeaders = (headersArray) => {
|
||||
return map(headersArray, (item) => {
|
||||
return {
|
||||
key: item.name,
|
||||
value: item.value,
|
||||
disabled: !item.enabled,
|
||||
type: 'default'
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const generateBody = (body) => {
|
||||
switch (body.mode) {
|
||||
case 'formUrlEncoded':
|
||||
return {
|
||||
mode: 'urlencoded',
|
||||
urlencoded: map(body.formUrlEncoded, (bodyItem) => {
|
||||
return {
|
||||
key: bodyItem.name,
|
||||
value: bodyItem.value,
|
||||
disabled: !bodyItem.enabled,
|
||||
type: 'default'
|
||||
};
|
||||
})
|
||||
};
|
||||
case 'multipartForm':
|
||||
return {
|
||||
mode: 'formdata',
|
||||
formdata: map(body.multipartForm, (bodyItem) => {
|
||||
return {
|
||||
key: bodyItem.name,
|
||||
value: bodyItem.value,
|
||||
disabled: !bodyItem.enabled,
|
||||
type: 'default'
|
||||
};
|
||||
})
|
||||
};
|
||||
case 'json':
|
||||
return {
|
||||
mode: 'raw',
|
||||
raw: body.json,
|
||||
options: {
|
||||
raw: {
|
||||
language: 'json'
|
||||
}
|
||||
}
|
||||
};
|
||||
case 'xml':
|
||||
return {
|
||||
mode: 'raw',
|
||||
raw: body.xml,
|
||||
options: {
|
||||
raw: {
|
||||
language: 'xml'
|
||||
}
|
||||
}
|
||||
};
|
||||
case 'text':
|
||||
return {
|
||||
mode: 'raw',
|
||||
raw: body.text,
|
||||
options: {
|
||||
raw: {
|
||||
language: 'text'
|
||||
}
|
||||
}
|
||||
};
|
||||
case 'graphql':
|
||||
return {
|
||||
mode: 'graphql',
|
||||
graphql: body.graphql
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const generateAuth = (itemAuth) => {
|
||||
switch (itemAuth?.mode) {
|
||||
case 'bearer':
|
||||
return {
|
||||
type: 'bearer',
|
||||
bearer: {
|
||||
key: 'token',
|
||||
value: itemAuth.bearer.token,
|
||||
type: 'string'
|
||||
}
|
||||
};
|
||||
case 'basic': {
|
||||
return {
|
||||
type: 'basic',
|
||||
basic: [
|
||||
{
|
||||
key: 'password',
|
||||
value: itemAuth.basic.password,
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
value: itemAuth.basic.username,
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
case 'apikey': {
|
||||
return {
|
||||
type: 'apikey',
|
||||
apikey: [
|
||||
{
|
||||
key: 'key',
|
||||
value: itemAuth.apikey.key,
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
value: itemAuth.apikey.value,
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
type: 'noauth'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const generateRequestSection = (itemRequest) => {
|
||||
const requestObject = {
|
||||
method: itemRequest.method,
|
||||
header: generateHeaders(itemRequest.headers),
|
||||
auth: generateAuth(itemRequest.auth),
|
||||
description: itemRequest.docs,
|
||||
// We sanitize the URL to make sure it's in the right format before passing it to the transformUrl func. This means changing backslashes to forward slashes and reducing multiple slashes to a single one, except in the protocol part.
|
||||
url: transformUrl(sanitizeUrl(itemRequest.url), itemRequest.params)
|
||||
};
|
||||
|
||||
if (itemRequest.body.mode !== 'none') {
|
||||
requestObject.body = generateBody(itemRequest.body);
|
||||
}
|
||||
return requestObject;
|
||||
};
|
||||
|
||||
const generateItemSection = (itemsArray) => {
|
||||
return map(itemsArray, (item) => {
|
||||
if (item.type === 'folder') {
|
||||
return {
|
||||
name: item.name,
|
||||
item: item.items.length ? generateItemSection(item.items) : []
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: item.name,
|
||||
event: generateEventSection(item),
|
||||
request: generateRequestSection(item.request)
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
const collectionToExport = {};
|
||||
collectionToExport.info = generateInfoSection();
|
||||
collectionToExport.item = generateItemSection(collection.items);
|
||||
collectionToExport.variable = generateCollectionVars(collection);
|
||||
const collectionToExport = brunoToPostman(collection);
|
||||
|
||||
const fileName = `${collection.name}.json`;
|
||||
const fileBlob = new Blob([JSON.stringify(collectionToExport, null, 2)], { type: 'application/json' });
|
||||
|
||||
@@ -2,7 +2,7 @@ import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid, normalizeFileName } from 'utils/common';
|
||||
import { uuid } from 'utils/common';
|
||||
import { isItemARequest } from 'utils/collections';
|
||||
import { collectionSchema } from '@usebruno/schema';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import jsyaml from 'js-yaml';
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
import brunoConverters from '@usebruno/converters';
|
||||
const { insomniaToBruno } = brunoConverters;
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -30,226 +28,11 @@ const readFile = (files) => {
|
||||
});
|
||||
};
|
||||
|
||||
const parseGraphQL = (text) => {
|
||||
try {
|
||||
const graphql = JSON.parse(text);
|
||||
|
||||
return {
|
||||
query: graphql.query,
|
||||
variables: JSON.stringify(graphql.variables, null, 2)
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
query: '',
|
||||
variables: ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const addSuffixToDuplicateName = (item, index, allItems) => {
|
||||
// Check if the request name already exist and if so add a number suffix
|
||||
const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {
|
||||
if (otherItem.name === item.name && otherIndex < index) {
|
||||
nameSuffix++;
|
||||
}
|
||||
return nameSuffix;
|
||||
}, 0);
|
||||
return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;
|
||||
};
|
||||
|
||||
const regexVariable = new RegExp('{{.*?}}', 'g');
|
||||
|
||||
const normalizeVariables = (value) => {
|
||||
value = value || '';
|
||||
const variables = value.match(regexVariable) || [];
|
||||
each(variables, (variable) => {
|
||||
value = value.replace(variable, variable.replace('_.', '').replaceAll(' ', ''));
|
||||
});
|
||||
return value;
|
||||
};
|
||||
|
||||
const transformInsomniaRequestItem = (request, index, allRequests) => {
|
||||
const name = addSuffixToDuplicateName(request, index, allRequests);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
digest: null
|
||||
},
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
each(request.headers, (header) => {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: header.name,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: !header.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(request.parameters, (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: 'query',
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(request.pathParameters, (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: '',
|
||||
type: 'path',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
|
||||
const authType = get(request, 'authentication.type', '');
|
||||
|
||||
if (authType === 'basic') {
|
||||
brunoRequestItem.request.auth.mode = 'basic';
|
||||
brunoRequestItem.request.auth.basic = {
|
||||
username: normalizeVariables(get(request, 'authentication.username', '')),
|
||||
password: normalizeVariables(get(request, 'authentication.password', ''))
|
||||
};
|
||||
} else if (authType === 'bearer') {
|
||||
brunoRequestItem.request.auth.mode = 'bearer';
|
||||
brunoRequestItem.request.auth.bearer = {
|
||||
token: normalizeVariables(get(request, 'authentication.token', ''))
|
||||
};
|
||||
}
|
||||
|
||||
const mimeType = get(request, 'body.mimeType', '').split(';')[0];
|
||||
|
||||
if (mimeType === 'application/json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
brunoRequestItem.request.body.json = request.body.text;
|
||||
} else if (mimeType === 'application/x-www-form-urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
each(request.body.params, (param) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
} else if (mimeType === 'multipart/form-data') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
each(request.body.params, (param) => {
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
type: 'text',
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
} else if (mimeType === 'text/plain') {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = request.body.text;
|
||||
} else if (mimeType === 'text/xml' || mimeType === 'application/xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = request.body.text;
|
||||
} else if (mimeType === 'application/graphql') {
|
||||
brunoRequestItem.type = 'graphql-request';
|
||||
brunoRequestItem.request.body.mode = 'graphql';
|
||||
brunoRequestItem.request.body.graphql = parseGraphQL(request.body.text);
|
||||
}
|
||||
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
const parseInsomniaCollection = (data) => {
|
||||
const brunoCollection = {
|
||||
name: '',
|
||||
uid: uuid(),
|
||||
version: '1',
|
||||
items: [],
|
||||
environments: []
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const insomniaExport = data;
|
||||
const insomniaResources = get(insomniaExport, 'resources', []);
|
||||
const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
|
||||
|
||||
if (!insomniaCollection) {
|
||||
reject(new BrunoError('Collection not found inside Insomnia export'));
|
||||
}
|
||||
|
||||
brunoCollection.name = insomniaCollection.name;
|
||||
|
||||
const requestsAndFolders =
|
||||
insomniaResources.filter((resource) => resource._type === 'request' || resource._type === 'request_group') ||
|
||||
[];
|
||||
|
||||
function createFolderStructure(resources, parentId = null) {
|
||||
const requestGroups =
|
||||
resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
|
||||
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
|
||||
|
||||
const folders = requestGroups.map((folder, index, allFolder) => {
|
||||
const name = addSuffixToDuplicateName(folder, index, allFolder);
|
||||
const requests = resources.filter(
|
||||
(resource) => resource._type === 'request' && resource.parentId === folder._id
|
||||
);
|
||||
|
||||
return {
|
||||
uid: uuid(),
|
||||
name,
|
||||
type: 'folder',
|
||||
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem))
|
||||
};
|
||||
});
|
||||
|
||||
return folders.concat(requests.map(transformInsomniaRequestItem));
|
||||
}
|
||||
|
||||
(brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id)),
|
||||
resolve(brunoCollection);
|
||||
} catch (err) {
|
||||
reject(new BrunoError('An error occurred while parsing the Insomnia collection'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
|
||||
.then(readFile)
|
||||
.then(parseInsomniaCollection)
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => insomniaToBruno(collection))
|
||||
.then((collection) => resolve({ collection }))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import jsyaml from 'js-yaml';
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
import brunoConverters from '@usebruno/converters';
|
||||
const { openApiToBruno } = brunoConverters;
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -30,435 +28,11 @@ const readFile = (files) => {
|
||||
});
|
||||
};
|
||||
|
||||
const ensureUrl = (url) => {
|
||||
// removing multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
|
||||
return url.replace(/([^:])\/{2,}/g, '$1/');
|
||||
};
|
||||
|
||||
const buildEmptyJsonBody = (bodySchema) => {
|
||||
let _jsonBody = {};
|
||||
each(bodySchema.properties || {}, (prop, name) => {
|
||||
if (prop.type === 'object') {
|
||||
_jsonBody[name] = buildEmptyJsonBody(prop);
|
||||
} else if (prop.type === 'array') {
|
||||
if (prop.items && prop.items.type === 'object') {
|
||||
_jsonBody[name] = [buildEmptyJsonBody(prop.items)];
|
||||
} else {
|
||||
_jsonBody[name] = [];
|
||||
}
|
||||
} else {
|
||||
_jsonBody[name] = '';
|
||||
}
|
||||
});
|
||||
return _jsonBody;
|
||||
};
|
||||
|
||||
const transformOpenapiRequestItem = (request) => {
|
||||
let _operationObject = request.operationObject;
|
||||
|
||||
let operationName = _operationObject.summary || _operationObject.operationId || _operationObject.description;
|
||||
if (!operationName) {
|
||||
operationName = `${request.method} ${request.path}`;
|
||||
}
|
||||
|
||||
// replace OpenAPI links in path by Bruno variables
|
||||
let path = request.path.replace(/{([a-zA-Z]+)}/g, `{{${_operationObject.operationId}_$1}}`);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: operationName,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: ensureUrl(request.global.server + path),
|
||||
method: request.method.toUpperCase(),
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
digest: null
|
||||
},
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
},
|
||||
script: {
|
||||
res: null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
each(_operationObject.parameters || [], (param) => {
|
||||
if (param.in === 'query') {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: '',
|
||||
description: param.description || '',
|
||||
enabled: param.required,
|
||||
type: 'query'
|
||||
});
|
||||
} else if (param.in === 'path') {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: '',
|
||||
description: param.description || '',
|
||||
enabled: param.required,
|
||||
type: 'path'
|
||||
});
|
||||
} else if (param.in === 'header') {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: '',
|
||||
description: param.description || '',
|
||||
enabled: param.required
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let auth;
|
||||
// allow operation override
|
||||
if (_operationObject.security && _operationObject.security.length > 0) {
|
||||
let schemeName = Object.keys(_operationObject.security[0])[0];
|
||||
auth = request.global.security.getScheme(schemeName);
|
||||
} else if (request.global.security.supported.length > 0) {
|
||||
auth = request.global.security.supported[0];
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
if (auth.type === 'http' && auth.scheme === 'basic') {
|
||||
brunoRequestItem.request.auth.mode = 'basic';
|
||||
brunoRequestItem.request.auth.basic = {
|
||||
username: '{{username}}',
|
||||
password: '{{password}}'
|
||||
};
|
||||
} else if (auth.type === 'http' && auth.scheme === 'bearer') {
|
||||
brunoRequestItem.request.auth.mode = 'bearer';
|
||||
brunoRequestItem.request.auth.bearer = {
|
||||
token: '{{token}}'
|
||||
};
|
||||
} else if (auth.type === 'apiKey' && auth.in === 'header') {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: auth.name,
|
||||
value: '{{apiKey}}',
|
||||
description: 'Authentication header',
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: handle allOf/anyOf/oneOf
|
||||
if (_operationObject.requestBody) {
|
||||
let content = get(_operationObject, 'requestBody.content', {});
|
||||
let mimeType = Object.keys(content)[0];
|
||||
let body = content[mimeType] || {};
|
||||
let bodySchema = body.schema;
|
||||
if (mimeType === 'application/json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
if (bodySchema && bodySchema.type === 'object') {
|
||||
let _jsonBody = buildEmptyJsonBody(bodySchema);
|
||||
brunoRequestItem.request.body.json = JSON.stringify(_jsonBody, null, 2);
|
||||
}
|
||||
if (bodySchema && bodySchema.type === 'array') {
|
||||
brunoRequestItem.request.body.json = JSON.stringify([buildEmptyJsonBody(bodySchema.items)], null, 2);
|
||||
}
|
||||
} else if (mimeType === 'application/x-www-form-urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
if (bodySchema && bodySchema.type === 'object') {
|
||||
each(bodySchema.properties || {}, (prop, name) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: '',
|
||||
description: prop.description || '',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (mimeType === 'multipart/form-data') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
if (bodySchema && bodySchema.type === 'object') {
|
||||
each(bodySchema.properties || {}, (prop, name) => {
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
type: 'text',
|
||||
name: name,
|
||||
value: '',
|
||||
description: prop.description || '',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (mimeType === 'text/plain') {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = '';
|
||||
} else if (mimeType === 'text/xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = '';
|
||||
}
|
||||
}
|
||||
|
||||
// build the extraction scripts from responses that have links
|
||||
// https://swagger.io/docs/specification/links/
|
||||
let script = [];
|
||||
each(_operationObject.responses || [], (response, responseStatus) => {
|
||||
if (Object.hasOwn(response, 'links')) {
|
||||
// only extract if the status code matches the response
|
||||
script.push(`if (res.status === ${responseStatus}) {`);
|
||||
each(response.links, (link) => {
|
||||
each(link.parameters || [], (expression, parameter) => {
|
||||
let value = openAPIRuntimeExpressionToScript(expression);
|
||||
script.push(` bru.setVar('${link.operationId}_${parameter}', ${value});`);
|
||||
});
|
||||
});
|
||||
script.push(`}`);
|
||||
}
|
||||
});
|
||||
if (script.length > 0) {
|
||||
brunoRequestItem.request.script.res = script.join('\n');
|
||||
}
|
||||
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
const resolveRefs = (spec, components = spec?.components, cache = new Map()) => {
|
||||
if (!spec || typeof spec !== 'object') {
|
||||
return spec;
|
||||
}
|
||||
|
||||
if (cache.has(spec)) {
|
||||
return cache.get(spec);
|
||||
}
|
||||
|
||||
if (Array.isArray(spec)) {
|
||||
return spec.map(item => resolveRefs(item, components, cache));
|
||||
}
|
||||
|
||||
if ('$ref' in spec) {
|
||||
const refPath = spec.$ref;
|
||||
|
||||
if (cache.has(refPath)) {
|
||||
return cache.get(refPath);
|
||||
}
|
||||
|
||||
if (refPath.startsWith('#/components/')) {
|
||||
const refKeys = refPath.replace('#/components/', '').split('/');
|
||||
let ref = components;
|
||||
|
||||
for (const key of refKeys) {
|
||||
if (ref && ref[key]) {
|
||||
ref = ref[key];
|
||||
} else {
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
|
||||
cache.set(refPath, {});
|
||||
const resolved = resolveRefs(ref, components, cache);
|
||||
cache.set(refPath, resolved);
|
||||
return resolved;
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
const resolved = {};
|
||||
cache.set(spec, resolved);
|
||||
|
||||
for (const [key, value] of Object.entries(spec)) {
|
||||
resolved[key] = resolveRefs(value, components, cache);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
};
|
||||
|
||||
const groupRequestsByTags = (requests) => {
|
||||
let _groups = {};
|
||||
let ungrouped = [];
|
||||
each(requests, (request) => {
|
||||
let tags = request.operationObject.tags || [];
|
||||
if (tags.length > 0) {
|
||||
let tag = tags[0].trim(); // take first tag and trim whitespace
|
||||
|
||||
if (tag) {
|
||||
if (!_groups[tag]) {
|
||||
_groups[tag] = [];
|
||||
}
|
||||
_groups[tag].push(request);
|
||||
} else {
|
||||
ungrouped.push(request);
|
||||
}
|
||||
} else {
|
||||
ungrouped.push(request);
|
||||
}
|
||||
});
|
||||
|
||||
let groups = Object.keys(_groups).map((groupName) => {
|
||||
return {
|
||||
name: groupName,
|
||||
requests: _groups[groupName]
|
||||
};
|
||||
});
|
||||
|
||||
return [groups, ungrouped];
|
||||
};
|
||||
|
||||
const getDefaultUrl = (serverObject) => {
|
||||
let url = serverObject.url;
|
||||
if (serverObject.variables) {
|
||||
each(serverObject.variables, (variable, variableName) => {
|
||||
let sub = variable.default || (variable.enum ? variable.enum[0] : `{{${variableName}}}`);
|
||||
url = url.replace(`{${variableName}}`, sub);
|
||||
});
|
||||
}
|
||||
return url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
};
|
||||
|
||||
const getSecurity = (apiSpec) => {
|
||||
let defaultSchemes = apiSpec.security || [];
|
||||
|
||||
let securitySchemes = get(apiSpec, 'components.securitySchemes', {});
|
||||
if (Object.keys(securitySchemes) === 0) {
|
||||
return {
|
||||
supported: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
supported: defaultSchemes.map((scheme) => {
|
||||
var schemeName = Object.keys(scheme)[0];
|
||||
return securitySchemes[schemeName];
|
||||
}),
|
||||
schemes: securitySchemes,
|
||||
getScheme: (schemeName) => {
|
||||
return securitySchemes[schemeName];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const openAPIRuntimeExpressionToScript = (expression) => {
|
||||
// see https://swagger.io/docs/specification/links/#runtime-expressions
|
||||
if (expression === '$response.body') {
|
||||
return 'res.body';
|
||||
} else if (expression.startsWith('$response.body#')) {
|
||||
let pointer = expression.substring(15);
|
||||
// could use https://www.npmjs.com/package/json-pointer for better support
|
||||
return `res.body${pointer.replace('/', '.')}`;
|
||||
}
|
||||
return expression;
|
||||
};
|
||||
|
||||
export const parseOpenApiCollection = (data) => {
|
||||
const brunoCollection = {
|
||||
name: '',
|
||||
uid: uuid(),
|
||||
version: '1',
|
||||
items: [],
|
||||
environments: []
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const collectionData = resolveRefs(data);
|
||||
if (!collectionData) {
|
||||
reject(new BrunoError('Invalid OpenAPI collection. Failed to resolve refs.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Currently parsing of openapi spec is "do your best", that is
|
||||
// allows "invalid" openapi spec
|
||||
|
||||
// Assumes v3 if not defined. v2 is not supported yet
|
||||
if (collectionData.openapi && !collectionData.openapi.startsWith('3')) {
|
||||
reject(new BrunoError('Only OpenAPI v3 is supported currently.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO what if info.title not defined?
|
||||
brunoCollection.name = collectionData.info.title;
|
||||
let servers = collectionData.servers || [];
|
||||
|
||||
// Create environments based on the servers
|
||||
servers.forEach((server, index) => {
|
||||
let baseUrl = getDefaultUrl(server);
|
||||
let environmentName = server.description ? server.description : `Environment ${index + 1}`;
|
||||
|
||||
brunoCollection.environments.push({
|
||||
uid: uuid(),
|
||||
name: environmentName,
|
||||
variables: [
|
||||
{
|
||||
uid: uuid(),
|
||||
name: 'baseUrl',
|
||||
value: baseUrl,
|
||||
type: 'text',
|
||||
enabled: true,
|
||||
secret: false
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
let securityConfig = getSecurity(collectionData);
|
||||
|
||||
let allRequests = Object.entries(collectionData.paths)
|
||||
.map(([path, methods]) => {
|
||||
return Object.entries(methods)
|
||||
.filter(([method, op]) => {
|
||||
return ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(
|
||||
method.toLowerCase()
|
||||
);
|
||||
})
|
||||
.map(([method, operationObject]) => {
|
||||
return {
|
||||
method: method,
|
||||
path: path.replace(/{([^}]+)}/g, ':$1'), // Replace placeholders enclosed in curly braces with colons
|
||||
operationObject: operationObject,
|
||||
global: {
|
||||
server: '{{baseUrl}}',
|
||||
security: securityConfig
|
||||
}
|
||||
};
|
||||
});
|
||||
})
|
||||
.reduce((acc, val) => acc.concat(val), []); // flatten
|
||||
|
||||
let [groups, ungroupedRequests] = groupRequestsByTags(allRequests);
|
||||
let brunoFolders = groups.map((group) => {
|
||||
return {
|
||||
uid: uuid(),
|
||||
name: group.name,
|
||||
type: 'folder',
|
||||
items: group.requests.map(transformOpenapiRequestItem)
|
||||
};
|
||||
});
|
||||
|
||||
let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem);
|
||||
let brunoCollectionItems = brunoFolders.concat(ungroupedItems);
|
||||
brunoCollection.items = brunoCollectionItems;
|
||||
resolve(brunoCollection);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
reject(new BrunoError('An error occurred while parsing the OpenAPI collection'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
|
||||
.then(readFile)
|
||||
.then(parseOpenApiCollection)
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => openApiToBruno(collection))
|
||||
.then((collection) => resolve({ collection }))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { parseOpenApiCollection } from './openapi-collection';
|
||||
import { uuid } from 'utils/common';
|
||||
|
||||
jest.mock('utils/common');
|
||||
|
||||
describe('openapi importer util functions', () => {
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
it('should convert openapi object to bruno collection correctly', async () => {
|
||||
const input = {
|
||||
openapi: '3.0.3',
|
||||
info: {
|
||||
title: 'Sample API with Multiple Servers',
|
||||
description: 'API spec with multiple servers.',
|
||||
version: '1.0.0'
|
||||
},
|
||||
servers: [
|
||||
{ url: 'https://api.example.com/v1', description: 'Production Server' },
|
||||
{ url: 'https://staging-api.example.com/v1', description: 'Staging Server' },
|
||||
{ url: 'http://localhost:3000/v1', description: 'Local Server' }
|
||||
],
|
||||
paths: {
|
||||
'/users': {
|
||||
get: {
|
||||
summary: 'Get a list of users',
|
||||
parameters: [
|
||||
{ name: 'page', in: 'query', required: false, schema: { type: 'integer' } },
|
||||
{ name: 'limit', in: 'query', required: false, schema: { type: 'integer' } }
|
||||
],
|
||||
responses: {
|
||||
'200': { description: 'A list of users' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
name: 'Sample API with Multiple Servers',
|
||||
version: '1',
|
||||
items: [
|
||||
{
|
||||
name: 'Get a list of users',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: '{{baseUrl}}/users',
|
||||
method: 'GET',
|
||||
params: [
|
||||
{ name: 'page', value: '', enabled: false, type: 'query' },
|
||||
{ name: 'limit', value: '', enabled: false, type: 'query' }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
environments: [
|
||||
{ name: 'Production Server', variables: [{ name: 'baseUrl', value: 'https://api.example.com/v1' }] },
|
||||
{ name: 'Staging Server', variables: [{ name: 'baseUrl', value: 'https://staging-api.example.com/v1' }] },
|
||||
{ name: 'Local Server', variables: [{ name: 'baseUrl', value: 'http://localhost:3000/v1' }] }
|
||||
]
|
||||
};
|
||||
|
||||
const result = await parseOpenApiCollection(input);
|
||||
|
||||
expect(result).toMatchObject(expectedOutput);
|
||||
expect(uuid).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
});
|
||||
@@ -1,650 +1,29 @@
|
||||
import get from 'lodash/get';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
import { postmanTranslation } from 'utils/importers/translators/postman_translation';
|
||||
import each from 'lodash/each';
|
||||
import brunoConverters from '@usebruno/converters';
|
||||
import { safeParseJSON } from 'utils/common/index';
|
||||
const { postmanToBruno } = brunoConverters;
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => resolve(e.target.result);
|
||||
fileReader.onload = (e) => resolve(safeParseJSON(e.target.result));
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
const parseGraphQLRequest = (graphqlSource) => {
|
||||
try {
|
||||
let queryResultObject = {
|
||||
query: '',
|
||||
variables: ''
|
||||
};
|
||||
|
||||
if (typeof graphqlSource === 'string') {
|
||||
graphqlSource = JSON.parse(graphqlSource);
|
||||
}
|
||||
|
||||
if (graphqlSource.hasOwnProperty('variables') && graphqlSource.variables !== '') {
|
||||
queryResultObject.variables = graphqlSource.variables;
|
||||
}
|
||||
|
||||
if (graphqlSource.hasOwnProperty('query') && graphqlSource.query !== '') {
|
||||
queryResultObject.query = graphqlSource.query;
|
||||
}
|
||||
|
||||
return queryResultObject;
|
||||
} catch (e) {
|
||||
return {
|
||||
query: '',
|
||||
variables: ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const isItemAFolder = (item) => {
|
||||
return !item.request;
|
||||
};
|
||||
|
||||
const convertV21Auth = (array) => {
|
||||
return array.reduce((accumulator, currentValue) => {
|
||||
accumulator[currentValue.key] = currentValue.value;
|
||||
return accumulator;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const constructUrlFromParts = (url) => {
|
||||
const { protocol = 'http', host, path, port, query, hash } = url || {};
|
||||
const hostStr = Array.isArray(host) ? host.filter(Boolean).join('.') : host || '';
|
||||
const pathStr = Array.isArray(path) ? path.filter(Boolean).join('/') : path || '';
|
||||
const portStr = port ? `:${port}` : '';
|
||||
const queryStr =
|
||||
query && Array.isArray(query) && query.length > 0
|
||||
? `?${query
|
||||
.filter((q) => q.key)
|
||||
.map((q) => `${q.key}=${q.value || ''}`)
|
||||
.join('&')}`
|
||||
: '';
|
||||
const urlStr = `${protocol}://${hostStr}${portStr}${pathStr ? `/${pathStr}` : ''}${queryStr}`;
|
||||
return urlStr;
|
||||
};
|
||||
|
||||
const constructUrl = (url) => {
|
||||
if (!url) return '';
|
||||
|
||||
if (typeof url === 'string') {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (typeof url === 'object') {
|
||||
const { raw } = url;
|
||||
|
||||
if (raw && typeof raw === 'string') {
|
||||
// If the raw URL contains url-fragments remove it
|
||||
if (raw.includes('#')) {
|
||||
return raw.split('#')[0]; // Returns the part of raw URL without the url-fragment part.
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
// If no raw value exists, construct the URL from parts
|
||||
return constructUrlFromParts(url);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
let translationLog = {};
|
||||
|
||||
/* struct of translation log
|
||||
{
|
||||
[collectionName]: {
|
||||
script: [index1, index2],
|
||||
test: [index1, index2]
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const pushTranslationLog = (type, index) => {
|
||||
if (!translationLog[i.name]) {
|
||||
translationLog[i.name] = {};
|
||||
}
|
||||
if (!translationLog[i.name][type]) {
|
||||
translationLog[i.name][type] = [];
|
||||
}
|
||||
translationLog[i.name][type].push(index + 1);
|
||||
};
|
||||
|
||||
const importScriptsFromEvents = (events, requestObject, options, pushTranslationLog) => {
|
||||
events.forEach((event) => {
|
||||
if (event.script && event.script.exec) {
|
||||
if (event.listen === 'prerequest') {
|
||||
if (!requestObject.script) {
|
||||
requestObject.script = {};
|
||||
}
|
||||
|
||||
if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
|
||||
requestObject.script.req = event.script.exec
|
||||
.map((line, index) =>
|
||||
options.enablePostmanTranslations.enabled
|
||||
? postmanTranslation(line, () => pushTranslationLog('script', index))
|
||||
: `// ${line}`
|
||||
)
|
||||
.join('\n');
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
requestObject.script.req = options.enablePostmanTranslations.enabled
|
||||
? postmanTranslation(event.script.exec, () => pushTranslationLog('script', 0))
|
||||
: `// ${event.script.exec}`;
|
||||
} else {
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.listen === 'test') {
|
||||
if (!requestObject.tests) {
|
||||
requestObject.tests = {};
|
||||
}
|
||||
|
||||
if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
|
||||
requestObject.tests = event.script.exec
|
||||
.map((line, index) =>
|
||||
options.enablePostmanTranslations.enabled
|
||||
? postmanTranslation(line, () => pushTranslationLog('test', index))
|
||||
: `// ${line}`
|
||||
)
|
||||
.join('\n');
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
requestObject.tests = options.enablePostmanTranslations.enabled
|
||||
? postmanTranslation(event.script.exec, () => pushTranslationLog('test', 0))
|
||||
: `// ${event.script.exec}`;
|
||||
} else {
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importCollectionLevelVariables = (variables, requestObject) => {
|
||||
const vars = variables.map((v) => ({
|
||||
uid: uuid(),
|
||||
name: v.key,
|
||||
value: v.value,
|
||||
enabled: true
|
||||
}));
|
||||
|
||||
requestObject.vars.req = vars;
|
||||
};
|
||||
|
||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
|
||||
brunoParent.items = brunoParent.items || [];
|
||||
const folderMap = {};
|
||||
const requestMap = {};
|
||||
const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE']
|
||||
|
||||
each(item, (i) => {
|
||||
if (isItemAFolder(i)) {
|
||||
const baseFolderName = i.name;
|
||||
let folderName = baseFolderName;
|
||||
let count = 1;
|
||||
|
||||
while (folderMap[folderName]) {
|
||||
folderName = `${baseFolderName}_${count}`;
|
||||
count++;
|
||||
}
|
||||
|
||||
const brunoFolderItem = {
|
||||
uid: uuid(),
|
||||
name: folderName,
|
||||
type: 'folder',
|
||||
items: [],
|
||||
root: {
|
||||
docs: i.description || '',
|
||||
meta: {
|
||||
name: folderName
|
||||
},
|
||||
request: {
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null
|
||||
},
|
||||
headers: [],
|
||||
script: {},
|
||||
tests: '',
|
||||
vars: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (i.item && i.item.length) {
|
||||
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth, options);
|
||||
}
|
||||
|
||||
if (i.event) {
|
||||
importScriptsFromEvents(i.event, brunoFolderItem.root.request, options, pushTranslationLog);
|
||||
}
|
||||
|
||||
brunoParent.items.push(brunoFolderItem);
|
||||
folderMap[folderName] = brunoFolderItem;
|
||||
|
||||
} else {
|
||||
if (i.request) {
|
||||
if(!requestMethods.includes(i?.request?.method.toUpperCase())){
|
||||
console.warn("Unexpected request.method")
|
||||
return;
|
||||
}
|
||||
|
||||
const baseRequestName = i.name;
|
||||
let requestName = baseRequestName;
|
||||
let count = 1;
|
||||
|
||||
while (requestMap[requestName]) {
|
||||
requestName = `${baseRequestName}_${count}`;
|
||||
count++;
|
||||
}
|
||||
|
||||
const url = constructUrl(i.request.url);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: requestName,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: url,
|
||||
method: i?.request?.method?.toUpperCase(),
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null
|
||||
},
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
},
|
||||
docs: i.request.description
|
||||
}
|
||||
};
|
||||
|
||||
if (i.event) {
|
||||
i.event.forEach((event) => {
|
||||
if (event.listen === 'prerequest' && event.script && event.script.exec) {
|
||||
if (!brunoRequestItem.request.script) {
|
||||
brunoRequestItem.request.script = {};
|
||||
}
|
||||
if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
|
||||
brunoRequestItem.request.script.req = event.script.exec
|
||||
.map((line, index) =>
|
||||
options.enablePostmanTranslations.enabled
|
||||
? postmanTranslation(line, () => pushTranslationLog('script', index))
|
||||
: `// ${line}`
|
||||
)
|
||||
.join('\n');
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
brunoRequestItem.request.script.req = options.enablePostmanTranslations.enabled
|
||||
? postmanTranslation(event.script.exec, () => pushTranslationLog('script', 0))
|
||||
: `// ${event.script.exec}`;
|
||||
} else {
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
if (event.listen === 'test' && event.script && event.script.exec) {
|
||||
if (!brunoRequestItem.request.tests) {
|
||||
brunoRequestItem.request.tests = {};
|
||||
}
|
||||
if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
|
||||
brunoRequestItem.request.tests = event.script.exec
|
||||
.map((line, index) =>
|
||||
options.enablePostmanTranslations.enabled
|
||||
? postmanTranslation(line, () => pushTranslationLog('test', index))
|
||||
: `// ${line}`
|
||||
)
|
||||
.join('\n');
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
brunoRequestItem.request.tests = options.enablePostmanTranslations.enabled
|
||||
? postmanTranslation(event.script.exec, () => pushTranslationLog('test', 0))
|
||||
: `// ${event.script.exec}`;
|
||||
} else {
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const bodyMode = get(i, 'request.body.mode');
|
||||
if (bodyMode) {
|
||||
if (bodyMode === 'formdata') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
|
||||
each(i.request.body.formdata, (param) => {
|
||||
const isFile = param.type === 'file';
|
||||
let value;
|
||||
let type;
|
||||
|
||||
if (isFile) {
|
||||
// If param.src is an array, keep it as it is.
|
||||
// If param.src is a string, convert it into an array with a single element.
|
||||
value = Array.isArray(param.src) ? param.src : typeof param.src === 'string' ? [param.src] : null;
|
||||
type = 'file';
|
||||
} else {
|
||||
value = param.value;
|
||||
type = 'text';
|
||||
}
|
||||
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
type: type,
|
||||
name: param.key,
|
||||
value: value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (bodyMode === 'urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
each(i.request.body.urlencoded, (param) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (bodyMode === 'raw') {
|
||||
let language = get(i, 'request.body.options.raw.language');
|
||||
if (!language) {
|
||||
language = searchLanguageByHeader(i.request.header);
|
||||
}
|
||||
if (language === 'json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
brunoRequestItem.request.body.json = i.request.body.raw;
|
||||
} else if (language === 'xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = i.request.body.raw;
|
||||
} else {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = i.request.body.raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bodyMode === 'graphql') {
|
||||
brunoRequestItem.type = 'graphql-request';
|
||||
brunoRequestItem.request.body.mode = 'graphql';
|
||||
brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
|
||||
}
|
||||
|
||||
each(i.request.header, (header) => {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: header.key,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: !header.disabled
|
||||
});
|
||||
});
|
||||
|
||||
const auth = i.request.auth ?? parentAuth;
|
||||
if (auth?.[auth.type] && auth.type !== 'noauth') {
|
||||
let authValues = auth[auth.type];
|
||||
if (Array.isArray(authValues)) {
|
||||
authValues = convertV21Auth(authValues);
|
||||
}
|
||||
if (auth.type === 'basic') {
|
||||
brunoRequestItem.request.auth.mode = 'basic';
|
||||
brunoRequestItem.request.auth.basic = {
|
||||
username: authValues.username,
|
||||
password: authValues.password
|
||||
};
|
||||
} else if (auth.type === 'bearer') {
|
||||
brunoRequestItem.request.auth.mode = 'bearer';
|
||||
brunoRequestItem.request.auth.bearer = {
|
||||
token: authValues.token
|
||||
};
|
||||
} else if (auth.type === 'awsv4') {
|
||||
brunoRequestItem.request.auth.mode = 'awsv4';
|
||||
brunoRequestItem.request.auth.awsv4 = {
|
||||
accessKeyId: authValues.accessKey,
|
||||
secretAccessKey: authValues.secretKey,
|
||||
sessionToken: authValues.sessionToken,
|
||||
service: authValues.service,
|
||||
region: authValues.region,
|
||||
profileName: ''
|
||||
};
|
||||
} else if (auth.type === 'apikey'){
|
||||
brunoRequestItem.request.auth.mode = 'apikey';
|
||||
brunoRequestItem.request.auth.apikey = {
|
||||
key: authValues.key,
|
||||
value: authValues.value?.toString(), // Convert the value to a string as Postman's schema does not rigidly define the type of it,
|
||||
placement: "header" //By default we are placing the apikey values in headers!
|
||||
}
|
||||
} else if (auth.type === 'oauth2'){
|
||||
const findValueUsingKey = (key) => {
|
||||
return auth?.oauth2?.find(v => v?.key == key)?.value || ''
|
||||
}
|
||||
const oauth2GrantTypeMaps = {
|
||||
'authorization_code_with_pkce': 'authorization_code',
|
||||
'authorization_code': 'authorization_code',
|
||||
'client_credentials': 'client_credentials',
|
||||
'password_credentials': 'password_credentials'
|
||||
}
|
||||
const grantType = oauth2GrantTypeMaps[findValueUsingKey('grant_type')] || 'authorization_code';
|
||||
if (grantType) {
|
||||
brunoRequestItem.request.auth.mode = 'oauth2';
|
||||
switch(grantType) {
|
||||
case 'authorization_code':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'authorization_code',
|
||||
authorizationUrl: findValueUsingKey('authUrl'),
|
||||
callbackUrl: findValueUsingKey('redirect_uri'),
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshTokenUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
pkce: Boolean(findValueUsingKey('grant_type') == 'authorization_code_with_pkce'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
|
||||
};
|
||||
break;
|
||||
case 'password_credentials':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'password',
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshTokenUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
username: findValueUsingKey('username'),
|
||||
password: findValueUsingKey('password'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshTokenUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
each(get(i, 'request.url.query'), (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: 'query',
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(get(i, 'request.url.variable', []), (param) => {
|
||||
if (!param.key) {
|
||||
// If no key, skip this iteration and discard the param
|
||||
return;
|
||||
}
|
||||
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value ?? '',
|
||||
description: param.description ?? '',
|
||||
type: 'path',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
|
||||
brunoParent.items.push(brunoRequestItem);
|
||||
requestMap[requestName] = brunoRequestItem;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const searchLanguageByHeader = (headers) => {
|
||||
let contentType;
|
||||
each(headers, (header) => {
|
||||
if (header.key.toLowerCase() === 'content-type' && !header.disabled) {
|
||||
if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(header.value)) {
|
||||
contentType = 'json';
|
||||
} else if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(header.value)) {
|
||||
contentType = 'xml';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const importPostmanV2Collection = (collection, options) => {
|
||||
const brunoCollection = {
|
||||
name: collection.info.name,
|
||||
uid: uuid(),
|
||||
version: '1',
|
||||
items: [],
|
||||
environments: [],
|
||||
root: {
|
||||
docs: collection.info.description || '',
|
||||
meta: {
|
||||
name: collection.info.name
|
||||
},
|
||||
request: {
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null
|
||||
},
|
||||
headers: [],
|
||||
script: {},
|
||||
tests: '',
|
||||
vars: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (collection.event) {
|
||||
importScriptsFromEvents(collection.event, brunoCollection.root.request, options, pushTranslationLog);
|
||||
}
|
||||
|
||||
if (collection?.variable){
|
||||
importCollectionLevelVariables(collection.variable, brunoCollection.root.request);
|
||||
}
|
||||
|
||||
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, options);
|
||||
|
||||
return brunoCollection;
|
||||
};
|
||||
|
||||
const parsePostmanCollection = (str, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let collection = JSON.parse(str);
|
||||
let schema = get(collection, 'info.schema');
|
||||
|
||||
let v2Schemas = [
|
||||
'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',
|
||||
'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
||||
'https://schema.postman.com/json/collection/v2.0.0/collection.json',
|
||||
'https://schema.postman.com/json/collection/v2.1.0/collection.json'
|
||||
];
|
||||
|
||||
if (v2Schemas.includes(schema)) {
|
||||
return resolve(importPostmanV2Collection(collection, options));
|
||||
}
|
||||
|
||||
throw new BrunoError('Unknown postman schema');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if (err instanceof BrunoError) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return reject(new BrunoError('Unable to parse the postman collection json file'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const logTranslationDetails = (translationLog) => {
|
||||
if (Object.keys(translationLog || {}).length > 0) {
|
||||
console.warn(
|
||||
`[Postman Translation Logs]
|
||||
Collections incomplete : ${Object.keys(translationLog || {}).length}` +
|
||||
`\nTotal lines incomplete : ${Object.values(translationLog || {}).reduce(
|
||||
(acc, curr) => acc + (curr.script?.length || 0) + (curr.test?.length || 0),
|
||||
0
|
||||
)}` +
|
||||
`\nSee details below :`,
|
||||
translationLog
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const importCollection = (options) => {
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then((str) => parsePostmanCollection(str, options))
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => resolve({ collection, translationLog }))
|
||||
.then((collection) => postmanToBruno(collection))
|
||||
.then((collection) => resolve({ collection }))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
translationLog = {};
|
||||
reject(new BrunoError('Import collection failed'));
|
||||
})
|
||||
.then(() => {
|
||||
logTranslationDetails(translationLog);
|
||||
translationLog = {};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import each from 'lodash/each';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import brunoConverters from '@usebruno/converters';
|
||||
const { postmanToBrunoEnvironment } = brunoConverters;
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -11,50 +12,6 @@ const readFile = (files) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isSecret = (type) => {
|
||||
return type === 'secret';
|
||||
};
|
||||
|
||||
const importPostmanEnvironmentVariables = (brunoEnvironment, values) => {
|
||||
brunoEnvironment.variables = brunoEnvironment.variables || [];
|
||||
|
||||
each(values, (i) => {
|
||||
const brunoEnvironmentVariable = {
|
||||
name: i.key,
|
||||
value: i.value,
|
||||
enabled: i.enabled,
|
||||
secret: isSecret(i.type)
|
||||
};
|
||||
|
||||
brunoEnvironment.variables.push(brunoEnvironmentVariable);
|
||||
});
|
||||
};
|
||||
|
||||
const importPostmanEnvironment = (environment) => {
|
||||
const brunoEnvironment = {
|
||||
name: environment.name,
|
||||
variables: []
|
||||
};
|
||||
|
||||
importPostmanEnvironmentVariables(brunoEnvironment, environment.values);
|
||||
return brunoEnvironment;
|
||||
};
|
||||
|
||||
const parsePostmanEnvironment = (str) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let environment = JSON.parse(str);
|
||||
return resolve(importPostmanEnvironment(environment));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if (err instanceof BrunoError) {
|
||||
return reject(err);
|
||||
}
|
||||
return reject(new BrunoError('Unable to parse the postman environment json file'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importEnvironment = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ multiple: true, accept: 'application/json' })
|
||||
@@ -62,7 +19,7 @@ const importEnvironment = () => {
|
||||
return Promise.all(
|
||||
Object.values(files ?? {}).map((file) =>
|
||||
readFile([file])
|
||||
.then(parsePostmanEnvironment)
|
||||
.then((environment) => postmanToBrunoEnvironment(environment))
|
||||
.catch((err) => {
|
||||
console.error(`Error processing file: ${file.name || 'undefined'}`, err);
|
||||
throw err;
|
||||
|
||||
@@ -5,6 +5,10 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
sendHttpRequest(item, collection, environment, runtimeVariables)
|
||||
.then((response) => {
|
||||
// if there is an error, we return the response object as is
|
||||
if (response?.error) {
|
||||
resolve(response)
|
||||
}
|
||||
resolve({
|
||||
state: 'success',
|
||||
data: response.data,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# bruno-cli
|
||||
# Bruno CLI
|
||||
|
||||
With Bruno CLI, you can now run your API collections with ease using simple command line commands.
|
||||
|
||||
This makes it easier to test your APIs in different environments, automate your testing process, and integrate your API tests with your continuous integration and deployment workflows.
|
||||
|
||||
For detailed documentation, visit [Bruno CLI Documentation](https://docs.usebruno.com/bru-cli/overview).
|
||||
|
||||
## Installation
|
||||
|
||||
To install the Bruno CLI, use the node package manager of your choice, such as NPM:
|
||||
@@ -56,6 +58,30 @@ If you need to limit the trusted CA to a specified set when validating the reque
|
||||
bru run request.bru --cacert myCustomCA.pem --ignore-truststore
|
||||
```
|
||||
|
||||
## Command Line Options
|
||||
|
||||
| Option | Details |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------- |
|
||||
| -h, --help | Show help |
|
||||
| --version | Show version number |
|
||||
| -r | Indicates a recursive run (default: false) |
|
||||
| --cacert [string] | CA certificate to verify peer against |
|
||||
| --env [string] | Specify environment to run with |
|
||||
| --env-var [string] | Overwrite a single environment variable, multiple usages possible |
|
||||
| -o, --output [string] | Path to write file results to |
|
||||
| -f, --format [string] | Format of the file results; available formats are "json" (default) or "junit" |
|
||||
| --reporter-json [string] | Path to generate a JSON report |
|
||||
| --reporter-junit [string] | Path to generate a JUnit report |
|
||||
| --reporter-html [string] | Path to generate an HTML report |
|
||||
| --insecure | Allow insecure server connections |
|
||||
| --tests-only | Only run requests that have tests |
|
||||
| --bail | Stop execution after a failure of a request, test, or assertion |
|
||||
| --csv-file-path | CSV file to run the collection with |
|
||||
| --reporter--skip-all-headers | Skip all headers in the report |
|
||||
| --reporter-skip-headers | Skip specific headers in the report |
|
||||
| --client-cert-config | Client certificate configuration by passing a JSON file |
|
||||
| --delay [number] | Add delay to each request |
|
||||
|
||||
## Scripting
|
||||
|
||||
Bruno cli returns the following exit status codes:
|
||||
|
||||
@@ -177,7 +177,7 @@ const getBruFilesRecursively = (dir, testsOnly) => {
|
||||
|
||||
for (const file of filesInCurrentDir) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
// todo: we might need a ignore config inside bruno.json
|
||||
if (
|
||||
|
||||
@@ -29,7 +29,7 @@ const isFile = (filepath) => {
|
||||
|
||||
const isDirectory = (dirPath) => {
|
||||
try {
|
||||
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
|
||||
return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
22
packages/bruno-converters/.gitignore
vendored
Normal file
22
packages/bruno-converters/.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# dependencies
|
||||
node_modules
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
||||
# production
|
||||
dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
3
packages/bruno-converters/babel.config.js
Normal file
3
packages/bruno-converters/babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
|
||||
};
|
||||
13
packages/bruno-converters/jest.config.js
Normal file
13
packages/bruno-converters/jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
'^.+\\.js$': 'babel-jest',
|
||||
},
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(nanoid)/)'
|
||||
],
|
||||
testEnvironment: 'node',
|
||||
moduleNameMapper: {
|
||||
'^nanoid(/(.*)|$)': 'nanoid$1'
|
||||
}
|
||||
};
|
||||
11
packages/bruno-converters/jest.setup.js
Normal file
11
packages/bruno-converters/jest.setup.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// Mock the uuid function
|
||||
jest.mock('./src/common', () => {
|
||||
// Import the original module to keep other functions intact
|
||||
const originalModule = jest.requireActual('./src/common');
|
||||
|
||||
return {
|
||||
__esModule: true, // Use this property to indicate it's an ES module
|
||||
...originalModule,
|
||||
uuid: jest.fn(() => 'mockeduuidvalue123456'), // Mock uuid to return a fixed value
|
||||
};
|
||||
});
|
||||
21
packages/bruno-converters/license.md
Normal file
21
packages/bruno-converters/license.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anoop M D, Anusree P S and Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
45
packages/bruno-converters/package.json
Normal file
45
packages/bruno-converters/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@usebruno/converters",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"test": "node --experimental-vm-modules $(npx which jest) --colors --collectCoverage",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "rollup -c",
|
||||
"watch": "rollup -c -w",
|
||||
"prepack": "npm run test && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@usebruno/schema": "^0.7.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"nanoid": "3.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"babel-jest": "^29.7.0",
|
||||
"rimraf": "^5.0.7",
|
||||
"rollup": "3.2.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
}
|
||||
}
|
||||
45
packages/bruno-converters/readme.md
Normal file
45
packages/bruno-converters/readme.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# bruno-converters
|
||||
|
||||
The converters package is responsible for converting collections from one format to a Bruno collection.
|
||||
It can be used as a standalone package or as a part of the Bruno framework.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @usebruno/converters
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Convert Postman collection to Bruno collection
|
||||
|
||||
```javascript
|
||||
const { postmanToBruno } = require('@usebruno/converters');
|
||||
|
||||
// Convert Postman collection to Bruno collection
|
||||
const brunoCollection = postmanToBruno(postmanCollection);
|
||||
```
|
||||
|
||||
### Convert Postman Environment to Bruno Environment
|
||||
|
||||
```javascript
|
||||
const { postmanToBrunoEnvironment } = require('@usebruno/converters');
|
||||
|
||||
const brunoEnvironment = postmanToBrunoEnvironment(postmanEnvironment);
|
||||
```
|
||||
|
||||
### Convert Insomnia collection to Bruno collection
|
||||
|
||||
```javascript
|
||||
import { insomniaToBruno } from '@usebruno/converters';
|
||||
|
||||
const brunoCollection = insomniaToBruno(insomniaCollection);
|
||||
```
|
||||
|
||||
### Convert OpenAPI specification to Bruno collection
|
||||
|
||||
```javascript
|
||||
import { openApiToBruno } from '@usebruno/converters';
|
||||
|
||||
const brunoCollection = openApiToBruno(openApiSpecification);
|
||||
```
|
||||
38
packages/bruno-converters/rollup.config.js
Normal file
38
packages/bruno-converters/rollup.config.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const { terser } = require('rollup-plugin-terser');
|
||||
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
const alias = require('@rollup/plugin-alias');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
input: 'src/index.js',
|
||||
output: [
|
||||
{
|
||||
file: packageJson.main,
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
},
|
||||
{
|
||||
file: packageJson.module,
|
||||
format: 'esm',
|
||||
sourcemap: true
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
peerDepsExternal(),
|
||||
nodeResolve({
|
||||
preferBuiltins: true,
|
||||
extensions: ['.js', '.css'] // Resolve .js files
|
||||
}),
|
||||
commonjs(),
|
||||
terser(),
|
||||
alias({
|
||||
entries: [{ find: 'src', replacement: path.resolve(__dirname, 'src') }]
|
||||
})
|
||||
]
|
||||
}
|
||||
];
|
||||
211
packages/bruno-converters/src/common/index.js
Normal file
211
packages/bruno-converters/src/common/index.js
Normal file
@@ -0,0 +1,211 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { collectionSchema } from '@usebruno/schema';
|
||||
|
||||
export const safeParseJSON = (str) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
export const safeStringifyJSON = (obj, indent = false) => {
|
||||
if (obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
try {
|
||||
if (indent) {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
}
|
||||
return JSON.stringify(obj);
|
||||
} catch (e) {
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
|
||||
export const isItemARequest = (item) => {
|
||||
return item.hasOwnProperty('request') && ['http-request', 'graphql-request'].includes(item.type) && !item.items;
|
||||
};
|
||||
|
||||
// a customized version of nanoid without using _ and -
|
||||
export const uuid = () => {
|
||||
// https://github.com/ai/nanoid/blob/main/url-alphabet/index.js
|
||||
const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';
|
||||
const customNanoId = customAlphabet(urlAlphabet, 21);
|
||||
|
||||
return customNanoId();
|
||||
};
|
||||
|
||||
export const validateSchema = (collection = {}) => {
|
||||
try {
|
||||
collectionSchema.validateSync(collection);
|
||||
return collection;
|
||||
} catch (err) {
|
||||
throw new Error('The Collection has an invalid schema');
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUidsInCollection = (_collection) => {
|
||||
const collection = cloneDeep(_collection);
|
||||
|
||||
collection.uid = uuid();
|
||||
|
||||
const updateItemUids = (items = []) => {
|
||||
each(items, (item) => {
|
||||
item.uid = uuid();
|
||||
|
||||
each(get(item, 'request.headers'), (header) => (header.uid = uuid()));
|
||||
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.vars.req'), (v) => (v.uid = uuid()));
|
||||
each(get(item, 'request.vars.res'), (v) => (v.uid = uuid()));
|
||||
each(get(item, 'request.assertions'), (a) => (a.uid = uuid()));
|
||||
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
updateItemUids(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
updateItemUids(collection.items);
|
||||
|
||||
const updateEnvUids = (envs = []) => {
|
||||
each(envs, (env) => {
|
||||
env.uid = uuid();
|
||||
each(env.variables, (variable) => (variable.uid = uuid()));
|
||||
});
|
||||
};
|
||||
updateEnvUids(collection.environments);
|
||||
|
||||
return collection;
|
||||
};
|
||||
|
||||
// todo
|
||||
// need to eventually get rid of supporting old collection app models
|
||||
// 1. start with making request type a constant fetched from a single place
|
||||
// 2. move references of param and replace it with query inside the app
|
||||
export const transformItemsInCollection = (collection) => {
|
||||
const transformItems = (items = []) => {
|
||||
each(items, (item) => {
|
||||
|
||||
if (['http', 'graphql'].includes(item.type)) {
|
||||
item.type = `${item.type}-request`;
|
||||
|
||||
if (item.request.query) {
|
||||
item.request.params = item.request.query.map((queryItem) => ({
|
||||
...queryItem,
|
||||
type: 'query',
|
||||
uid: queryItem.uid || uuid()
|
||||
}));
|
||||
}
|
||||
|
||||
delete item.request.query;
|
||||
|
||||
// from 5 feb 2024, multipartFormData needs to have a type
|
||||
// this was introduced when we added support for file uploads
|
||||
// below logic is to make older collection exports backward compatible
|
||||
let multipartFormData = get(item, 'request.body.multipartForm');
|
||||
if (multipartFormData) {
|
||||
each(multipartFormData, (form) => {
|
||||
if (!form.type) {
|
||||
form.type = 'text';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
transformItems(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
transformItems(collection.items);
|
||||
|
||||
return collection;
|
||||
};
|
||||
|
||||
export const hydrateSeqInCollection = (collection) => {
|
||||
const hydrateSeq = (items = []) => {
|
||||
let index = 1;
|
||||
each(items, (item) => {
|
||||
if (isItemARequest(item) && !item.seq) {
|
||||
item.seq = index;
|
||||
index++;
|
||||
}
|
||||
if (item.items && item.items.length) {
|
||||
hydrateSeq(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
hydrateSeq(collection.items);
|
||||
|
||||
return collection;
|
||||
};
|
||||
|
||||
export const deleteUidsInItems = (items) => {
|
||||
each(items, (item) => {
|
||||
delete item.uid;
|
||||
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
each(get(item, 'request.headers'), (header) => delete header.uid);
|
||||
each(get(item, 'request.params'), (param) => delete param.uid);
|
||||
each(get(item, 'request.vars.req'), (v) => delete v.uid);
|
||||
each(get(item, 'request.vars.res'), (v) => delete v.uid);
|
||||
each(get(item, 'request.vars.assertions'), (a) => delete a.uid);
|
||||
each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);
|
||||
each(get(item, 'request.body.file'), (param) => delete param.uid);
|
||||
}
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
deleteUidsInItems(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Some of the models in the app are not consistent with the Collection Json format
|
||||
* This function is used to transform the models to the Collection Json format
|
||||
*/
|
||||
export const transformItem = (items = []) => {
|
||||
each(items, (item) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
if (item.type === 'graphql-request') {
|
||||
item.type = 'graphql';
|
||||
}
|
||||
|
||||
if (item.type === 'http-request') {
|
||||
item.type = 'http';
|
||||
}
|
||||
}
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
transformItem(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteUidsInEnvs = (envs) => {
|
||||
each(envs, (env) => {
|
||||
delete env.uid;
|
||||
each(env.variables, (variable) => delete variable.uid);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteSecretsInEnvs = (envs) => {
|
||||
each(envs, (env) => {
|
||||
each(env.variables, (variable) => {
|
||||
if (variable.secret) {
|
||||
variable.value = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
16
packages/bruno-converters/src/index.js
Normal file
16
packages/bruno-converters/src/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import postmanToBruno from './postman/postman-to-bruno.js';
|
||||
import postmanToBrunoEnvironment from './postman/postman-env-to-bruno-env.js';
|
||||
|
||||
import brunoToPostman from './postman/bruno-to-postman.js';
|
||||
|
||||
import openApiToBruno from './openapi/openapi-to-bruno.js';
|
||||
|
||||
import insomniaToBruno from './insomnia/insomnia-to-bruno.js';
|
||||
|
||||
export default {
|
||||
postmanToBruno,
|
||||
postmanToBrunoEnvironment,
|
||||
brunoToPostman,
|
||||
openApiToBruno,
|
||||
insomniaToBruno
|
||||
};
|
||||
228
packages/bruno-converters/src/insomnia/insomnia-to-bruno.js
Normal file
228
packages/bruno-converters/src/insomnia/insomnia-to-bruno.js
Normal file
@@ -0,0 +1,228 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
|
||||
|
||||
const parseGraphQL = (text) => {
|
||||
try {
|
||||
const graphql = JSON.parse(text);
|
||||
|
||||
return {
|
||||
query: graphql.query,
|
||||
variables: JSON.stringify(graphql.variables, null, 2)
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
query: '',
|
||||
variables: ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const addSuffixToDuplicateName = (item, index, allItems) => {
|
||||
// Check if the request name already exist and if so add a number suffix
|
||||
const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {
|
||||
if (otherItem.name === item.name && otherIndex < index) {
|
||||
nameSuffix++;
|
||||
}
|
||||
return nameSuffix;
|
||||
}, 0);
|
||||
return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;
|
||||
};
|
||||
|
||||
const regexVariable = new RegExp('{{.*?}}', 'g');
|
||||
|
||||
const normalizeVariables = (value) => {
|
||||
value = value || '';
|
||||
const variables = value.match(regexVariable) || [];
|
||||
each(variables, (variable) => {
|
||||
value = value.replace(variable, variable.replace('_.', '').replaceAll(' ', ''));
|
||||
});
|
||||
return value;
|
||||
};
|
||||
|
||||
const transformInsomniaRequestItem = (request, index, allRequests) => {
|
||||
const name = addSuffixToDuplicateName(request, index, allRequests);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
digest: null
|
||||
},
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
each(request.headers, (header) => {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: header.name,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: !header.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(request.parameters, (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: 'query',
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(request.pathParameters, (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: '',
|
||||
type: 'path',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
|
||||
const authType = get(request, 'authentication.type', '');
|
||||
|
||||
if (authType === 'basic') {
|
||||
brunoRequestItem.request.auth.mode = 'basic';
|
||||
brunoRequestItem.request.auth.basic = {
|
||||
username: normalizeVariables(get(request, 'authentication.username', '')),
|
||||
password: normalizeVariables(get(request, 'authentication.password', ''))
|
||||
};
|
||||
} else if (authType === 'bearer') {
|
||||
brunoRequestItem.request.auth.mode = 'bearer';
|
||||
brunoRequestItem.request.auth.bearer = {
|
||||
token: normalizeVariables(get(request, 'authentication.token', ''))
|
||||
};
|
||||
}
|
||||
|
||||
const mimeType = get(request, 'body.mimeType', '').split(';')[0];
|
||||
|
||||
if (mimeType === 'application/json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
brunoRequestItem.request.body.json = request.body.text;
|
||||
} else if (mimeType === 'application/x-www-form-urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
each(request.body.params, (param) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
} else if (mimeType === 'multipart/form-data') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
each(request.body.params, (param) => {
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
type: 'text',
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
} else if (mimeType === 'text/plain') {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = request.body.text;
|
||||
} else if (mimeType === 'text/xml' || mimeType === 'application/xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = request.body.text;
|
||||
} else if (mimeType === 'application/graphql') {
|
||||
brunoRequestItem.type = 'graphql-request';
|
||||
brunoRequestItem.request.body.mode = 'graphql';
|
||||
brunoRequestItem.request.body.graphql = parseGraphQL(request.body.text);
|
||||
}
|
||||
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
const parseInsomniaCollection = (_insomniaCollection) => {
|
||||
const brunoCollection = {
|
||||
name: '',
|
||||
uid: uuid(),
|
||||
version: '1',
|
||||
items: [],
|
||||
environments: []
|
||||
};
|
||||
|
||||
try {
|
||||
const insomniaExport = _insomniaCollection;
|
||||
const insomniaResources = get(insomniaExport, 'resources', []);
|
||||
const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
|
||||
|
||||
if (!insomniaCollection) {
|
||||
throw new Error('Collection not found inside Insomnia export');
|
||||
}
|
||||
|
||||
brunoCollection.name = insomniaCollection.name;
|
||||
|
||||
const requestsAndFolders =
|
||||
insomniaResources.filter((resource) => resource._type === 'request' || resource._type === 'request_group') ||
|
||||
[];
|
||||
|
||||
function createFolderStructure(resources, parentId = null) {
|
||||
const requestGroups =
|
||||
resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
|
||||
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
|
||||
|
||||
const folders = requestGroups.map((folder, index, allFolder) => {
|
||||
const name = addSuffixToDuplicateName(folder, index, allFolder);
|
||||
const requests = resources.filter(
|
||||
(resource) => resource._type === 'request' && resource.parentId === folder._id
|
||||
);
|
||||
|
||||
return {
|
||||
uid: uuid(),
|
||||
name,
|
||||
type: 'folder',
|
||||
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem))
|
||||
};
|
||||
});
|
||||
|
||||
return folders.concat(requests.map(transformInsomniaRequestItem));
|
||||
}
|
||||
|
||||
brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id);
|
||||
return brunoCollection;
|
||||
} catch (err) {
|
||||
throw new Error('An error occurred while parsing the Insomnia collection');
|
||||
}
|
||||
};
|
||||
|
||||
export const insomniaToBruno = (insomniaCollection) => {
|
||||
try {
|
||||
const collection = parseInsomniaCollection(insomniaCollection);
|
||||
const transformedCollection = transformItemsInCollection(collection);
|
||||
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
|
||||
const validatedCollection = validateSchema(hydratedCollection);
|
||||
return validatedCollection;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw new Error('Import collection failed');
|
||||
}
|
||||
};
|
||||
|
||||
export default insomniaToBruno;
|
||||
436
packages/bruno-converters/src/openapi/openapi-to-bruno.js
Normal file
436
packages/bruno-converters/src/openapi/openapi-to-bruno.js
Normal file
@@ -0,0 +1,436 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
|
||||
|
||||
const ensureUrl = (url) => {
|
||||
// removing multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
|
||||
return url.replace(/([^:])\/{2,}/g, '$1/');
|
||||
};
|
||||
|
||||
const buildEmptyJsonBody = (bodySchema) => {
|
||||
let _jsonBody = {};
|
||||
each(bodySchema.properties || {}, (prop, name) => {
|
||||
if (prop.type === 'object') {
|
||||
_jsonBody[name] = buildEmptyJsonBody(prop);
|
||||
} else if (prop.type === 'array') {
|
||||
if (prop.items && prop.items.type === 'object') {
|
||||
_jsonBody[name] = [buildEmptyJsonBody(prop.items)];
|
||||
} else {
|
||||
_jsonBody[name] = [];
|
||||
}
|
||||
} else {
|
||||
_jsonBody[name] = '';
|
||||
}
|
||||
});
|
||||
return _jsonBody;
|
||||
};
|
||||
|
||||
const transformOpenapiRequestItem = (request) => {
|
||||
let _operationObject = request.operationObject;
|
||||
|
||||
let operationName = _operationObject.summary || _operationObject.operationId || _operationObject.description;
|
||||
if (!operationName) {
|
||||
operationName = `${request.method} ${request.path}`;
|
||||
}
|
||||
|
||||
// replace OpenAPI links in path by Bruno variables
|
||||
let path = request.path.replace(/{([a-zA-Z]+)}/g, `{{${_operationObject.operationId}_$1}}`);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: operationName,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: ensureUrl(request.global.server + path),
|
||||
method: request.method.toUpperCase(),
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
digest: null
|
||||
},
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
},
|
||||
script: {
|
||||
res: null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
each(_operationObject.parameters || [], (param) => {
|
||||
if (param.in === 'query') {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: '',
|
||||
description: param.description || '',
|
||||
enabled: param.required,
|
||||
type: 'query'
|
||||
});
|
||||
} else if (param.in === 'path') {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: '',
|
||||
description: param.description || '',
|
||||
enabled: param.required,
|
||||
type: 'path'
|
||||
});
|
||||
} else if (param.in === 'header') {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: '',
|
||||
description: param.description || '',
|
||||
enabled: param.required
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let auth;
|
||||
// allow operation override
|
||||
if (_operationObject.security && _operationObject.security.length > 0) {
|
||||
let schemeName = Object.keys(_operationObject.security[0])[0];
|
||||
auth = request.global.security.getScheme(schemeName);
|
||||
} else if (request.global.security.supported.length > 0) {
|
||||
auth = request.global.security.supported[0];
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
if (auth.type === 'http' && auth.scheme === 'basic') {
|
||||
brunoRequestItem.request.auth.mode = 'basic';
|
||||
brunoRequestItem.request.auth.basic = {
|
||||
username: '{{username}}',
|
||||
password: '{{password}}'
|
||||
};
|
||||
} else if (auth.type === 'http' && auth.scheme === 'bearer') {
|
||||
brunoRequestItem.request.auth.mode = 'bearer';
|
||||
brunoRequestItem.request.auth.bearer = {
|
||||
token: '{{token}}'
|
||||
};
|
||||
} else if (auth.type === 'apiKey' && auth.in === 'header') {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: auth.name,
|
||||
value: '{{apiKey}}',
|
||||
description: 'Authentication header',
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: handle allOf/anyOf/oneOf
|
||||
if (_operationObject.requestBody) {
|
||||
let content = get(_operationObject, 'requestBody.content', {});
|
||||
let mimeType = Object.keys(content)[0];
|
||||
let body = content[mimeType] || {};
|
||||
let bodySchema = body.schema;
|
||||
if (mimeType === 'application/json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
if (bodySchema && bodySchema.type === 'object') {
|
||||
let _jsonBody = buildEmptyJsonBody(bodySchema);
|
||||
brunoRequestItem.request.body.json = JSON.stringify(_jsonBody, null, 2);
|
||||
}
|
||||
if (bodySchema && bodySchema.type === 'array') {
|
||||
brunoRequestItem.request.body.json = JSON.stringify([buildEmptyJsonBody(bodySchema.items)], null, 2);
|
||||
}
|
||||
} else if (mimeType === 'application/x-www-form-urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
if (bodySchema && bodySchema.type === 'object') {
|
||||
each(bodySchema.properties || {}, (prop, name) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: '',
|
||||
description: prop.description || '',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (mimeType === 'multipart/form-data') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
if (bodySchema && bodySchema.type === 'object') {
|
||||
each(bodySchema.properties || {}, (prop, name) => {
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
type: 'text',
|
||||
name: name,
|
||||
value: '',
|
||||
description: prop.description || '',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (mimeType === 'text/plain') {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = '';
|
||||
} else if (mimeType === 'text/xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = '';
|
||||
}
|
||||
}
|
||||
|
||||
// build the extraction scripts from responses that have links
|
||||
// https://swagger.io/docs/specification/links/
|
||||
let script = [];
|
||||
each(_operationObject.responses || [], (response, responseStatus) => {
|
||||
if (Object.hasOwn(response, 'links')) {
|
||||
// only extract if the status code matches the response
|
||||
script.push(`if (res.status === ${responseStatus}) {`);
|
||||
each(response.links, (link) => {
|
||||
each(link.parameters || [], (expression, parameter) => {
|
||||
let value = openAPIRuntimeExpressionToScript(expression);
|
||||
script.push(` bru.setVar('${link.operationId}_${parameter}', ${value});`);
|
||||
});
|
||||
});
|
||||
script.push(`}`);
|
||||
}
|
||||
});
|
||||
if (script.length > 0) {
|
||||
brunoRequestItem.request.script.res = script.join('\n');
|
||||
}
|
||||
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
const resolveRefs = (spec, components = spec?.components, cache = new Map()) => {
|
||||
if (!spec || typeof spec !== 'object') {
|
||||
return spec;
|
||||
}
|
||||
|
||||
if (cache.has(spec)) {
|
||||
return cache.get(spec);
|
||||
}
|
||||
|
||||
if (Array.isArray(spec)) {
|
||||
return spec.map(item => resolveRefs(item, components, cache));
|
||||
}
|
||||
|
||||
if ('$ref' in spec) {
|
||||
const refPath = spec.$ref;
|
||||
|
||||
if (cache.has(refPath)) {
|
||||
return cache.get(refPath);
|
||||
}
|
||||
|
||||
if (refPath.startsWith('#/components/')) {
|
||||
const refKeys = refPath.replace('#/components/', '').split('/');
|
||||
let ref = components;
|
||||
|
||||
for (const key of refKeys) {
|
||||
if (ref && ref[key]) {
|
||||
ref = ref[key];
|
||||
} else {
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
|
||||
cache.set(refPath, {});
|
||||
const resolved = resolveRefs(ref, components, cache);
|
||||
cache.set(refPath, resolved);
|
||||
return resolved;
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
const resolved = {};
|
||||
cache.set(spec, resolved);
|
||||
|
||||
for (const [key, value] of Object.entries(spec)) {
|
||||
resolved[key] = resolveRefs(value, components, cache);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
};
|
||||
|
||||
const groupRequestsByTags = (requests) => {
|
||||
let _groups = {};
|
||||
let ungrouped = [];
|
||||
each(requests, (request) => {
|
||||
let tags = request.operationObject.tags || [];
|
||||
if (tags.length > 0) {
|
||||
let tag = tags[0].trim(); // take first tag and trim whitespace
|
||||
|
||||
if (tag) {
|
||||
if (!_groups[tag]) {
|
||||
_groups[tag] = [];
|
||||
}
|
||||
_groups[tag].push(request);
|
||||
} else {
|
||||
ungrouped.push(request);
|
||||
}
|
||||
} else {
|
||||
ungrouped.push(request);
|
||||
}
|
||||
});
|
||||
|
||||
let groups = Object.keys(_groups).map((groupName) => {
|
||||
return {
|
||||
name: groupName,
|
||||
requests: _groups[groupName]
|
||||
};
|
||||
});
|
||||
|
||||
return [groups, ungrouped];
|
||||
};
|
||||
|
||||
const getDefaultUrl = (serverObject) => {
|
||||
let url = serverObject.url;
|
||||
if (serverObject.variables) {
|
||||
each(serverObject.variables, (variable, variableName) => {
|
||||
let sub = variable.default || (variable.enum ? variable.enum[0] : `{{${variableName}}}`);
|
||||
url = url.replace(`{${variableName}}`, sub);
|
||||
});
|
||||
}
|
||||
return url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
};
|
||||
|
||||
const getSecurity = (apiSpec) => {
|
||||
let defaultSchemes = apiSpec.security || [];
|
||||
|
||||
let securitySchemes = get(apiSpec, 'components.securitySchemes', {});
|
||||
if (Object.keys(securitySchemes) === 0) {
|
||||
return {
|
||||
supported: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
supported: defaultSchemes.map((scheme) => {
|
||||
var schemeName = Object.keys(scheme)[0];
|
||||
return securitySchemes[schemeName];
|
||||
}),
|
||||
schemes: securitySchemes,
|
||||
getScheme: (schemeName) => {
|
||||
return securitySchemes[schemeName];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const openAPIRuntimeExpressionToScript = (expression) => {
|
||||
// see https://swagger.io/docs/specification/links/#runtime-expressions
|
||||
if (expression === '$response.body') {
|
||||
return 'res.body';
|
||||
} else if (expression.startsWith('$response.body#')) {
|
||||
let pointer = expression.substring(15);
|
||||
// could use https://www.npmjs.com/package/json-pointer for better support
|
||||
return `res.body${pointer.replace('/', '.')}`;
|
||||
}
|
||||
return expression;
|
||||
};
|
||||
|
||||
export const parseOpenApiCollection = (data) => {
|
||||
const brunoCollection = {
|
||||
name: '',
|
||||
uid: uuid(),
|
||||
version: '1',
|
||||
items: [],
|
||||
environments: []
|
||||
};
|
||||
try {
|
||||
const collectionData = resolveRefs(data);
|
||||
if (!collectionData) {
|
||||
throw new Error('Invalid OpenAPI collection. Failed to resolve refs.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Currently parsing of openapi spec is "do your best", that is
|
||||
// allows "invalid" openapi spec
|
||||
|
||||
// Assumes v3 if not defined. v2 is not supported yet
|
||||
if (collectionData.openapi && !collectionData.openapi.startsWith('3')) {
|
||||
throw new Error('Only OpenAPI v3 is supported currently.');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO what if info.title not defined?
|
||||
brunoCollection.name = collectionData.info.title;
|
||||
let servers = collectionData.servers || [];
|
||||
|
||||
// Create environments based on the servers
|
||||
servers.forEach((server, index) => {
|
||||
let baseUrl = getDefaultUrl(server);
|
||||
let environmentName = server.description ? server.description : `Environment ${index + 1}`;
|
||||
|
||||
brunoCollection.environments.push({
|
||||
uid: uuid(),
|
||||
name: environmentName,
|
||||
variables: [
|
||||
{
|
||||
uid: uuid(),
|
||||
name: 'baseUrl',
|
||||
value: baseUrl,
|
||||
type: 'text',
|
||||
enabled: true,
|
||||
secret: false
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
let securityConfig = getSecurity(collectionData);
|
||||
|
||||
let allRequests = Object.entries(collectionData.paths)
|
||||
.map(([path, methods]) => {
|
||||
return Object.entries(methods)
|
||||
.filter(([method, op]) => {
|
||||
return ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(
|
||||
method.toLowerCase()
|
||||
);
|
||||
})
|
||||
.map(([method, operationObject]) => {
|
||||
return {
|
||||
method: method,
|
||||
path: path.replace(/{([^}]+)}/g, ':$1'), // Replace placeholders enclosed in curly braces with colons
|
||||
operationObject: operationObject,
|
||||
global: {
|
||||
server: '{{baseUrl}}',
|
||||
security: securityConfig
|
||||
}
|
||||
};
|
||||
});
|
||||
})
|
||||
.reduce((acc, val) => acc.concat(val), []); // flatten
|
||||
|
||||
let [groups, ungroupedRequests] = groupRequestsByTags(allRequests);
|
||||
let brunoFolders = groups.map((group) => {
|
||||
return {
|
||||
uid: uuid(),
|
||||
name: group.name,
|
||||
type: 'folder',
|
||||
items: group.requests.map(transformOpenapiRequestItem)
|
||||
};
|
||||
});
|
||||
|
||||
let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem);
|
||||
let brunoCollectionItems = brunoFolders.concat(ungroupedItems);
|
||||
brunoCollection.items = brunoCollectionItems;
|
||||
return brunoCollection;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw new Error('An error occurred while parsing the OpenAPI collection');
|
||||
}
|
||||
};
|
||||
|
||||
export const openApiToBruno = (openApiSpecification) => {
|
||||
try {
|
||||
const collection = parseOpenApiCollection(openApiSpecification);
|
||||
const transformedCollection = transformItemsInCollection(collection);
|
||||
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
|
||||
const validatedCollection = validateSchema(hydratedCollection);
|
||||
return validatedCollection
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw new Error('Import collection failed');
|
||||
}
|
||||
};
|
||||
|
||||
export default openApiToBruno;
|
||||
340
packages/bruno-converters/src/postman/bruno-to-postman.js
Normal file
340
packages/bruno-converters/src/postman/bruno-to-postman.js
Normal file
@@ -0,0 +1,340 @@
|
||||
import map from 'lodash/map';
|
||||
import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from '../common';
|
||||
|
||||
/**
|
||||
* Transforms a given URL string into an object representing the protocol, host, path, query, and variables.
|
||||
*
|
||||
* @param {string} url - The raw URL to be transformed.
|
||||
* @param {Object} params - The params object.
|
||||
* @returns {Object|null} An object containing the URL's protocol, host, path, query, and variables, or {} if an error occurs.
|
||||
*/
|
||||
export const transformUrl = (url, params) => {
|
||||
if (typeof url !== 'string' || !url.trim()) {
|
||||
url = "";
|
||||
console.error("Invalid URL input:", url);
|
||||
}
|
||||
|
||||
const urlRegexPatterns = {
|
||||
protocolAndRestSeparator: /:\/\//,
|
||||
hostAndPathSeparator: /\/(.+)/,
|
||||
domainSegmentSeparator: /\./,
|
||||
pathSegmentSeparator: /\//,
|
||||
queryStringSeparator: /\?/
|
||||
};
|
||||
|
||||
const postmanUrl = { raw: url };
|
||||
|
||||
/**
|
||||
* Splits a URL into its protocol, host and path.
|
||||
*
|
||||
* @param {string} url - The URL to be split.
|
||||
* @returns {Object} An object containing the protocol and the raw host/path string.
|
||||
*/
|
||||
const splitUrl = (url) => {
|
||||
const urlParts = url.split(urlRegexPatterns.protocolAndRestSeparator);
|
||||
if (urlParts.length === 1) {
|
||||
return { protocol: '', rawHostAndPath: urlParts[0] };
|
||||
} else if (urlParts.length === 2) {
|
||||
const [hostAndPath, _] = urlParts[1].split(urlRegexPatterns.queryStringSeparator);
|
||||
return { protocol: urlParts[0], rawHostAndPath: hostAndPath };
|
||||
} else {
|
||||
throw new Error(`Invalid URL format: ${url}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits the host and path from a raw host/path string.
|
||||
*
|
||||
* @param {string} rawHostAndPath - The raw host and path string to be split.
|
||||
* @returns {Object} An object containing the host and path.
|
||||
*/
|
||||
const splitHostAndPath = (rawHostAndPath) => {
|
||||
const [host, path = ''] = rawHostAndPath.split(urlRegexPatterns.hostAndPathSeparator);
|
||||
return { host, path };
|
||||
};
|
||||
|
||||
try {
|
||||
const { protocol, rawHostAndPath } = splitUrl(url);
|
||||
postmanUrl.protocol = protocol;
|
||||
|
||||
const { host, path } = splitHostAndPath(rawHostAndPath);
|
||||
postmanUrl.host = host ? host.split(urlRegexPatterns.domainSegmentSeparator) : [];
|
||||
postmanUrl.path = path ? path.split(urlRegexPatterns.pathSegmentSeparator) : [];
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Construct query params.
|
||||
postmanUrl.query = params
|
||||
.filter((param) => param.type === 'query')
|
||||
.map(({ name, value, description }) => ({ key: name, value, description }));
|
||||
|
||||
// Construct path params.
|
||||
postmanUrl.variable = params
|
||||
.filter((param) => param.type === 'path')
|
||||
.map(({ name, value, description }) => ({ key: name, value, description }));
|
||||
|
||||
return postmanUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Collapses multiple consecutive slashes (`//`) into a single slash, while skipping the protocol (e.g., `http://` or `https://`).
|
||||
*
|
||||
* @param {String} url - A URL string
|
||||
* @returns {String} The sanitized URL
|
||||
*
|
||||
*/
|
||||
const collapseDuplicateSlashes = (url) => {
|
||||
return url.replace(/(?<!:)\/{2,}/g, '/');
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces all `\\` (backslashes) with `//` (forward slashes) and collapses multiple slashes into one.
|
||||
*
|
||||
* @param {string} url - The URL to sanitize.
|
||||
* @returns {string} The sanitized URL.
|
||||
*
|
||||
*/
|
||||
export const sanitizeUrl = (url) => {
|
||||
let sanitizedUrl = collapseDuplicateSlashes(url.replace(/\\/g, '//'));
|
||||
return sanitizedUrl;
|
||||
};
|
||||
|
||||
export const brunoToPostman = (collection) => {
|
||||
delete collection.uid;
|
||||
delete collection.processEnvVariables;
|
||||
deleteUidsInItems(collection.items);
|
||||
deleteUidsInEnvs(collection.environments);
|
||||
deleteSecretsInEnvs(collection.environments);
|
||||
|
||||
const generateInfoSection = () => {
|
||||
return {
|
||||
name: collection.name,
|
||||
description: collection.root?.docs,
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
};
|
||||
};
|
||||
|
||||
const generateCollectionVars = (collection) => {
|
||||
const pattern = /{{[^{}]+}}/g;
|
||||
let listOfVars = [];
|
||||
|
||||
const findOccurrences = (obj, results) => {
|
||||
if (typeof obj === 'object') {
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach((item) => findOccurrences(item, results));
|
||||
} else {
|
||||
for (const key in obj) {
|
||||
findOccurrences(obj[key], results);
|
||||
}
|
||||
}
|
||||
} else if (typeof obj === 'string') {
|
||||
obj.replace(pattern, (match) => {
|
||||
results.push(match.replace(/{{|}}/g, ''));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
findOccurrences(collection, listOfVars);
|
||||
|
||||
const finalArrayOfVars = [...new Set(listOfVars)];
|
||||
|
||||
return finalArrayOfVars.map((variable) => ({
|
||||
key: variable,
|
||||
value: '',
|
||||
type: 'default'
|
||||
}));
|
||||
};
|
||||
|
||||
const generateEventSection = (item) => {
|
||||
const eventArray = [];
|
||||
if (item?.request?.tests?.length) {
|
||||
eventArray.push({
|
||||
listen: 'test',
|
||||
script: {
|
||||
exec: item.request.tests.split('\n')
|
||||
// type: 'text/javascript'
|
||||
}
|
||||
});
|
||||
}
|
||||
if (item?.request?.script?.req) {
|
||||
eventArray.push({
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
exec: item.request.script.req.split('\n')
|
||||
// type: 'text/javascript'
|
||||
}
|
||||
});
|
||||
}
|
||||
return eventArray;
|
||||
};
|
||||
|
||||
const generateHeaders = (headersArray) => {
|
||||
return map(headersArray, (item) => {
|
||||
return {
|
||||
key: item.name,
|
||||
value: item.value,
|
||||
disabled: !item.enabled,
|
||||
type: 'default'
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const generateBody = (body) => {
|
||||
switch (body.mode) {
|
||||
case 'formUrlEncoded':
|
||||
return {
|
||||
mode: 'urlencoded',
|
||||
urlencoded: map(body.formUrlEncoded, (bodyItem) => {
|
||||
return {
|
||||
key: bodyItem.name,
|
||||
value: bodyItem.value,
|
||||
disabled: !bodyItem.enabled,
|
||||
type: 'default'
|
||||
};
|
||||
})
|
||||
};
|
||||
case 'multipartForm':
|
||||
return {
|
||||
mode: 'formdata',
|
||||
formdata: map(body.multipartForm, (bodyItem) => {
|
||||
return {
|
||||
key: bodyItem.name,
|
||||
value: bodyItem.value,
|
||||
disabled: !bodyItem.enabled,
|
||||
type: 'default'
|
||||
};
|
||||
})
|
||||
};
|
||||
case 'json':
|
||||
return {
|
||||
mode: 'raw',
|
||||
raw: body.json,
|
||||
options: {
|
||||
raw: {
|
||||
language: 'json'
|
||||
}
|
||||
}
|
||||
};
|
||||
case 'xml':
|
||||
return {
|
||||
mode: 'raw',
|
||||
raw: body.xml,
|
||||
options: {
|
||||
raw: {
|
||||
language: 'xml'
|
||||
}
|
||||
}
|
||||
};
|
||||
case 'text':
|
||||
return {
|
||||
mode: 'raw',
|
||||
raw: body.text,
|
||||
options: {
|
||||
raw: {
|
||||
language: 'text'
|
||||
}
|
||||
}
|
||||
};
|
||||
case 'graphql':
|
||||
return {
|
||||
mode: 'graphql',
|
||||
graphql: body.graphql
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const generateAuth = (itemAuth) => {
|
||||
switch (itemAuth?.mode) {
|
||||
case 'bearer':
|
||||
return {
|
||||
type: 'bearer',
|
||||
bearer: {
|
||||
key: 'token',
|
||||
value: itemAuth.bearer.token,
|
||||
type: 'string'
|
||||
}
|
||||
};
|
||||
case 'basic': {
|
||||
return {
|
||||
type: 'basic',
|
||||
basic: [
|
||||
{
|
||||
key: 'password',
|
||||
value: itemAuth.basic.password,
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
value: itemAuth.basic.username,
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
case 'apikey': {
|
||||
return {
|
||||
type: 'apikey',
|
||||
apikey: [
|
||||
{
|
||||
key: 'key',
|
||||
value: itemAuth.apikey.key,
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
value: itemAuth.apikey.value,
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
type: 'noauth'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const generateRequestSection = (itemRequest) => {
|
||||
const requestObject = {
|
||||
method: itemRequest.method,
|
||||
header: generateHeaders(itemRequest.headers),
|
||||
auth: generateAuth(itemRequest.auth),
|
||||
description: itemRequest.docs,
|
||||
// We sanitize the URL to make sure it's in the right format before passing it to the transformUrl func. This means changing backslashes to forward slashes and reducing multiple slashes to a single one, except in the protocol part.
|
||||
url: transformUrl(sanitizeUrl(itemRequest.url), itemRequest.params)
|
||||
};
|
||||
|
||||
if (itemRequest.body.mode !== 'none') {
|
||||
requestObject.body = generateBody(itemRequest.body);
|
||||
}
|
||||
return requestObject;
|
||||
};
|
||||
|
||||
const generateItemSection = (itemsArray) => {
|
||||
return map(itemsArray, (item) => {
|
||||
if (item.type === 'folder') {
|
||||
return {
|
||||
name: item.name,
|
||||
item: item.items.length ? generateItemSection(item.items) : []
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: item.name,
|
||||
event: generateEventSection(item),
|
||||
request: generateRequestSection(item.request)
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
const collectionToExport = {};
|
||||
collectionToExport.info = generateInfoSection();
|
||||
collectionToExport.item = generateItemSection(collection.items);
|
||||
collectionToExport.variable = generateCollectionVars(collection);
|
||||
return collectionToExport;
|
||||
};
|
||||
|
||||
export default brunoToPostman;
|
||||
@@ -0,0 +1,40 @@
|
||||
import each from 'lodash/each';
|
||||
const isSecret = (type) => {
|
||||
return type === 'secret';
|
||||
};
|
||||
|
||||
const importPostmanEnvironmentVariables = (brunoEnvironment, values) => {
|
||||
brunoEnvironment.variables = brunoEnvironment.variables || [];
|
||||
|
||||
each(values, (i) => {
|
||||
const brunoEnvironmentVariable = {
|
||||
name: i.key,
|
||||
value: i.value,
|
||||
enabled: i.enabled,
|
||||
secret: isSecret(i.type)
|
||||
};
|
||||
|
||||
brunoEnvironment.variables.push(brunoEnvironmentVariable);
|
||||
});
|
||||
};
|
||||
|
||||
const importPostmanEnvironment = (environment) => {
|
||||
const brunoEnvironment = {
|
||||
name: environment.name,
|
||||
variables: []
|
||||
};
|
||||
|
||||
importPostmanEnvironmentVariables(brunoEnvironment, environment.values);
|
||||
return brunoEnvironment;
|
||||
};
|
||||
|
||||
export const postmanToBrunoEnvironment = (postmanEnvironment) => {
|
||||
try {
|
||||
return importPostmanEnvironment(postmanEnvironment);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new Error('Unable to parse the postman environment json file');
|
||||
}
|
||||
};
|
||||
|
||||
export default postmanToBrunoEnvironment;
|
||||
584
packages/bruno-converters/src/postman/postman-to-bruno.js
Normal file
584
packages/bruno-converters/src/postman/postman-to-bruno.js
Normal file
@@ -0,0 +1,584 @@
|
||||
import get from 'lodash/get';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
|
||||
import each from 'lodash/each';
|
||||
import postmanTranslation from './postman-translations';
|
||||
|
||||
const parseGraphQLRequest = (graphqlSource) => {
|
||||
try {
|
||||
let queryResultObject = {
|
||||
query: '',
|
||||
variables: ''
|
||||
};
|
||||
|
||||
if (typeof graphqlSource === 'string') {
|
||||
graphqlSource = JSON.parse(graphqlSource);
|
||||
}
|
||||
|
||||
if (graphqlSource.hasOwnProperty('variables') && graphqlSource.variables !== '') {
|
||||
queryResultObject.variables = graphqlSource.variables;
|
||||
}
|
||||
|
||||
if (graphqlSource.hasOwnProperty('query') && graphqlSource.query !== '') {
|
||||
queryResultObject.query = graphqlSource.query;
|
||||
}
|
||||
|
||||
return queryResultObject;
|
||||
} catch (e) {
|
||||
return {
|
||||
query: '',
|
||||
variables: ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const isItemAFolder = (item) => {
|
||||
return !item.request;
|
||||
};
|
||||
|
||||
const convertV21Auth = (array) => {
|
||||
return array.reduce((accumulator, currentValue) => {
|
||||
accumulator[currentValue.key] = currentValue.value;
|
||||
return accumulator;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const constructUrlFromParts = (url) => {
|
||||
const { protocol = 'http', host, path, port, query, hash } = url || {};
|
||||
const hostStr = Array.isArray(host) ? host.filter(Boolean).join('.') : host || '';
|
||||
const pathStr = Array.isArray(path) ? path.filter(Boolean).join('/') : path || '';
|
||||
const portStr = port ? `:${port}` : '';
|
||||
const queryStr =
|
||||
query && Array.isArray(query) && query.length > 0
|
||||
? `?${query
|
||||
.filter((q) => q.key)
|
||||
.map((q) => `${q.key}=${q.value || ''}`)
|
||||
.join('&')}`
|
||||
: '';
|
||||
const urlStr = `${protocol}://${hostStr}${portStr}${pathStr ? `/${pathStr}` : ''}${queryStr}`;
|
||||
return urlStr;
|
||||
};
|
||||
|
||||
const constructUrl = (url) => {
|
||||
if (!url) return '';
|
||||
|
||||
if (typeof url === 'string') {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (typeof url === 'object') {
|
||||
const { raw } = url;
|
||||
|
||||
if (raw && typeof raw === 'string') {
|
||||
// If the raw URL contains url-fragments remove it
|
||||
if (raw.includes('#')) {
|
||||
return raw.split('#')[0]; // Returns the part of raw URL without the url-fragment part.
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
// If no raw value exists, construct the URL from parts
|
||||
return constructUrlFromParts(url);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const importScriptsFromEvents = (events, requestObject) => {
|
||||
events.forEach((event) => {
|
||||
if (event.script && event.script.exec) {
|
||||
if (event.listen === 'prerequest') {
|
||||
if (!requestObject.script) {
|
||||
requestObject.script = {};
|
||||
}
|
||||
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
if (event.script.exec.length > 0) {
|
||||
requestObject.script.req = event.script.exec
|
||||
.map((line) => postmanTranslation(line))
|
||||
.join('\n');
|
||||
} else {
|
||||
requestObject.script.req = '';
|
||||
}
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
requestObject.script.req = postmanTranslation(event.script.exec);
|
||||
} else {
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.listen === 'test') {
|
||||
if (!requestObject.tests) {
|
||||
requestObject.tests = {};
|
||||
}
|
||||
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
if (event.script.exec.length > 0) {
|
||||
requestObject.tests = event.script.exec
|
||||
.map((line) => postmanTranslation(line))
|
||||
.join('\n');
|
||||
} else {
|
||||
requestObject.tests = '';
|
||||
}
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
requestObject.tests = postmanTranslation(event.script.exec);
|
||||
} else {
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importCollectionLevelVariables = (variables, requestObject) => {
|
||||
const vars = variables.map((v) => ({
|
||||
uid: uuid(),
|
||||
name: v.key,
|
||||
value: v.value,
|
||||
enabled: true
|
||||
}));
|
||||
|
||||
requestObject.vars.req = vars;
|
||||
};
|
||||
|
||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
brunoParent.items = brunoParent.items || [];
|
||||
const folderMap = {};
|
||||
const requestMap = {};
|
||||
const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE']
|
||||
|
||||
each(item, (i) => {
|
||||
if (isItemAFolder(i)) {
|
||||
const baseFolderName = i.name || 'Untitled Folder';
|
||||
let folderName = baseFolderName;
|
||||
let count = 1;
|
||||
|
||||
while (folderMap[folderName]) {
|
||||
folderName = `${baseFolderName}_${count}`;
|
||||
count++;
|
||||
}
|
||||
|
||||
const brunoFolderItem = {
|
||||
uid: uuid(),
|
||||
name: folderName,
|
||||
type: 'folder',
|
||||
items: [],
|
||||
root: {
|
||||
docs: i.description || '',
|
||||
meta: {
|
||||
name: folderName
|
||||
},
|
||||
request: {
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null
|
||||
},
|
||||
headers: [],
|
||||
script: {},
|
||||
tests: '',
|
||||
vars: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (i.item && i.item.length) {
|
||||
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth);
|
||||
}
|
||||
|
||||
if (i.event) {
|
||||
importScriptsFromEvents(i.event, brunoFolderItem.root.request);
|
||||
}
|
||||
|
||||
brunoParent.items.push(brunoFolderItem);
|
||||
folderMap[folderName] = brunoFolderItem;
|
||||
|
||||
} else {
|
||||
if (i.request) {
|
||||
if(!requestMethods.includes(i?.request?.method.toUpperCase())){
|
||||
console.warn("Unexpected request.method")
|
||||
return;
|
||||
}
|
||||
|
||||
const baseRequestName = i.name || 'Untitled Request';
|
||||
let requestName = baseRequestName;
|
||||
let count = 1;
|
||||
|
||||
while (requestMap[requestName]) {
|
||||
requestName = `${baseRequestName}_${count}`;
|
||||
count++;
|
||||
}
|
||||
|
||||
const url = constructUrl(i.request.url);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: requestName,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: url,
|
||||
method: i?.request?.method?.toUpperCase(),
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null
|
||||
},
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
},
|
||||
docs: i.request.description || ''
|
||||
}
|
||||
};
|
||||
|
||||
if (i.event) {
|
||||
i.event.forEach((event) => {
|
||||
if (event.listen === 'prerequest' && event.script && event.script.exec) {
|
||||
if (!brunoRequestItem.request.script) {
|
||||
brunoRequestItem.request.script = {};
|
||||
}
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
if (event.script.exec.length > 0) {
|
||||
brunoRequestItem.request.script.req = event.script.exec
|
||||
.map((line) => postmanTranslation(line))
|
||||
.join('\n');
|
||||
} else {
|
||||
brunoRequestItem.request.script.req = '';
|
||||
}
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
brunoRequestItem.request.script.req = postmanTranslation(event.script.exec);
|
||||
} else {
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
if (event.listen === 'test' && event.script && event.script.exec) {
|
||||
if (!brunoRequestItem.request.tests) {
|
||||
brunoRequestItem.request.tests = {};
|
||||
}
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
if (event.script.exec.length > 0) {
|
||||
brunoRequestItem.request.tests = event.script.exec
|
||||
.map((line) => postmanTranslation(line))
|
||||
.join('\n');
|
||||
} else {
|
||||
brunoRequestItem.request.tests = '';
|
||||
}
|
||||
} else if (typeof event.script.exec === 'string') {
|
||||
brunoRequestItem.request.tests = postmanTranslation(event.script.exec);
|
||||
} else {
|
||||
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const bodyMode = get(i, 'request.body.mode');
|
||||
if (bodyMode) {
|
||||
if (bodyMode === 'formdata') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
|
||||
each(i.request.body.formdata, (param) => {
|
||||
const isFile = param.type === 'file';
|
||||
let value;
|
||||
let type;
|
||||
|
||||
if (isFile) {
|
||||
// If param.src is an array, keep it as it is.
|
||||
// If param.src is a string, convert it into an array with a single element.
|
||||
value = Array.isArray(param.src) ? param.src : typeof param.src === 'string' ? [param.src] : null;
|
||||
type = 'file';
|
||||
} else {
|
||||
value = param.value;
|
||||
type = 'text';
|
||||
}
|
||||
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
type: type,
|
||||
name: param.key,
|
||||
value: value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (bodyMode === 'urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
each(i.request.body.urlencoded, (param) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (bodyMode === 'raw') {
|
||||
let language = get(i, 'request.body.options.raw.language');
|
||||
if (!language) {
|
||||
language = searchLanguageByHeader(i.request.header);
|
||||
}
|
||||
if (language === 'json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
brunoRequestItem.request.body.json = i.request.body.raw;
|
||||
} else if (language === 'xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = i.request.body.raw;
|
||||
} else {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = i.request.body.raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bodyMode === 'graphql') {
|
||||
brunoRequestItem.type = 'graphql-request';
|
||||
brunoRequestItem.request.body.mode = 'graphql';
|
||||
brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
|
||||
}
|
||||
|
||||
each(i.request.header, (header) => {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: header.key,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: !header.disabled
|
||||
});
|
||||
});
|
||||
|
||||
const auth = i.request.auth ?? parentAuth;
|
||||
if (auth?.[auth.type] && auth.type !== 'noauth') {
|
||||
let authValues = auth[auth.type];
|
||||
if (Array.isArray(authValues)) {
|
||||
authValues = convertV21Auth(authValues);
|
||||
}
|
||||
if (auth.type === 'basic') {
|
||||
brunoRequestItem.request.auth.mode = 'basic';
|
||||
brunoRequestItem.request.auth.basic = {
|
||||
username: authValues.username,
|
||||
password: authValues.password
|
||||
};
|
||||
} else if (auth.type === 'bearer') {
|
||||
brunoRequestItem.request.auth.mode = 'bearer';
|
||||
brunoRequestItem.request.auth.bearer = {
|
||||
token: authValues.token
|
||||
};
|
||||
} else if (auth.type === 'awsv4') {
|
||||
brunoRequestItem.request.auth.mode = 'awsv4';
|
||||
brunoRequestItem.request.auth.awsv4 = {
|
||||
accessKeyId: authValues.accessKey,
|
||||
secretAccessKey: authValues.secretKey,
|
||||
sessionToken: authValues.sessionToken,
|
||||
service: authValues.service,
|
||||
region: authValues.region,
|
||||
profileName: ''
|
||||
};
|
||||
} else if (auth.type === 'apikey'){
|
||||
brunoRequestItem.request.auth.mode = 'apikey';
|
||||
brunoRequestItem.request.auth.apikey = {
|
||||
key: authValues.key,
|
||||
value: authValues.value?.toString(), // Convert the value to a string as Postman's schema does not rigidly define the type of it,
|
||||
placement: "header" //By default we are placing the apikey values in headers!
|
||||
}
|
||||
} else if (auth.type === 'oauth2'){
|
||||
const findValueUsingKey = (key) => {
|
||||
return auth?.oauth2?.find(v => v?.key == key)?.value || ''
|
||||
}
|
||||
const oauth2GrantTypeMaps = {
|
||||
'authorization_code_with_pkce': 'authorization_code',
|
||||
'authorization_code': 'authorization_code',
|
||||
'client_credentials': 'client_credentials',
|
||||
'password_credentials': 'password_credentials'
|
||||
}
|
||||
const grantType = oauth2GrantTypeMaps[findValueUsingKey('grant_type')] || 'authorization_code';
|
||||
if (grantType) {
|
||||
brunoRequestItem.request.auth.mode = 'oauth2';
|
||||
switch(grantType) {
|
||||
case 'authorization_code':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'authorization_code',
|
||||
authorizationUrl: findValueUsingKey('authUrl'),
|
||||
callbackUrl: findValueUsingKey('redirect_uri'),
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshTokenUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
pkce: Boolean(findValueUsingKey('grant_type') == 'authorization_code_with_pkce'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
|
||||
};
|
||||
break;
|
||||
case 'password_credentials':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'password',
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshTokenUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
username: findValueUsingKey('username'),
|
||||
password: findValueUsingKey('password'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshTokenUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
each(get(i, 'request.url.query'), (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: 'query',
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(get(i, 'request.url.variable', []), (param) => {
|
||||
if (!param.key) {
|
||||
// If no key, skip this iteration and discard the param
|
||||
return;
|
||||
}
|
||||
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value ?? '',
|
||||
description: param.description ?? '',
|
||||
type: 'path',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
|
||||
brunoParent.items.push(brunoRequestItem);
|
||||
requestMap[requestName] = brunoRequestItem;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const searchLanguageByHeader = (headers) => {
|
||||
let contentType;
|
||||
each(headers, (header) => {
|
||||
if (header.key.toLowerCase() === 'content-type' && !header.disabled) {
|
||||
if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(header.value)) {
|
||||
contentType = 'json';
|
||||
} else if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(header.value)) {
|
||||
contentType = 'xml';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const importPostmanV2Collection = (collection) => {
|
||||
const brunoCollection = {
|
||||
name: collection.info.name || 'Untitled Collection',
|
||||
uid: uuid(),
|
||||
version: '1',
|
||||
items: [],
|
||||
environments: [],
|
||||
root: {
|
||||
docs: collection.info.description || '',
|
||||
meta: {
|
||||
name: collection.info.name || 'Untitled Collection'
|
||||
},
|
||||
request: {
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null
|
||||
},
|
||||
headers: [],
|
||||
script: {},
|
||||
tests: '',
|
||||
vars: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (collection.event) {
|
||||
importScriptsFromEvents(collection.event, brunoCollection.root.request);
|
||||
}
|
||||
|
||||
if (collection?.variable){
|
||||
importCollectionLevelVariables(collection.variable, brunoCollection.root.request);
|
||||
}
|
||||
|
||||
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth);
|
||||
|
||||
return brunoCollection;
|
||||
};
|
||||
|
||||
const parsePostmanCollection = (collection) => {
|
||||
try {
|
||||
let schema = get(collection, 'info.schema');
|
||||
|
||||
let v2Schemas = [
|
||||
'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',
|
||||
'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
||||
'https://schema.postman.com/json/collection/v2.0.0/collection.json',
|
||||
'https://schema.postman.com/json/collection/v2.1.0/collection.json'
|
||||
];
|
||||
|
||||
if (v2Schemas.includes(schema)) {
|
||||
return importPostmanV2Collection(collection);
|
||||
}
|
||||
|
||||
throw new Error('Unknown postman schema');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if (err instanceof Error) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new Error('Unable to parse the postman collection json file');
|
||||
}
|
||||
};
|
||||
|
||||
const postmanToBruno = (postmanCollection) => {
|
||||
try {
|
||||
const parsedPostmanCollection = parsePostmanCollection(postmanCollection);
|
||||
const transformedCollection = transformItemsInCollection(parsedPostmanCollection);
|
||||
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
|
||||
const validatedCollection = validateSchema(hydratedCollection);
|
||||
return validatedCollection;
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
throw new Error('Import collection failed');
|
||||
}
|
||||
};
|
||||
|
||||
export default postmanToBruno;
|
||||
@@ -42,7 +42,7 @@ const compiledReplacements = Object.entries(extendedReplacements).map(([pattern,
|
||||
replacement
|
||||
}));
|
||||
|
||||
export const postmanTranslation = (script, logCallback) => {
|
||||
const postmanTranslation = (script) => {
|
||||
try {
|
||||
let modifiedScript = script;
|
||||
let modified = false;
|
||||
@@ -54,10 +54,11 @@ export const postmanTranslation = (script, logCallback) => {
|
||||
}
|
||||
if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) {
|
||||
modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1');
|
||||
//logCallback?.();
|
||||
}
|
||||
return modifiedScript;
|
||||
} catch (e) {
|
||||
return script;
|
||||
}
|
||||
};
|
||||
|
||||
export default postmanTranslation;
|
||||
@@ -0,0 +1,190 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import insomniaToBruno from '../../src/insomnia/insomnia-to-bruno';
|
||||
|
||||
describe('insomnia-collection', () => {
|
||||
it('should correctly import a valid Insomnia collection file', async () => {
|
||||
const brunoCollection = insomniaToBruno(insomniaCollection);
|
||||
|
||||
expect(brunoCollection).toMatchObject(expectedOutput)
|
||||
});
|
||||
});
|
||||
|
||||
const insomniaCollection = {
|
||||
"_type": "export",
|
||||
"__export_format": 4,
|
||||
"__export_date": "2024-05-20T10:02:44.123Z",
|
||||
"__export_source": "insomnia.desktop.app:v2021.5.2",
|
||||
"resources": [
|
||||
{
|
||||
"_id": "req_1",
|
||||
"_type": "request",
|
||||
"parentId": "fld_1",
|
||||
"name": "Request1",
|
||||
"method": "GET",
|
||||
"url": "https://httpbin.org/get",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"_id": "req_2",
|
||||
"_type": "request",
|
||||
"parentId": "fld_2",
|
||||
"name": "Request2",
|
||||
"method": "GET",
|
||||
"url": "https://httpbin.org/get",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"_id": "fld_1",
|
||||
"_type": "request_group",
|
||||
"parentId": "wrk_1",
|
||||
"name": "Folder1"
|
||||
},
|
||||
{
|
||||
"_id": "fld_2",
|
||||
"_type": "request_group",
|
||||
"parentId": "wrk_1",
|
||||
"name": "Folder2"
|
||||
},
|
||||
{
|
||||
"_id": "wrk_1",
|
||||
"_type": "workspace",
|
||||
"name": "Hello World Workspace Insomnia"
|
||||
},
|
||||
{
|
||||
"_id": "env_1",
|
||||
"_type": "environment",
|
||||
"parentId": "wrk_1",
|
||||
"data": {
|
||||
"var1": "value1",
|
||||
"var2": "value2"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
"environments": [],
|
||||
"items": [
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "Request1",
|
||||
"request": {
|
||||
"auth": {
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"digest": null,
|
||||
"mode": "none",
|
||||
},
|
||||
"body": {
|
||||
"formUrlEncoded": [],
|
||||
"json": null,
|
||||
"mode": "none",
|
||||
"multipartForm": [],
|
||||
"text": null,
|
||||
"xml": null,
|
||||
},
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"url": "https://httpbin.org/get",
|
||||
},
|
||||
"seq": 1,
|
||||
"type": "http-request",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
},
|
||||
{
|
||||
"name": "Request1",
|
||||
"request": {
|
||||
"auth": {
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"digest": null,
|
||||
"mode": "none",
|
||||
},
|
||||
"body": {
|
||||
"formUrlEncoded": [],
|
||||
"json": null,
|
||||
"mode": "none",
|
||||
"multipartForm": [],
|
||||
"text": null,
|
||||
"xml": null,
|
||||
},
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"url": "https://httpbin.org/get",
|
||||
},
|
||||
"seq": 2,
|
||||
"type": "http-request",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
},
|
||||
],
|
||||
"name": "Folder1",
|
||||
"type": "folder",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
},
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "Request2",
|
||||
"request": {
|
||||
"auth": {
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"digest": null,
|
||||
"mode": "none",
|
||||
},
|
||||
"body": {
|
||||
"formUrlEncoded": [],
|
||||
"json": null,
|
||||
"mode": "none",
|
||||
"multipartForm": [],
|
||||
"text": null,
|
||||
"xml": null,
|
||||
},
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"url": "https://httpbin.org/get",
|
||||
},
|
||||
"seq": 1,
|
||||
"type": "http-request",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
},
|
||||
{
|
||||
"name": "Request2",
|
||||
"request": {
|
||||
"auth": {
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"digest": null,
|
||||
"mode": "none",
|
||||
},
|
||||
"body": {
|
||||
"formUrlEncoded": [],
|
||||
"json": null,
|
||||
"mode": "none",
|
||||
"multipartForm": [],
|
||||
"text": null,
|
||||
"xml": null,
|
||||
},
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"url": "https://httpbin.org/get",
|
||||
},
|
||||
"seq": 2,
|
||||
"type": "http-request",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
},
|
||||
],
|
||||
"name": "Folder2",
|
||||
"type": "folder",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
},
|
||||
],
|
||||
"name": "Hello World Workspace Insomnia",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"version": "1",
|
||||
};
|
||||
108
packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js
Normal file
108
packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import jsyaml from 'js-yaml';
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
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);
|
||||
|
||||
expect(brunoCollection).toMatchObject(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
const openApiCollectionString = `
|
||||
openapi: "3.0.0"
|
||||
info:
|
||||
version: "1.0.0"
|
||||
title: "Hello World OpenAPI"
|
||||
paths:
|
||||
/get:
|
||||
get:
|
||||
tags:
|
||||
- Folder1
|
||||
- Folder2
|
||||
summary: "Request1 and Request2"
|
||||
operationId: "getRequests"
|
||||
responses:
|
||||
'200':
|
||||
description: "Successful response"
|
||||
components:
|
||||
parameters:
|
||||
var1:
|
||||
in: "query"
|
||||
name: "var1"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
default: "value1"
|
||||
var2:
|
||||
in: "query"
|
||||
name: "var2"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
default: "value2"
|
||||
servers:
|
||||
- url: "https://httpbin.org"
|
||||
`;
|
||||
|
||||
const expectedOutput = {
|
||||
"environments": [
|
||||
{
|
||||
"name": "Environment 1",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"variables": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "baseUrl",
|
||||
"secret": false,
|
||||
"type": "text",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"value": "https://httpbin.org",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "Request1 and Request2",
|
||||
"request": {
|
||||
"auth": {
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"digest": null,
|
||||
"mode": "none",
|
||||
},
|
||||
"body": {
|
||||
"formUrlEncoded": [],
|
||||
"json": null,
|
||||
"mode": "none",
|
||||
"multipartForm": [],
|
||||
"text": null,
|
||||
"xml": null,
|
||||
},
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"script": {
|
||||
"res": null,
|
||||
},
|
||||
"url": "{{baseUrl}}/get",
|
||||
},
|
||||
"seq": 1,
|
||||
"type": "http-request",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
},
|
||||
],
|
||||
"name": "Folder1",
|
||||
"type": "folder",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
},
|
||||
],
|
||||
"name": "Hello World OpenAPI",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"version": "1",
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
const { sanitizeUrl, transformUrl } = require('./postman-collection');
|
||||
import { sanitizeUrl, transformUrl } from "../../src/postman/bruno-to-postman";
|
||||
|
||||
describe('transformUrl', () => {
|
||||
it('should handle basic URL with path variables', () => {
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import postmanToBrunoEnvironment from '../../src/postman/postman-env-to-bruno-env';
|
||||
|
||||
describe('postmanToBrunoEnvironment Function', () => {
|
||||
it('should correctly import a valid Postman environment file', async () => {
|
||||
const postmanEnvironment = {
|
||||
"id": "some-id",
|
||||
"name": "My Environment",
|
||||
"values": [
|
||||
{
|
||||
"key": "var1",
|
||||
"value": "value1",
|
||||
"enabled": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "var2",
|
||||
"value": "value2",
|
||||
"enabled": false,
|
||||
"type": "secret"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const brunoEnvironment = await postmanToBrunoEnvironment(postmanEnvironment);
|
||||
|
||||
const expectedEnvironment = {
|
||||
name: 'My Environment',
|
||||
variables: [
|
||||
{
|
||||
name: 'var1',
|
||||
value: 'value1',
|
||||
enabled: true,
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
name: 'var2',
|
||||
value: 'value2',
|
||||
enabled: false,
|
||||
secret: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(brunoEnvironment).toEqual(expectedEnvironment);
|
||||
});
|
||||
|
||||
it.skip('should throw Error when JSON parsing fails', async () => {
|
||||
const invalidBrunoEnvironment = {
|
||||
"id": "some-id",
|
||||
"name": "My Environment",
|
||||
"values": [
|
||||
{
|
||||
"key": "var1",
|
||||
"value": "value1",
|
||||
"enabled": true,
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
await expect(postmanToBrunoEnvironment(invalidBrunoEnvironment)).rejects.toThrow(Error);
|
||||
await expect(postmanToBrunoEnvironment(invalidBrunoEnvironment)).rejects.toThrow(
|
||||
'Unable to parse the postman environment json file'
|
||||
);
|
||||
});
|
||||
});
|
||||
734
packages/bruno-converters/tests/postman/postman-to-bruno.spec.js
Normal file
734
packages/bruno-converters/tests/postman/postman-to-bruno.spec.js
Normal file
@@ -0,0 +1,734 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import postmanToBruno from '../../src/postman/postman-to-bruno';
|
||||
|
||||
describe('postman-collection', () => {
|
||||
it('should correctly import a valid Postman collection file', async () => {
|
||||
const brunoCollection = postmanToBruno(postmanCollection);
|
||||
expect(brunoCollection).toMatchObject(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
const postmanCollection = {
|
||||
"info": {
|
||||
"_postman_id": "0596d399-cfd2-4f8f-9869-65238eb40a45",
|
||||
"name": "CRUD",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
|
||||
"_exporter_id": "32111649",
|
||||
"_collection_link": "https://www.postman.com/fudzi9/workspace/nodejs/collection/16541095-0596d399-cfd2-4f8f-9869-65238eb40a45?action=share&source=collection_link&creator=32111649"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "GET",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/"
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "1.GET",
|
||||
"originalRequest": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/"
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Server",
|
||||
"value": "Cowboy"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "X-Powered-By",
|
||||
"value": "Express"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"key": "Etag",
|
||||
"value": "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\""
|
||||
},
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Tue, 06 Jul 2021 21:30:45 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Via",
|
||||
"value": "1.1 vegur"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "[]"
|
||||
},
|
||||
{
|
||||
"name": "3.GET",
|
||||
"originalRequest": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/"
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Server",
|
||||
"value": "Cowboy"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "X-Powered-By",
|
||||
"value": "Express"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "96"
|
||||
},
|
||||
{
|
||||
"key": "Etag",
|
||||
"value": "W/\"60-ixboSJswZpL0hV7rJrY1IE5nQlM\""
|
||||
},
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Tue, 06 Jul 2021 21:58:32 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Via",
|
||||
"value": "1.1 vegur"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "[\n {\n \"id\": 1,\n \"title\": \"first\",\n \"content\": \"some text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"name": "5.GET",
|
||||
"originalRequest": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/"
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Server",
|
||||
"value": "Cowboy"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "X-Powered-By",
|
||||
"value": "Express"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "192"
|
||||
},
|
||||
{
|
||||
"key": "Etag",
|
||||
"value": "W/\"c0-rg+VAYKuV+nAzdAnddMXRNSM3tg\""
|
||||
},
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Tue, 06 Jul 2021 22:01:36 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Via",
|
||||
"value": "1.1 vegur"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "[\n {\n \"id\": 1,\n \"title\": \"first\",\n \"content\": \"some text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n },\n {\n \"id\": 2,\n \"title\": \"second\",\n \"content\": \"some text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"name": "7.GET",
|
||||
"originalRequest": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/"
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Server",
|
||||
"value": "Cowboy"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "X-Powered-By",
|
||||
"value": "Express"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "199"
|
||||
},
|
||||
{
|
||||
"key": "Etag",
|
||||
"value": "W/\"c7-SBFGBh+BSdmKqSUIW4VDODIOnaI\""
|
||||
},
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Tue, 06 Jul 2021 22:38:51 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Via",
|
||||
"value": "1.1 vegur"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "[\n {\n \"id\": 2,\n \"title\": \"second\",\n \"content\": \"some text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n },\n {\n \"id\": 1,\n \"title\": \"first changed\",\n \"content\": \"new text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"name": "9.GET",
|
||||
"originalRequest": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/"
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Server",
|
||||
"value": "Cowboy"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "X-Powered-By",
|
||||
"value": "Express"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "103"
|
||||
},
|
||||
{
|
||||
"key": "Etag",
|
||||
"value": "W/\"67-aR9NxSbB5ab73lSksdIWZNuQyq8\""
|
||||
},
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Tue, 06 Jul 2021 22:40:55 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Via",
|
||||
"value": "1.1 vegur"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "[\n {\n \"id\": 1,\n \"title\": \"first changed\",\n \"content\": \"new text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n }\n]"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "POST",
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"exec": [
|
||||
""
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
""
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\"id\": 1, \"title\": \"first\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/"
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "2.POST",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\"id\": 1, \"title\": \"first\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/"
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Server",
|
||||
"value": "Cowboy"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "X-Powered-By",
|
||||
"value": "Express"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "123"
|
||||
},
|
||||
{
|
||||
"key": "Etag",
|
||||
"value": "W/\"7b-Zs+ZSZvDSG55ZK90aBqfAjoxdAg\""
|
||||
},
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Tue, 06 Jul 2021 21:58:17 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Via",
|
||||
"value": "1.1 vegur"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "\"{\\\"id\\\": 1, \\\"title\\\": \\\"first\\\", \\\"content\\\": \\\"some text\\\", \\\"createdAt\\\": \\\"some date\\\", \\\"updatedAt\\\": \\\"some date\\\"}\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "POST",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\"id\": 2, \"title\": \"second\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/"
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "4.POST",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\"id\": 2, \"title\": \"second\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/"
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Server",
|
||||
"value": "Cowboy"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "X-Powered-By",
|
||||
"value": "Express"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "124"
|
||||
},
|
||||
{
|
||||
"key": "Etag",
|
||||
"value": "W/\"7c-vtAEN2HlKwhD6OkasvICg9Ni+g0\""
|
||||
},
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Tue, 06 Jul 2021 22:00:49 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Via",
|
||||
"value": "1.1 vegur"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "\"{\\\"id\\\": 2, \\\"title\\\": \\\"second\\\", \\\"content\\\": \\\"some text\\\", \\\"createdAt\\\": \\\"some date\\\", \\\"updatedAt\\\": \\\"some date\\\"}\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PUT",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\"id\": 1, \"title\": \"first changed\", \"content\": \"new text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/1"
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "6.PUT",
|
||||
"originalRequest": {
|
||||
"method": "PUT",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\"id\": 1, \"title\": \"first changed\", \"content\": \"new text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/1"
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Server",
|
||||
"value": "Cowboy"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "X-Powered-By",
|
||||
"value": "Express"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "130"
|
||||
},
|
||||
{
|
||||
"key": "Etag",
|
||||
"value": "W/\"82-QdzTirfdP1+K+iNOkslStk0OPpg\""
|
||||
},
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Tue, 06 Jul 2021 22:03:36 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Via",
|
||||
"value": "1.1 vegur"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "\"{\\\"id\\\": 1, \\\"title\\\": \\\"first changed\\\", \\\"content\\\": \\\"new text\\\", \\\"createdAt\\\": \\\"some date\\\", \\\"updatedAt\\\": \\\"some date\\\"}\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DELETE",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [],
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/2"
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "8.DELETE",
|
||||
"originalRequest": {
|
||||
"method": "DELETE",
|
||||
"header": [],
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/2"
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Server",
|
||||
"value": "Cowboy"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "X-Powered-By",
|
||||
"value": "Express"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "23"
|
||||
},
|
||||
{
|
||||
"key": "Etag",
|
||||
"value": "W/\"17-bCXlhEBJSVIeQ+m1i+6p7+rrNak\""
|
||||
},
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Tue, 06 Jul 2021 22:40:08 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Via",
|
||||
"value": "1.1 vegur"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"success\": true,\n \"id\": 2\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
"name": "CRUD",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"version": "1",
|
||||
"items": [
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "GET",
|
||||
"type": "http-request",
|
||||
"request": {
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/",
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "none",
|
||||
"json": null,
|
||||
"text": null,
|
||||
"xml": null,
|
||||
"formUrlEncoded": [],
|
||||
"multipartForm": []
|
||||
},
|
||||
"docs": ""
|
||||
},
|
||||
"seq": 1
|
||||
},
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "POST",
|
||||
"type": "http-request",
|
||||
"request": {
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/",
|
||||
"method": "POST",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "json",
|
||||
"json": "{\"id\": 1, \"title\": \"first\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
|
||||
"text": null,
|
||||
"xml": null,
|
||||
"formUrlEncoded": [],
|
||||
"multipartForm": []
|
||||
},
|
||||
"docs": "",
|
||||
"script": {
|
||||
"req": ""
|
||||
},
|
||||
"tests": ""
|
||||
},
|
||||
"seq": 2
|
||||
},
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "POST_1",
|
||||
"type": "http-request",
|
||||
"request": {
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/",
|
||||
"method": "POST",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "json",
|
||||
"json": "{\"id\": 2, \"title\": \"second\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
|
||||
"text": null,
|
||||
"xml": null,
|
||||
"formUrlEncoded": [],
|
||||
"multipartForm": []
|
||||
},
|
||||
"docs": ""
|
||||
},
|
||||
"seq": 3
|
||||
},
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "PUT",
|
||||
"type": "http-request",
|
||||
"request": {
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/1",
|
||||
"method": "PUT",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "json",
|
||||
"json": "{\"id\": 1, \"title\": \"first changed\", \"content\": \"new text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
|
||||
"text": null,
|
||||
"xml": null,
|
||||
"formUrlEncoded": [],
|
||||
"multipartForm": []
|
||||
},
|
||||
"docs": ""
|
||||
},
|
||||
"seq": 4
|
||||
},
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "DELETE",
|
||||
"type": "http-request",
|
||||
"request": {
|
||||
"url": "https://node-task2.herokuapp.com/api/notes/2",
|
||||
"method": "DELETE",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "none",
|
||||
"json": null,
|
||||
"text": null,
|
||||
"xml": null,
|
||||
"formUrlEncoded": [],
|
||||
"multipartForm": []
|
||||
},
|
||||
"docs": ""
|
||||
},
|
||||
"seq": 5
|
||||
}
|
||||
],
|
||||
"environments": [],
|
||||
"root": {
|
||||
"docs": "",
|
||||
"meta": {
|
||||
"name": "CRUD"
|
||||
},
|
||||
"request": {
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null
|
||||
},
|
||||
"headers": [],
|
||||
"script": {},
|
||||
"tests": "",
|
||||
"vars": {}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
const { postmanTranslation } = require('./postman_translation'); // Adjust path as needed
|
||||
const { default: postmanTranslation } = require("../../src/postman/postman-translations");
|
||||
|
||||
describe('postmanTranslation function', () => {
|
||||
test('should translate pm commands correctly', () => {
|
||||
@@ -11,9 +11,6 @@ describe('postmanTranslation function', () => {
|
||||
pm.collectionVariables.set('key', 'value');
|
||||
const data = pm.response.json();
|
||||
pm.expect(pm.environment.has('key')).to.be.true;
|
||||
postman.setEnvironmentVariable('key', 'value');
|
||||
postman.getEnvironmentVariable('key');
|
||||
postman.clearEnvironmentVariable('key');
|
||||
`;
|
||||
const expectedOutput = `
|
||||
bru.getEnvVar('key');
|
||||
@@ -24,9 +21,6 @@ describe('postmanTranslation function', () => {
|
||||
bru.setVar('key', 'value');
|
||||
const data = res.getBody();
|
||||
expect(bru.getEnvVar('key') !== undefined && bru.getEnvVar('key') !== null).to.be.true;
|
||||
bru.setEnvVar('key', 'value');
|
||||
bru.getEnvVar('key');
|
||||
bru.deleteEnvVar('key');
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
@@ -53,7 +47,7 @@ describe('postmanTranslation function', () => {
|
||||
test('should handle multiple pm commands on the same line', () => {
|
||||
const inputScript = "pm.environment.get('key'); pm.environment.set('key', 'value');";
|
||||
const expectedOutput = "bru.getEnvVar('key'); bru.setEnvVar('key', 'value');";
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
test('should handle comments and other JavaScript code', () => {
|
||||
const inputScript = `
|
||||
@@ -157,13 +151,3 @@ test('should handle response commands', () => {
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle tests object', () => {
|
||||
const inputScript = `
|
||||
tests['Status code is 200'] = responseCode.code === 200;
|
||||
`;
|
||||
const expectedOutput = `
|
||||
test("Status code is 200", function() { expect(Boolean(responseCode.code === 200)).to.be.true; });
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
19
packages/bruno-converters/tsconfig.json
Normal file
19
packages/bruno-converters/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react",
|
||||
"module": "ESNext",
|
||||
"declaration": true,
|
||||
"declarationDir": "types",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
"emitDeclarationOnly": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"exclude": ["dist", "node_modules", "tests"]
|
||||
}
|
||||
6
packages/bruno-converters/types/common.d.ts
vendored
Normal file
6
packages/bruno-converters/types/common.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export declare const uuid: () => string;
|
||||
export declare const normalizeFileName: (name: string) => string;
|
||||
export declare const validateSchema: (collection?: {}) => Promise<unknown>;
|
||||
export declare const updateUidsInCollection: (_collection: any) => any;
|
||||
export declare const transformItemsInCollection: (collection: any) => any;
|
||||
export declare const hydrateSeqInCollection: (collection: any) => any;
|
||||
@@ -26,6 +26,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
|
||||
@@ -11,7 +11,7 @@ const { safeStringifyJSON } = require('../../utils/common');
|
||||
const LOCAL_IPV6 = '::1';
|
||||
const LOCAL_IPV4 = '127.0.0.1';
|
||||
const LOCALHOST = 'localhost';
|
||||
const version = electronApp?.app?.getVersion()?.substring(1) ?? "";
|
||||
const version = electronApp?.app?.getVersion() ?? "";
|
||||
const redirectResponseCodes = [301, 302, 303, 307, 308];
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
@@ -102,9 +102,12 @@ function makeAxiosInstance({
|
||||
const url = URL.parse(config.url);
|
||||
config.metadata = config.metadata || {};
|
||||
config.metadata.startTime = new Date().getTime();
|
||||
const timeline = config.metadata.timeline || []
|
||||
|
||||
const timeline = config.metadata.timeline || [];
|
||||
// Add initial request details to the timeline
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'separator'
|
||||
});
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
@@ -173,10 +176,13 @@ function makeAxiosInstance({
|
||||
});
|
||||
}
|
||||
catch(err) {
|
||||
if (err.timeline) {
|
||||
timeline = err.timeline;
|
||||
}
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: err?.message,
|
||||
message: `Error setting up proxy agents: ${err?.message}`,
|
||||
});
|
||||
}
|
||||
config.metadata.timeline = timeline;
|
||||
@@ -264,21 +270,12 @@ function makeAxiosInstance({
|
||||
|
||||
if (redirectCount >= requestMaxRedirects) {
|
||||
const errorResponseData = error.response.data;
|
||||
const dataBuffer = Buffer.isBuffer(errorResponseData) ? errorResponseData : Buffer.from(errorResponseData);
|
||||
timeline?.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: safeStringifyJSON(errorResponseData?.toString?.())
|
||||
});
|
||||
return {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: errorResponseData?.toString?.(),
|
||||
size: Buffer.byteLength(dataBuffer),
|
||||
duration: error.response.headers.get('request-duration') ?? 0,
|
||||
timeline: error.response.timeline
|
||||
};
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Increase redirect count
|
||||
@@ -319,14 +316,26 @@ function makeAxiosInstance({
|
||||
}
|
||||
}
|
||||
|
||||
setupProxyAgents({
|
||||
requestConfig,
|
||||
proxyMode,
|
||||
proxyConfig,
|
||||
httpsAgentRequestFields,
|
||||
interpolationOptions,
|
||||
timeline
|
||||
});
|
||||
try {
|
||||
setupProxyAgents({
|
||||
requestConfig,
|
||||
proxyMode,
|
||||
proxyConfig,
|
||||
httpsAgentRequestFields,
|
||||
interpolationOptions,
|
||||
timeline
|
||||
});
|
||||
}
|
||||
catch(err) {
|
||||
if (err.timeline) {
|
||||
timeline = err.timeline;
|
||||
}
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: `Error setting up proxy agents: ${err?.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
requestConfig.metadata.timeline = timeline;
|
||||
// Make the redirected request
|
||||
@@ -334,7 +343,11 @@ function makeAxiosInstance({
|
||||
}
|
||||
else {
|
||||
const errorResponseData = error.response.data;
|
||||
const dataBuffer = Buffer.isBuffer(errorResponseData) ? errorResponseData : Buffer.from(errorResponseData);
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'response',
|
||||
message: `HTTP/${error.response.httpVersion || '1.1'} ${error.response.status} ${error.response.statusText}`,
|
||||
});
|
||||
Object.entries(error?.response?.headers || {}).forEach(([key, value]) => {
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
@@ -357,15 +370,8 @@ function makeAxiosInstance({
|
||||
type: 'error',
|
||||
message: safeStringifyJSON(error?.errors)
|
||||
});
|
||||
return {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: errorResponseData?.toString?.(),
|
||||
size: Buffer.byteLength(dataBuffer),
|
||||
duration: error.response.headers.get('request-duration') ?? 0,
|
||||
timeline
|
||||
};
|
||||
error.response.timeline = timeline;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
else if (error?.code) {
|
||||
@@ -386,13 +392,9 @@ function makeAxiosInstance({
|
||||
type: 'error',
|
||||
message: safeStringifyJSON(error?.errors)
|
||||
});
|
||||
return {
|
||||
status: '-',
|
||||
statusText: error.code,
|
||||
headers: error?.config?.headers,
|
||||
data: 'request failed, check timeline network logs',
|
||||
timeline
|
||||
};
|
||||
error.timeline = timeline;
|
||||
error.statusText = error.code;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
127
packages/bruno-electron/src/ipc/network/faker-functions.js
Normal file
127
packages/bruno-electron/src/ipc/network/faker-functions.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const { faker } = require('@faker-js/faker');
|
||||
|
||||
const mockDataFunctions = {
|
||||
guid: () => faker.string.uuid(),
|
||||
timestamp: () => faker.date.anytime().getTime().toString(),
|
||||
isoTimestamp: () => faker.date.anytime().toISOString(),
|
||||
randomUUID: () => faker.string.uuid(),
|
||||
randomAlphaNumeric: () => faker.string.alphanumeric(),
|
||||
randomBoolean: () => faker.datatype.boolean(),
|
||||
randomInt: () => faker.number.int(),
|
||||
randomColor: () => faker.color.human(),
|
||||
randomHexColor: () => faker.internet.color(),
|
||||
randomAbbreviation: () => faker.hacker.abbreviation(),
|
||||
randomIP: () => faker.internet.ip(),
|
||||
randomIPV4: () => faker.internet.ipv4(),
|
||||
randomIPV6: () => faker.internet.ipv6(),
|
||||
randomMACAddress: () => faker.internet.mac(),
|
||||
randomPassword: () => faker.internet.password(),
|
||||
randomLocale: () => faker.location.countryCode(),
|
||||
randomUserAgent: () => faker.internet.userAgent(),
|
||||
randomProtocol: () => faker.internet.protocol(),
|
||||
randomSemver: () => faker.system.semver(),
|
||||
randomFirstName: () => faker.person.firstName(),
|
||||
randomLastName: () => faker.person.lastName(),
|
||||
randomFullName: () => faker.person.fullName(),
|
||||
randomNamePrefix: () => faker.person.prefix(),
|
||||
randomNameSuffix: () => faker.person.suffix(),
|
||||
randomJobArea: () => faker.person.jobArea(),
|
||||
randomJobDescriptor: () => faker.person.jobDescriptor(),
|
||||
randomJobTitle: () => faker.person.jobTitle(),
|
||||
randomJobType: () => faker.person.jobType(),
|
||||
randomPhoneNumber: () => faker.phone.number(),
|
||||
randomPhoneNumberExt: () => faker.phone.number(),
|
||||
randomCity: () => faker.location.city(),
|
||||
randomStreetName: () => faker.location.street(),
|
||||
randomStreetAddress: () => faker.location.streetAddress(),
|
||||
randomCountry: () => faker.location.country(),
|
||||
randomCountryCode: () => faker.location.countryCode(),
|
||||
randomLatitude: () => faker.location.latitude(),
|
||||
randomLongitude: () => faker.location.longitude(),
|
||||
randomAvatarImage: () => faker.image.avatar(),
|
||||
randomImageUrl: () => faker.image.url(),
|
||||
randomAbstractImage: () => faker.image.urlLoremFlickr({ category: 'abstract' }),
|
||||
randomAnimalsImage: () => faker.image.urlLoremFlickr({ category: 'animals' }),
|
||||
randomBusinessImage: () => faker.image.urlLoremFlickr({ category: 'business' }),
|
||||
randomCatsImage: () => faker.image.urlLoremFlickr({ category: 'cats' }),
|
||||
randomCityImage: () => faker.image.urlLoremFlickr({ category: 'city' }),
|
||||
randomFoodImage: () => faker.image.urlLoremFlickr({ category: 'food' }),
|
||||
randomNightlifeImage: () => faker.image.urlLoremFlickr({ category: 'nightlife' }),
|
||||
randomFashionImage: () => faker.image.urlLoremFlickr({ category: 'fashion' }),
|
||||
randomPeopleImage: () => faker.image.urlLoremFlickr({ category: 'people' }),
|
||||
randomNatureImage: () => faker.image.urlLoremFlickr({ category: 'nature' }),
|
||||
randomSportsImage: () => faker.image.urlLoremFlickr({ category: 'sports' }),
|
||||
randomTransportImage: () => faker.image.urlLoremFlickr({ category: 'transport' }),
|
||||
randomImageDataUri: () => faker.image.dataUri(),
|
||||
randomBankAccount: () => faker.finance.accountNumber(),
|
||||
randomBankAccountName: () => faker.finance.accountName(),
|
||||
randomCreditCardMask: () => faker.finance.iban().replace(/(?<=.{4})\w(?=.{2})/g, '*'),
|
||||
randomBankAccountBic: () => faker.finance.bic(),
|
||||
randomBankAccountIban: () => faker.finance.iban(),
|
||||
randomTransactionType: () => faker.finance.transactionType(),
|
||||
randomCurrencyCode: () => faker.finance.currencyCode(),
|
||||
randomCurrencyName: () => faker.finance.currencyName(),
|
||||
randomCurrencySymbol: () => faker.finance.currencySymbol(),
|
||||
randomBitcoin: () => faker.finance.bitcoinAddress(),
|
||||
randomCompanyName: () => faker.company.name(),
|
||||
randomCompanySuffix: () => faker.company.name(),
|
||||
randomBs: () => faker.company.buzzPhrase(),
|
||||
randomBsAdjective: () => faker.company.buzzAdjective(),
|
||||
randomBsBuzz: () => faker.company.buzzVerb(),
|
||||
randomBsNoun: () => faker.company.buzzNoun(),
|
||||
randomCatchPhrase: () => faker.company.catchPhrase(),
|
||||
randomCatchPhraseAdjective: () => faker.company.catchPhraseAdjective(),
|
||||
randomCatchPhraseDescriptor: () => faker.company.catchPhraseDescriptor(),
|
||||
randomCatchPhraseNoun: () => faker.company.catchPhraseNoun(),
|
||||
randomDatabaseColumn: () => faker.database.column(),
|
||||
randomDatabaseType: () => faker.database.type(),
|
||||
randomDatabaseCollation: () => faker.database.collation(),
|
||||
randomDatabaseEngine: () => faker.database.engine(),
|
||||
randomDateFuture: () => faker.date.future().toISOString(),
|
||||
randomDatePast: () => faker.date.past().toISOString(),
|
||||
randomDateRecent: () => faker.date.recent().toISOString(),
|
||||
randomWeekday: () => faker.date.weekday(),
|
||||
randomMonth: () => faker.date.month(),
|
||||
randomDomainName: () => faker.internet.domainName(),
|
||||
randomDomainSuffix: () => faker.internet.domainSuffix(),
|
||||
randomDomainWord: () => faker.internet.domainWord(),
|
||||
randomEmail: () => faker.internet.email(),
|
||||
randomExampleEmail: () => faker.internet.exampleEmail(),
|
||||
randomUserName: () => faker.internet.username(),
|
||||
randomUrl: () => faker.internet.url(),
|
||||
randomFileName: () => faker.system.fileName(),
|
||||
randomFileType: () => faker.system.fileType(),
|
||||
randomFileExt: () => faker.system.fileExt(),
|
||||
randomCommonFileName: () => faker.system.commonFileName(),
|
||||
randomCommonFileType: () => faker.system.commonFileType(),
|
||||
randomCommonFileExt: () => faker.system.commonFileExt(),
|
||||
randomFilePath: () => faker.system.filePath(),
|
||||
randomDirectoryPath: () => faker.system.directoryPath(),
|
||||
randomMimeType: () => faker.system.mimeType(),
|
||||
randomPrice: () => faker.commerce.price(),
|
||||
randomProduct: () => faker.commerce.product(),
|
||||
randomProductAdjective: () => faker.commerce.productAdjective(),
|
||||
randomProductMaterial: () => faker.commerce.productMaterial(),
|
||||
randomProductName: () => faker.commerce.productName(),
|
||||
randomDepartment: () => faker.commerce.department(),
|
||||
randomNoun: () => faker.hacker.noun(),
|
||||
randomVerb: () => faker.hacker.verb(),
|
||||
randomIngverb: () => faker.hacker.ingverb(),
|
||||
randomAdjective: () => faker.hacker.adjective(),
|
||||
randomWord: () => faker.hacker.noun(),
|
||||
randomWords: () => faker.lorem.words(),
|
||||
randomPhrase: () => faker.hacker.phrase(),
|
||||
randomLoremWord: () => faker.lorem.word(),
|
||||
randomLoremWords: () => faker.lorem.words(),
|
||||
randomLoremSentence: () => faker.lorem.sentence(),
|
||||
randomLoremSentences: () => faker.lorem.sentences(),
|
||||
randomLoremParagraph: () => faker.lorem.paragraph(),
|
||||
randomLoremParagraphs: () => faker.lorem.paragraphs(),
|
||||
randomLoremText: () => faker.lorem.text(),
|
||||
randomLoremSlug: () => faker.lorem.slug(),
|
||||
randomLoremLines: () => faker.lorem.lines()
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
mockDataFunctions
|
||||
};
|
||||
@@ -20,7 +20,7 @@ const { prepareRequest } = require('./prepare-request');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { makeAxiosInstance } = require('./axios-instance');
|
||||
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
|
||||
const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse } = require('../../utils/common');
|
||||
const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest } = require('../../utils/common');
|
||||
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
|
||||
const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');
|
||||
const { createFormData } = require('../../utils/form-data');
|
||||
@@ -557,16 +557,14 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
);
|
||||
const requestData = request.mode == 'file'? "<request body redacted>": (typeof request?.data === 'string' ? request?.data : safeStringifyJSON(request?.data));
|
||||
|
||||
const { data: requestData, dataBuffer: requestDataBuffer } = parseDataFromRequest(request);
|
||||
let requestSent = {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
data: requestData,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
if (requestData) {
|
||||
requestSent.dataBuffer = Buffer.from(requestData);
|
||||
dataBuffer: requestDataBuffer
|
||||
}
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
@@ -602,9 +600,14 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// if it's a cancel request, don't continue
|
||||
if (axios.isCancel(error)) {
|
||||
let error = new Error('Request cancelled');
|
||||
error.isCancel = true;
|
||||
return Promise.reject(error);
|
||||
// we are not rejecting the promise here and instead returning a response object with `error` which is handled in the `send-http-request` invocation
|
||||
// timeline prop won't be accessible in the usual way in the renderer process if we reject the promise
|
||||
return {
|
||||
statusText: 'REQUEST_CANCELLED',
|
||||
isCancel: true,
|
||||
error: 'REQUEST_CANCELLED',
|
||||
timeline: error.timeline
|
||||
};
|
||||
}
|
||||
|
||||
if (error?.response) {
|
||||
@@ -615,7 +618,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
response.headers.delete('request-duration');
|
||||
} else {
|
||||
// if it's not a network error, don't continue
|
||||
return Promise.reject(error);
|
||||
// we are not rejecting the promise here and instead returning a response object with `error` which is handled in the `send-http-request` invocation
|
||||
// timeline prop won't be accessible in the usual way in the renderer process if we reject the promise
|
||||
return {
|
||||
statusText: error.statusText,
|
||||
error: error.message,
|
||||
timeline: error.timeline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,7 +752,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
} catch (error) {
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
|
||||
return Promise.reject(error);
|
||||
// we are not rejecting the promise here and instead returning a response object with `error` which is handled in the `send-http-request` invocation
|
||||
// timeline prop won't be accessible in the usual way in the renderer process if we reject the promise
|
||||
return {
|
||||
status: error?.status,
|
||||
error: error?.message || 'an error ocurred: debug',
|
||||
timeline: error?.timeline
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -992,15 +1007,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
const requestData = request.mode == 'file'? "<request body redacted>": (typeof request?.data === 'string' ? request?.data : safeStringifyJSON(request?.data));
|
||||
const { data: requestData, dataBuffer: requestDataBuffer } = parseDataFromRequest(request);
|
||||
let requestSent = {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
data: requestData
|
||||
}
|
||||
if (requestData) {
|
||||
requestSent.dataBuffer = Buffer.from(requestData);
|
||||
data: requestData,
|
||||
dataBuffer: requestDataBuffer
|
||||
}
|
||||
|
||||
// todo:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { interpolate } = require('@usebruno/common');
|
||||
const { each, forOwn, cloneDeep, find } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const { mockDataFunctions } = require('./faker-functions');
|
||||
|
||||
const getContentType = (headers = {}) => {
|
||||
let contentType = '';
|
||||
@@ -13,6 +14,14 @@ const getContentType = (headers = {}) => {
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const interpolateMockVars = (str) => {
|
||||
const patternRegex = /\{\{\$(\w+)\}\}/g;
|
||||
return str.replace(patternRegex, (match, keyword) => {
|
||||
const replacement = mockDataFunctions[keyword]?.();
|
||||
return replacement || match;
|
||||
});
|
||||
};
|
||||
|
||||
const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
||||
@@ -55,7 +64,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
}
|
||||
};
|
||||
|
||||
return interpolate(str, combinedVars);
|
||||
return interpolateMockVars(interpolate(str, combinedVars));
|
||||
};
|
||||
|
||||
request.url = _interpolate(request.url);
|
||||
@@ -239,7 +248,6 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
request.wsse.password = _interpolate(request.wsse.password) || '';
|
||||
}
|
||||
|
||||
|
||||
// interpolate vars for ntlmConfig auth
|
||||
if (request.ntlmConfig) {
|
||||
request.ntlmConfig.username = _interpolate(request.ntlmConfig.username) || '';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { customAlphabet } = require('nanoid');
|
||||
const iconv = require('iconv-lite');
|
||||
const { cloneDeep } = require('lodash');
|
||||
|
||||
// a customized version of nanoid without using _ and -
|
||||
const uuid = () => {
|
||||
@@ -26,10 +27,24 @@ const parseJson = async (obj) => {
|
||||
}
|
||||
};
|
||||
|
||||
const safeStringifyJSON = (data) => {
|
||||
const getCircularReplacer = () => {
|
||||
const seen = new WeakSet();
|
||||
return (key, value) => {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (seen.has(value)) return "[Circular]";
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
const safeStringifyJSON = (data, indent = null) => {
|
||||
if (data === undefined) return undefined;
|
||||
try {
|
||||
return JSON.stringify(data);
|
||||
// getCircularReplacer - removes circular references that cause an error when stringifying
|
||||
return JSON.stringify(data, getCircularReplacer(), indent);
|
||||
} catch (e) {
|
||||
console.warn('Failed to stringify data:', e.message);
|
||||
return data;
|
||||
}
|
||||
};
|
||||
@@ -112,6 +127,16 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
|
||||
return { data, dataBuffer };
|
||||
};
|
||||
|
||||
const parseDataFromRequest = (request) => {
|
||||
const requestDataString = request.mode == 'file'? "<request body redacted>": (typeof request?.data === 'string' ? request?.data : safeStringifyJSON(request?.data));
|
||||
const requestCopy = cloneDeep(request);
|
||||
if (!requestCopy.data) {
|
||||
return { data: null, dataBuffer: null };
|
||||
}
|
||||
requestCopy.data = requestDataString;
|
||||
return parseDataFromResponse(requestCopy);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
uuid,
|
||||
stringifyJson,
|
||||
@@ -121,5 +146,6 @@ module.exports = {
|
||||
simpleHash,
|
||||
generateUidBasedOnHash,
|
||||
flattenDataForDotNotation,
|
||||
parseDataFromResponse
|
||||
parseDataFromResponse,
|
||||
parseDataFromRequest
|
||||
};
|
||||
|
||||
@@ -42,6 +42,10 @@ const isTokenExpired = (credentials) => {
|
||||
return Date.now() > expiryTime;
|
||||
};
|
||||
|
||||
const safeParseJSONBuffer = (data) => {
|
||||
return safeParseJSON(Buffer.isBuffer(data) ? data.toString() : data);
|
||||
}
|
||||
|
||||
// AUTHORIZATION CODE
|
||||
|
||||
const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, forceFetch = false, certsAndProxyConfig }) => {
|
||||
@@ -143,68 +147,46 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
|
||||
requestCopy.data = qs.stringify(data);
|
||||
requestCopy.url = url;
|
||||
requestCopy.responseType = 'arraybuffer';
|
||||
|
||||
// Initialize variables to hold request and response data for debugging
|
||||
let axiosRequestInfo = null;
|
||||
let axiosResponseInfo = null;
|
||||
|
||||
try {
|
||||
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
|
||||
// Interceptor to capture request data
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
|
||||
axiosRequestInfo = {
|
||||
method: config.method.toUpperCase(),
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
data: requestData,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to capture response data
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
axiosResponseInfo = {
|
||||
let responseInfo, parsedResponseData;
|
||||
try {
|
||||
const response = await axiosInstance(requestCopy);
|
||||
parsedResponseData = safeParseJSONBuffer(response.data);
|
||||
responseInfo = {
|
||||
url: response?.url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
status: response?.status,
|
||||
statusText: response?.statusText,
|
||||
headers: response?.headers,
|
||||
data: parsedResponseData,
|
||||
timestamp: Date.now(),
|
||||
timeline: response?.timeline
|
||||
};
|
||||
return response;
|
||||
}, (error) => {
|
||||
}
|
||||
catch(error) {
|
||||
if (error.response) {
|
||||
axiosResponseInfo = {
|
||||
responseInfo = {
|
||||
url: error?.response?.url,
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
status: error?.response?.status,
|
||||
statusText: error?.response?.statusText,
|
||||
headers: error?.response?.headers,
|
||||
data: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
|
||||
timestamp: Date.now(),
|
||||
timeline: error?.response?.timeline,
|
||||
error: 'fetching access token failed! check timeline network logs'
|
||||
error: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
|
||||
};
|
||||
}
|
||||
else if(error?.code) {
|
||||
axiosResponseInfo = {
|
||||
responseInfo = {
|
||||
status: '-',
|
||||
statusText: error.code,
|
||||
statusText: error?.code,
|
||||
headers: error?.config?.headers,
|
||||
data: safeStringifyJSON(error?.errors),
|
||||
timeline: error?.response?.timeline
|
||||
};
|
||||
}
|
||||
return axiosResponseInfo;
|
||||
});
|
||||
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const parsedResponseData = safeParseJSON(
|
||||
Buffer.isBuffer(response.data) ? response.data?.toString() : response.data
|
||||
);
|
||||
}
|
||||
// Ensure debugInfo.data is initialized
|
||||
if (!debugInfo) {
|
||||
debugInfo = { data: [] };
|
||||
@@ -216,33 +198,32 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
|
||||
const axiosMainRequest = {
|
||||
requestId: Date.now().toString(),
|
||||
request: {
|
||||
url: axiosRequestInfo?.url,
|
||||
method: axiosRequestInfo?.method,
|
||||
headers: axiosRequestInfo?.headers || {},
|
||||
data: axiosRequestInfo?.data,
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: requestCopy?.headers,
|
||||
data: requestCopy?.data,
|
||||
error: null
|
||||
},
|
||||
response: {
|
||||
url: axiosResponseInfo?.url,
|
||||
headers: axiosResponseInfo?.headers,
|
||||
data: parsedResponseData,
|
||||
status: axiosResponseInfo?.status,
|
||||
statusText: axiosResponseInfo?.statusText,
|
||||
error: axiosResponseInfo?.error,
|
||||
timeline: axiosResponseInfo?.timeline
|
||||
url: responseInfo?.url,
|
||||
headers: responseInfo?.headers,
|
||||
data: responseInfo?.data,
|
||||
status: responseInfo?.status,
|
||||
statusText: responseInfo?.statusText,
|
||||
error: responseInfo?.error,
|
||||
timeline: responseInfo?.timeline
|
||||
},
|
||||
fromCache: false,
|
||||
completed: true,
|
||||
requests: [], // No sub-requests in this context
|
||||
};
|
||||
|
||||
debugInfo.data.push(axiosMainRequest);
|
||||
|
||||
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
parsedResponseData && persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
|
||||
} catch (error) {
|
||||
return Promise.reject(safeStringifyJSON(error?.response?.data));
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -369,96 +350,79 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo
|
||||
requestCopy.data = qs.stringify(data);
|
||||
requestCopy.url = url;
|
||||
requestCopy.responseType = 'arraybuffer';
|
||||
|
||||
// Initialize variables to hold request and response data for debugging
|
||||
let axiosRequestInfo = null;
|
||||
let axiosResponseInfo = null;
|
||||
let debugInfo = { data: [] };
|
||||
|
||||
try {
|
||||
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
|
||||
axiosRequestInfo = {
|
||||
method: config.method.toUpperCase(),
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
data: requestData,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to capture response data
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
axiosResponseInfo = {
|
||||
let responseInfo, parsedResponseData;
|
||||
try {
|
||||
const response = await axiosInstance(requestCopy);
|
||||
parsedResponseData = safeParseJSONBuffer(response.data);
|
||||
responseInfo = {
|
||||
url: response?.url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
status: response?.status,
|
||||
statusText: response?.statusText,
|
||||
headers: response?.headers,
|
||||
data: parsedResponseData,
|
||||
timestamp: Date.now(),
|
||||
timeline: response?.timeline
|
||||
};
|
||||
return response;
|
||||
}, (error) => {
|
||||
}
|
||||
catch(error) {
|
||||
if (error.response) {
|
||||
axiosResponseInfo = {
|
||||
responseInfo = {
|
||||
url: error?.response?.url,
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
status: error?.response?.status,
|
||||
statusText: error?.response?.statusText,
|
||||
headers: error?.response?.headers,
|
||||
data: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
|
||||
timestamp: Date.now(),
|
||||
timeline: error?.response?.timeline,
|
||||
error: 'fetching access token failed! check timeline network logs'
|
||||
error: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
|
||||
};
|
||||
}
|
||||
else if(error?.code) {
|
||||
axiosResponseInfo = {
|
||||
responseInfo = {
|
||||
status: '-',
|
||||
statusText: error.code,
|
||||
statusText: error?.code,
|
||||
headers: error?.config?.headers,
|
||||
data: safeStringifyJSON(error?.errors),
|
||||
timeline: error?.response?.timeline
|
||||
};
|
||||
}
|
||||
return axiosResponseInfo;
|
||||
});
|
||||
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const parsedResponseData = safeParseJSON(
|
||||
Buffer.isBuffer(response.data) ? response.data.toString() : response.data
|
||||
);
|
||||
}
|
||||
if (!debugInfo) {
|
||||
debugInfo = { data: [] };
|
||||
} else if (!debugInfo.data) {
|
||||
debugInfo.data = [];
|
||||
}
|
||||
|
||||
// Add the axios request and response info as a main request in debugInfo
|
||||
const axiosMainRequest = {
|
||||
requestId: Date.now().toString(),
|
||||
request: {
|
||||
url: axiosRequestInfo?.url,
|
||||
method: axiosRequestInfo?.method,
|
||||
headers: axiosRequestInfo?.headers || {},
|
||||
data: axiosRequestInfo?.data,
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: requestCopy?.headers,
|
||||
data: requestCopy?.data,
|
||||
error: null
|
||||
},
|
||||
response: {
|
||||
url: axiosResponseInfo.url,
|
||||
headers: axiosResponseInfo?.headers,
|
||||
data: parsedResponseData,
|
||||
status: axiosResponseInfo?.status,
|
||||
statusText: axiosResponseInfo?.statusText,
|
||||
timeline: axiosResponseInfo?.timeline,
|
||||
error: null
|
||||
url: responseInfo?.url,
|
||||
headers: responseInfo?.headers,
|
||||
data: responseInfo?.data,
|
||||
status: responseInfo?.status,
|
||||
statusText: responseInfo?.statusText,
|
||||
error: responseInfo?.error,
|
||||
timeline: responseInfo?.timeline
|
||||
},
|
||||
fromCache: false,
|
||||
completed: true,
|
||||
requests: [], // No sub-requests in this context
|
||||
};
|
||||
|
||||
debugInfo.data.push(axiosMainRequest);
|
||||
|
||||
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
parsedResponseData && persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
|
||||
} catch (error) {
|
||||
return Promise.reject(safeStringifyJSON(error?.response?.data));
|
||||
@@ -557,95 +521,79 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid,
|
||||
requestCopy.data = qs.stringify(data);
|
||||
requestCopy.url = url;
|
||||
requestCopy.responseType = 'arraybuffer';
|
||||
|
||||
// Initialize variables to hold request and response data for debugging
|
||||
let axiosRequestInfo = null;
|
||||
let axiosResponseInfo = null;
|
||||
let debugInfo = { data: [] };
|
||||
|
||||
try {
|
||||
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
|
||||
axiosRequestInfo = {
|
||||
method: config.method.toUpperCase(),
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
data: requestData,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to capture response data
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
axiosResponseInfo = {
|
||||
let responseInfo, parsedResponseData;
|
||||
try {
|
||||
const response = await axiosInstance(requestCopy);
|
||||
parsedResponseData = safeParseJSONBuffer(response.data);
|
||||
responseInfo = {
|
||||
url: response?.url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
status: response?.status,
|
||||
statusText: response?.statusText,
|
||||
headers: response?.headers,
|
||||
data: parsedResponseData,
|
||||
timestamp: Date.now(),
|
||||
timeline: response?.timeline
|
||||
};
|
||||
return response;
|
||||
}, (error) => {
|
||||
}
|
||||
catch(error) {
|
||||
if (error.response) {
|
||||
axiosResponseInfo = {
|
||||
responseInfo = {
|
||||
url: error?.response?.url,
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
status: error?.response?.status,
|
||||
statusText: error?.response?.statusText,
|
||||
headers: error?.response?.headers,
|
||||
data: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
|
||||
timestamp: Date.now(),
|
||||
timeline: error?.response?.timeline,
|
||||
error: 'fetching access token failed! check timeline network logs'
|
||||
error: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
|
||||
};
|
||||
}
|
||||
else if(error?.code) {
|
||||
axiosResponseInfo = {
|
||||
responseInfo = {
|
||||
status: '-',
|
||||
statusText: error.code,
|
||||
statusText: error?.code,
|
||||
headers: error?.config?.headers,
|
||||
data: safeStringifyJSON(error?.errors),
|
||||
timeline: error?.response?.timeline
|
||||
};
|
||||
}
|
||||
return axiosResponseInfo;
|
||||
});
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const parsedResponseData = safeParseJSON(
|
||||
Buffer.isBuffer(response.data) ? response.data.toString() : response.data
|
||||
);
|
||||
}
|
||||
if (!debugInfo) {
|
||||
debugInfo = { data: [] };
|
||||
} else if (!debugInfo.data) {
|
||||
debugInfo.data = [];
|
||||
}
|
||||
|
||||
// Add the axios request and response info as a main request in debugInfo
|
||||
const axiosMainRequest = {
|
||||
requestId: Date.now().toString(),
|
||||
request: {
|
||||
url: axiosRequestInfo?.url,
|
||||
method: axiosRequestInfo?.method,
|
||||
headers: axiosRequestInfo?.headers || {},
|
||||
data: axiosRequestInfo?.data,
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: requestCopy?.headers,
|
||||
data: requestCopy?.data,
|
||||
error: null
|
||||
},
|
||||
response: {
|
||||
url: axiosResponseInfo?.url,
|
||||
headers: axiosResponseInfo?.headers,
|
||||
data: parsedResponseData,
|
||||
status: axiosResponseInfo?.status,
|
||||
statusText: axiosResponseInfo?.statusText,
|
||||
timeline: axiosResponseInfo?.timeline,
|
||||
error: null
|
||||
url: responseInfo?.url,
|
||||
headers: responseInfo?.headers,
|
||||
data: responseInfo?.data,
|
||||
status: responseInfo?.status,
|
||||
statusText: responseInfo?.statusText,
|
||||
error: responseInfo?.error,
|
||||
timeline: responseInfo?.timeline
|
||||
},
|
||||
fromCache: false,
|
||||
completed: true,
|
||||
requests: [], // No sub-requests in this context
|
||||
};
|
||||
|
||||
debugInfo.data.push(axiosMainRequest);
|
||||
|
||||
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
parsedResponseData && persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
|
||||
} catch (error) {
|
||||
return Promise.reject(safeStringifyJSON(error?.response?.data));
|
||||
@@ -677,101 +625,82 @@ const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyCon
|
||||
requestCopy.data = qs.stringify(data);
|
||||
requestCopy.url = url;
|
||||
requestCopy.responseType = 'arraybuffer';
|
||||
|
||||
// Initialize variables to hold request and response data for debugging
|
||||
let axiosRequestInfo = null;
|
||||
let axiosResponseInfo = null;
|
||||
let debugInfo = { data: [] };
|
||||
|
||||
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
|
||||
axiosRequestInfo = {
|
||||
method: config.method.toUpperCase(),
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
data: requestData,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to capture response data
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
axiosResponseInfo = {
|
||||
url: response?.url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
timestamp: Date.now(),
|
||||
timeline: response?.timeline
|
||||
};
|
||||
return response;
|
||||
}, (error) => {
|
||||
if (error.response) {
|
||||
axiosResponseInfo = {
|
||||
url: error?.response?.url,
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
timestamp: Date.now(),
|
||||
timeline: error?.response?.timeline,
|
||||
error: 'fetching access token failed! check timeline network logs'
|
||||
};
|
||||
}
|
||||
else if(error?.code) {
|
||||
axiosResponseInfo = {
|
||||
status: '-',
|
||||
statusText: error.code,
|
||||
headers: error?.config?.headers,
|
||||
data: safeStringifyJSON(error?.errors),
|
||||
timeline: error?.response?.timeline
|
||||
};
|
||||
}
|
||||
return axiosResponseInfo;
|
||||
});
|
||||
|
||||
|
||||
try {
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const parsedResponseData = safeParseJSON(
|
||||
Buffer.isBuffer(response.data) ? response.data.toString() : response.data
|
||||
);
|
||||
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
|
||||
let responseInfo, parsedResponseData;
|
||||
try {
|
||||
const response = await axiosInstance(requestCopy);
|
||||
parsedResponseData = safeParseJSONBuffer(response.data);
|
||||
responseInfo = {
|
||||
url: response?.url,
|
||||
status: response?.status,
|
||||
statusText: response?.statusText,
|
||||
headers: response?.headers,
|
||||
data: parsedResponseData,
|
||||
timestamp: Date.now(),
|
||||
timeline: response?.timeline
|
||||
};
|
||||
}
|
||||
catch(error) {
|
||||
if (error.response) {
|
||||
responseInfo = {
|
||||
url: error?.response?.url,
|
||||
status: error?.response?.status,
|
||||
statusText: error?.response?.statusText,
|
||||
headers: error?.response?.headers,
|
||||
data: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
|
||||
timestamp: Date.now(),
|
||||
timeline: error?.response?.timeline,
|
||||
error: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
|
||||
};
|
||||
}
|
||||
else if(error?.code) {
|
||||
responseInfo = {
|
||||
status: '-',
|
||||
statusText: error?.code,
|
||||
headers: error?.config?.headers,
|
||||
data: safeStringifyJSON(error?.errors),
|
||||
timeline: error?.response?.timeline
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!debugInfo) {
|
||||
debugInfo = { data: [] };
|
||||
} else if (!debugInfo.data) {
|
||||
debugInfo.data = [];
|
||||
}
|
||||
|
||||
// Add the axios request and response info as a main request in debugInfo
|
||||
const axiosMainRequest = {
|
||||
requestId: Date.now().toString(),
|
||||
request: {
|
||||
url: axiosRequestInfo?.url,
|
||||
method: axiosRequestInfo?.method,
|
||||
headers: axiosRequestInfo?.headers || {},
|
||||
data: axiosRequestInfo?.data,
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: requestCopy?.headers,
|
||||
data: requestCopy?.data,
|
||||
error: null
|
||||
},
|
||||
response: {
|
||||
url: axiosResponseInfo?.url,
|
||||
headers: axiosResponseInfo?.headers,
|
||||
data: parsedResponseData,
|
||||
status: axiosResponseInfo?.status,
|
||||
statusText: axiosResponseInfo?.statusText,
|
||||
timeline: axiosResponseInfo?.timeline,
|
||||
error: null
|
||||
url: responseInfo?.url,
|
||||
headers: responseInfo?.headers,
|
||||
data: responseInfo?.data,
|
||||
status: responseInfo?.status,
|
||||
statusText: responseInfo?.statusText,
|
||||
error: responseInfo?.error,
|
||||
timeline: responseInfo?.timeline
|
||||
},
|
||||
fromCache: false,
|
||||
completed: true,
|
||||
requests: [], // No sub-requests in this context
|
||||
};
|
||||
|
||||
debugInfo.data.push(axiosMainRequest);
|
||||
|
||||
if (parsedResponseData?.error) {
|
||||
if (!parsedResponseData || parsedResponseData?.error) {
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
return { collectionUid, url, credentials: null, credentialsId, debugInfo };
|
||||
}
|
||||
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
parsedResponseData && persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
|
||||
} catch (error) {
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
|
||||
@@ -168,10 +168,21 @@ function createTimelineAgentClass(BaseAgentClass) {
|
||||
message: `Trying ${host}:${port}...`,
|
||||
});
|
||||
|
||||
const socket = super.createConnection(options, callback);
|
||||
let socket;
|
||||
try {
|
||||
socket = super.createConnection(options, callback);
|
||||
} catch (error) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: `Error creating connection: ${error.message}`,
|
||||
});
|
||||
error.timeline = this.timeline;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Attach event listeners to the socket
|
||||
socket.on('lookup', (err, address, family, host) => {
|
||||
socket?.on('lookup', (err, address, family, host) => {
|
||||
if (err) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
@@ -187,7 +198,7 @@ function createTimelineAgentClass(BaseAgentClass) {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket?.on('connect', () => {
|
||||
const address = socket.remoteAddress || host;
|
||||
const remotePort = socket.remotePort || port;
|
||||
|
||||
@@ -198,7 +209,7 @@ function createTimelineAgentClass(BaseAgentClass) {
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('secureConnect', () => {
|
||||
socket?.on('secureConnect', () => {
|
||||
const protocol = socket.getProtocol() || 'SSL/TLS';
|
||||
const cipher = socket.getCipher();
|
||||
const cipherSuite = cipher ? `${cipher.name} (${cipher.version})` : 'Unknown cipher';
|
||||
@@ -270,7 +281,7 @@ function createTimelineAgentClass(BaseAgentClass) {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
socket?.on('error', (err) => {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
@@ -294,6 +305,10 @@ function setupProxyAgents({
|
||||
// Ensure TLS options are properly set
|
||||
const tlsOptions = {
|
||||
...httpsAgentRequestFields,
|
||||
// Enable all secure protocols by default
|
||||
secureProtocol: undefined,
|
||||
// Allow Node.js to choose the protocol
|
||||
minVersion: 'TLSv1',
|
||||
rejectUnauthorized: httpsAgentRequestFields.rejectUnauthorized !== undefined ? httpsAgentRequestFields.rejectUnauthorized : true,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { get } = require('@usebruno/query');
|
||||
const _ = require('lodash');
|
||||
|
||||
class BrunoResponse {
|
||||
constructor(res) {
|
||||
@@ -46,8 +47,9 @@ class BrunoResponse {
|
||||
return;
|
||||
}
|
||||
|
||||
this.body = data;
|
||||
this.res.data = data;
|
||||
const clonedData = _.cloneDeep(data);
|
||||
this.res.data = clonedData;
|
||||
this.body = clonedData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -193,6 +193,11 @@ class ScriptRuntime {
|
||||
const res = new BrunoResponse(response);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
|
||||
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
||||
const additionalContextRootsAbsolute = lodash
|
||||
.chain(additionalContextRoots)
|
||||
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
|
||||
.value();
|
||||
|
||||
const whitelistedModules = {};
|
||||
|
||||
@@ -255,7 +260,7 @@ class ScriptRuntime {
|
||||
context: 'sandbox',
|
||||
builtin: [ "*" ],
|
||||
external: true,
|
||||
root: [collectionPath],
|
||||
root: [collectionPath, ...additionalContextRootsAbsolute],
|
||||
mock: {
|
||||
// node libs
|
||||
path,
|
||||
|
||||
@@ -7,8 +7,9 @@ auth:oauth2 {
|
||||
callback_url: {{key-host}}/realms/bruno/account
|
||||
authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth
|
||||
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
|
||||
refresh_token_url:
|
||||
client_id: account
|
||||
client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
|
||||
client_secret: {{client_secret}}
|
||||
scope: openid
|
||||
state:
|
||||
pkce: true
|
||||
@@ -16,5 +17,6 @@ auth:oauth2 {
|
||||
credentials_id: credentials
|
||||
token_placement: header
|
||||
token_header_prefix: Bearer
|
||||
reuse_token:
|
||||
auto_fetch_token: true
|
||||
auto_refresh_token: false
|
||||
}
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
vars {
|
||||
host: http://localhost:8081
|
||||
bearer_auth_token: your_secret_token
|
||||
basic_auth_password: della
|
||||
client_id: client_id_1
|
||||
client_secret: client_secret_1
|
||||
password_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/password_credentials/token
|
||||
password_credentials_username: foo
|
||||
password_credentials_password: bar
|
||||
password_credentials_scope:
|
||||
authorization_code_authorize_url: http://localhost:8081/api/auth/oauth2/authorization_code/authorize
|
||||
authorization_code_callback_url: http://localhost:8081/api/auth/oauth2/authorization_code/callback
|
||||
authorization_code_access_token_url: http://localhost:8081/api/auth/oauth2/authorization_code/token
|
||||
authorization_code_access_token: null
|
||||
client_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/client_credentials/token
|
||||
client_credentials_client_id: client_id_1
|
||||
client_credentials_client_secret: client_secret_1
|
||||
client_credentials_scope: admin
|
||||
client_credentials_access_token: 870132a2ed28a3c94d34f868e6514720
|
||||
key-host: http://localhost:8080
|
||||
}
|
||||
vars:secret [
|
||||
client_secret
|
||||
]
|
||||
|
||||
@@ -17,7 +17,7 @@ auth:oauth2 {
|
||||
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
|
||||
refresh_token_url:
|
||||
client_id: account
|
||||
client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
|
||||
client_secret: {{client_secret}}
|
||||
scope: openid
|
||||
state:
|
||||
pkce: true
|
||||
|
||||
@@ -3,18 +3,16 @@ auth {
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: authorization_code
|
||||
callback_url: {{key-host}}/realms/bruno/account
|
||||
authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth
|
||||
grant_type: client_credentials
|
||||
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
|
||||
refresh_token_url:
|
||||
client_id: account
|
||||
client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
|
||||
client_secret: {{client_secret}}
|
||||
scope: openid
|
||||
state:
|
||||
pkce: true
|
||||
tokenId: keycloak
|
||||
tokenPlacement: header
|
||||
tokenHeaderPrefix: Bearer
|
||||
tokenQueryKey: access_token
|
||||
reuseToken:
|
||||
credentials_placement: body
|
||||
credentials_id: credentials
|
||||
token_placement: header
|
||||
token_header_prefix: Bearer
|
||||
auto_fetch_token: true
|
||||
auto_refresh_token: false
|
||||
}
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
vars {
|
||||
host: http://localhost:8080
|
||||
bearer_auth_token: your_secret_token
|
||||
basic_auth_password: della
|
||||
client_id: client_id_1
|
||||
client_secret: client_secret_1
|
||||
password_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/password_credentials/token
|
||||
password_credentials_username: foo
|
||||
password_credentials_password: bar
|
||||
password_credentials_scope:
|
||||
authorization_code_authorize_url: http://localhost:8081/api/auth/oauth2/authorization_code/authorize
|
||||
authorization_code_callback_url: http://localhost:8081/api/auth/oauth2/authorization_code/callback
|
||||
authorization_code_access_token_url: http://localhost:8081/api/auth/oauth2/authorization_code/token
|
||||
authorization_code_access_token: null
|
||||
client_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/client_credentials/token
|
||||
client_credentials_client_id: client_id_1
|
||||
client_credentials_client_secret: client_secret_1
|
||||
client_credentials_scope: admin
|
||||
client_credentials_access_token: 870132a2ed28a3c94d34f868e6514720
|
||||
key-host: http://localhost:8080
|
||||
key-host-1: http://localhost:8082
|
||||
}
|
||||
vars:secret [
|
||||
client_secret
|
||||
]
|
||||
|
||||
@@ -11,5 +11,5 @@ get {
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{$oauth2.keycloak.access_token}}
|
||||
token: {{$oauth2.credentials.access_token}}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ get {
|
||||
auth:oauth2 {
|
||||
grant_type: client_credentials
|
||||
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
|
||||
refresh_token_url:
|
||||
client_id: account
|
||||
client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
|
||||
client_secret: {{client_secret}}a
|
||||
scope: openid
|
||||
credentials_placement: body
|
||||
credentials_id: credentials
|
||||
token_placement: header
|
||||
token_header_prefix: Bearer
|
||||
reuse_token:
|
||||
auto_fetch_token: true
|
||||
auto_refresh_token: false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "keycloak-password-credentials",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
auth {
|
||||
mode: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: password
|
||||
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
|
||||
refresh_token_url:
|
||||
username: bruno
|
||||
password: bruno
|
||||
client_id: account
|
||||
client_secret: {{client_secret}}
|
||||
scope: openid
|
||||
credentials_placement: body
|
||||
credentials_id: credentials
|
||||
token_placement: header
|
||||
token_header_prefix: Bearer
|
||||
auto_fetch_token: true
|
||||
auto_refresh_token: false
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
vars {
|
||||
key-host: http://localhost:8080
|
||||
}
|
||||
vars:secret [
|
||||
client_secret
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: user_info_coll-auth
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: user_info_custom
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{$oauth2.credentials.access_token}}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
meta {
|
||||
name: user_info_request-auth
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: password
|
||||
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
|
||||
refresh_token_url:
|
||||
username: admin
|
||||
password: admin
|
||||
client_id: account
|
||||
client_secret: {{client_secret}}
|
||||
scope: openid
|
||||
credentials_placement: body
|
||||
credentials_id: credentials
|
||||
token_placement: header
|
||||
token_header_prefix: Bearer
|
||||
auto_fetch_token: true
|
||||
auto_refresh_token: false
|
||||
}
|
||||
@@ -74,6 +74,7 @@ async function setup() {
|
||||
execCommand('npm run build:graphql-docs', 'Building graphql-docs');
|
||||
execCommand('npm run build:bruno-query', 'Building bruno-query');
|
||||
execCommand('npm run build:bruno-common', 'Building bruno-common');
|
||||
execCommand('npm run build:bruno-converters', 'Building bruno-converters');
|
||||
|
||||
// Bundle JS sandbox libraries
|
||||
execCommand(
|
||||
|
||||
Reference in New Issue
Block a user