Compare commits

..

1 Commits

Author SHA1 Message Date
ramki-bruno
5af8034c74 Added Playwright-codegen setup 2025-04-01 17:58:35 +05:30
146 changed files with 2937 additions and 7640 deletions

View File

@@ -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

View File

@@ -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: |

View File

@@ -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.

View File

@@ -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
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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;

View File

@@ -35,7 +35,6 @@ if (!SERVER_RENDERED) {
'res.getHeader(name)',
'res.getHeaders()',
'res.getBody()',
'res.setBody(data)',
'res.getResponseTime()',
'req',
'req.url',

View File

@@ -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(() => {

View File

@@ -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(() => {

View File

@@ -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>
);
};

View File

@@ -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">

View File

@@ -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" />

View File

@@ -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!');
}
};

View File

@@ -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;

View File

@@ -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" />

View File

@@ -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} />;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
)

View File

@@ -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} />

View File

@@ -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;

View File

@@ -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">

View File

@@ -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;

View File

@@ -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} />

View File

@@ -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';

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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}
/>

View File

@@ -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}

View File

@@ -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 })

View File

@@ -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;

View File

@@ -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;
});

View File

@@ -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)) {

View File

@@ -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' });

View File

@@ -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);
});
})

View File

@@ -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';

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
});
});

View File

@@ -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 = {};
});
});
};

View File

@@ -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;

View 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);
});

View File

@@ -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;

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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:

View File

@@ -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) {

View File

@@ -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') {

View File

@@ -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 || {};

View File

@@ -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);

View File

@@ -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;
}

View 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);
});
});

View 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));
});
});

View File

@@ -1,6 +0,0 @@
module.exports = {
presets: [
['@babel/preset-env', { modules: 'auto' }],
'@babel/preset-typescript',
],
};

View File

@@ -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'
};

View File

@@ -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"
}
}

View File

@@ -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()]
}
];

View File

@@ -1 +1,5 @@
export { default as interpolate } from './interpolate';
import interpolate from './interpolate';
export default {
interpolate
};

View File

@@ -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"}');
})
})

View File

@@ -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;

View File

@@ -1,4 +0,0 @@
import { generateHtmlReport } from "./reports/html/generate-report";
import { getRunnerSummary } from "./runner-summary";
export { generateHtmlReport, getRunnerSummary };

View File

@@ -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 }

View File

@@ -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;

View File

@@ -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,
};
};

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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"));
}
});
});

View File

@@ -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()
};

View 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 });
});
});

View 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;
}, {});
};

View File

@@ -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"]
}

View File

@@ -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*

View File

@@ -1,3 +0,0 @@
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
};

View File

@@ -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'
}
};

View File

@@ -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
};
});

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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);
```

View File

@@ -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') }]
})
]
}
];

View File

@@ -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 = '';
}
});
});
};

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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",
};

View File

@@ -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",
};

View File

@@ -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",
};

View File

@@ -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'
}
]
});
});
});

View File

@@ -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'
);
});
});

View 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'
}
});
});
});

View File

@@ -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' }
});
});
});

View File

@@ -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": {}
}
}
};

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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