mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 09:28:33 +00:00
Compare commits
1 Commits
v2.2.0
...
feature/pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5af8034c74 |
4
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
4
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
@@ -27,9 +27,7 @@ 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
|
||||
|
||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@@ -28,8 +28,6 @@ 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
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
|
||||
# tests
|
||||
- name: Test Package bruno-js
|
||||
@@ -47,8 +45,6 @@ jobs:
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-common
|
||||
run: npm run test --workspace=packages/bruno-common
|
||||
- name: Test Package bruno-converters
|
||||
run: npm run test --workspace=packages/bruno-converters
|
||||
- name: Test Package bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron
|
||||
|
||||
@@ -75,8 +71,6 @@ 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
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
|
||||
@@ -37,15 +37,15 @@ Libraries we use
|
||||
- Filesystem Watcher - chokidar
|
||||
- i18n - i18next
|
||||
|
||||
> [!IMPORTANT]
|
||||
> You would need [Node v22.x or the latest LTS version](https://nodejs.org/en/). We use npm workspaces in the project
|
||||
### 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
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
## Install Dependencies
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# use nodejs 20 version
|
||||
@@ -53,11 +53,7 @@ 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
|
||||
@@ -66,23 +62,13 @@ npm run build:bruno-common
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
|
||||
# run react app (terminal 1)
|
||||
# run next 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.
|
||||
|
||||
@@ -21,7 +21,7 @@ Bibliotheken die wir benutzen
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
Du benötigst [Node v22.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
|
||||
Du benötigst [Node v20.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
|
||||
|
||||
### Lass uns coden
|
||||
|
||||
@@ -42,12 +42,12 @@ Bruno wird als Desktop-Anwendung entwickelt. Um die App zu starten, musst Du zue
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- NodeJS v22
|
||||
- NodeJS v18
|
||||
|
||||
### Lokales Entwickeln
|
||||
|
||||
```bash
|
||||
# use nodejs 22 version
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
|
||||
800
package-lock.json
generated
800
package-lock.json
generated
@@ -10,27 +10,23 @@
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-common",
|
||||
"packages/bruno-converters",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-tests",
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs",
|
||||
"packages/bruno-requests"
|
||||
"packages/bruno-graphql-docs"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"concurrently": "^8.2.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
@@ -54,6 +50,7 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@@ -1474,6 +1471,7 @@
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
|
||||
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
@@ -1504,6 +1502,7 @@
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -1521,6 +1520,7 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
@@ -1800,6 +1800,7 @@
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
|
||||
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.25.9",
|
||||
@@ -6342,24 +6343,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -7784,6 +7767,7 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
@@ -7792,20 +7776,11 @@
|
||||
"integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash-es": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "12.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
|
||||
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "*",
|
||||
@@ -7816,6 +7791,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
@@ -7961,10 +7937,6 @@
|
||||
"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",
|
||||
@@ -7997,10 +7969,6 @@
|
||||
"resolved": "packages/bruno-query",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@usebruno/requests": {
|
||||
"resolved": "packages/bruno-requests",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@usebruno/schema": {
|
||||
"resolved": "packages/bruno-schema",
|
||||
"link": true
|
||||
@@ -11060,6 +11028,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
@@ -12670,6 +12639,7 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -13637,6 +13607,7 @@
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -16536,9 +16507,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-typed": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.1.tgz",
|
||||
"integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==",
|
||||
"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==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
@@ -24196,7 +24167,7 @@
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -25213,7 +25184,6 @@
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "^1.8.3",
|
||||
@@ -26294,729 +26264,25 @@
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-typescript": "^7.27.0",
|
||||
"@faker-js/faker": "^9.7.0",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-jest": "^29.7.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"playwright": "^1.52.0",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/compat-data": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
|
||||
"integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/generator": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
|
||||
"integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz",
|
||||
"integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.26.8",
|
||||
"@babel/helper-validator-option": "^7.25.9",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz",
|
||||
"integrity": "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.25.9",
|
||||
"@babel/helper-member-expression-to-functions": "^7.25.9",
|
||||
"@babel/helper-optimise-call-expression": "^7.25.9",
|
||||
"@babel/helper-replace-supers": "^7.26.5",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
|
||||
"@babel/traverse": "^7.27.0",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/helper-plugin-utils": {
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
|
||||
"integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/helper-replace-supers": {
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz",
|
||||
"integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.25.9",
|
||||
"@babel/helper-optimise-call-expression": "^7.25.9",
|
||||
"@babel/traverse": "^7.26.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/parser": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
|
||||
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/plugin-transform-async-generator-functions": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz",
|
||||
"integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.26.5",
|
||||
"@babel/helper-remap-async-to-generator": "^7.25.9",
|
||||
"@babel/traverse": "^7.26.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/plugin-transform-block-scoped-functions": {
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz",
|
||||
"integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.26.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/plugin-transform-for-of": {
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz",
|
||||
"integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.26.5",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
|
||||
"version": "7.26.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz",
|
||||
"integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.26.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/plugin-transform-template-literals": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz",
|
||||
"integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.26.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/plugin-transform-typeof-symbol": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.0.tgz",
|
||||
"integrity": "sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.26.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/plugin-transform-typescript": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.0.tgz",
|
||||
"integrity": "sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.25.9",
|
||||
"@babel/helper-create-class-features-plugin": "^7.27.0",
|
||||
"@babel/helper-plugin-utils": "^7.26.5",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
|
||||
"@babel/plugin-syntax-typescript": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/preset-env": {
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz",
|
||||
"integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.26.8",
|
||||
"@babel/helper-compilation-targets": "^7.26.5",
|
||||
"@babel/helper-plugin-utils": "^7.26.5",
|
||||
"@babel/helper-validator-option": "^7.25.9",
|
||||
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9",
|
||||
"@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9",
|
||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9",
|
||||
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9",
|
||||
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9",
|
||||
"@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
|
||||
"@babel/plugin-syntax-import-assertions": "^7.26.0",
|
||||
"@babel/plugin-syntax-import-attributes": "^7.26.0",
|
||||
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
|
||||
"@babel/plugin-transform-arrow-functions": "^7.25.9",
|
||||
"@babel/plugin-transform-async-generator-functions": "^7.26.8",
|
||||
"@babel/plugin-transform-async-to-generator": "^7.25.9",
|
||||
"@babel/plugin-transform-block-scoped-functions": "^7.26.5",
|
||||
"@babel/plugin-transform-block-scoping": "^7.25.9",
|
||||
"@babel/plugin-transform-class-properties": "^7.25.9",
|
||||
"@babel/plugin-transform-class-static-block": "^7.26.0",
|
||||
"@babel/plugin-transform-classes": "^7.25.9",
|
||||
"@babel/plugin-transform-computed-properties": "^7.25.9",
|
||||
"@babel/plugin-transform-destructuring": "^7.25.9",
|
||||
"@babel/plugin-transform-dotall-regex": "^7.25.9",
|
||||
"@babel/plugin-transform-duplicate-keys": "^7.25.9",
|
||||
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9",
|
||||
"@babel/plugin-transform-dynamic-import": "^7.25.9",
|
||||
"@babel/plugin-transform-exponentiation-operator": "^7.26.3",
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.25.9",
|
||||
"@babel/plugin-transform-for-of": "^7.26.9",
|
||||
"@babel/plugin-transform-function-name": "^7.25.9",
|
||||
"@babel/plugin-transform-json-strings": "^7.25.9",
|
||||
"@babel/plugin-transform-literals": "^7.25.9",
|
||||
"@babel/plugin-transform-logical-assignment-operators": "^7.25.9",
|
||||
"@babel/plugin-transform-member-expression-literals": "^7.25.9",
|
||||
"@babel/plugin-transform-modules-amd": "^7.25.9",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
|
||||
"@babel/plugin-transform-modules-systemjs": "^7.25.9",
|
||||
"@babel/plugin-transform-modules-umd": "^7.25.9",
|
||||
"@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9",
|
||||
"@babel/plugin-transform-new-target": "^7.25.9",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6",
|
||||
"@babel/plugin-transform-numeric-separator": "^7.25.9",
|
||||
"@babel/plugin-transform-object-rest-spread": "^7.25.9",
|
||||
"@babel/plugin-transform-object-super": "^7.25.9",
|
||||
"@babel/plugin-transform-optional-catch-binding": "^7.25.9",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.25.9",
|
||||
"@babel/plugin-transform-parameters": "^7.25.9",
|
||||
"@babel/plugin-transform-private-methods": "^7.25.9",
|
||||
"@babel/plugin-transform-private-property-in-object": "^7.25.9",
|
||||
"@babel/plugin-transform-property-literals": "^7.25.9",
|
||||
"@babel/plugin-transform-regenerator": "^7.25.9",
|
||||
"@babel/plugin-transform-regexp-modifiers": "^7.26.0",
|
||||
"@babel/plugin-transform-reserved-words": "^7.25.9",
|
||||
"@babel/plugin-transform-shorthand-properties": "^7.25.9",
|
||||
"@babel/plugin-transform-spread": "^7.25.9",
|
||||
"@babel/plugin-transform-sticky-regex": "^7.25.9",
|
||||
"@babel/plugin-transform-template-literals": "^7.26.8",
|
||||
"@babel/plugin-transform-typeof-symbol": "^7.26.7",
|
||||
"@babel/plugin-transform-unicode-escapes": "^7.25.9",
|
||||
"@babel/plugin-transform-unicode-property-regex": "^7.25.9",
|
||||
"@babel/plugin-transform-unicode-regex": "^7.25.9",
|
||||
"@babel/plugin-transform-unicode-sets-regex": "^7.25.9",
|
||||
"@babel/preset-modules": "0.1.6-no-external-plugins",
|
||||
"babel-plugin-polyfill-corejs2": "^0.4.10",
|
||||
"babel-plugin-polyfill-corejs3": "^0.11.0",
|
||||
"babel-plugin-polyfill-regenerator": "^0.6.1",
|
||||
"core-js-compat": "^3.40.0",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/preset-typescript": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.0.tgz",
|
||||
"integrity": "sha512-vxaPFfJtHhgeOVXRKuHpHPAOgymmy8V8I65T1q53R7GCZlefKeCaTyDs3zOPHTTbmquvNlQYC5klEvWsBAtrBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.26.5",
|
||||
"@babel/helper-validator-option": "^7.25.9",
|
||||
"@babel/plugin-syntax-jsx": "^7.25.9",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
|
||||
"@babel/plugin-transform-typescript": "^7.27.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/template": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
|
||||
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/types": "^7.27.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/traverse": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
|
||||
"integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/generator": "^7.27.0",
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/template": "^7.27.0",
|
||||
"@babel/types": "^7.27.0",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@babel/types": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
|
||||
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@faker-js/faker": {
|
||||
"version": "9.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.7.0.tgz",
|
||||
"integrity": "sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/@rollup/plugin-typescript": {
|
||||
"version": "12.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.2.tgz",
|
||||
"integrity": "sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.1.0",
|
||||
"resolve": "^1.22.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.14.0||^3.0.0||^4.0.0",
|
||||
"tslib": "*",
|
||||
"typescript": ">=3.7.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
},
|
||||
"tslib": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/babel-plugin-polyfill-corejs3": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz",
|
||||
"integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.3",
|
||||
"core-js-compat": "^3.40.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/browserslist": {
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
||||
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001688",
|
||||
"electron-to-chromium": "^1.5.73",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/core-js-compat": {
|
||||
"version": "3.41.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
|
||||
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.24.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-common/node_modules/playwright": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
|
||||
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.52.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/playwright-core": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
|
||||
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"@usebruno/node-machine-id": "^2.0.0",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
"@usebruno/schema": "0.7.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"about-window": "^1.15.2",
|
||||
@@ -27520,23 +26786,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -28291,21 +27540,6 @@
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-requests": {
|
||||
"name": "@usebruno/requests",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"rollup": "3.29.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-schema": {
|
||||
"name": "@usebruno/schema",
|
||||
"version": "0.7.0",
|
||||
|
||||
17
package.json
17
package.json
@@ -6,15 +6,13 @@
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-common",
|
||||
"packages/bruno-converters",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-tests",
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs",
|
||||
"packages/bruno-requests"
|
||||
"packages/bruno-graphql-docs"
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"devDependencies": {
|
||||
@@ -22,12 +20,10 @@
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"concurrently": "^8.2.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
@@ -35,7 +31,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
|
||||
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
@@ -43,8 +38,6 @@
|
||||
"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-requests": "npm run build --workspace=packages/bruno-requests",
|
||||
"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,7 +47,6 @@
|
||||
"build:electron:deb": "./scripts/build-electron.sh deb",
|
||||
"build:electron:rpm": "./scripts/build-electron.sh rpm",
|
||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||
"watch:common": "npm run watch --workspace=packages/bruno-common",
|
||||
"test:codegen": "npm run dev:web & node ./scripts/playwright-codegen.js",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report",
|
||||
@@ -62,11 +54,6 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5",
|
||||
"electron-store": {
|
||||
"conf": {
|
||||
"json-schema-typed": "8.0.1"
|
||||
}
|
||||
}
|
||||
"rollup": "3.29.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,13 +102,6 @@ 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,7 +35,6 @@ if (!SERVER_RENDERED) {
|
||||
'res.getHeader(name)',
|
||||
'res.getHeaders()',
|
||||
'res.getBody()',
|
||||
'res.setBody(data)',
|
||||
'res.getResponseTime()',
|
||||
'req',
|
||||
'req.url',
|
||||
|
||||
@@ -28,10 +28,7 @@ const ImportEnvironment = ({ collection, onClose }) => {
|
||||
.then(() => {
|
||||
toast.success('Environment imported successfully');
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error('An error occurred while importing the environment');
|
||||
console.error(error);
|
||||
});
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
@@ -34,10 +34,7 @@ const ImportEnvironment = ({ onClose }) => {
|
||||
.then(() => {
|
||||
toast.success('Global Environment imported successfully');
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error('An error occurred while importing the environment');
|
||||
console.error(error);
|
||||
});
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
@@ -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" strokeWidth="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" stroke-width="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,6 +239,17 @@ 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" />
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useDispatch } from "react-redux";
|
||||
import toast from 'react-hot-toast';
|
||||
import { cloneDeep, find } from 'lodash';
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
import { interpolate } from '@usebruno/common';
|
||||
import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
import { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { getAllVariables } from "utils/collections/index";
|
||||
|
||||
@@ -34,14 +35,14 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
toast.success('token fetched successfully!');
|
||||
}
|
||||
else {
|
||||
toast.error('An error occurred while fetching token!');
|
||||
toast.error('An error occured while fetching token!');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('could not fetch the token!');
|
||||
console.error(error);
|
||||
toggleFetchingToken(false);
|
||||
toast.error('An error occurred while fetching token!');
|
||||
toast.error('An error occured while fetching token!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,13 +58,13 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
toast.success('token refreshed successfully!');
|
||||
}
|
||||
else {
|
||||
toast.error('An error occurred while refreshing token!');
|
||||
toast.error('An error occured while refreshing token!');
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
toggleRefreshingToken(false);
|
||||
toast.error('An error occurred while refreshing token!');
|
||||
toast.error('An error occured while refreshing token!');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import StyledWrapper from "./StyledWrapper";
|
||||
import { useState, useEffect } from "react";
|
||||
import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
import { interpolate } from '@usebruno/common';
|
||||
import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
|
||||
const TokenSection = ({ title, token }) => {
|
||||
if (!token) return null;
|
||||
|
||||
@@ -242,6 +242,17 @@ 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" />
|
||||
|
||||
@@ -25,7 +25,6 @@ import { produce } from 'immer';
|
||||
import CollectionOverview from 'components/CollectionSettings/Overview';
|
||||
import RequestNotLoaded from './RequestNotLoaded';
|
||||
import RequestIsLoading from './RequestIsLoading';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 350;
|
||||
@@ -164,14 +163,6 @@ const RequestTabPanel = () => {
|
||||
|
||||
if (focusedTab.type === 'folder-settings') {
|
||||
const folder = findItemInCollection(collection, focusedTab.folderUid);
|
||||
if (!folder) {
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: [activeTabUid]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return <FolderSettings collection={collection} folder={folder} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
padding-top: 20%;
|
||||
width: 100%;
|
||||
.send-icon {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseSendIcon};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconCircleOff } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const SkippedRequest = () => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="send-icon flex justify-center" style={{ fontSize: 200 }}>
|
||||
<IconCircleOff size={150} strokeWidth={1} />
|
||||
</div>
|
||||
<div className="flex mt-4 justify-center" style={{ fontSize: 25 }}>
|
||||
Request skipped
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkippedRequest;
|
||||
@@ -12,10 +12,6 @@ const StyledWrapper = styled.div`
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.skipped-request {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -2,17 +2,9 @@ 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((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"/>}
|
||||
</>;
|
||||
})}
|
||||
{logs.map((entry, index) => (
|
||||
<NetworkLogsEntry key={index} entry={entry} />
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@ import ScriptErrorIcon from './ScriptErrorIcon';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseSave from 'src/components/ResponsePane/ResponseSave';
|
||||
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
|
||||
import SkippedRequest from './SkippedRequest';
|
||||
import ClearTimeline from './ClearTimeline/index';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
@@ -81,14 +80,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (item.response && item.status === 'skipped') {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<SkippedRequest />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading && !item.response) {
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
@@ -150,7 +141,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
)}
|
||||
{focusedTab?.responsePaneTab === "timeline" ? (
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
) : (item?.response && !item?.response?.error) ? (
|
||||
) : item?.response ? (
|
||||
<>
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
|
||||
@@ -33,10 +33,6 @@ const StyledWrapper = styled.div`
|
||||
.all-tests-passed {
|
||||
color: ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
|
||||
.skipped-request {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -10,7 +10,6 @@ import ResponseSize from 'components/ResponsePane/ResponseSize';
|
||||
import TestResults from 'components/ResponsePane/TestResults';
|
||||
import TestResultsLabel from 'components/ResponsePane/TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import SkippedRequest from 'components/ResponsePane/SkippedRequest';
|
||||
import RunnerTimeline from 'components/ResponsePane/RunnerTimeline';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
@@ -64,14 +63,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
});
|
||||
};
|
||||
|
||||
if (item.status === 'skipped') {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<SkippedRequest />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center px-3 tabs" role="tablist">
|
||||
|
||||
@@ -39,10 +39,6 @@ const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
.skipped-request {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { get, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
@@ -102,9 +102,6 @@ export default function RunnerResults({ collection }) {
|
||||
return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
|
||||
});
|
||||
|
||||
const skippedRequests = items.filter((item) => {
|
||||
return item.status === 'skipped';
|
||||
});
|
||||
let isCollectionLoading = areItemsLoading(collection);
|
||||
|
||||
if (!items || !items.length) {
|
||||
@@ -162,8 +159,7 @@ export default function RunnerResults({ collection }) {
|
||||
ref={runnerBodyRef}
|
||||
>
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}, Skipped:{' '}
|
||||
{skippedRequests.length}
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
|
||||
</div>
|
||||
{runnerInfo?.statusText ?
|
||||
<div className="pb-2 font-medium danger">
|
||||
@@ -176,18 +172,14 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{item.testStatus === 'pass' && item.assertionStatus === 'pass' ?
|
||||
{item.status !== 'error' && item.testStatus === 'pass' && item.status !== 'skipped' ? (
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{item.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
{item.status === 'error' || item.testStatus === 'fail' || item.assertionStatus === 'fail' ?
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : item.status === 'error' || item.testStatus === 'fail' || item.assertionStatus === 'fail' ? 'danger' : ''}`}
|
||||
className={`mr-1 ml-2 ${item.status == 'error' || item.status == 'skipped' || item.testStatus == 'fail' ? 'danger' : ''}`}
|
||||
>
|
||||
{item.displayName}
|
||||
</span>
|
||||
@@ -271,15 +263,11 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="flex items-center px-3 mb-4 font-medium">
|
||||
<span className="mr-2">{selectedItem.displayName}</span>
|
||||
<span>
|
||||
{selectedItem.testStatus === 'pass' && selectedItem.assertionStatus === 'pass' ?
|
||||
{selectedItem.testStatus === 'pass' ? (
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'error' || selectedItem.testStatus === 'fail' || selectedItem.assertionStatus === 'fail' ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<ResponsePane item={selectedItem} collection={collection} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
||||
@@ -7,6 +7,14 @@ 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 }) => {
|
||||
@@ -16,9 +24,9 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
};
|
||||
|
||||
const handleImportPostmanCollection = () => {
|
||||
importPostmanCollection()
|
||||
.then(({ collection }) => {
|
||||
handleSubmit({ collection });
|
||||
importPostmanCollection(options)
|
||||
.then(({ collection, translationLog }) => {
|
||||
handleSubmit({ collection, translationLog });
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'));
|
||||
};
|
||||
@@ -38,6 +46,15 @@ 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
|
||||
@@ -60,6 +77,31 @@ 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,8 +4,105 @@ 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 ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
|
||||
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 inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -53,6 +150,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
||||
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,14 +14,18 @@ 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 }) => {
|
||||
const handleImportCollection = ({ collection, translationLog }) => {
|
||||
setImportedCollection(collection);
|
||||
if (translationLog) {
|
||||
setImportedTranslationLog(translationLog);
|
||||
}
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
@@ -71,6 +75,7 @@ const TitleBar = () => {
|
||||
{importCollectionLocationModalOpen ? (
|
||||
<ImportCollectionLocation
|
||||
collectionName={importedCollection.name}
|
||||
translationLog={importedTranslationLog}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ 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);
|
||||
@@ -23,8 +24,11 @@ const Welcome = () => {
|
||||
dispatch(openCollection()).catch((err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR')));
|
||||
};
|
||||
|
||||
const handleImportCollection = ({ collection }) => {
|
||||
const handleImportCollection = ({ collection, translationLog }) => {
|
||||
setImportedCollection(collection);
|
||||
if (translationLog) {
|
||||
setImportedTranslationLog(translationLog);
|
||||
}
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
@@ -51,6 +55,7 @@ const Welcome = () => {
|
||||
) : null}
|
||||
{importCollectionLocationModalOpen ? (
|
||||
<ImportCollectionLocation
|
||||
translationLog={importedTranslationLog}
|
||||
collectionName={importedCollection.name}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
|
||||
@@ -1272,10 +1272,6 @@ 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 })
|
||||
@@ -1299,10 +1295,6 @@ 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 })
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
* LICENSE file at https://github.com/graphql/codemirror-graphql/tree/v0.8.3
|
||||
*/
|
||||
|
||||
import { interpolate } from '@usebruno/common';
|
||||
// Todo: Fix this
|
||||
// import { interpolate } from '@usebruno/common';
|
||||
import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
@@ -76,18 +76,6 @@ if (!SERVER_RENDERED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Filter out errors due to atob/btoa redefinition
|
||||
*
|
||||
* - W079: Redefinition of '{a}'
|
||||
* This JSHint warning triggers when a variable name conflicts with a built-in global.
|
||||
* We filter this for atob/btoa to allow explicit requires in Node.js environments
|
||||
* where these browser functions might not be available.
|
||||
*/
|
||||
if (error.code === 'W079' && (error.a === 'atob' || error.a === 'btoa')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
|
||||
import { uuid } from 'utils/common';
|
||||
import path from 'utils/common/path';
|
||||
import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
|
||||
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
||||
if (!str || !str.length || !isString(str)) {
|
||||
|
||||
@@ -1,9 +1,340 @@
|
||||
import map from 'lodash/map';
|
||||
import * as FileSaver from 'file-saver';
|
||||
import { brunoToPostman } from '@usebruno/converters';
|
||||
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;
|
||||
};
|
||||
|
||||
export const exportCollection = (collection) => {
|
||||
delete collection.uid;
|
||||
delete collection.processEnvVariables;
|
||||
deleteUidsInItems(collection.items);
|
||||
deleteUidsInEnvs(collection.environments);
|
||||
deleteSecretsInEnvs(collection.environments);
|
||||
|
||||
const collectionToExport = brunoToPostman(collection);
|
||||
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 fileName = `${collection.name}.json`;
|
||||
const fileBlob = new Blob([JSON.stringify(collectionToExport, null, 2)], { type: 'application/json' });
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
const { sanitizeUrl, transformUrl } = require('./postman-collection');
|
||||
|
||||
describe('transformUrl', () => {
|
||||
it('should handle basic URL with path variables', () => {
|
||||
const url = 'https://example.com/{{username}}/api/resource/:id';
|
||||
const params = [
|
||||
{ name: 'id', value: '123', type: 'path' },
|
||||
];
|
||||
|
||||
const result = transformUrl(url, params);
|
||||
|
||||
expect(result).toEqual({
|
||||
raw: 'https://example.com/{{username}}/api/resource/:id',
|
||||
protocol: 'https',
|
||||
host: ['example', 'com'],
|
||||
path: ['{{username}}', 'api', 'resource', ':id'],
|
||||
query: [],
|
||||
variable: [
|
||||
{ key: 'id', value: '123' },
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URL with query parameters', () => {
|
||||
const url = 'https://example.com/api/resource?limit=10&offset=20';
|
||||
const params = [
|
||||
{ name: 'limit', value: '10', type: 'query' },
|
||||
{ name: 'offset', value: '20', type: 'query' }
|
||||
];
|
||||
|
||||
const result = transformUrl(url, params);
|
||||
|
||||
expect(result).toEqual({
|
||||
raw: 'https://example.com/api/resource?limit=10&offset=20',
|
||||
protocol: 'https',
|
||||
host: ['example', 'com'],
|
||||
path: ['api', 'resource'],
|
||||
query: [
|
||||
{ key: 'limit', value: '10' },
|
||||
{ key: 'offset', value: '20' }
|
||||
],
|
||||
variable: []
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URL without protocol', () => {
|
||||
const url = 'example.com/api/resource';
|
||||
const params = [];
|
||||
|
||||
const result = transformUrl(url, params);
|
||||
|
||||
expect(result).toEqual({
|
||||
raw: 'example.com/api/resource',
|
||||
protocol: '',
|
||||
host: ['example', 'com'],
|
||||
path: ['api', 'resource'],
|
||||
query: [],
|
||||
variable: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeUrl', () => {
|
||||
it('should replace backslashes with slashes', () => {
|
||||
const input = 'http:\\\\example.com\\path\\to\\file';
|
||||
const expected = 'http://example.com/path/to/file';
|
||||
expect(sanitizeUrl(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should collapse multiple slashes into a single slash', () => {
|
||||
const input = 'http://example.com//path///to////file';
|
||||
const expected = 'http://example.com/path/to/file';
|
||||
expect(sanitizeUrl(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle URLs with mixed slashes', () => {
|
||||
const input = 'http:\\example.com//path\\to//file';
|
||||
const expected = 'http://example.com/path/to/file';
|
||||
expect(sanitizeUrl(input)).toBe(expected);
|
||||
});
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import { uuid, normalizeFileName } from 'utils/common';
|
||||
import { isItemARequest } from 'utils/collections';
|
||||
import { collectionSchema } from '@usebruno/schema';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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 { insomniaToBruno } from '@usebruno/converters';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -27,11 +30,226 @@ 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((collection) => insomniaToBruno(collection))
|
||||
.then(parseInsomniaCollection)
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => resolve({ collection }))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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 { openApiToBruno } from '@usebruno/converters';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -27,11 +30,435 @@ 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((collection) => openApiToBruno(collection))
|
||||
.then(parseOpenApiCollection)
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => resolve({ collection }))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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,28 +1,650 @@
|
||||
import get from 'lodash/get';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { postmanToBruno } from '@usebruno/converters';
|
||||
import { safeParseJSON } from 'utils/common/index';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
import { postmanTranslation } from 'utils/importers/translators/postman_translation';
|
||||
import each from 'lodash/each';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => resolve(safeParseJSON(e.target.result));
|
||||
fileReader.onload = (e) => resolve(e.target.result);
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
const parseGraphQLRequest = (graphqlSource) => {
|
||||
try {
|
||||
let queryResultObject = {
|
||||
query: '',
|
||||
variables: ''
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
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) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then((collection) => postmanToBruno(collection))
|
||||
.then((collection) => resolve({ collection }))
|
||||
.then((str) => parsePostmanCollection(str, options))
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => resolve({ collection, translationLog }))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
translationLog = {};
|
||||
reject(new BrunoError('Import collection failed'));
|
||||
})
|
||||
.then(() => {
|
||||
logTranslationDetails(translationLog);
|
||||
translationLog = {};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,24 +1,60 @@
|
||||
import each from 'lodash/each';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { postmanToBrunoEnvironment } from '@usebruno/converters';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => {
|
||||
try {
|
||||
let parsedPostmanEnvironment = JSON.parse(e.target.result);
|
||||
resolve(parsedPostmanEnvironment);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
reject(new BrunoError('Unable to parse the postman environment json file'));
|
||||
}
|
||||
}
|
||||
fileReader.onload = (e) => resolve(e.target.result);
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
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' })
|
||||
@@ -26,7 +62,7 @@ const importEnvironment = () => {
|
||||
return Promise.all(
|
||||
Object.values(files ?? {}).map((file) =>
|
||||
readFile([file])
|
||||
.then((environment) => postmanToBrunoEnvironment(environment))
|
||||
.then(parsePostmanEnvironment)
|
||||
.catch((err) => {
|
||||
console.error(`Error processing file: ${file.name || 'undefined'}`, err);
|
||||
throw err;
|
||||
|
||||
169
packages/bruno-app/src/utils/importers/translators/index.spec.js
Normal file
169
packages/bruno-app/src/utils/importers/translators/index.spec.js
Normal file
@@ -0,0 +1,169 @@
|
||||
const { postmanTranslation } = require('./postman_translation'); // Adjust path as needed
|
||||
|
||||
describe('postmanTranslation function', () => {
|
||||
test('should translate pm commands correctly', () => {
|
||||
const inputScript = `
|
||||
pm.environment.get('key');
|
||||
pm.environment.set('key', 'value');
|
||||
pm.variables.get('key');
|
||||
pm.variables.set('key', 'value');
|
||||
pm.collectionVariables.get('key');
|
||||
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');
|
||||
bru.setEnvVar('key', 'value');
|
||||
bru.getVar('key');
|
||||
bru.setVar('key', 'value');
|
||||
bru.getVar('key');
|
||||
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);
|
||||
});
|
||||
|
||||
test('should not translate non-pm commands', () => {
|
||||
const inputScript = `
|
||||
console.log('This script does not contain pm commands.');
|
||||
const data = pm.environment.get('key');
|
||||
pm.collectionVariables.set('key', data);
|
||||
`;
|
||||
const expectedOutput = `
|
||||
console.log('This script does not contain pm commands.');
|
||||
const data = bru.getEnvVar('key');
|
||||
bru.setVar('key', data);
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should comment non-translated pm commands', () => {
|
||||
const inputScript = "pm.test('random test', () => postman.variables.replaceIn('{{$guid}}'));";
|
||||
const expectedOutput = "// test('random test', () => postman.variables.replaceIn('{{$guid}}'));";
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
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);
|
||||
});
|
||||
test('should handle comments and other JavaScript code', () => {
|
||||
const inputScript = `
|
||||
// This is a comment
|
||||
const value = 'test';
|
||||
pm.environment.set('key', value);
|
||||
/*
|
||||
Multi-line comment
|
||||
*/
|
||||
const result = pm.environment.get('key');
|
||||
console.log('Result:', result);
|
||||
`;
|
||||
const expectedOutput = `
|
||||
// This is a comment
|
||||
const value = 'test';
|
||||
bru.setEnvVar('key', value);
|
||||
/*
|
||||
Multi-line comment
|
||||
*/
|
||||
const result = bru.getEnvVar('key');
|
||||
console.log('Result:', result);
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle nested commands and edge cases', () => {
|
||||
const inputScript = `
|
||||
const sampleObjects = [
|
||||
{
|
||||
key: pm.environment.get('key'),
|
||||
value: pm.variables.get('value')
|
||||
},
|
||||
{
|
||||
key: pm.collectionVariables.get('key'),
|
||||
value: pm.collectionVariables.get('value')
|
||||
}
|
||||
];
|
||||
const dataTesting = Object.entries(sampleObjects || {}).reduce((acc, [key, value]) => {
|
||||
// this is a comment
|
||||
acc[key] = pm.collectionVariables.get(pm.environment.get(value));
|
||||
return acc; // Return the accumulator
|
||||
}, {});
|
||||
Object.values(dataTesting).forEach((data) => {
|
||||
pm.environment.set(data.key, pm.variables.get(data.value));
|
||||
});
|
||||
`;
|
||||
const expectedOutput = `
|
||||
const sampleObjects = [
|
||||
{
|
||||
key: bru.getEnvVar('key'),
|
||||
value: bru.getVar('value')
|
||||
},
|
||||
{
|
||||
key: bru.getVar('key'),
|
||||
value: bru.getVar('value')
|
||||
}
|
||||
];
|
||||
const dataTesting = Object.entries(sampleObjects || {}).reduce((acc, [key, value]) => {
|
||||
// this is a comment
|
||||
acc[key] = bru.getVar(bru.getEnvVar(value));
|
||||
return acc; // Return the accumulator
|
||||
}, {});
|
||||
Object.values(dataTesting).forEach((data) => {
|
||||
bru.setEnvVar(data.key, bru.getVar(data.value));
|
||||
});
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle test commands', () => {
|
||||
const inputScript = `
|
||||
pm.test('Status code is 200', () => {
|
||||
pm.response.to.have.status(200);
|
||||
});
|
||||
pm.test('this test will fail', () => {
|
||||
return false
|
||||
});
|
||||
`;
|
||||
const expectedOutput = `
|
||||
test('Status code is 200', () => {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
test('this test will fail', () => {
|
||||
return false
|
||||
});
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle response commands', () => {
|
||||
const inputScript = `
|
||||
const responseTime = pm.response.responseTime;
|
||||
const responseCode = pm.response.code;
|
||||
const responseText = pm.response.text();
|
||||
`;
|
||||
const expectedOutput = `
|
||||
const responseTime = res.getResponseTime();
|
||||
const responseCode = res.getStatus();
|
||||
const responseText = res.getBody()?.toString();
|
||||
`;
|
||||
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);
|
||||
});
|
||||
@@ -15,17 +15,11 @@ const replacements = {
|
||||
'pm\\.expect\\(': 'expect(',
|
||||
'pm\\.environment\\.has\\(([^)]+)\\)': 'bru.getEnvVar($1) !== undefined && bru.getEnvVar($1) !== null',
|
||||
'pm\\.response\\.code': 'res.getStatus()',
|
||||
'pm\\.response\\.text\\(\\)': 'JSON.stringify(res.getBody())',
|
||||
'pm\\.response\\.text\\(': 'res.getBody()?.toString(',
|
||||
'pm\\.expect\\.fail\\(': 'expect.fail(',
|
||||
'pm\\.response\\.responseTime': 'res.getResponseTime()',
|
||||
'pm\\.environment\\.name': 'bru.getEnvName()',
|
||||
'pm\\.response\\.status': 'res.statusText',
|
||||
'pm\\.response\\.headers': 'req.getHeaders()',
|
||||
"tests\\['([^']+)'\\]\\s*=\\s*([^;]+);": 'test("$1", function() { expect(Boolean($2)).to.be.true; });',
|
||||
'pm\\.request\\.url': 'req.getUrl()',
|
||||
'pm\\.request\\.method': 'req.getMethod()',
|
||||
'pm\\.request\\.headers': 'req.getHeaders()',
|
||||
'pm\\.request\\.body': 'req.getBody()',
|
||||
// deprecated translations
|
||||
'postman\\.setEnvironmentVariable\\(': 'bru.setEnvVar(',
|
||||
'postman\\.getEnvironmentVariable\\(': 'bru.getEnvVar(',
|
||||
@@ -48,7 +42,7 @@ const compiledReplacements = Object.entries(extendedReplacements).map(([pattern,
|
||||
replacement
|
||||
}));
|
||||
|
||||
const postmanTranslation = (script) => {
|
||||
export const postmanTranslation = (script, logCallback) => {
|
||||
try {
|
||||
let modifiedScript = script;
|
||||
let modified = false;
|
||||
@@ -60,11 +54,10 @@ const postmanTranslation = (script) => {
|
||||
}
|
||||
if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) {
|
||||
modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1');
|
||||
//logCallback?.();
|
||||
}
|
||||
return modifiedScript;
|
||||
} catch (e) {
|
||||
return script;
|
||||
}
|
||||
};
|
||||
|
||||
export default postmanTranslation;
|
||||
@@ -5,10 +5,6 @@ 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 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import trim from 'lodash/trim';
|
||||
import each from 'lodash/each';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
|
||||
import { interpolate } from '@usebruno/common';
|
||||
import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
|
||||
const hasLength = (str) => {
|
||||
if (!str || !str.length) {
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "^1.8.3",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
# 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:
|
||||
@@ -58,30 +56,6 @@ 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:
|
||||
|
||||
@@ -2,7 +2,6 @@ const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
const path = require('path');
|
||||
const { forOwn, cloneDeep } = require('lodash');
|
||||
const { getRunnerSummary } = require('@usebruno/common/runner');
|
||||
const { exists, isFile, isDirectory } = require('../utils/filesystem');
|
||||
const { runSingleRequest } = require('../runner/run-single-request');
|
||||
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
|
||||
@@ -17,18 +16,50 @@ const command = 'run [filename]';
|
||||
const desc = 'Run a request';
|
||||
|
||||
const printRunSummary = (results) => {
|
||||
const {
|
||||
totalRequests,
|
||||
passedRequests,
|
||||
failedRequests,
|
||||
skippedRequests,
|
||||
totalAssertions,
|
||||
passedAssertions,
|
||||
failedAssertions,
|
||||
totalTests,
|
||||
passedTests,
|
||||
failedTests
|
||||
} = getRunnerSummary(results);
|
||||
let totalRequests = 0;
|
||||
let passedRequests = 0;
|
||||
let failedRequests = 0;
|
||||
let skippedRequests = 0;
|
||||
let totalAssertions = 0;
|
||||
let passedAssertions = 0;
|
||||
let failedAssertions = 0;
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
|
||||
for (const result of results) {
|
||||
totalRequests += 1;
|
||||
totalTests += result.testResults.length;
|
||||
totalAssertions += result.assertionResults.length;
|
||||
let anyFailed = false;
|
||||
let hasAnyTestsOrAssertions = false;
|
||||
for (const testResult of result.testResults) {
|
||||
hasAnyTestsOrAssertions = true;
|
||||
if (testResult.status === 'pass') {
|
||||
passedTests += 1;
|
||||
} else {
|
||||
anyFailed = true;
|
||||
failedTests += 1;
|
||||
}
|
||||
}
|
||||
for (const assertionResult of result.assertionResults) {
|
||||
hasAnyTestsOrAssertions = true;
|
||||
if (assertionResult.status === 'pass') {
|
||||
passedAssertions += 1;
|
||||
} else {
|
||||
anyFailed = true;
|
||||
failedAssertions += 1;
|
||||
}
|
||||
}
|
||||
if (!hasAnyTestsOrAssertions && result.skipped) {
|
||||
skippedRequests += 1;
|
||||
}
|
||||
else if (!hasAnyTestsOrAssertions && result.error) {
|
||||
failedRequests += 1;
|
||||
} else {
|
||||
passedRequests += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const maxLength = 12;
|
||||
|
||||
@@ -68,7 +99,7 @@ const printRunSummary = (results) => {
|
||||
totalTests,
|
||||
passedTests,
|
||||
failedTests
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const createCollectionFromPath = (collectionPath) => {
|
||||
@@ -146,7 +177,7 @@ const getBruFilesRecursively = (dir, testsOnly) => {
|
||||
|
||||
for (const file of filesInCurrentDir) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
const stats = fs.lstatSync(filePath);
|
||||
|
||||
// todo: we might need a ignore config inside bruno.json
|
||||
if (
|
||||
@@ -715,7 +746,7 @@ const handler = async function (argv) {
|
||||
|
||||
// bail if option is set and there is a failure
|
||||
if (bail) {
|
||||
const requestFailure = result?.error && !result?.skipped;
|
||||
const requestFailure = result?.error;
|
||||
const testFailure = result?.testResults?.find((iter) => iter.status === 'fail');
|
||||
const assertionFailure = result?.assertionResults?.find((iter) => iter.status === 'fail');
|
||||
if (requestFailure || testFailure || assertionFailure) {
|
||||
|
||||
@@ -32,7 +32,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
});
|
||||
});
|
||||
|
||||
const _interpolate = (str, { escapeJSONStrings } = {}) => {
|
||||
const _interpolate = (str) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
}
|
||||
};
|
||||
|
||||
return interpolate(str, combinedVars, { escapeJSONStrings });
|
||||
return interpolate(str, combinedVars);
|
||||
};
|
||||
|
||||
request.url = _interpolate(request.url);
|
||||
@@ -67,14 +67,14 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
if (typeof request.data === 'object') {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
parsed = _interpolate(parsed, { escapeJSONStrings: true });
|
||||
parsed = _interpolate(parsed);
|
||||
request.data = JSON.parse(parsed);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
if (typeof request.data === 'string') {
|
||||
if (request?.data?.length) {
|
||||
request.data = _interpolate(request.data, { escapeJSONStrings: true });
|
||||
request.data = _interpolate(request.data);
|
||||
}
|
||||
}
|
||||
} else if (contentType === 'application/x-www-form-urlencoded') {
|
||||
|
||||
@@ -65,13 +65,6 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (collectionAuth.mode === 'digest') {
|
||||
axiosRequest.digestConfig = {
|
||||
username: get(collectionAuth, 'digest.username'),
|
||||
password: get(collectionAuth, 'digest.password')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (request.auth && request.auth.mode !== 'inherit') {
|
||||
@@ -122,13 +115,6 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
'X-WSSE'
|
||||
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'digest') {
|
||||
axiosRequest.digestConfig = {
|
||||
username: get(request, 'auth.digest.username'),
|
||||
password: get(request, 'auth.digest.password')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
request.body = request.body || {};
|
||||
|
||||
@@ -24,7 +24,7 @@ const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../uti
|
||||
const { createFormData } = require('../utils/form-data');
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { addDigestInterceptor } = require('@usebruno/requests');
|
||||
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
@@ -333,11 +333,6 @@ const runSingleRequest = async function (
|
||||
delete request.awsv4config;
|
||||
}
|
||||
|
||||
if (request.digestConfig) {
|
||||
addDigestInterceptor(axiosInstance, request);
|
||||
delete request.digestConfig;
|
||||
}
|
||||
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const isFile = (filepath) => {
|
||||
|
||||
const isDirectory = (dirPath) => {
|
||||
try {
|
||||
return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
|
||||
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
67
packages/bruno-cli/tests/commands/run.spec.js
Normal file
67
packages/bruno-cli/tests/commands/run.spec.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
const { printRunSummary } = require('../../src/commands/run');
|
||||
|
||||
describe('printRunSummary', () => {
|
||||
// Suppress console.log output
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
it('should produce the correct summary for a successful run', () => {
|
||||
const results = [
|
||||
{
|
||||
testResults: [{ status: 'pass' }, { status: 'pass' }, { status: 'pass' }],
|
||||
assertionResults: [{ status: 'pass' }, { status: 'pass' }],
|
||||
error: null
|
||||
},
|
||||
{
|
||||
testResults: [{ status: 'pass' }, { status: 'pass' }],
|
||||
assertionResults: [{ status: 'pass' }, { status: 'pass' }, { status: 'pass' }],
|
||||
error: null
|
||||
}
|
||||
];
|
||||
|
||||
const summary = printRunSummary(results);
|
||||
|
||||
expect(summary.totalRequests).toBe(2);
|
||||
expect(summary.passedRequests).toBe(2);
|
||||
expect(summary.failedRequests).toBe(0);
|
||||
expect(summary.totalAssertions).toBe(5);
|
||||
expect(summary.passedAssertions).toBe(5);
|
||||
expect(summary.failedAssertions).toBe(0);
|
||||
expect(summary.totalTests).toBe(5);
|
||||
expect(summary.passedTests).toBe(5);
|
||||
expect(summary.failedTests).toBe(0);
|
||||
});
|
||||
|
||||
it('should produce the correct summary for a failed run', () => {
|
||||
const results = [
|
||||
{
|
||||
testResults: [{ status: 'fail' }, { status: 'pass' }, { status: 'pass' }],
|
||||
assertionResults: [{ status: 'pass' }, { status: 'fail' }],
|
||||
error: null
|
||||
},
|
||||
{
|
||||
testResults: [{ status: 'pass' }, { status: 'fail' }],
|
||||
assertionResults: [{ status: 'pass' }, { status: 'fail' }, { status: 'fail' }],
|
||||
error: null
|
||||
},
|
||||
{
|
||||
testResults: [],
|
||||
assertionResults: [],
|
||||
error: new Error('Request failed')
|
||||
}
|
||||
];
|
||||
|
||||
const summary = printRunSummary(results);
|
||||
|
||||
expect(summary.totalRequests).toBe(3);
|
||||
expect(summary.passedRequests).toBe(2);
|
||||
expect(summary.failedRequests).toBe(1);
|
||||
expect(summary.totalAssertions).toBe(5);
|
||||
expect(summary.passedAssertions).toBe(2);
|
||||
expect(summary.failedAssertions).toBe(3);
|
||||
expect(summary.totalTests).toBe(5);
|
||||
expect(summary.passedTests).toBe(3);
|
||||
expect(summary.failedTests).toBe(2);
|
||||
});
|
||||
});
|
||||
81
packages/bruno-cli/tests/reporters/html.spec.js
Normal file
81
packages/bruno-cli/tests/reporters/html.spec.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
const fs = require('fs');
|
||||
|
||||
const makeHtmlOutput = require('../../src/reporters/html');
|
||||
|
||||
describe('makeHtmlOutput', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should produce an html report', () => {
|
||||
const outputJson = {
|
||||
summary: {
|
||||
totalRequests: 1,
|
||||
passedRequests: 1,
|
||||
failedRequests: 1,
|
||||
totalAssertions: 1,
|
||||
passedAssertions: 1,
|
||||
failedAssertions: 1,
|
||||
totalTests: 1,
|
||||
passedTests: 1,
|
||||
failedTests: 1
|
||||
},
|
||||
results: [
|
||||
{
|
||||
description: 'description provided',
|
||||
suitename: 'Tests/Suite A',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://ima.test'
|
||||
},
|
||||
assertionResults: [
|
||||
{
|
||||
lhsExpr: 'res.status',
|
||||
rhsExpr: 'eq 200',
|
||||
status: 'pass'
|
||||
},
|
||||
{
|
||||
lhsExpr: 'res.status',
|
||||
rhsExpr: 'neq 200',
|
||||
status: 'fail',
|
||||
error: 'expected 200 to not equal 200'
|
||||
}
|
||||
],
|
||||
runtime: 1.2345678
|
||||
},
|
||||
{
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://imanother.test'
|
||||
},
|
||||
suitename: 'Tests/Suite B',
|
||||
testResults: [
|
||||
{
|
||||
lhsExpr: 'res.status',
|
||||
rhsExpr: 'eq 200',
|
||||
description: 'A test that passes',
|
||||
status: 'pass'
|
||||
},
|
||||
{
|
||||
description: 'A test that fails',
|
||||
status: 'fail',
|
||||
error: 'expected 200 to not equal 200',
|
||||
status: 'fail'
|
||||
}
|
||||
],
|
||||
runtime: 2.3456789
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
makeHtmlOutput(outputJson, '/tmp/testfile.html');
|
||||
|
||||
const htmlReport = fs.writeFileSync.mock.calls[0][1];
|
||||
expect(htmlReport).toContain(JSON.stringify(outputJson, null, 2));
|
||||
});
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@babel/preset-env', { modules: 'auto' }],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
};
|
||||
@@ -1,9 +1,5 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
transform: {
|
||||
'^.+\\.(ts|js)$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(lodash-es)/)',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node'
|
||||
};
|
||||
|
||||
@@ -5,18 +5,6 @@
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/cjs/index.js",
|
||||
"import": "./dist/esm/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./runner": {
|
||||
"require": "./dist/runner/cjs/index.js",
|
||||
"import": "./dist/runner/esm/index.js",
|
||||
"types": "./dist/runner/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
@@ -27,28 +15,20 @@
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "rollup -c rollup.config.js",
|
||||
"watch": "rollup -c -w",
|
||||
"build": "rollup -c",
|
||||
"prepack": "npm run test && npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-typescript": "^7.27.0",
|
||||
"@faker-js/faker": "^9.7.0",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-jest": "^29.7.0",
|
||||
"moment": "^2.29.4",
|
||||
"rollup": "3.29.5",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"rollup":"3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"typescript": "^5.8.3"
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5"
|
||||
"rollup":"3.29.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,53 +7,34 @@ const peerDepsExternal = require('rollup-plugin-peer-deps-external');
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
function createBuildConfig({ inputDir, input, cjsOutput, esmOutput }) {
|
||||
return [
|
||||
{
|
||||
input,
|
||||
output: [
|
||||
{
|
||||
file: cjsOutput,
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
},
|
||||
{
|
||||
file: esmOutput,
|
||||
format: 'esm',
|
||||
sourcemap: true
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
peerDepsExternal(),
|
||||
nodeResolve(),
|
||||
commonjs(),
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
include: [inputDir]
|
||||
}),
|
||||
terser()
|
||||
],
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// todo: configure declarations
|
||||
module.exports = [
|
||||
// Main package build
|
||||
...createBuildConfig({
|
||||
inputDir: 'src/**/*',
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
cjsOutput: packageJson.main,
|
||||
esmOutput: packageJson.module
|
||||
}),
|
||||
// reports/html
|
||||
...createBuildConfig({
|
||||
inputDir: 'src/runner/**/*',
|
||||
input: 'src/runner/index.ts',
|
||||
cjsOutput: 'dist/runner/cjs/index.js',
|
||||
esmOutput: 'dist/runner/esm/index.js'
|
||||
})
|
||||
output: [
|
||||
{
|
||||
file: packageJson.main,
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
},
|
||||
{
|
||||
file: packageJson.module,
|
||||
format: 'esm',
|
||||
sourcemap: true
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
peerDepsExternal(),
|
||||
nodeResolve({
|
||||
extensions: ['.css']
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({ tsconfig: './tsconfig.json' }),
|
||||
terser()
|
||||
]
|
||||
},
|
||||
{
|
||||
input: 'dist/esm/index.d.ts',
|
||||
output: [{ file: 'dist/index.d.ts', format: 'esm' }],
|
||||
plugins: [dts.default()]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export { default as interpolate } from './interpolate';
|
||||
import interpolate from './interpolate';
|
||||
|
||||
export default {
|
||||
interpolate
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import interpolate from './index';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('interpolate', () => {
|
||||
it('should replace placeholders with values from the object', () => {
|
||||
const inputString = 'Hello, my name is {{user.name}} and I am {{user.age}} years old';
|
||||
@@ -41,7 +41,7 @@ describe('interpolate', () => {
|
||||
Hi, I am {{user.full_name}},
|
||||
I am {{user.age}} years old.
|
||||
My favorite food is {{user.fav-food[0]}} and {{user.fav-food[1]}}.
|
||||
I like attention: {{user['want.attention']}}
|
||||
I like attention: {{user.want.attention}}
|
||||
`;
|
||||
const expectedStr = `
|
||||
Hi, I am Bruno,
|
||||
@@ -67,21 +67,19 @@ describe('interpolate', () => {
|
||||
expect(result).toBe('Hello, my name is {{ user.name }} and I am 4 years old');
|
||||
});
|
||||
|
||||
test('should give precedence to the last key in case of duplicates (not at the top level)', () => {
|
||||
const inputString = `Hello, my name is {{data['user.name']}} and {{data.user.name}} I am {{data.user.age}} years old`;
|
||||
it('should give precedence to the last key in case of duplicates', () => {
|
||||
const inputString = 'Hello, my name is {{user.name}} and I am {{user.age}} years old';
|
||||
const inputObject = {
|
||||
data: {
|
||||
'user.name': 'Bruno',
|
||||
user: {
|
||||
name: 'Not _Bruno_',
|
||||
age: 4
|
||||
}
|
||||
'user.name': 'Bruno',
|
||||
user: {
|
||||
name: 'Not Bruno',
|
||||
age: 4
|
||||
}
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('Hello, my name is Bruno and Not _Bruno_ I am 4 years old');
|
||||
expect(result).toBe('Hello, my name is Not Bruno and I am 4 years old');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -240,7 +238,7 @@ describe('interpolate - recursive', () => {
|
||||
Hi, I am {{user.full_name}},
|
||||
I am {{user.age}} years old.
|
||||
My favorite food is {{user.fav-food[0]}} and {{user.fav-food[1]}}.
|
||||
I like attention: {{user['want.attention']}}
|
||||
I like attention: {{user.want.attention}}
|
||||
`;
|
||||
const inputObject = {
|
||||
user: {
|
||||
@@ -356,218 +354,3 @@ describe('interpolate - recursive', () => {
|
||||
}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interpolate - object handling', () => {
|
||||
it('should stringify simple objects', () => {
|
||||
const inputString = 'User: {{user}}';
|
||||
const inputObject = {
|
||||
'user': { name: 'Bruno', age: 4 }
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('User: {"name":"Bruno","age":4}');
|
||||
});
|
||||
|
||||
it('should stringify simple objects (dot notation)', () => {
|
||||
const inputString = 'User: {{user.data}}';
|
||||
const inputObject = {
|
||||
'user.data': { name: 'Bruno', age: 4 }
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('User: {"name":"Bruno","age":4}');
|
||||
});
|
||||
|
||||
it('should stringify nested objects', () => {
|
||||
const inputString = 'User: {{user}}';
|
||||
const inputObject = {
|
||||
'user': {
|
||||
name: 'Bruno',
|
||||
age: 4,
|
||||
preferences: {
|
||||
food: ['egg', 'meat'],
|
||||
toys: { favorite: 'ball' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('User: {"name":"Bruno","age":4,"preferences":{"food":["egg","meat"],"toys":{"favorite":"ball"}}}');
|
||||
});
|
||||
|
||||
it('should stringify arrays', () => {
|
||||
const inputString = 'User favorites: {{favorites}}';
|
||||
const inputObject = {
|
||||
favorites: ['egg', 'meat', 'treats']
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('User favorites: ["egg","meat","treats"]');
|
||||
});
|
||||
|
||||
it('should handle null values correctly', () => {
|
||||
const inputString = 'User: {{user}}';
|
||||
const inputObject = {
|
||||
'user': null
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('User: null');
|
||||
});
|
||||
|
||||
it('should handle objects with nested interpolation', () => {
|
||||
const inputString = 'User: {{user}}';
|
||||
const inputObject = {
|
||||
'user': {
|
||||
name: 'Bruno',
|
||||
message: '{{user.greeting}}'
|
||||
},
|
||||
'user.greeting': 'Hello there!'
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('User: {"name":"Bruno","message":"Hello there!"}');
|
||||
});
|
||||
|
||||
it('should handle objects within arrays', () => {
|
||||
const inputString = 'Items: {{items}}';
|
||||
const inputObject = {
|
||||
'items': [
|
||||
{ id: 1, name: 'Toy' },
|
||||
{ id: 2, name: 'Bone' },
|
||||
{ id: 3, name: 'Ball', colors: ['red', 'blue'] }
|
||||
]
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('Items: [{"id":1,"name":"Toy"},{"id":2,"name":"Bone"},{"id":3,"name":"Ball","colors":["red","blue"]}]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('interpolate - mock variable interpolation', () => {
|
||||
it('should replace mock variables with generated values', () => {
|
||||
const inputString = '{{$randomInt}}, {{$randomIP}}, {{$randomIPV4}}, {{$randomIPV6}}, {{$randomBoolean}}';
|
||||
|
||||
const result = interpolate(inputString, {});
|
||||
|
||||
// Validate the result using regex patterns
|
||||
const randomIntPattern = /^\d+$/;
|
||||
const randomIPPattern = /^([\da-f]{1,4}:){7}[\da-f]{1,4}$|^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
const randomIPV4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
const randomIPV6Pattern = /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/;
|
||||
const randomBooleanPattern = /^(true|false)$/;
|
||||
|
||||
const [randomInt, randomIP, randomIPV4, randomIPV6, randomBoolean] = result.split(', ');
|
||||
|
||||
expect(randomIntPattern.test(randomInt)).toBe(true);
|
||||
expect(randomIPPattern.test(randomIP)).toBe(true);
|
||||
expect(randomIPV4Pattern.test(randomIPV4)).toBe(true);
|
||||
expect(randomIPV6Pattern.test(randomIPV6)).toBe(true);
|
||||
expect(randomBooleanPattern.test(randomBoolean)).toBe(true);
|
||||
});
|
||||
|
||||
it('should leave mock variables unchanged if no corresponding function exists', () => {
|
||||
const inputString = 'Random number: {{$nonExistentMock}}';
|
||||
|
||||
const result = interpolate(inputString, {});
|
||||
|
||||
expect(result).toBe('Random number: {{$nonExistentMock}}');
|
||||
});
|
||||
|
||||
it('should escape special characters in mock variable values and produce valid JSON when escapeJSONStrings is true', () => {
|
||||
const inputString = '{"escapedValue": "{{$randomLoremParagraphs}}"}';
|
||||
|
||||
expect(() => {
|
||||
const result = interpolate(inputString, {}, { escapeJSONStrings: true });
|
||||
JSON.parse(result); // This should not throw an error
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not produce valid JSON when escapeJSONStrings is false', () => {
|
||||
const inputString = '{"escapedValue": "{{$randomLoremParagraphs}}"}';
|
||||
|
||||
expect(() => {
|
||||
const result = interpolate(inputString, {}, { escapeJSONStrings: false });
|
||||
JSON.parse(result); // This should throw an error
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should throw an error when producing invalid JSON regardless of escapeJSONStrings option', () => {
|
||||
const inputString = '{"escapedValue": "{{$randomLoremParagraphs}}"}';
|
||||
|
||||
// Test without providing the options argument
|
||||
expect(() => {
|
||||
const result = interpolate(inputString, {});
|
||||
JSON.parse(result); // This should throw an error
|
||||
}).toThrow();
|
||||
|
||||
// Test with escapeJSONStrings explicitly set to false
|
||||
expect(() => {
|
||||
const result = interpolate(inputString, {}, { escapeJSONStrings: false });
|
||||
JSON.parse(result); // This should throw an error
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('interpolate - Date() handling', () => {
|
||||
it('should interpolate Date() using JSON.stringify', () => {
|
||||
const inputString = 'Date is {{date}}';
|
||||
const inputObject = {
|
||||
date: new Date("2025-04-17T15:33:41.117Z")
|
||||
};
|
||||
|
||||
const jsonStringifiedDate = JSON.stringify(inputObject.date);
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('Date is "2025-04-17T15:33:41.117Z"');
|
||||
expect(result).toBe(`Date is ${jsonStringifiedDate}`);
|
||||
})
|
||||
|
||||
it('should interpolate Date() when its nested in an object', () => {
|
||||
const inputString = 'Date is {{date}}';
|
||||
const inputObject = {
|
||||
date: {
|
||||
now: new Date("2025-04-17T15:33:41.117Z")
|
||||
}
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('Date is {"now":"2025-04-17T15:33:41.117Z"}');
|
||||
})
|
||||
});
|
||||
|
||||
describe('interpolate - moment() handling', () => {
|
||||
it('should interpolate moment() using JSON.stringify', () => {
|
||||
const inputString = 'Date is {{date}}';
|
||||
const inputObject = {
|
||||
date: moment("2025-04-17T15:33:41.117Z")
|
||||
};
|
||||
|
||||
const jsonStringifiedDate = JSON.stringify(inputObject.date);
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('Date is "2025-04-17T15:33:41.117Z"');
|
||||
expect(result).toBe(`Date is ${jsonStringifiedDate}`);
|
||||
})
|
||||
|
||||
it('should interpolate moment() when its nested in an object', () => {
|
||||
const inputString = 'Date is {{date}}';
|
||||
const inputObject = {
|
||||
date: {
|
||||
now: moment("2025-04-17T15:33:41.117Z")
|
||||
}
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('Date is {"now":"2025-04-17T15:33:41.117Z"}');
|
||||
})
|
||||
})
|
||||
@@ -11,51 +11,22 @@
|
||||
* Output: Hello, my name is Bruno and I am 4 years old
|
||||
*/
|
||||
|
||||
import { mockDataFunctions } from '../utils/faker-functions';
|
||||
import { get } from "lodash-es";
|
||||
import { Set } from 'typescript';
|
||||
import { flattenObject } from '../utils';
|
||||
|
||||
const interpolate = (
|
||||
str: string,
|
||||
obj: Record<string, any>,
|
||||
options: { escapeJSONStrings?: boolean } = { escapeJSONStrings: false }
|
||||
): string => {
|
||||
if (!str || typeof str !== 'string') {
|
||||
const interpolate = (str: string, obj: Record<string, any>): string => {
|
||||
if (!str || typeof str !== 'string' || !obj || typeof obj !== 'object') {
|
||||
return str;
|
||||
}
|
||||
|
||||
const { escapeJSONStrings } = options;
|
||||
const flattenedObj = flattenObject(obj);
|
||||
|
||||
const patternRegex = /\{\{\$(\w+)\}\}/g;
|
||||
str = str.replace(patternRegex, (match, keyword) => {
|
||||
let replacement = mockDataFunctions[keyword as keyof typeof mockDataFunctions]?.();
|
||||
|
||||
if (replacement === undefined) return match;
|
||||
replacement = String(replacement);
|
||||
|
||||
if (!escapeJSONStrings) return replacement;
|
||||
|
||||
// All the below chars inside of a JSON String field
|
||||
// will make it invalid JSON. So we will have to escape them with `\`.
|
||||
// This is not exhaustive but selective to what faker-js can output.
|
||||
if (!/[\\\n\r\t\"]/.test(replacement)) return replacement;
|
||||
return replacement
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\t/g, '\\t')
|
||||
.replace(/\"/g, '\\"');
|
||||
});
|
||||
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return str;
|
||||
}
|
||||
|
||||
return replace(str, obj);
|
||||
return replace(str, flattenedObj);
|
||||
};
|
||||
|
||||
const replace = (
|
||||
str: string,
|
||||
obj: Record<string, any>,
|
||||
flattenedObj: Record<string, any>,
|
||||
visited = new Set<string>(),
|
||||
results = new Map<string, string>()
|
||||
): string => {
|
||||
@@ -66,10 +37,7 @@ const replace = (
|
||||
const patternRegex = /\{\{([^}]+)\}\}/g;
|
||||
matchFound = false;
|
||||
resultStr = resultStr.replace(patternRegex, (match, placeholder) => {
|
||||
let replacement = get(obj, placeholder);
|
||||
if (typeof replacement === 'object' && replacement !== null) {
|
||||
replacement = JSON.stringify(replacement);
|
||||
}
|
||||
const replacement = flattenedObj[placeholder];
|
||||
|
||||
if (results.has(match)) {
|
||||
return results.get(match);
|
||||
@@ -77,7 +45,7 @@ const replace = (
|
||||
|
||||
if (patternRegex.test(replacement) && !visited.has(match)) {
|
||||
visited.add(match);
|
||||
const result = replace(replacement, obj, visited, results);
|
||||
const result = replace(replacement, flattenedObj, visited, results);
|
||||
results.set(match, result);
|
||||
|
||||
matchFound = true;
|
||||
@@ -96,4 +64,4 @@ const replace = (
|
||||
return resultStr;
|
||||
};
|
||||
|
||||
export default interpolate;
|
||||
export default interpolate;
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { generateHtmlReport } from "./reports/html/generate-report";
|
||||
import { getRunnerSummary } from "./runner-summary";
|
||||
|
||||
export { generateHtmlReport, getRunnerSummary };
|
||||
@@ -1,39 +0,0 @@
|
||||
import { T_RunnerResults } from "../../types";
|
||||
import { isHtmlContentType, getContentType, redactImageData, encodeBase64 } from "../../utils";
|
||||
import { getRunnerSummary } from "../../runner-summary";
|
||||
import htmlTemplateString from "./template";
|
||||
|
||||
const generateHtmlReport = ({
|
||||
runnerResults
|
||||
}: {
|
||||
runnerResults: T_RunnerResults[]
|
||||
}): string => {
|
||||
const resultsWithSummaryAndCleanData = runnerResults.map(({ iterationIndex, results }) => {
|
||||
return {
|
||||
iterationIndex,
|
||||
results: results.map((result) => {
|
||||
const { request, response } = result || {};
|
||||
const requestContentType = request?.headers ? getContentType(request?.headers) : '';
|
||||
const responseContentType = response?.headers ? getContentType(response?.headers) : '';
|
||||
return {
|
||||
...result,
|
||||
request: {
|
||||
...result.request,
|
||||
data: request?.data ? redactImageData(request?.data, requestContentType) : request?.data,
|
||||
isHtml: isHtmlContentType(requestContentType)
|
||||
},
|
||||
response: {
|
||||
...result.response,
|
||||
data: response?.data ? redactImageData(response?.data, responseContentType) : response?.data,
|
||||
isHtml: isHtmlContentType(responseContentType)
|
||||
}
|
||||
}
|
||||
}),
|
||||
summary: getRunnerSummary(results)
|
||||
}
|
||||
});
|
||||
const htmlString = htmlTemplateString(encodeBase64(JSON.stringify(resultsWithSummaryAndCleanData)));
|
||||
return htmlString;
|
||||
};
|
||||
|
||||
export { generateHtmlReport }
|
||||
@@ -1,654 +0,0 @@
|
||||
export const htmlTemplateString = (resutsJsonString: string) =>`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<!-- Would use latest version, you'd better specify a version -->
|
||||
<script src="https://unpkg.com/naive-ui"></script>
|
||||
|
||||
<title>Bruno</title>
|
||||
<style>
|
||||
.error > .status {
|
||||
color: red;
|
||||
}
|
||||
.success > .status {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.n-collapse-item.success > .n-collapse-item__header {
|
||||
background-color: rgba(237, 247, 242, 1);
|
||||
}
|
||||
.n-collapse-item.error > .n-collapse-item__header {
|
||||
background-color: rgba(251, 238, 241, 1);
|
||||
}
|
||||
.skipped > .status {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.min-width-150 {
|
||||
min-width: 150px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<n-config-provider :theme="theme">
|
||||
<n-layout embedded position="absolute" content-style="padding: 24px;">
|
||||
<n-card>
|
||||
<n-flex>
|
||||
<n-page-header title="Bruno run dashboard">
|
||||
<template #avatar>
|
||||
<n-avatar size="large" style="background-color: transparent">
|
||||
<svg id="emoji" width="34" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="color">
|
||||
<path
|
||||
fill="#F4AA41"
|
||||
stroke="none"
|
||||
d="M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z"
|
||||
></path>
|
||||
<polygon
|
||||
fill="#EA5A47"
|
||||
stroke="none"
|
||||
points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855"
|
||||
></polygon>
|
||||
<polygon
|
||||
fill="#3F3F3F"
|
||||
stroke="none"
|
||||
points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855"
|
||||
></polygon>
|
||||
</g>
|
||||
<g id="hair"></g>
|
||||
<g id="skin"></g>
|
||||
<g id="skin-shadow"></g>
|
||||
<g id="line">
|
||||
<path
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
d="M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z"
|
||||
></path>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
d="M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754"
|
||||
></path>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
d="M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486"
|
||||
></path>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
d="M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875"
|
||||
></path>
|
||||
<path
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
d="M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z"
|
||||
></path>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
d="M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414"
|
||||
></path>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
d="M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759"
|
||||
></path>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712"
|
||||
></path>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632"
|
||||
></path>
|
||||
<line
|
||||
x1="36.2078"
|
||||
x2="36.2078"
|
||||
y1="47.3393"
|
||||
y2="44.3093"
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
></line>
|
||||
</g>
|
||||
</svg>
|
||||
</n-avatar>
|
||||
</template>
|
||||
<template #extra>
|
||||
<n-flex justify="end">
|
||||
<n-switch v-model:value="darkMode" :rail-style="darkModeRailStyle">
|
||||
<template #checked> Dark </template>
|
||||
<template #unchecked> Light </template>
|
||||
</n-switch>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-page-header>
|
||||
<n-tabs type="segment" animated v-model:value="currentTab">
|
||||
<n-tab-pane name="summary" tab="Summary">
|
||||
<n-flex justify="center" vertical>
|
||||
<x-summary v-for="(result, index) in res" :res="result" :key="index"></x-summary>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="requests" tab="Requests">
|
||||
<n-flex justify="center" vertical>
|
||||
<x-requests v-for="(result, index) in res" :res="result" :key="index"></x-requests>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</n-layout>
|
||||
</n-config-provider>
|
||||
</div>
|
||||
<script type="text/x-template" id="summary-component">
|
||||
<n-flex vertical style="margin-bottom: 50px;">
|
||||
<n-card>
|
||||
<template #header>
|
||||
<span style="font-size: 24px;">{{ iterationTitle }}</span>
|
||||
</template>
|
||||
<n-flex justify="center">
|
||||
<n-flex justify="center">
|
||||
<n-alert type="success">
|
||||
<n-statistic
|
||||
label="Total requests"
|
||||
:value="summaryTotalRequests"
|
||||
>
|
||||
</n-statistic>
|
||||
</n-alert>
|
||||
<n-alert :type="summaryErrors ? 'error' : 'success'">
|
||||
<n-statistic label="Total errors" :value="summaryErrors">
|
||||
</n-statistic>
|
||||
</n-alert>
|
||||
<n-alert type="success">
|
||||
<n-statistic
|
||||
label="Total Controls"
|
||||
:value="summaryTotalControls"
|
||||
>
|
||||
</n-statistic>
|
||||
</n-alert>
|
||||
<n-alert :type="summaryFailedControls ? 'error' : 'success'">
|
||||
<n-statistic
|
||||
label="Total Failed Controls"
|
||||
:value="summaryFailedControls"
|
||||
>
|
||||
</n-statistic>
|
||||
</n-alert>
|
||||
<n-alert type="warning" v-if="summarySkippedRequests">
|
||||
<n-statistic label="Skipped requests" :value="summarySkippedRequests">
|
||||
</n-statistic>
|
||||
</n-alert>
|
||||
<n-statistic
|
||||
label="Total run duration"
|
||||
:value="Math.round(totalRunDuration*1000)/1000"
|
||||
>
|
||||
<template #suffix>s</template>
|
||||
</n-statistic>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<n-data-table :columns="summaryColumns" :data="summaryData" />
|
||||
</n-flex>
|
||||
</script>
|
||||
<script type="text/x-template" id="requests-component">
|
||||
<n-card>
|
||||
<template #header>
|
||||
<span style="font-size: 24px;">{{ iterationTitle }}</span>
|
||||
</template>
|
||||
<n-flex vertical style="margin-bottom: 50px">
|
||||
<n-switch
|
||||
v-model:value="onlyFailed"
|
||||
:rail-style="railStyle"
|
||||
>
|
||||
<template #checked> Only Failed </template>
|
||||
<template #unchecked> Show All </template>
|
||||
</n-switch>
|
||||
|
||||
<n-collapse>
|
||||
<x-result v-for="(result, index) in results" :result="result" :key="results.length"></x-result>
|
||||
</n-collapse>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</script>
|
||||
<script type="text/x-template" id="result-component">
|
||||
<n-collapse-item
|
||||
:name="resultTitle"
|
||||
arrow-placement="right"
|
||||
>
|
||||
<template #header>
|
||||
<n-alert
|
||||
:type="getAlertType"
|
||||
:bordered="false"
|
||||
>
|
||||
<template #header>
|
||||
{{result.path}} - {{result.response.status === 'skipped' ? 'Request Skipped' : (totalPassed + '/' + total + ' Passed')}} {{hasError ? " - (request failed)" : "" }}
|
||||
</template>
|
||||
</n-alert>
|
||||
</template>
|
||||
<n-flex vertical>
|
||||
<n-grid x-gap="12" :cols="2">
|
||||
<n-gi>
|
||||
<n-card title="REQUEST INFORMATION">
|
||||
<n-list>
|
||||
<n-list-item>
|
||||
<n-thing
|
||||
title="File"
|
||||
:description="result.path"
|
||||
/>
|
||||
</n-list-item>
|
||||
<n-list-item>
|
||||
<n-thing
|
||||
title="Request Method"
|
||||
:description="result.request.method"
|
||||
/>
|
||||
</n-list-item>
|
||||
<n-list-item>
|
||||
<n-thing
|
||||
title="Request URL"
|
||||
:description="result.request.url"
|
||||
/>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card title="RESPONSE INFORMATION">
|
||||
<n-list>
|
||||
<n-list-item>
|
||||
<n-thing
|
||||
title="Response Code"
|
||||
:description="'' + result.response.status"
|
||||
/>
|
||||
</n-list-item>
|
||||
<n-list-item>
|
||||
<n-thing
|
||||
title="Response time"
|
||||
:description="result.response.responseTime + ' ms'"
|
||||
/>
|
||||
</n-list-item>
|
||||
<n-list-item>
|
||||
<n-thing
|
||||
title="Test duration"
|
||||
:description="testDuration"
|
||||
/>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-alert v-if="hasError" title="Error" type="error">
|
||||
{{result.error}}
|
||||
</n-alert>
|
||||
<n-card title="REQUEST HEADERS">
|
||||
<n-data-table
|
||||
:columns="headerColumns"
|
||||
:data="headerDataRequest"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card
|
||||
v-if="result.request.data"
|
||||
title="REQUEST BODY"
|
||||
>
|
||||
<iframe
|
||||
v-if="result.request.isHtml"
|
||||
:srcdoc="result.request.data"
|
||||
style="width: 100%; height: 400px; border: none;"
|
||||
></iframe>
|
||||
|
||||
<pre v-else>{{ result.request.data }}</pre>
|
||||
</n-card>
|
||||
<n-card title="RESPONSE HEADERS">
|
||||
<n-data-table
|
||||
:columns="headerColumns"
|
||||
:data="headerDataResponse"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card
|
||||
v-if="result.response.data"
|
||||
title="RESPONSE BODY"
|
||||
>
|
||||
<iframe
|
||||
v-if="result.response.isHtml"
|
||||
:srcdoc="result.response.data"
|
||||
style="width: 100%; height: 400px; border: none;"
|
||||
></iframe>
|
||||
|
||||
<pre v-else>{{ result.response.data }}</pre> </n-card>
|
||||
<n-card title="ASSERTIONS INFORMATION">
|
||||
<n-data-table
|
||||
:columns="assertionsColumns"
|
||||
:data="result.assertionResults"
|
||||
:row-class-name="assertionsRowClassName"
|
||||
/>
|
||||
</n-card>
|
||||
<n-card title="TESTS INFORMATION">
|
||||
<n-data-table
|
||||
:columns="testsColumns"
|
||||
:data="result.testResults"
|
||||
:row-class-name="testsRowClassName"
|
||||
/>
|
||||
</n-card>
|
||||
</n-flex>
|
||||
</n-collapse-item>
|
||||
</script>
|
||||
<script>
|
||||
const { createApp, ref, computed, onMounted } = Vue;
|
||||
|
||||
const App = {
|
||||
setup() {
|
||||
function decodeBase64(base64) {
|
||||
const binary = atob(base64);
|
||||
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
const res = JSON.parse(decodeBase64('${resutsJsonString}'));
|
||||
|
||||
const currentTab = ref('summary');
|
||||
|
||||
const getTabFromQueryParam = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const tab = urlParams.get('tab');
|
||||
return tab && ['summary', 'requests'].includes(tab) ? tab : 'summary';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
currentTab.value = getTabFromQueryParam();
|
||||
});
|
||||
|
||||
const darkMode = ref(false);
|
||||
const theme = computed(() => {
|
||||
return darkMode.value ? naive.darkTheme : null;
|
||||
});
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
darkMode.value = true;
|
||||
}
|
||||
// To watch for os theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
|
||||
darkMode.value = event.matches;
|
||||
});
|
||||
return {
|
||||
res,
|
||||
theme,
|
||||
darkMode,
|
||||
darkModeRailStyle: () => ({ background: 'var(--n-rail-color)' }),
|
||||
currentTab
|
||||
};
|
||||
}
|
||||
};
|
||||
const app = Vue.createApp(App);
|
||||
|
||||
app.component('x-summary', {
|
||||
template: '#summary-component',
|
||||
props: ['res'],
|
||||
setup(props) {
|
||||
const summaryColumns = [
|
||||
{
|
||||
title: 'SUMMARY ITEM',
|
||||
key: 'title'
|
||||
},
|
||||
{
|
||||
title: 'TOTAL',
|
||||
key: 'total'
|
||||
},
|
||||
{
|
||||
title: 'PASSED',
|
||||
key: 'passed'
|
||||
},
|
||||
{
|
||||
title: 'FAILED',
|
||||
key: 'failed'
|
||||
},
|
||||
{
|
||||
title: 'SKIPPED',
|
||||
key: 'skipped'
|
||||
},
|
||||
{
|
||||
title: 'ERROR',
|
||||
key: 'error'
|
||||
}
|
||||
];
|
||||
const summaryData = computed(() => [
|
||||
{
|
||||
title: 'Requests',
|
||||
total: props.res.summary.totalRequests,
|
||||
passed: props.res.summary.passedRequests,
|
||||
failed: props.res.summary.failedRequests,
|
||||
skipped: props.res.summary.skippedRequests,
|
||||
error: props.res.summary.errorRequests
|
||||
},
|
||||
{
|
||||
title: 'Assertions',
|
||||
total: props.res.summary.totalAssertions,
|
||||
passed: props.res.summary.passedAssertions,
|
||||
failed: props.res.summary.failedAssertions,
|
||||
skipped: '-',
|
||||
error: '-'
|
||||
},
|
||||
{
|
||||
title: 'Tests',
|
||||
total: props.res.summary.totalTests,
|
||||
passed: props.res.summary.passedTests,
|
||||
failed: props.res.summary.failedTests,
|
||||
skipped: '-',
|
||||
error: '-'
|
||||
}
|
||||
]);
|
||||
const summaryTotalRequests = computed(() => {
|
||||
return props.res.summary.totalRequests;
|
||||
});
|
||||
const summaryTotalControls = computed(() => {
|
||||
return props.res.summary.totalTests + props.res.summary.totalAssertions;
|
||||
});
|
||||
const summaryFailedControls = computed(
|
||||
() => props?.res?.summary?.failedTests + props?.res?.summary?.failedAssertions
|
||||
);
|
||||
const summarySkippedRequests = computed(() => props?.res?.summary?.skippedRequests || 0);
|
||||
const summaryErrors = computed(() => props?.res?.results?.filter((r) => r.error || r.status === 'error').length) || 0;
|
||||
const totalRunDuration = computed(() => props.res?.results?.reduce((total, result) => result.runDuration + total, 0));
|
||||
const iterationIndex = Number(props.res.iterationIndex) + 1;
|
||||
return {
|
||||
summaryColumns,
|
||||
summaryData,
|
||||
summaryTotalControls,
|
||||
summaryTotalRequests,
|
||||
summaryFailedControls,
|
||||
summarySkippedRequests,
|
||||
summaryErrors,
|
||||
totalRunDuration,
|
||||
iterationTitle: 'Iteration ' + iterationIndex
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
app.component('x-requests', {
|
||||
template: '#requests-component',
|
||||
props: ['res'],
|
||||
setup(props) {
|
||||
const onlyFailed = ref(false);
|
||||
const filteredResults = computed(() => {
|
||||
if (onlyFailed.value) {
|
||||
return props?.res?.results?.filter(
|
||||
(r) =>
|
||||
r.status === 'error' ||
|
||||
!!r?.testResults?.find((t) => t.status !== 'pass') ||
|
||||
!!r?.assertionResults?.find((t) => t.status !== 'pass')
|
||||
);
|
||||
}
|
||||
return props.res.results;
|
||||
});
|
||||
const iterationIndex = Number(props.res.iterationIndex) + 1;
|
||||
return {
|
||||
onlyFailed,
|
||||
results: filteredResults,
|
||||
railStyle: ({ checked }) => {
|
||||
const style = {};
|
||||
if (checked) {
|
||||
style.background = '#d03050';
|
||||
}
|
||||
return style;
|
||||
},
|
||||
iterationTitle: 'Iteration ' + iterationIndex
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
app.component('x-result', {
|
||||
template: '#result-component',
|
||||
props: ['result'],
|
||||
setup(props) {
|
||||
const headerColumns = [
|
||||
{
|
||||
title: 'Header Name',
|
||||
key: 'name',
|
||||
className: 'min-width-150'
|
||||
},
|
||||
{
|
||||
title: 'Header Value',
|
||||
key: 'value'
|
||||
}
|
||||
];
|
||||
const assertionsColumns = [
|
||||
{
|
||||
title: 'Expression',
|
||||
key: 'lhsExpr'
|
||||
},
|
||||
{
|
||||
title: 'Operator',
|
||||
key: 'operator'
|
||||
},
|
||||
{
|
||||
title: 'Operand',
|
||||
key: 'rhsOperand'
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
className: 'status'
|
||||
},
|
||||
{
|
||||
title: 'Error',
|
||||
key: 'error'
|
||||
}
|
||||
];
|
||||
const assertionsRowClassName = (row) => {
|
||||
return row.status === 'fail' ? 'error' : 'success';
|
||||
};
|
||||
const testsRowClassName = (row) => {
|
||||
if (row.status === 'skipped') return 'skipped';
|
||||
return row.status === 'fail' ? 'error' : 'success';
|
||||
};
|
||||
const testsColumns = [
|
||||
{
|
||||
title: 'Description',
|
||||
key: 'description'
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
className: 'status'
|
||||
},
|
||||
{
|
||||
title: 'Error',
|
||||
key: 'error'
|
||||
}
|
||||
];
|
||||
|
||||
function mapHeaderToTableData(headers) {
|
||||
if (!headers) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(headers).map((name) => ({
|
||||
name,
|
||||
value: headers[name]
|
||||
}));
|
||||
}
|
||||
const headerDataRequest = computed(() => {
|
||||
return mapHeaderToTableData(props?.result?.request?.headers);
|
||||
});
|
||||
const headerDataResponse = computed(() => {
|
||||
return mapHeaderToTableData(props?.result?.response?.headers);
|
||||
});
|
||||
const totalPassed = computed(() => {
|
||||
return (
|
||||
(props?.result?.testResults?.filter((t) => t.status === 'pass').length || 0) +
|
||||
(props?.result?.assertionResults?.filter((t) => t.status === 'pass').length || 0)
|
||||
);
|
||||
});
|
||||
const total = computed(() => {
|
||||
return (props?.result?.testResults?.length || 0) + (props?.result?.assertionResults?.length || 0);
|
||||
});
|
||||
|
||||
const hasError = computed(() => !!props?.result?.error || props?.result?.status === 'error');
|
||||
const hasFailure = computed(() => total.value !== totalPassed.value);
|
||||
const testDuration = computed(() => Math.round(props?.result?.runDuration * 1000) + ' ms');
|
||||
const resultTitle = computed(() => props?.result?.path + ' ' + props?.result?.response?.status + ' ' + props?.result?.response?.statusText);
|
||||
const getAlertType = computed(() => {
|
||||
if (props.result.response.status === 'skipped') {
|
||||
return 'warning';
|
||||
}
|
||||
return hasError.value || hasFailure.value ? 'error' : 'success';
|
||||
});
|
||||
return {
|
||||
headerColumns,
|
||||
headerDataRequest,
|
||||
headerDataResponse,
|
||||
assertionsColumns,
|
||||
assertionsRowClassName,
|
||||
testsRowClassName,
|
||||
totalPassed,
|
||||
total,
|
||||
hasFailure,
|
||||
hasError,
|
||||
testsColumns,
|
||||
result: props.result,
|
||||
testDuration,
|
||||
resultTitle,
|
||||
getAlertType,
|
||||
iterationIndex: props?.result?.iterationIndex
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
app.use(naive);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
export default htmlTemplateString;
|
||||
@@ -1,68 +0,0 @@
|
||||
import { T_RunnerRequestExecutionResult, T_RunSummary } from "./types";
|
||||
|
||||
// todo: this is generic, not specific to html, can be moved out of the report/html sub-package
|
||||
export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_RunSummary => {
|
||||
let totalRequests = 0;
|
||||
let passedRequests = 0;
|
||||
let failedRequests = 0;
|
||||
let errorRequests = 0;
|
||||
let skippedRequests = 0;
|
||||
let totalAssertions = 0;
|
||||
let passedAssertions = 0;
|
||||
let failedAssertions = 0;
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
|
||||
for (const result of results || []) {
|
||||
const { status, testResults, assertionResults } = result;
|
||||
totalRequests += 1;
|
||||
totalTests += Number(testResults?.length) || 0;
|
||||
totalAssertions += Number(assertionResults?.length) || 0;
|
||||
|
||||
if (status === 'skipped') {
|
||||
skippedRequests += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let anyFailed = false;
|
||||
for (const testResult of testResults || []) {
|
||||
if (testResult.status === "pass") {
|
||||
passedTests += 1;
|
||||
} else {
|
||||
anyFailed = true;
|
||||
failedTests += 1;
|
||||
}
|
||||
}
|
||||
for (const assertionResult of assertionResults || []) {
|
||||
if (assertionResult.status === "pass") {
|
||||
passedAssertions += 1;
|
||||
} else {
|
||||
anyFailed = true;
|
||||
failedAssertions += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyFailed && status !== "error") {
|
||||
passedRequests += 1;
|
||||
} else if (anyFailed) {
|
||||
failedRequests += 1;
|
||||
} else {
|
||||
errorRequests += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalRequests,
|
||||
passedRequests,
|
||||
failedRequests,
|
||||
errorRequests,
|
||||
skippedRequests,
|
||||
totalAssertions,
|
||||
passedAssertions,
|
||||
failedAssertions,
|
||||
totalTests,
|
||||
passedTests,
|
||||
failedTests,
|
||||
};
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
// assertion results types
|
||||
type T_AssertionPassResult = {
|
||||
lhsExpr: string;
|
||||
rhsExpr: string;
|
||||
rhsOperand: string;
|
||||
operator: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
type T_AssertionFailResult = {
|
||||
lhsExpr: string;
|
||||
rhsExpr: string;
|
||||
rhsOperand: string;
|
||||
operator: string;
|
||||
status: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
type T_AssertionResult = T_AssertionPassResult | T_AssertionFailResult;
|
||||
|
||||
// test results types
|
||||
type T_TestPassResult = {
|
||||
status: string;
|
||||
description: string;
|
||||
uid?: string;
|
||||
};
|
||||
|
||||
type T_TestFailResult = {
|
||||
status: string;
|
||||
description: string;
|
||||
error: string;
|
||||
uid?: string;
|
||||
};
|
||||
|
||||
type T_TestResult = T_TestPassResult | T_TestFailResult;
|
||||
|
||||
type T_EmptyRequest = {
|
||||
method?: null | undefined;
|
||||
url?: null | undefined;
|
||||
headers?: null | undefined;
|
||||
data?: null | undefined;
|
||||
isHtml?: boolean | undefined;
|
||||
}
|
||||
|
||||
// request types
|
||||
type T_Request = {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: Record<string, string | number | undefined>;
|
||||
data: string | object | null | boolean | number;
|
||||
isHtml?: boolean;
|
||||
};
|
||||
|
||||
type T_EmptyResponse = {
|
||||
status?: null | undefined;
|
||||
statusText?: null | undefined;
|
||||
headers?: null | undefined;
|
||||
data?: null | undefined;
|
||||
responseTime?: number | undefined;
|
||||
isHtml?: boolean | undefined;
|
||||
}
|
||||
|
||||
type T_SkippedResponse = {
|
||||
status?: string | null | undefined;
|
||||
statusText?: string | null | undefined;
|
||||
headers?: null | undefined;
|
||||
data?: null | undefined;
|
||||
responseTime?: number | undefined;
|
||||
isHtml?: boolean | undefined;
|
||||
}
|
||||
|
||||
// response types
|
||||
type T_Response = {
|
||||
status: number | string;
|
||||
statusText: string;
|
||||
headers: Record<string, string | number | undefined>;
|
||||
data: string | object | null | boolean | number;
|
||||
isHtml?: boolean;
|
||||
};
|
||||
|
||||
// result type
|
||||
export type T_RunnerRequestExecutionResult = {
|
||||
iterationIndex: number;
|
||||
name: string;
|
||||
path: string;
|
||||
request: T_EmptyRequest | T_Request;
|
||||
response: T_EmptyResponse | T_Response | T_SkippedResponse;
|
||||
status: null | undefined | string;
|
||||
error: null | undefined | string;
|
||||
assertionResults?: T_AssertionResult[];
|
||||
testResults?: T_TestResult[];
|
||||
runDuration: number;
|
||||
}
|
||||
|
||||
export type T_RunnerResults = {
|
||||
iterationIndex: number;
|
||||
iterationData?: any; // todo - csv/json row data
|
||||
results: T_RunnerRequestExecutionResult[];
|
||||
}
|
||||
|
||||
// run summary type
|
||||
export type T_RunSummary = {
|
||||
totalRequests: number;
|
||||
passedRequests: number;
|
||||
failedRequests: number;
|
||||
errorRequests: number;
|
||||
skippedRequests: number;
|
||||
totalAssertions: number;
|
||||
passedAssertions: number;
|
||||
failedAssertions: number;
|
||||
totalTests: number;
|
||||
passedTests: number;
|
||||
failedTests: number;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
export const encodeBase64 = (str: string) => {
|
||||
const bytes = new TextEncoder().encode(str);
|
||||
const binary = bytes.reduce((acc, byte) => acc + String.fromCharCode(byte), '');
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
export const decodeBase64 = (base64: string) => {
|
||||
const binary = atob(base64);
|
||||
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
export const getContentType = (headers: Record<string, string | number | undefined>): string => {
|
||||
if (!headers || typeof headers !== 'object') {
|
||||
return '';
|
||||
}
|
||||
const contentType = Object.entries(headers)
|
||||
.find(([key]) => key.toLowerCase() === 'content-type')?.[1];
|
||||
return typeof contentType === 'string' ? contentType : '';
|
||||
};
|
||||
|
||||
export const isHtmlContentType = (contentType: string) => {
|
||||
return contentType?.includes("html");
|
||||
};
|
||||
|
||||
export const redactImageData = (data: string | object | number | boolean, contentType: string) => {
|
||||
if (contentType?.includes("image")) {
|
||||
return "Response content redacted (image data)";
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import { mockDataFunctions } from "./faker-functions";
|
||||
|
||||
describe("mockDataFunctions Regex Validation", () => {
|
||||
test("all values should match their expected patterns", () => {
|
||||
const patterns: Record<string, RegExp> = {
|
||||
guid: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/,
|
||||
timestamp: /^\d{13,}$/,
|
||||
isoTimestamp: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
|
||||
randomUUID: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/,
|
||||
randomAlphaNumeric: /^[\w]$/,
|
||||
randomBoolean: /^(true|false)$/,
|
||||
randomInt: /^\d+$/,
|
||||
randomColor: /^[\w\s]+$/,
|
||||
randomHexColor: /^#[\da-f]{6}$/,
|
||||
randomAbbreviation: /^\w{2,6}$/,
|
||||
randomIP: /^([\da-f]{1,4}:){7}[\da-f]{1,4}$|^(\d{1,3}\.){3}\d{1,3}$/,
|
||||
randomIPV4: /^(\d{1,3}\.){3}\d{1,3}$/,
|
||||
randomIPV6: /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/,
|
||||
randomMACAddress: /^([\da-f]{2}:){5}[\da-f]{2}$/,
|
||||
randomPassword: /^[\w\d]{8,}$/,
|
||||
randomLocale: /^[A-Z]{2}$/,
|
||||
randomUserAgent: /^[\w\/\.\s\(\)\+\-;:_,]+$/,
|
||||
randomProtocol: /^(http|https|ftp)s?$/,
|
||||
randomSemver: /^\d+\.\d+\.\d+$/,
|
||||
randomFirstName: /^[\s\S]+$/,
|
||||
randomLastName: /^[\s\S]+$/,
|
||||
randomFullName: /^[\s\S]+$/,
|
||||
randomNamePrefix: /^[\s\S]+$/,
|
||||
randomNameSuffix: /^[\s\S]+$/,
|
||||
randomJobArea: /^[\s\S]+$/,
|
||||
randomJobDescriptor: /^[\s\S]+$/,
|
||||
randomJobTitle: /^[\s\S]+$/,
|
||||
randomJobType: /^[\s\S]+$/,
|
||||
randomPhoneNumber: /^[\s\S]+$/,
|
||||
randomPhoneNumberExt: /^[\s\S]+$/,
|
||||
randomCity: /^[\s\S]+$/,
|
||||
randomStreetName: /^[\s\S]+$/,
|
||||
randomStreetAddress: /^[\s\S]+$/,
|
||||
randomCountry: /^[\s\S]+$/,
|
||||
randomCountryCode: /^[\s\S]+$/,
|
||||
randomLatitude: /^[\s\S]+$/,
|
||||
randomLongitude: /^[\s\S]+$/,
|
||||
randomAvatarImage: /^[\s\S]+$/,
|
||||
randomImageUrl: /^[\s\S]+$/,
|
||||
randomAbstractImage: /^[\s\S]+$/,
|
||||
randomAnimalsImage: /^[\s\S]+$/,
|
||||
randomBusinessImage: /^[\s\S]+$/,
|
||||
randomCatsImage: /^[\s\S]+$/,
|
||||
randomCityImage: /^[\s\S]+$/,
|
||||
randomFoodImage: /^[\s\S]+$/,
|
||||
randomNightlifeImage: /^[\s\S]+$/,
|
||||
randomFashionImage: /^[\s\S]+$/,
|
||||
randomPeopleImage: /^[\s\S]+$/,
|
||||
randomNatureImage: /^[\s\S]+$/,
|
||||
randomSportsImage: /^[\s\S]+$/,
|
||||
randomTransportImage: /^[\s\S]+$/,
|
||||
randomImageDataUri: /^[\s\S]+$/,
|
||||
randomBankAccount: /^[\s\S]+$/,
|
||||
randomBankAccountName: /^[\s\S]+$/,
|
||||
randomCreditCardMask: /^[\s\S]+$/,
|
||||
randomBankAccountBic: /^[\s\S]+$/,
|
||||
randomBankAccountIban: /^[\s\S]+$/,
|
||||
randomTransactionType: /^[\s\S]+$/,
|
||||
randomCurrencyCode: /^[\s\S]+$/,
|
||||
randomCurrencyName: /^[\s\S]+$/,
|
||||
randomCurrencySymbol: /^[\s\S]+$/,
|
||||
randomBitcoin: /^[\s\S]+$/,
|
||||
randomCompanyName: /^[\s\S]+$/,
|
||||
randomCompanySuffix: /^[\s\S]+$/,
|
||||
randomBs: /^[\s\S]+$/,
|
||||
randomBsAdjective: /^[\s\S]+$/,
|
||||
randomBsBuzz: /^[\s\S]+$/,
|
||||
randomBsNoun: /^[\s\S]+$/,
|
||||
randomCatchPhrase: /^[\s\S]+$/,
|
||||
randomCatchPhraseAdjective: /^[\s\S]+$/,
|
||||
randomCatchPhraseDescriptor: /^[\s\S]+$/,
|
||||
randomCatchPhraseNoun: /^[\s\S]+$/,
|
||||
randomDatabaseColumn: /^[\s\S]+$/,
|
||||
randomDatabaseType: /^[\s\S]+$/,
|
||||
randomDatabaseCollation: /^[\s\S]+$/,
|
||||
randomDatabaseEngine: /^[\s\S]+$/,
|
||||
randomDateFuture: /^[\s\S]+$/,
|
||||
randomDatePast: /^[\s\S]+$/,
|
||||
randomDateRecent: /^[\s\S]+$/,
|
||||
randomWeekday: /^[\s\S]+$/,
|
||||
randomMonth: /^[\s\S]+$/,
|
||||
randomDomainName: /^[\s\S]+$/,
|
||||
randomDomainSuffix: /^[\s\S]+$/,
|
||||
randomDomainWord: /^[\s\S]+$/,
|
||||
randomEmail: /^[\w_.\-]+@[\w]+\.[a-z]+$/,
|
||||
randomExampleEmail: /^[\w\.-]+@example\.[a-z]+$/,
|
||||
randomUserName: /^[\w.\-]+$/,
|
||||
randomUrl: /^https:\/\/[\w\-]+\.[a-z]+\/?$/,
|
||||
randomFileName: /^[\w\_]+\.[\w\d]+$/,
|
||||
randomFileType: /^[\w]+$/,
|
||||
randomFileExt: /^[\w\d]+$/,
|
||||
randomCommonFileName: /^[\w\_]+\.[\w\d]+$/,
|
||||
randomCommonFileType: /^[\w]+$/,
|
||||
randomCommonFileExt: /^[\w\d]+$/,
|
||||
randomFilePath: /^[\s\S]+$/,
|
||||
randomDirectoryPath: /^\/[-\w\+\/]+$/,
|
||||
randomMimeType: /^[\w]+\/[\w\d\-\+\.]+$/,
|
||||
randomPrice: /^\d+\.\d{2}$/,
|
||||
randomProduct: /^[\s\S]+$/,
|
||||
randomProductAdjective: /^[\s\S]+$/,
|
||||
randomProductMaterial: /^[\s\S]+$/,
|
||||
randomProductName: /^[\s\S]+$/,
|
||||
randomDepartment: /^[\s\S]+$/,
|
||||
randomNoun: /^[\s\S]+$/,
|
||||
randomVerb: /^[\s\S]+$/,
|
||||
randomIngverb: /^[\s\S]+$/,
|
||||
randomAdjective: /^[\s\S]+$/,
|
||||
randomWord: /^[\s\S]+$/,
|
||||
randomWords: /^[\s\S]+$/,
|
||||
randomPhrase: /^[\s\S]+$/,
|
||||
randomLoremWord: /^[\s\S]+$/,
|
||||
randomLoremWords: /^[\s\S]+$/,
|
||||
randomLoremSentence: /^[\s\S]+$/,
|
||||
randomLoremSentences: /^[\s\S]+$/,
|
||||
randomLoremParagraph: /^[\s\S]+$/,
|
||||
randomLoremParagraphs: /^[\s\S]+$/,
|
||||
randomLoremText: /^[\s\S]+$/,
|
||||
randomLoremSlug: /^[\s\S]+$/,
|
||||
randomLoremLines: /^[\s\S]+$/,
|
||||
};
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
Object.entries(mockDataFunctions).forEach(([key, func]) => {
|
||||
const pattern = patterns[key];
|
||||
const value = String(func());
|
||||
if (!value.match(pattern)) {
|
||||
errors.push(`Pattern mismatch for ${key}: expected ${pattern}, received ${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join("\n"));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,123 +0,0 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
export 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.color.rgb(),
|
||||
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()
|
||||
};
|
||||
51
packages/bruno-common/src/utils/index.spec.ts
Normal file
51
packages/bruno-common/src/utils/index.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { flattenObject } from './index';
|
||||
|
||||
describe('flattenObject', () => {
|
||||
it('should flatten a simple object', () => {
|
||||
const input = { a: 1, b: { c: 2, d: { e: 3 } } };
|
||||
const output = flattenObject(input);
|
||||
expect(output).toEqual({ a: 1, 'b.c': 2, 'b.d.e': 3 });
|
||||
});
|
||||
|
||||
it('should flatten an object with arrays', () => {
|
||||
const input = { a: 1, b: { c: [2, 3, 4], d: { e: 5 } } };
|
||||
const output = flattenObject(input);
|
||||
expect(output).toEqual({ a: 1, 'b.c[0]': 2, 'b.c[1]': 3, 'b.c[2]': 4, 'b.d.e': 5 });
|
||||
});
|
||||
|
||||
it('should flatten an object with arrays having objects', () => {
|
||||
const input = { a: 1, b: { c: [{ d: 2 }, { e: 3 }], f: { g: 4 } } };
|
||||
const output = flattenObject(input);
|
||||
expect(output).toEqual({ a: 1, 'b.c[0].d': 2, 'b.c[1].e': 3, 'b.f.g': 4 });
|
||||
});
|
||||
|
||||
it('should handle null values', () => {
|
||||
const input = { a: 1, b: { c: null, d: { e: 3 } } };
|
||||
const output = flattenObject(input);
|
||||
expect(output).toEqual({ a: 1, 'b.c': null, 'b.d.e': 3 });
|
||||
});
|
||||
|
||||
it('should handle an empty object', () => {
|
||||
const input = {};
|
||||
const output = flattenObject(input);
|
||||
expect(output).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle an object with nested empty objects', () => {
|
||||
const input = { a: { b: {}, c: { d: {} } } };
|
||||
const output = flattenObject(input);
|
||||
expect(output).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle an object with duplicate keys - dot notation used to define the last duplicate key', () => {
|
||||
const input = { a: { b: 2 }, 'a.b': 1 };
|
||||
const output = flattenObject(input);
|
||||
expect(output).toEqual({ 'a.b': 1 });
|
||||
});
|
||||
|
||||
it('should handle an object with duplicate keys - inner object used to define the last duplicate key', () => {
|
||||
const input = { 'a.b': 1, a: { b: 2 } };
|
||||
const output = flattenObject(input);
|
||||
expect(output).toEqual({ 'a.b': 2 });
|
||||
});
|
||||
});
|
||||
11
packages/bruno-common/src/utils/index.ts
Normal file
11
packages/bruno-common/src/utils/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const flattenObject = (obj: Record<string, any>, parentKey: string = ''): Record<string, any> => {
|
||||
return Object.entries(obj).reduce((acc: Record<string, any>, [key, value]: [string, any]) => {
|
||||
const newKey = parentKey ? (Array.isArray(obj) ? `${parentKey}[${key}]` : `${parentKey}.${key}`) : key;
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
Object.assign(acc, flattenObject(value, newKey));
|
||||
} else {
|
||||
acc[newKey] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
@@ -6,14 +6,14 @@
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react",
|
||||
"module": "ESNext",
|
||||
"declaration": true,
|
||||
"declarationDir": "types",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
"emitDeclarationOnly": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js"],
|
||||
"exclude": ["dist", "node_modules", "tests"]
|
||||
}
|
||||
|
||||
22
packages/bruno-converters/.gitignore
vendored
22
packages/bruno-converters/.gitignore
vendored
@@ -1,22 +0,0 @@
|
||||
# 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*
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
'^.+\\.js$': 'babel-jest',
|
||||
},
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(nanoid)/)'
|
||||
],
|
||||
testEnvironment: 'node',
|
||||
moduleNameMapper: {
|
||||
'^nanoid(/(.*)|$)': 'nanoid$1'
|
||||
}
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
// 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
|
||||
};
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
# 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);
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```bash copy
|
||||
|
||||
const { postmanToBruno } = require('@usebruno/converters');
|
||||
const fs = require('fs/promises');
|
||||
const path = require('path');
|
||||
|
||||
async function convertPostmanToBruno(inputFile, outputFile) {
|
||||
try {
|
||||
// Read Postman collection file
|
||||
const inputData = await fs.readFile(inputFile, 'utf8');
|
||||
|
||||
// Convert to Bruno collection
|
||||
const brunoCollection = postmanToBruno(JSON.parse(inputData));
|
||||
|
||||
// Save Bruno collection
|
||||
await fs.writeFile(outputFile, JSON.stringify(brunoCollection, null, 2));
|
||||
|
||||
console.log('Conversion successful!');
|
||||
} catch (error) {
|
||||
console.error('Error during conversion:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const inputFilePath = path.resolve(__dirname, 'demo_collection.postman_collection.json');
|
||||
const outputFilePath = path.resolve(__dirname, 'bruno-collection.json');
|
||||
|
||||
convertPostmanToBruno(inputFilePath, outputFilePath);
|
||||
|
||||
```
|
||||
@@ -1,38 +0,0 @@
|
||||
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') }]
|
||||
})
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -1,211 +0,0 @@
|
||||
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 = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export { default as postmanToBruno } from './postman/postman-to-bruno.js';
|
||||
export { default as postmanToBrunoEnvironment } from './postman/postman-env-to-bruno-env.js';
|
||||
export { default as brunoToPostman } from './postman/bruno-to-postman.js';
|
||||
export { default as openApiToBruno } from './openapi/openapi-to-bruno.js';
|
||||
export { default as insomniaToBruno } from './insomnia/insomnia-to-bruno.js';
|
||||
@@ -1,308 +0,0 @@
|
||||
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 isInsomniaV5Export = (data) => {
|
||||
// V5 format has a type property at the root level
|
||||
if (data.type && data.type.startsWith('collection.insomnia.rest/5')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const parseInsomniaV5Collection = (data) => {
|
||||
const brunoCollection = {
|
||||
name: data.name || 'Untitled Collection',
|
||||
uid: uuid(),
|
||||
version: '1',
|
||||
items: [],
|
||||
environments: []
|
||||
};
|
||||
|
||||
try {
|
||||
// Parse the collection items
|
||||
const parseCollectionItems = (items, allItems = []) => {
|
||||
if (!Array.isArray(items)) {
|
||||
throw new Error('Invalid items format: expected array');
|
||||
}
|
||||
|
||||
return items.map((item, index) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// In v5, requests might be defined with method property or meta.type
|
||||
if (item.method && item.url) {
|
||||
const request = {
|
||||
_id: item.meta?.id || uuid(),
|
||||
name: item.name || 'Untitled Request',
|
||||
url: item.url || '',
|
||||
method: item.method || '',
|
||||
headers: item.headers || [],
|
||||
parameters: item.parameters || [],
|
||||
pathParameters: item.pathParameters || [],
|
||||
authentication: item.authentication || {},
|
||||
body: item.body || {}
|
||||
};
|
||||
return transformInsomniaRequestItem(request, index, allItems);
|
||||
} else if (item.children && Array.isArray(item.children)) {
|
||||
// Process folder
|
||||
return {
|
||||
uid: uuid(),
|
||||
name: item.name || 'Untitled Folder',
|
||||
type: 'folder',
|
||||
items: parseCollectionItems(item.children, item.children)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean);
|
||||
};
|
||||
|
||||
if (data.collection && Array.isArray(data.collection)) {
|
||||
brunoCollection.items = parseCollectionItems(data.collection, data.collection);
|
||||
}
|
||||
|
||||
// Parse environments if available
|
||||
if (data.environments) {
|
||||
// Handle environments implementation if needed
|
||||
}
|
||||
|
||||
return brunoCollection;
|
||||
} catch (err) {
|
||||
console.error('Error parsing collection:', err);
|
||||
throw new Error('An error occurred while parsing the Insomnia v5 collection: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const parseInsomniaCollection = (data) => {
|
||||
const brunoCollection = {
|
||||
name: '',
|
||||
uid: uuid(),
|
||||
version: '1',
|
||||
items: [],
|
||||
environments: []
|
||||
};
|
||||
|
||||
try {
|
||||
const insomniaResources = get(data, '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.filter(r => r.parentId === folder._id).map(transformInsomniaRequestItem)
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
return folders.concat(requests.map(transformInsomniaRequestItem));
|
||||
}
|
||||
|
||||
brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id);
|
||||
return brunoCollection;
|
||||
} catch (err) {
|
||||
console.error('Error parsing collection:', err);
|
||||
throw new Error('An error occurred while parsing the Insomnia collection: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const insomniaToBruno = (insomniaCollection) => {
|
||||
try {
|
||||
let collection;
|
||||
if (isInsomniaV5Export(insomniaCollection)) {
|
||||
collection = parseInsomniaV5Collection(insomniaCollection);
|
||||
} else {
|
||||
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: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
export default insomniaToBruno;
|
||||
@@ -1,436 +0,0 @@
|
||||
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;
|
||||
@@ -1,367 +0,0 @@
|
||||
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) => {
|
||||
if (!headersArray || !Array.isArray(headersArray)) {
|
||||
return [];
|
||||
}
|
||||
return map(headersArray, (item) => {
|
||||
return {
|
||||
key: item.name || '',
|
||||
value: item.value || '',
|
||||
disabled: !item.enabled,
|
||||
type: 'default'
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const generateBody = (body) => {
|
||||
if (!body || !body.mode) {
|
||||
return {
|
||||
mode: 'raw',
|
||||
raw: ''
|
||||
};
|
||||
}
|
||||
|
||||
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 || {}
|
||||
};
|
||||
default:
|
||||
return {
|
||||
mode: 'raw',
|
||||
raw: ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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) => {
|
||||
if (!itemRequest) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const requestObject = {
|
||||
method: itemRequest.method || 'GET',
|
||||
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 && itemRequest.body.mode !== 'none') {
|
||||
requestObject.body = generateBody(itemRequest.body);
|
||||
}
|
||||
return requestObject;
|
||||
};
|
||||
|
||||
const generateItemSection = (itemsArray) => {
|
||||
if (!itemsArray || !Array.isArray(itemsArray)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return map(itemsArray, (item) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item.type === 'folder') {
|
||||
return {
|
||||
name: item.name || 'Untitled Folder',
|
||||
item: item.items && item.items.length ? generateItemSection(item.items) : []
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: item.name || 'Untitled Request',
|
||||
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;
|
||||
@@ -1,40 +0,0 @@
|
||||
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;
|
||||
@@ -1,618 +0,0 @@
|
||||
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) => {
|
||||
if (!url) return '';
|
||||
|
||||
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 && 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 processAuth = (auth, requestObject) => {
|
||||
if (!auth || !auth.type || auth.type === 'noauth') {
|
||||
return;
|
||||
}
|
||||
|
||||
let authValues = auth[auth.type];
|
||||
if (Array.isArray(authValues)) {
|
||||
authValues = convertV21Auth(authValues);
|
||||
}
|
||||
|
||||
if (auth.type === 'basic') {
|
||||
requestObject.auth.mode = 'basic';
|
||||
requestObject.auth.basic = {
|
||||
username: authValues.username || '',
|
||||
password: authValues.password || ''
|
||||
};
|
||||
} else if (auth.type === 'bearer') {
|
||||
requestObject.auth.mode = 'bearer';
|
||||
requestObject.auth.bearer = {
|
||||
token: authValues.token || ''
|
||||
};
|
||||
} else if (auth.type === 'awsv4') {
|
||||
requestObject.auth.mode = 'awsv4';
|
||||
requestObject.auth.awsv4 = {
|
||||
accessKeyId: authValues.accessKey || '',
|
||||
secretAccessKey: authValues.secretKey || '',
|
||||
sessionToken: authValues.sessionToken || '',
|
||||
service: authValues.service || '',
|
||||
region: authValues.region || '',
|
||||
profileName: ''
|
||||
};
|
||||
} else if (auth.type === 'apikey') {
|
||||
requestObject.auth.mode = 'apikey';
|
||||
requestObject.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 === 'digest') {
|
||||
requestObject.auth.mode = 'digest';
|
||||
requestObject.auth.digest = {
|
||||
username: authValues.username || '',
|
||||
password: authValues.password || ''
|
||||
};
|
||||
} else if (auth.type === 'oauth2') {
|
||||
const findValueUsingKey = (key) => {
|
||||
return authValues[key] || '';
|
||||
};
|
||||
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';
|
||||
|
||||
requestObject.auth.mode = 'oauth2';
|
||||
if (grantType === 'authorization_code') {
|
||||
requestObject.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'
|
||||
};
|
||||
} else if (grantType === 'password_credentials') {
|
||||
requestObject.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'
|
||||
};
|
||||
} else if (grantType === 'client_credentials') {
|
||||
requestObject.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'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.warn('Unexpected auth.type', auth.type);
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: null
|
||||
},
|
||||
headers: [],
|
||||
script: {},
|
||||
tests: '',
|
||||
vars: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Folder level auth
|
||||
if (i.auth) {
|
||||
processAuth(i.auth, brunoFolderItem.root.request);
|
||||
} else if (parentAuth) {
|
||||
// Inherit parent auth if folder doesn't define its own
|
||||
processAuth(parentAuth, brunoFolderItem.root.request);
|
||||
}
|
||||
|
||||
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', i?.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,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: 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
|
||||
});
|
||||
});
|
||||
|
||||
// Handle request-level auth or inherit from parent
|
||||
const auth = i.request.auth ?? parentAuth;
|
||||
processAuth(auth, brunoRequestItem.request);
|
||||
|
||||
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,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: null
|
||||
},
|
||||
headers: [],
|
||||
script: {},
|
||||
tests: '',
|
||||
vars: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (collection.event) {
|
||||
importScriptsFromEvents(collection.event, brunoCollection.root.request);
|
||||
}
|
||||
|
||||
if (collection?.variable) {
|
||||
importCollectionLevelVariables(collection.variable, brunoCollection.root.request);
|
||||
}
|
||||
|
||||
// Collection level auth
|
||||
processAuth(collection.auth, 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('Unsupported Postman schema version. Only Postman Collection v2.0 and v2.1 are supported.');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if (err instanceof Error) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new Error('Invalid Postman collection format. Please check your 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: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
export default postmanToBruno;
|
||||
@@ -1,160 +0,0 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import insomniaToBruno from '../../src/insomnia/insomnia-to-bruno';
|
||||
import jsyaml from 'js-yaml';
|
||||
|
||||
describe('insomnia-collection', () => {
|
||||
it('should correctly import a valid Insomnia v5 collection file', async () => {
|
||||
const brunoCollection = insomniaToBruno(jsyaml.load(insomniaCollection));
|
||||
|
||||
expect(brunoCollection).toMatchObject(expectedOutput)
|
||||
});
|
||||
});
|
||||
|
||||
const insomniaCollection = `
|
||||
type: collection.insomnia.rest/5.0
|
||||
name: Hello World Workspace Insomnia
|
||||
meta:
|
||||
id: wrk_9381cf78cb0a4eaaab1d571f29f928dc
|
||||
created: 1744194421962
|
||||
modified: 1744194421962
|
||||
collection:
|
||||
- name: Folder1
|
||||
meta:
|
||||
id: fld_6beacec0bd2f4370be98169217e82a2c
|
||||
created: 1744194421968
|
||||
modified: 1744194421968
|
||||
sortKey: -1744194421968
|
||||
children:
|
||||
- url: https://httpbin.org/get
|
||||
name: Request1
|
||||
meta:
|
||||
id: req_e9fbdc9c88984068a04f442e052d4ff1
|
||||
created: 1744194421965
|
||||
modified: 1744194421965
|
||||
isPrivate: false
|
||||
sortKey: -1744194421965
|
||||
method: GET
|
||||
settings:
|
||||
renderRequestBody: true
|
||||
encodeUrl: true
|
||||
followRedirects: global
|
||||
cookies:
|
||||
send: true
|
||||
store: true
|
||||
rebuildPath: true
|
||||
- name: Folder2
|
||||
meta:
|
||||
id: fld_96508d79bf06420a853b07482ab280d7
|
||||
created: 1744194421969
|
||||
modified: 1744194421969
|
||||
sortKey: -1744194421969
|
||||
children:
|
||||
- url: https://httpbin.org/get
|
||||
name: Request2
|
||||
meta:
|
||||
id: req_3c572aa26a964f1f800bfa5c53cacb75
|
||||
created: 1744194421967
|
||||
modified: 1744194421967
|
||||
isPrivate: false
|
||||
sortKey: -1744194421968
|
||||
method: GET
|
||||
settings:
|
||||
renderRequestBody: true
|
||||
encodeUrl: true
|
||||
followRedirects: global
|
||||
cookies:
|
||||
send: true
|
||||
store: true
|
||||
rebuildPath: true
|
||||
cookieJar:
|
||||
name: Default Jar
|
||||
meta:
|
||||
id: jar_9ecb97079037c7d5bb888f0bfdec9b0e1275c6d1
|
||||
created: 1744194421971
|
||||
modified: 1744194421971
|
||||
environments:
|
||||
name: Imported Environment
|
||||
meta:
|
||||
id: env_a8a9a8ff952d4d079edf53f8ee22a423
|
||||
created: 1744194421970
|
||||
modified: 1744194421970
|
||||
isPrivate: false
|
||||
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": "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": "Folder2",
|
||||
"type": "folder",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
},
|
||||
],
|
||||
"name": "Hello World Workspace Insomnia",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"version": "1",
|
||||
};
|
||||
@@ -1,190 +0,0 @@
|
||||
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",
|
||||
};
|
||||
@@ -1,108 +0,0 @@
|
||||
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,494 +0,0 @@
|
||||
import { sanitizeUrl, transformUrl, brunoToPostman } from "../../src/postman/bruno-to-postman";
|
||||
|
||||
describe('transformUrl', () => {
|
||||
it('should handle basic URL with path variables', () => {
|
||||
const url = 'https://example.com/{{username}}/api/resource/:id';
|
||||
const params = [
|
||||
{ name: 'id', value: '123', type: 'path' },
|
||||
];
|
||||
|
||||
const result = transformUrl(url, params);
|
||||
|
||||
expect(result).toEqual({
|
||||
raw: 'https://example.com/{{username}}/api/resource/:id',
|
||||
protocol: 'https',
|
||||
host: ['example', 'com'],
|
||||
path: ['{{username}}', 'api', 'resource', ':id'],
|
||||
query: [],
|
||||
variable: [
|
||||
{ key: 'id', value: '123' },
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URL with query parameters', () => {
|
||||
const url = 'https://example.com/api/resource?limit=10&offset=20';
|
||||
const params = [
|
||||
{ name: 'limit', value: '10', type: 'query' },
|
||||
{ name: 'offset', value: '20', type: 'query' }
|
||||
];
|
||||
|
||||
const result = transformUrl(url, params);
|
||||
|
||||
expect(result).toEqual({
|
||||
raw: 'https://example.com/api/resource?limit=10&offset=20',
|
||||
protocol: 'https',
|
||||
host: ['example', 'com'],
|
||||
path: ['api', 'resource'],
|
||||
query: [
|
||||
{ key: 'limit', value: '10' },
|
||||
{ key: 'offset', value: '20' }
|
||||
],
|
||||
variable: []
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URL without protocol', () => {
|
||||
const url = 'example.com/api/resource';
|
||||
const params = [];
|
||||
|
||||
const result = transformUrl(url, params);
|
||||
|
||||
expect(result).toEqual({
|
||||
raw: 'example.com/api/resource',
|
||||
protocol: '',
|
||||
host: ['example', 'com'],
|
||||
path: ['api', 'resource'],
|
||||
query: [],
|
||||
variable: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeUrl', () => {
|
||||
it('should replace backslashes with slashes', () => {
|
||||
const input = 'http:\\\\example.com\\path\\to\\file';
|
||||
const expected = 'http://example.com/path/to/file';
|
||||
expect(sanitizeUrl(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should collapse multiple slashes into a single slash', () => {
|
||||
const input = 'http://example.com//path///to////file';
|
||||
const expected = 'http://example.com/path/to/file';
|
||||
expect(sanitizeUrl(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle URLs with mixed slashes', () => {
|
||||
const input = 'http:\\example.com//path\\to//file';
|
||||
const expected = 'http://example.com/path/to/file';
|
||||
expect(sanitizeUrl(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('brunoToPostman null checks and fallbacks', () => {
|
||||
it('should handle null or undefined headers', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
headers: null
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.header).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle null or undefined items in headers', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
headers: [
|
||||
{ name: null, value: 'test-value', enabled: true },
|
||||
{ name: 'Content-Type', value: null, enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.header).toEqual([
|
||||
{ key: '', value: 'test-value', disabled: false, type: 'default' },
|
||||
{ key: 'Content-Type', value: '', disabled: false, type: 'default' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle null or undefined body', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
body: null
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
// Should not have body property since we're checking for body before adding it
|
||||
expect(result.item[0].request.body).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle null or undefined body mode', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
body: {}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
// Should use default raw mode for undefined body mode
|
||||
expect(result.item[0].request.body).toEqual({
|
||||
mode: 'raw',
|
||||
raw: ''
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null or undefined formUrlEncoded array', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://example.com',
|
||||
body: {
|
||||
mode: 'formUrlEncoded',
|
||||
formUrlEncoded: null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.body).toEqual({
|
||||
mode: 'urlencoded',
|
||||
urlencoded: []
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null or undefined multipartForm array', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://example.com',
|
||||
body: {
|
||||
mode: 'multipartForm',
|
||||
multipartForm: null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.body).toEqual({
|
||||
mode: 'formdata',
|
||||
formdata: []
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null or undefined items in form data', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://example.com',
|
||||
body: {
|
||||
mode: 'formUrlEncoded',
|
||||
formUrlEncoded: [
|
||||
{ name: null, value: 'test-value', enabled: true },
|
||||
{ name: 'field', value: null, enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.body.urlencoded).toEqual([
|
||||
{ key: '', value: 'test-value', disabled: false, type: 'default' },
|
||||
{ key: 'field', value: '', disabled: false, type: 'default' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle null or undefined method', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: 'https://example.com',
|
||||
method: null
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.method).toBe('GET');
|
||||
});
|
||||
|
||||
it('should handle null or undefined url', () => {
|
||||
// Mock console.error to prevent it from logging during test
|
||||
const originalConsoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: null
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.url.raw).toBe('');
|
||||
});
|
||||
|
||||
it('should handle null or undefined params', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
params: null
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.url.variable).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle null or undefined docs', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
docs: null
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.description).toBe('');
|
||||
});
|
||||
|
||||
it('should handle null or undefined folder name', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
type: 'folder',
|
||||
name: null,
|
||||
items: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].name).toBe('Untitled Folder');
|
||||
});
|
||||
|
||||
it('should handle null or undefined request name', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
type: 'http-request',
|
||||
name: null,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].name).toBe('Untitled Request');
|
||||
});
|
||||
|
||||
it('should handle null or undefined folder items', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
type: 'folder',
|
||||
name: 'Test Folder',
|
||||
items: null
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].item).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle null or undefined auth object', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
auth: null
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.auth).toEqual({ type: 'noauth' });
|
||||
});
|
||||
|
||||
it('should handle missing token in bearer auth', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
auth: {
|
||||
mode: 'bearer',
|
||||
bearer: { token: null }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.auth).toEqual({
|
||||
type: 'bearer',
|
||||
bearer: {
|
||||
key: 'token',
|
||||
value: '',
|
||||
type: 'string'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing username/password in basic auth', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
auth: {
|
||||
mode: 'basic',
|
||||
basic: { username: null, password: undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.auth).toEqual({
|
||||
type: 'basic',
|
||||
basic: [
|
||||
{
|
||||
key: 'password',
|
||||
value: '',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
value: '',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing key/value in apikey auth', () => {
|
||||
const simpleCollection = {
|
||||
items: [
|
||||
{
|
||||
name: 'Test Request',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
auth: {
|
||||
mode: 'apikey',
|
||||
apikey: { key: null, value: undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = brunoToPostman(simpleCollection);
|
||||
expect(result.item[0].request.auth).toEqual({
|
||||
type: 'apikey',
|
||||
apikey: [
|
||||
{
|
||||
key: 'key',
|
||||
value: '',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
value: '',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,238 +0,0 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import postmanToBruno from '../../../src/postman/postman-to-bruno';
|
||||
|
||||
describe('Collection Authentication', () => {
|
||||
it('should handle basic auth at collection level', () => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Collection level basic auth',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
},
|
||||
item: [],
|
||||
auth: {
|
||||
type: 'basic',
|
||||
basic: [
|
||||
{
|
||||
key: 'password',
|
||||
value: 'testpass',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
value: 'testuser',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
event: [
|
||||
{
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
},
|
||||
{
|
||||
listen: 'test',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
// console.log('result', JSON.stringify(result, null, 2));
|
||||
|
||||
expect(result.root.request.auth).toEqual({
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: 'testuser',
|
||||
password: 'testpass'
|
||||
},
|
||||
bearer: null,
|
||||
awsv4: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle bearer token auth at collection level', () => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Collection level bearer token',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
},
|
||||
item: [],
|
||||
auth: {
|
||||
type: 'bearer',
|
||||
bearer: [
|
||||
{
|
||||
key: 'token',
|
||||
value: 'token',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
event: [
|
||||
{
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
},
|
||||
{
|
||||
listen: 'test',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
// console.log('result', JSON.stringify(result, null, 2));
|
||||
|
||||
expect(result.root.request.auth).toEqual({
|
||||
mode: 'bearer',
|
||||
basic: null,
|
||||
bearer: {
|
||||
token: 'token'
|
||||
},
|
||||
awsv4: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle API key auth at collection level', () => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Collection level api key',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
},
|
||||
item: [],
|
||||
auth: {
|
||||
type: 'apikey',
|
||||
apikey: [
|
||||
{
|
||||
key: 'value',
|
||||
value: 'apikey',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: 'apikey',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
event: [
|
||||
{
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
},
|
||||
{
|
||||
listen: 'test',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.root.request.auth).toEqual({
|
||||
mode: 'apikey',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null,
|
||||
apikey: {
|
||||
key: 'apikey',
|
||||
value: 'apikey',
|
||||
placement: 'header'
|
||||
},
|
||||
oauth2: null,
|
||||
digest: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle digest auth at collection level', () => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Collection level digest auth',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
},
|
||||
item: [],
|
||||
auth: {
|
||||
type: 'digest',
|
||||
digest: [
|
||||
{
|
||||
key: 'password',
|
||||
value: 'digest auth',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
value: 'digest auth',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'algorithm',
|
||||
value: 'MD5',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
event: [
|
||||
{
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
},
|
||||
{
|
||||
listen: 'test',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.root.request.auth).toEqual({
|
||||
mode: 'digest',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: {
|
||||
username: 'digest auth',
|
||||
password: 'digest auth'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,247 +0,0 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import postmanToBruno from '../../../src/postman/postman-to-bruno';
|
||||
|
||||
describe('Folder Authentication', () => {
|
||||
it('should handle basic auth at folder level', () => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Folder level basic auth',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'folder',
|
||||
item: [],
|
||||
auth: {
|
||||
type: 'basic',
|
||||
basic: [
|
||||
{
|
||||
key: 'password',
|
||||
value: 'testpass',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
value: 'testuser',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
event: [
|
||||
{
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
},
|
||||
{
|
||||
listen: 'test',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].root.request.auth).toEqual({
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: 'testuser',
|
||||
password: 'testpass'
|
||||
},
|
||||
bearer: null,
|
||||
awsv4: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle bearer token auth at folder level', () => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Folder level bearer token',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'folder',
|
||||
item: [],
|
||||
auth: {
|
||||
type: 'bearer',
|
||||
bearer: [
|
||||
{
|
||||
key: 'token',
|
||||
value: 'token',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
event: [
|
||||
{
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
},
|
||||
{
|
||||
listen: 'test',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].root.request.auth).toEqual({
|
||||
mode: 'bearer',
|
||||
basic: null,
|
||||
bearer: { token: 'token' },
|
||||
awsv4: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle API key auth at folder level', () => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Folder level API key',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'folder',
|
||||
item: [],
|
||||
auth: {
|
||||
type: 'apikey',
|
||||
apikey: [
|
||||
{
|
||||
key: 'value',
|
||||
value: 'apikey',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: 'apikey',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
event: [
|
||||
{
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
},
|
||||
{
|
||||
listen: 'test',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].root.request.auth).toEqual({
|
||||
mode: 'apikey',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null,
|
||||
apikey: { key: 'apikey', value: 'apikey', placement: 'header' },
|
||||
oauth2: null,
|
||||
digest: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle digest auth at folder level', () => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Folder level digest auth',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'folder',
|
||||
item: [],
|
||||
auth: {
|
||||
type: 'digest',
|
||||
digest: [
|
||||
{
|
||||
key: 'password',
|
||||
value: 'digest pass',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
value: 'digest user',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'algorithm',
|
||||
value: 'MD5',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
event: [
|
||||
{
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
},
|
||||
{
|
||||
listen: 'test',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
exec: ['']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].root.request.auth).toEqual({
|
||||
mode: 'digest',
|
||||
basic: null,
|
||||
bearer: null,
|
||||
awsv4: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: { username: 'digest user', password: 'digest pass' }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,186 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// Simple Collection (postman)
|
||||
// ├── folder
|
||||
// │ └── request (GET)
|
||||
// └── request (GET)
|
||||
|
||||
const postmanCollection = {
|
||||
"info": {
|
||||
"_postman_id": "7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9",
|
||||
"name": "simple collection",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_exporter_id": "21992467",
|
||||
"_collection_link": "https://random-user-007.postman.co/workspace/testing~7523f559-3d5f-4c30-8315-3cb3c3ff98b7/collection/21992467-7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9?action=share&source=collection_link&creator=007"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "folder",
|
||||
"item": [
|
||||
{
|
||||
"name": "request",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://usebruno.com",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"usebruno",
|
||||
"com"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "request",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://usebruno.com",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"usebruno",
|
||||
"com"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Simple Collection (bruno)
|
||||
// ├── folder
|
||||
// │ └── request (GET)
|
||||
// └── request (GET)
|
||||
|
||||
const expectedOutput = {
|
||||
"name": "simple collection",
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"version": "1",
|
||||
"items": [
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "folder",
|
||||
"type": "folder",
|
||||
"items": [
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "request",
|
||||
"type": "http-request",
|
||||
"request": {
|
||||
"url": "https://usebruno.com",
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null,
|
||||
"apikey": null,
|
||||
"oauth2": null,
|
||||
"digest": null
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "none",
|
||||
"json": null,
|
||||
"text": null,
|
||||
"xml": null,
|
||||
"formUrlEncoded": [],
|
||||
"multipartForm": []
|
||||
},
|
||||
"docs": ""
|
||||
},
|
||||
"seq": 1
|
||||
}
|
||||
],
|
||||
"root": {
|
||||
"docs": "",
|
||||
"meta": {
|
||||
"name": "folder"
|
||||
},
|
||||
"request": {
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null,
|
||||
"apikey": null,
|
||||
"oauth2": null,
|
||||
"digest": null
|
||||
},
|
||||
"headers": [],
|
||||
"script": {},
|
||||
"tests": "",
|
||||
"vars": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "mockeduuidvalue123456",
|
||||
"name": "request",
|
||||
"type": "http-request",
|
||||
"request": {
|
||||
"url": "https://usebruno.com",
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null,
|
||||
"apikey": null,
|
||||
"oauth2": null,
|
||||
"digest": null
|
||||
},
|
||||
"headers": [],
|
||||
"params": [],
|
||||
"body": {
|
||||
"mode": "none",
|
||||
"json": null,
|
||||
"text": null,
|
||||
"xml": null,
|
||||
"formUrlEncoded": [],
|
||||
"multipartForm": []
|
||||
},
|
||||
"docs": ""
|
||||
},
|
||||
"seq": 1
|
||||
}
|
||||
],
|
||||
"environments": [],
|
||||
"root": {
|
||||
"docs": "",
|
||||
"meta": {
|
||||
"name": "simple collection"
|
||||
},
|
||||
"request": {
|
||||
"auth": {
|
||||
"mode": "none",
|
||||
"basic": null,
|
||||
"bearer": null,
|
||||
"awsv4": null,
|
||||
"apikey": null,
|
||||
"oauth2": null,
|
||||
"digest": null
|
||||
},
|
||||
"headers": [],
|
||||
"script": {},
|
||||
"tests": "",
|
||||
"vars": {}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
const { default: postmanTranslation } = require("../../../../src/postman/postman-translations");
|
||||
|
||||
describe('postmanTranslations - request commands', () => {
|
||||
test('should handle request commands', () => {
|
||||
const inputScript = `
|
||||
const requestUrl = pm.request.url;
|
||||
const requestMethod = pm.request.method;
|
||||
const requestHeaders = pm.request.headers;
|
||||
const requestBody = pm.request.body;
|
||||
|
||||
pm.test('Request method is POST', function() {
|
||||
pm.expect(pm.request.method).to.equal('POST');
|
||||
});
|
||||
`;
|
||||
const expectedOutput = `
|
||||
const requestUrl = req.getUrl();
|
||||
const requestMethod = req.getMethod();
|
||||
const requestHeaders = req.getHeaders();
|
||||
const requestBody = req.getBody();
|
||||
|
||||
test('Request method is POST', function() {
|
||||
expect(req.getMethod()).to.equal('POST');
|
||||
});
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
const { default: postmanTranslation } = require("../../../../src/postman/postman-translations");
|
||||
|
||||
describe('postmanTranslations - response commands', () => {
|
||||
test('should handle response commands', () => {
|
||||
const inputScript = `
|
||||
const responseTime = pm.response.responseTime;
|
||||
const responseCode = pm.response.code;
|
||||
const responseText = pm.response.text();
|
||||
const responseJson = pm.response.json();
|
||||
const responseStatus = pm.response.status;
|
||||
const responseHeaders = pm.response.headers;
|
||||
|
||||
pm.test('Status code is 200', function() {
|
||||
pm.response.to.have.status(200);
|
||||
});
|
||||
`;
|
||||
const expectedOutput = `
|
||||
const responseTime = res.getResponseTime();
|
||||
const responseCode = res.getStatus();
|
||||
const responseText = JSON.stringify(res.getBody());
|
||||
const responseJson = res.getBody();
|
||||
const responseStatus = res.statusText;
|
||||
const responseHeaders = req.getHeaders();
|
||||
|
||||
test('Status code is 200', function() {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import postmanToBruno from '../../../src/postman/postman-to-bruno';
|
||||
|
||||
describe('Request Authentication', () => {
|
||||
it('should handle basic auth at request level', () => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Request Auth Collection',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'Basic Auth Request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/test',
|
||||
auth: {
|
||||
type: 'basic',
|
||||
basic: [
|
||||
{ key: 'username', value: 'requestuser' },
|
||||
{ key: 'password', value: 'requestpass' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].request.auth).toEqual({
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: 'requestuser',
|
||||
password: 'requestpass'
|
||||
},
|
||||
bearer: null,
|
||||
awsv4: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should inherit folder auth when request has no auth', () => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Inherit Request Auth Collection',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'Auth Folder',
|
||||
auth: {
|
||||
type: 'bearer',
|
||||
bearer: [{ key: 'token', value: 'foldertoken' }]
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'No Auth Request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/test'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].items[0].request.auth).toEqual({
|
||||
mode: 'bearer',
|
||||
basic: null,
|
||||
bearer: {
|
||||
token: 'foldertoken'
|
||||
},
|
||||
awsv4: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should override folder auth with request auth', () => {
|
||||
const postmanCollection = {
|
||||
info: {
|
||||
name: 'Override Request Auth Collection',
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'Auth Folder',
|
||||
auth: {
|
||||
type: 'basic',
|
||||
basic: [
|
||||
{ key: 'username', value: 'folderuser' },
|
||||
{ key: 'password', value: 'folderpass' }
|
||||
]
|
||||
},
|
||||
item: [
|
||||
{
|
||||
name: 'Override Auth Request',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/test',
|
||||
auth: {
|
||||
type: 'bearer',
|
||||
bearer: [{ key: 'token', value: 'requesttoken' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = postmanToBruno(postmanCollection);
|
||||
|
||||
expect(result.items[0].items[0].request.auth).toEqual({
|
||||
mode: 'bearer',
|
||||
basic: null,
|
||||
bearer: {
|
||||
token: 'requesttoken'
|
||||
},
|
||||
awsv4: null,
|
||||
apikey: null,
|
||||
oauth2: null,
|
||||
digest: null
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user