mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 20:01:28 +00:00
Merge remote-tracking branch 'origin/main' into oauth2_additional_params
This commit is contained in:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
||||
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno
|
||||
* @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno
|
||||
|
||||
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@@ -30,6 +30,7 @@ jobs:
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
npm run build --workspace=packages/bruno-filestore
|
||||
|
||||
- name: Lint Check
|
||||
run: npm run lint
|
||||
@@ -80,6 +81,7 @@ jobs:
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
npm run build --workspace=packages/bruno-filestore
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
@@ -125,6 +127,7 @@ jobs:
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
npm run build:bruno-filestore
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
|
||||
302
package-lock.json
generated
302
package-lock.json
generated
@@ -18,7 +18,8 @@
|
||||
"packages/bruno-tests",
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs",
|
||||
"packages/bruno-requests"
|
||||
"packages/bruno-requests",
|
||||
"packages/bruno-filestore"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
@@ -3650,9 +3651,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get/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==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8725,6 +8726,10 @@
|
||||
"integrity": "sha512-khvEnRF6/UVDw4df06j+6lFWGNDYWlcWnxfmEgU2o/CdsGY291NC1Cexz99ud7sbGBQP2d8JUXZe4zXPkGNJpQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@usebruno/filestore": {
|
||||
"resolved": "packages/bruno-filestore",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@usebruno/graphql-docs": {
|
||||
"resolved": "packages/bruno-graphql-docs",
|
||||
"link": true
|
||||
@@ -11519,6 +11524,33 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-regexp": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz",
|
||||
"integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-regexp": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-regexp/node_modules/is-regexp": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz",
|
||||
"integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-response": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||
@@ -11951,6 +11983,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-hrtime": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz",
|
||||
"integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -13423,15 +13467,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "33.2.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-33.2.1.tgz",
|
||||
"integrity": "sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==",
|
||||
"version": "37.2.6",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-37.2.6.tgz",
|
||||
"integrity": "sha512-Ns6xyxE+hIK5UlujtRlw7w4e2Ju/ImCWXf1Q/PoOhc0N3/6SN6YW7+ujCarsHbxWnolbW+1RlkHtdklUJpjbPA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/node": "^22.7.7",
|
||||
"extract-zip": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
@@ -13656,23 +13700,6 @@
|
||||
"integrity": "sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron/node_modules/@types/node": {
|
||||
"version": "20.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz",
|
||||
"integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/elliptic": {
|
||||
"version": "6.6.1",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
|
||||
@@ -14849,9 +14876,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip/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==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -15556,6 +15583,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/function-timeout": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz",
|
||||
"integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/functions-have-names": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||
@@ -15819,9 +15858,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent/node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
@@ -26252,9 +26291,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sumchecker/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==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -26276,6 +26315,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/super-regex": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz",
|
||||
"integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"clone-regexp": "^3.0.0",
|
||||
"function-timeout": "^0.1.0",
|
||||
"time-span": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -26808,6 +26864,21 @@
|
||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/time-span": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz",
|
||||
"integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"convert-hrtime": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/timers-browserify": {
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
|
||||
@@ -26905,6 +26976,7 @@
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
||||
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
@@ -26920,6 +26992,7 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
@@ -29922,6 +29995,7 @@
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/filestore": "^0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
@@ -29941,7 +30015,6 @@
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
},
|
||||
@@ -31015,6 +31088,7 @@
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-jest": "^29.7.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"moment": "^2.29.4",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
@@ -31535,6 +31609,34 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/ip-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/is-ip": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.1.tgz",
|
||||
"integrity": "sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ip-regex": "^5.0.0",
|
||||
"super-regex": "^0.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -31576,7 +31678,7 @@
|
||||
"@web/rollup-plugin-copy": "^0.5.1",
|
||||
"babel-jest": "^29.7.0",
|
||||
"rimraf": "^5.0.7",
|
||||
"rollup": "3.2.5",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
@@ -31664,23 +31766,6 @@
|
||||
"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",
|
||||
@@ -31688,6 +31773,7 @@
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/filestore": "^0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/node-machine-id": "^2.0.0",
|
||||
@@ -31720,12 +31806,11 @@
|
||||
"nanoid": "3.3.8",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"uuid": "^9.0.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "33.2.1",
|
||||
"electron": "~37.2.6",
|
||||
"electron-builder": "25.1.8",
|
||||
"electron-devtools-installer": "^4.0.0"
|
||||
},
|
||||
@@ -32803,6 +32888,113 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore": {
|
||||
"name": "@usebruno/filestore",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.0",
|
||||
"@babel/preset-typescript": "^7.22.0",
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^24.1.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"jest": "^29.2.0",
|
||||
"rimraf": "^3.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-filestore/node_modules/@types/node": {
|
||||
"version": "24.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore/node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore/node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore/node_modules/undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-graphql-docs": {
|
||||
"name": "@usebruno/graphql-docs",
|
||||
"version": "0.1.0",
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"packages/bruno-tests",
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs",
|
||||
"packages/bruno-requests"
|
||||
"packages/bruno-requests",
|
||||
"packages/bruno-filestore"
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"devDependencies": {
|
||||
@@ -40,6 +41,7 @@
|
||||
"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\"",
|
||||
"watch": "npm run dev:watch",
|
||||
"dev:watch": "node ./scripts/dev-hot-reload.js",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
@@ -48,6 +50,7 @@
|
||||
"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-filestore": "npm run build --workspace=packages/bruno-filestore",
|
||||
"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",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const AwsV4Auth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -131,7 +135,7 @@ const AwsV4Auth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<div className="single-line-editor-wrapper mb-2 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
@@ -140,6 +144,7 @@ const AwsV4Auth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="awsv4-secret-access-key" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const BasicAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = get(collection, 'root.request.auth.basic', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -55,7 +59,7 @@ const BasicAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -64,6 +68,7 @@ const BasicAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const BearerAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const bearerToken = get(collection, 'root.request.auth.bearer.token', '');
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(bearerToken);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -30,7 +34,7 @@ const BearerAuth = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
theme={storedTheme}
|
||||
@@ -39,6 +43,7 @@ const BearerAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const DigestAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = get(collection, 'root.request.auth.digest', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -55,7 +59,7 @@ const DigestAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -64,6 +68,7 @@ const DigestAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -18,6 +20,8 @@ const NTLMAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = get(collection, 'root.request.auth.ntlm', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -82,7 +86,7 @@ const NTLMAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -91,6 +95,7 @@ const NTLMAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="ntlm-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Domain</label>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const WsseAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = get(collection, 'root.request.auth.wsse', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -55,14 +59,16 @@ const WsseAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ const Docs = ({ collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-1 h-full w-full relative flex flex-col">
|
||||
<StyledWrapper className="h-full w-full relative flex flex-col">
|
||||
<div className='flex flex-row w-full justify-between items-center mb-4'>
|
||||
<div className='text-lg font-medium flex items-center gap-2'>
|
||||
<IconFileText size={20} strokeWidth={1.5} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
@@ -7,19 +7,30 @@ import { useTheme } from 'providers/Theme';
|
||||
import {
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader
|
||||
deleteCollectionHeader,
|
||||
setCollectionHeaders
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
import BulkEditor from 'components/BulkEditor/index';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(collection, 'root.request.headers', []);
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
setIsBulkEditMode(!isBulkEditMode);
|
||||
};
|
||||
|
||||
const handleBulkHeadersChange = (newHeaders) => {
|
||||
dispatch(setCollectionHeaders({ collectionUid: collection.uid, headers: newHeaders }));
|
||||
};
|
||||
|
||||
const addHeader = () => {
|
||||
dispatch(
|
||||
@@ -63,6 +74,22 @@ const Headers = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
if (isBulkEditMode) {
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Add request headers that will be sent with every request in this collection.
|
||||
</div>
|
||||
<BulkEditor
|
||||
params={headers}
|
||||
onChange={handleBulkHeadersChange}
|
||||
onToggle={toggleBulkEditMode}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
@@ -141,9 +168,14 @@ const Headers = ({ collection }) => {
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<div className="flex justify-between mt-2">
|
||||
<button className="btn-add-header text-link pr-2 py-3 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<button className="text-link select-none" onClick={toggleBulkEditMode}>
|
||||
Bulk Edit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
|
||||
@@ -132,7 +132,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4 overflow-scroll">
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4 overflow-hidden">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('overview')} role="tab" onClick={() => setTab('overview')}>
|
||||
Overview
|
||||
@@ -169,7 +169,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
{clientCertConfig.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
</div>
|
||||
<section className="mt-4 h-full overflow-scroll">{getTabPanel(tab)}</section>
|
||||
<section className="mt-4 h-full overflow-auto">{getTabPanel(tab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const sensitiveFields = [
|
||||
'request.auth.oauth2.clientSecret',
|
||||
'request.auth.basic.password',
|
||||
'request.auth.digest.password',
|
||||
'request.auth.wsse.password',
|
||||
'request.auth.ntlm.password',
|
||||
'request.auth.awsv4.secretAccessKey',
|
||||
'request.auth.bearer.token'
|
||||
];
|
||||
|
||||
export { sensitiveFields };
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useMemo } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { get } from 'lodash';
|
||||
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -13,7 +14,9 @@ import { variableNameRegex } from 'utils/common/regex';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { getGlobalEnvironmentVariables, flattenItems, isItemARequest } from 'utils/collections';
|
||||
import { sensitiveFields } from './constants';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -26,6 +29,50 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const nonSecretSensitiveVarUsageMap = useMemo(() => {
|
||||
const result = {};
|
||||
if (!collection || !environment?.variables) {
|
||||
return result;
|
||||
}
|
||||
const nonSecretVars = environment.variables.filter((v) => v.enabled && !v.secret && v.name);
|
||||
if (!nonSecretVars.length) {
|
||||
return result;
|
||||
}
|
||||
const varNames = new Set(nonSecretVars.map((v) => v.name));
|
||||
|
||||
const checkSensitiveField = (obj, fieldPath) => {
|
||||
const value = get(obj, fieldPath);
|
||||
if (typeof value === 'string') {
|
||||
varNames.forEach((varName) => {
|
||||
if (new RegExp(`\{\{\s*${varName}\s*\}\}`).test(value)) {
|
||||
result[varName] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getObjectToProcess = (item) => {
|
||||
if (isItemARequest(item)) {
|
||||
return item.draft || item;
|
||||
}
|
||||
return item.root;
|
||||
};
|
||||
|
||||
const collectionObj = getObjectToProcess(collection);
|
||||
sensitiveFields.forEach((fieldPath) => {
|
||||
checkSensitiveField(collectionObj, fieldPath);
|
||||
});
|
||||
|
||||
const items = flattenItems(collection.items || []);
|
||||
items.forEach((item) => {
|
||||
const objToProcess = getObjectToProcess(item);
|
||||
sensitiveFields.forEach((fieldPath) => {
|
||||
checkSensitiveField(objToProcess, fieldPath);
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}, [collection, environment]);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: environment.variables || [],
|
||||
@@ -61,6 +108,8 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
}
|
||||
});
|
||||
|
||||
const hasSensitiveUsage = (name) => !!nonSecretSensitiveVarUsageMap[name];
|
||||
|
||||
// Effect to track modifications.
|
||||
React.useEffect(() => {
|
||||
setIsModified(formik.dirty);
|
||||
@@ -163,7 +212,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap">
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
@@ -174,6 +223,12 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
</div>
|
||||
{!variable.secret && hasSensitiveUsage(variable.name) && (
|
||||
<SensitiveFieldWarning
|
||||
fieldName={variable.name}
|
||||
warningMessage="This variable is used in sensitive fields. Mark it as a secret for security"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<input
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addFolderHeader, updateFolderHeader, deleteFolderHeader } from 'providers/ReduxStore/slices/collections';
|
||||
import { addFolderHeader, updateFolderHeader, deleteFolderHeader, setFolderHeaders } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
import BulkEditor from 'components/BulkEditor/index';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
setIsBulkEditMode(!isBulkEditMode);
|
||||
};
|
||||
|
||||
const handleBulkHeadersChange = (newHeaders) => {
|
||||
dispatch(setFolderHeaders({ collectionUid: collection.uid, folderUid: folder.uid, headers: newHeaders }));
|
||||
};
|
||||
|
||||
const addHeader = () => {
|
||||
dispatch(
|
||||
@@ -62,6 +72,22 @@ const Headers = ({ collection, folder }) => {
|
||||
);
|
||||
};
|
||||
|
||||
if (isBulkEditMode) {
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Request headers that will be sent with every request inside this folder.
|
||||
</div>
|
||||
<BulkEditor
|
||||
params={headers}
|
||||
onChange={handleBulkHeadersChange}
|
||||
onToggle={toggleBulkEditMode}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
@@ -141,9 +167,14 @@ const Headers = ({ collection, folder }) => {
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<div className="flex justify-between mt-2">
|
||||
<button className="btn-add-header text-link pr-2 py-3 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<button className="text-link select-none" onClick={toggleBulkEditMode}>
|
||||
Bulk Edit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
|
||||
@@ -74,7 +74,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full overflow-scroll">
|
||||
<StyledWrapper className="flex flex-col h-full overflow-auto">
|
||||
<div className="flex flex-col h-full relative px-4 py-4">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
@@ -101,7 +101,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
Docs
|
||||
</div>
|
||||
</div>
|
||||
<section className={`flex mt-4 h-full overflow-scroll`}>{getTabPanel(tab)}</section>
|
||||
<section className={`flex mt-4 h-full overflow-auto`}>{getTabPanel(tab)}</section>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -6,13 +6,16 @@ import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { update } from 'lodash';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
|
||||
const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = get(request, 'auth.awsv4', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -144,7 +147,7 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<div className="single-line-editor-wrapper mb-2 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
@@ -155,6 +158,8 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
|
||||
{showWarning && <SensitiveFieldWarning fieldName="awsv4-secret-access-key" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = get(request, 'auth.basic', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -63,7 +67,7 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -74,6 +78,7 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -13,6 +15,8 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
|
||||
// Use the request prop directly like OAuth2ClientCredentials does
|
||||
const bearerToken = get(request, 'auth.bearer.token', '');
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(bearerToken);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -36,7 +40,7 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
theme={storedTheme}
|
||||
@@ -47,6 +51,7 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -11,6 +13,8 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = get(request, 'auth.digest', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -62,7 +66,7 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -73,6 +77,7 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = get(request, 'auth.ntlm', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -80,7 +84,7 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -91,6 +95,7 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="ntlm-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Domain</label>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -10,13 +11,14 @@ import { inputsConfig } from './inputsConfig';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import AdditionalParams from '../AdditionalParams/index';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const {
|
||||
callbackUrl,
|
||||
@@ -133,12 +135,15 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
const value = oAuth[key] || '';
|
||||
const { showWarning, warningMessage } = isSensitive(value);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="single-line-editor-wrapper flex-1 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -147,6 +152,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -10,13 +11,14 @@ import Dropdown from 'components/Dropdown';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import AdditionalParams from '../AdditionalParams/index';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
|
||||
const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
|
||||
const {
|
||||
@@ -99,12 +101,15 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
const value = oAuth[key] || '';
|
||||
const { showWarning, warningMessage } = isSensitive(value);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="single-line-editor-wrapper flex-1 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -113,6 +118,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -10,14 +11,15 @@ import Dropdown from 'components/Dropdown';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import AdditionalParams from '../AdditionalParams/index';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';
|
||||
|
||||
const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
|
||||
const {
|
||||
accessTokenUrl,
|
||||
@@ -102,12 +104,15 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
const value = oAuth[key] || '';
|
||||
const { showWarning, warningMessage } = isSensitive(value);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="single-line-editor-wrapper flex-1 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -116,6 +121,7 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = get(request, 'auth.wsse', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -63,7 +67,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -74,6 +78,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" message={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -109,7 +109,7 @@ const Auth = ({ item, collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-1 overflow-y-scroll">
|
||||
<StyledWrapper className="w-full mt-1 overflow-auto">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<AuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
|
||||
@@ -176,7 +176,7 @@ const ResponsePane = ({ item, collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={`flex flex-col min-h-0 relative px-4 auto overflow-scroll`}
|
||||
className={`flex flex-col min-h-0 relative px-4 auto overflow-auto`}
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
height: hasScriptError && showScriptErrorCard ? 'auto' : '100%'
|
||||
|
||||
@@ -96,7 +96,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative overflow-scroll">
|
||||
<StyledWrapper className="flex flex-col h-full relative overflow-auto">
|
||||
<div className="flex items-center tabs overflow-visible" role="tablist">
|
||||
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
|
||||
Response
|
||||
@@ -128,7 +128,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
<ResponseSize size={size} />
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex flex-col flex-grow overflow-scroll">
|
||||
<section className="flex flex-col flex-grow overflow-auto">
|
||||
{hasScriptError && showScriptErrorCard && (
|
||||
<ScriptError
|
||||
item={item}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
background-color: ${props => props.theme.sidebar.bg};
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid ${props => props.theme.sidebar.dragbar};
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.counter {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-select-all,
|
||||
.btn-reset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: ${props => props.theme.textLink};
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.request-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: ${props => props.theme.console.scrollbarThumb};
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.loading-message,
|
||||
.empty-message {
|
||||
padding: 0.75rem;
|
||||
color: ${props => props.theme.colors.text.muted};
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.requests-container {
|
||||
padding: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.request-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.25rem;
|
||||
position: relative;
|
||||
height: 2.5rem;
|
||||
border: 1px solid transparent;
|
||||
background-color: ${props => props.theme.sidebar.bg};
|
||||
transition: transform 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&.is-selected {
|
||||
background-color: ${props => props.theme.requestTabs.active.bg};
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
opacity: 0.5;
|
||||
background-color: ${props => props.theme.sidebar.bg};
|
||||
border: 1px dashed ${props => props.theme.sidebar.dragbar};
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink};
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
&.drop-target-above {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
height: 2px;
|
||||
background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
&.drop-target-below {
|
||||
&::after {
|
||||
opacity: 1;
|
||||
height: 2px;
|
||||
background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
margin-right: 0.25rem;
|
||||
color: ${props => props.theme.sidebar.muted};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: ${props => props.theme.text};
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
color: ${props => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
|
||||
.checkbox {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 1px solid ${props => props.theme.sidebar.dragbar};
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: ${props => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.method {
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-right: 0.5rem;
|
||||
min-width: 3rem;
|
||||
color: ${props => props.theme.sidebar.muted}; // Default color for unknown methods
|
||||
|
||||
&.method-get {
|
||||
color: ${props => props.theme.request.methods.get};
|
||||
}
|
||||
|
||||
&.method-post {
|
||||
color: ${props => props.theme.request.methods.post};
|
||||
}
|
||||
|
||||
&.method-put {
|
||||
color: ${props => props.theme.request.methods.put};
|
||||
}
|
||||
|
||||
&.method-delete {
|
||||
color: ${props => props.theme.request.methods.delete};
|
||||
}
|
||||
|
||||
&.method-patch {
|
||||
color: ${props => props.theme.request.methods.patch};
|
||||
}
|
||||
|
||||
&.method-options {
|
||||
color: ${props => props.theme.request.methods.options};
|
||||
}
|
||||
|
||||
&.method-head {
|
||||
color: ${props => props.theme.request.methods.head};
|
||||
}
|
||||
}
|
||||
|
||||
.request-name {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.folder-path {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: ${props => props.theme.sidebar.muted};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,327 @@
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { IconGripVertical, IconCheck, IconAdjustmentsAlt } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isItemARequest } from 'utils/collections';
|
||||
import path from 'utils/common/path';
|
||||
import { cloneDeep, get } from 'lodash';
|
||||
|
||||
const ItemTypes = {
|
||||
REQUEST_ITEM: 'request-item'
|
||||
};
|
||||
|
||||
const RequestItem = ({ item, index, moveItem, isSelected, onSelect, onDrop }) => {
|
||||
const ref = useRef(null);
|
||||
const [dropType, setDropType] = useState(null);
|
||||
|
||||
const determineDropType = (monitor) => {
|
||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
if (!hoverBoundingRect || !clientOffset) return null;
|
||||
|
||||
const clientY = clientOffset.y - hoverBoundingRect.top;
|
||||
const middleY = hoverBoundingRect.height / 2;
|
||||
|
||||
return clientY < middleY ? 'above' : 'below';
|
||||
};
|
||||
|
||||
const [{ isDragging }, drag, preview] = useDrag({
|
||||
type: ItemTypes.REQUEST_ITEM,
|
||||
item: { uid: item.uid, name: item.name, request: item.request, index },
|
||||
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
|
||||
options: {
|
||||
dropEffect: "move"
|
||||
},
|
||||
end: (draggedItem, monitor) => {
|
||||
if (monitor.didDrop()) {
|
||||
onDrop();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [{ isOver, canDrop }, drop] = useDrop({
|
||||
accept: ItemTypes.REQUEST_ITEM,
|
||||
hover: (draggedItem, monitor) => {
|
||||
if (draggedItem.uid === item.uid) {
|
||||
setDropType(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const dropType = determineDropType(monitor);
|
||||
setDropType(dropType);
|
||||
},
|
||||
drop: (draggedItem, monitor) => {
|
||||
if (draggedItem.uid === item.uid) return;
|
||||
|
||||
const dropType = determineDropType(monitor);
|
||||
let targetIndex = index;
|
||||
|
||||
if (dropType === 'below') {
|
||||
targetIndex = index + 1;
|
||||
}
|
||||
|
||||
if (draggedItem.index < targetIndex) {
|
||||
targetIndex = targetIndex - 1;
|
||||
}
|
||||
|
||||
moveItem(draggedItem.uid, targetIndex);
|
||||
setDropType(null);
|
||||
return { item: draggedItem };
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop()
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
preview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, []);
|
||||
|
||||
// Clear drop type when not hovering
|
||||
useEffect(() => {
|
||||
if (!isOver) {
|
||||
setDropType(null);
|
||||
}
|
||||
}, [isOver]);
|
||||
|
||||
drag(drop(ref));
|
||||
|
||||
const itemClasses = [
|
||||
'request-item',
|
||||
isDragging ? 'is-dragging' : '',
|
||||
isSelected ? 'is-selected' : '',
|
||||
isOver && canDrop && dropType === 'above' ? 'drop-target-above' : '',
|
||||
isOver && canDrop && dropType === 'below' ? 'drop-target-below' : ''
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div ref={ref} className={itemClasses}>
|
||||
<div className="drag-handle">
|
||||
<IconGripVertical size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<div className="checkbox-container" onClick={() => onSelect(item)}>
|
||||
<div className="checkbox">
|
||||
{isSelected && <IconCheck size={12} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`method method-${item.request?.method.toLowerCase()}`}>
|
||||
{item.request?.method.toUpperCase()}
|
||||
</div>
|
||||
|
||||
<div className="request-name">
|
||||
<span>{item.name}</span>
|
||||
{item.folderPath && (
|
||||
<span className="folder-path">{item.folderPath}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [flattenedRequests, setFlattenedRequests] = useState([]);
|
||||
const [originalRequests, setOriginalRequests] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const flattenRequests = useCallback((collection) => {
|
||||
const result = [];
|
||||
|
||||
const processItems = (items) => {
|
||||
if (!items?.length) return;
|
||||
|
||||
items.forEach(item => {
|
||||
if (isItemARequest(item) && !item.partial) {
|
||||
const relativePath = path.relative(collection.pathname, path.dirname(item.pathname));
|
||||
const folderPath = relativePath !== '.' ? relativePath : '';
|
||||
|
||||
result.push({
|
||||
...item,
|
||||
folderPath: folderPath.replace(/\\/g, '/')
|
||||
});
|
||||
}
|
||||
|
||||
if (item.items?.length) {
|
||||
processItems(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
processItems(collection.items);
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const structureCopy = cloneDeep(collection);
|
||||
const requests = flattenRequests(structureCopy);
|
||||
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
if (savedConfiguration?.requestItemsOrder?.length > 0) {
|
||||
const orderedRequests = [];
|
||||
const requestMap = new Map(requests.map(req => [req.uid, req]));
|
||||
|
||||
savedConfiguration.requestItemsOrder.forEach(uid => {
|
||||
const request = requestMap.get(uid);
|
||||
if (request) {
|
||||
orderedRequests.push(request);
|
||||
requestMap.delete(uid);
|
||||
}
|
||||
});
|
||||
|
||||
requestMap.forEach(request => {
|
||||
orderedRequests.push(request);
|
||||
});
|
||||
|
||||
setFlattenedRequests(orderedRequests);
|
||||
} else {
|
||||
setFlattenedRequests(requests);
|
||||
}
|
||||
|
||||
setOriginalRequests(cloneDeep(requests));
|
||||
} catch (error) {
|
||||
console.error("Error loading collection structure:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [collection, flattenRequests]);
|
||||
|
||||
const moveItem = useCallback((draggedItemUid, hoverIndex) => {
|
||||
setFlattenedRequests((prevRequests) => {
|
||||
const dragIndex = prevRequests.findIndex(item => item.uid === draggedItemUid);
|
||||
|
||||
if (dragIndex === -1 || dragIndex === hoverIndex) {
|
||||
return prevRequests;
|
||||
}
|
||||
|
||||
const updatedRequests = [...prevRequests];
|
||||
const [draggedItem] = updatedRequests.splice(dragIndex, 1);
|
||||
updatedRequests.splice(hoverIndex, 0, draggedItem);
|
||||
|
||||
return updatedRequests;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback(() => {
|
||||
const selectedUids = new Set(selectedItems);
|
||||
|
||||
setFlattenedRequests(currentRequests => {
|
||||
const newOrderedSelectedUids = currentRequests
|
||||
.filter(item => selectedUids.has(item.uid))
|
||||
.map(item => item.uid);
|
||||
|
||||
const allRequestUidsOrder = currentRequests.map(item => item.uid);
|
||||
|
||||
setSelectedItems(newOrderedSelectedUids);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, newOrderedSelectedUids, allRequestUidsOrder));
|
||||
|
||||
return currentRequests;
|
||||
});
|
||||
}, [selectedItems, collection.uid, dispatch, setSelectedItems]);
|
||||
|
||||
const handleRequestSelect = useCallback((item) => {
|
||||
try {
|
||||
if (selectedItems.includes(item.uid)) {
|
||||
const newSelectedUids = selectedItems.filter(uid => uid !== item.uid);
|
||||
setSelectedItems(newSelectedUids);
|
||||
|
||||
const allRequestUidsOrder = flattenedRequests.map(item => item.uid);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, newSelectedUids, allRequestUidsOrder));
|
||||
} else {
|
||||
const newSelectedUids = [...selectedItems, item.uid];
|
||||
|
||||
const orderedSelectedUids = flattenedRequests
|
||||
.filter(req => newSelectedUids.includes(req.uid))
|
||||
.map(req => req.uid);
|
||||
|
||||
setSelectedItems(orderedSelectedUids);
|
||||
|
||||
const allRequestUidsOrder = flattenedRequests.map(item => item.uid);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, orderedSelectedUids, allRequestUidsOrder));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error selecting item:", error);
|
||||
}
|
||||
}, [selectedItems, setSelectedItems, flattenedRequests, dispatch, collection.uid]);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
try {
|
||||
const allRequestUidsOrder = flattenedRequests.map(item => item.uid);
|
||||
|
||||
if (selectedItems.length === flattenedRequests.length) {
|
||||
setSelectedItems([]);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, [], allRequestUidsOrder));
|
||||
} else {
|
||||
setSelectedItems(allRequestUidsOrder);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, allRequestUidsOrder, allRequestUidsOrder));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error selecting/deselecting all items:", error);
|
||||
}
|
||||
}, [flattenedRequests, selectedItems, setSelectedItems, dispatch, collection.uid]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
try {
|
||||
setFlattenedRequests(cloneDeep(originalRequests));
|
||||
setSelectedItems([]);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, [], []));
|
||||
} catch (error) {
|
||||
console.error("Error resetting configuration:", error);
|
||||
}
|
||||
}, [originalRequests, setSelectedItems, collection.uid, dispatch]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="header">
|
||||
<div className="counter">
|
||||
{selectedItems.length} of {flattenedRequests.length} selected
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button className="btn-select-all" onClick={handleSelectAll}>
|
||||
{selectedItems.length === flattenedRequests.length ? "Deselect All" : "Select All"}
|
||||
</button>
|
||||
<button className="btn-reset" onClick={handleReset} title="Reset selection and order">
|
||||
<IconAdjustmentsAlt size={16} strokeWidth={1.5} />
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="request-list">
|
||||
{isLoading ? (
|
||||
<div className="loading-message">Loading requests...</div>
|
||||
) : flattenedRequests.length === 0 ? (
|
||||
<div className="empty-message">No requests found in this collection</div>
|
||||
) : (
|
||||
<div className="requests-container">
|
||||
{flattenedRequests.map((item, idx) => {
|
||||
const isSelected = selectedItems.includes(item.uid);
|
||||
|
||||
return (
|
||||
<RequestItem
|
||||
key={item.uid}
|
||||
item={item}
|
||||
index={idx}
|
||||
isSelected={isSelected}
|
||||
onSelect={() => handleRequestSelect(item)}
|
||||
moveItem={moveItem}
|
||||
onDrop={handleDrop}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunConfigurationPanel;
|
||||
@@ -89,13 +89,15 @@ const RunnerTags = ({ collectionUid, className = '' }) => {
|
||||
return (
|
||||
<div className={`mt-6 flex flex-col ${className}`}>
|
||||
<div className="flex gap-2">
|
||||
<label className="block font-medium">Filter requests with tags</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
id="filter-tags"
|
||||
type="radio"
|
||||
name="filterMode"
|
||||
checked={tagsEnabled}
|
||||
onChange={() => setTagsEnabled(!tagsEnabled)}
|
||||
/>
|
||||
<label htmlFor="filter-tags" className="block font-medium">Filter requests with tags</label>
|
||||
</div>
|
||||
{tagsEnabled && (
|
||||
<div className="flex flex-row mt-4 gap-4 w-full">
|
||||
|
||||
@@ -2,15 +2,17 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import path from 'utils/common/path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { runCollectionFolder, cancelRunnerExecution, mountCollection, updateRunnerConfiguration } 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, IconCircleOff, IconCheck, IconX, IconRun, IconLoader2 } from '@tabler/icons';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import RunnerTags from './RunnerTags/index';
|
||||
import RunConfigurationPanel from './RunConfigurationPanel';
|
||||
import { getRequestItemsForCollectionRun } from 'utils/collections/index';
|
||||
import { updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const getDisplayName = (fullPath, pathname, name = '') => {
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
@@ -25,25 +27,27 @@ const getTestStatus = (results) => {
|
||||
};
|
||||
|
||||
const allTestsPassed = (item) => {
|
||||
return item.status !== 'error' &&
|
||||
item.testStatus === 'pass' &&
|
||||
item.assertionStatus === 'pass' &&
|
||||
item.preRequestTestStatus === 'pass' &&
|
||||
item.postResponseTestStatus === 'pass';
|
||||
return item.status !== 'error' &&
|
||||
item.testStatus === 'pass' &&
|
||||
item.assertionStatus === 'pass' &&
|
||||
item.preRequestTestStatus === 'pass' &&
|
||||
item.postResponseTestStatus === 'pass';
|
||||
};
|
||||
|
||||
const anyTestFailed = (item) => {
|
||||
return item.status === 'error' ||
|
||||
item.testStatus === 'fail' ||
|
||||
item.assertionStatus === 'fail' ||
|
||||
item.preRequestTestStatus === 'fail' ||
|
||||
item.postResponseTestStatus === 'fail';
|
||||
return item.status === 'error' ||
|
||||
item.testStatus === 'fail' ||
|
||||
item.assertionStatus === 'fail' ||
|
||||
item.preRequestTestStatus === 'fail' ||
|
||||
item.postResponseTestStatus === 'fail';
|
||||
};
|
||||
|
||||
export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [delay, setDelay] = useState(null);
|
||||
const [selectedRequestItems, setSelectedRequestItems] = useState([]);
|
||||
const [configureMode, setConfigureMode] = useState(false);
|
||||
|
||||
// ref for the runner output body
|
||||
const runnerBodyRef = useRef();
|
||||
@@ -62,6 +66,22 @@ export default function RunnerResults({ collection }) {
|
||||
autoScrollRunnerBody();
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
useEffect(() => {
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
if (runnerInfo.status === 'running') {
|
||||
setConfigureMode(false);
|
||||
}
|
||||
}, [collection.runnerResult]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
if (savedConfiguration && configureMode) {
|
||||
if (savedConfiguration.selectedRequestItems) {
|
||||
setSelectedRequestItems(savedConfiguration.selectedRequestItems);
|
||||
}
|
||||
}
|
||||
}, [collection.runnerConfiguration, configureMode]);
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
|
||||
@@ -103,18 +123,39 @@ export default function RunnerResults({ collection }) {
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const ensureCollectionIsMounted = () => {
|
||||
if(collection.mountStatus === 'mounted'){
|
||||
return;
|
||||
}
|
||||
dispatch(mountCollection({
|
||||
collectionUid: collection.uid,
|
||||
collectionPathname: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig
|
||||
}));
|
||||
};
|
||||
|
||||
const runCollection = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));
|
||||
if (configureMode && selectedRequestItems.length > 0) {
|
||||
dispatch(updateRunnerConfiguration(collection.uid, selectedRequestItems, selectedRequestItems));
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags, selectedRequestItems));
|
||||
} else {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));
|
||||
}
|
||||
};
|
||||
|
||||
const runAgain = () => {
|
||||
ensureCollectionIsMounted();
|
||||
// Get the saved configuration to determine what to run
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
const savedSelectedItems = savedConfiguration?.selectedRequestItems || [];
|
||||
dispatch(
|
||||
runCollectionFolder(
|
||||
collection.uid,
|
||||
runnerInfo.folderUid,
|
||||
runnerInfo.isRecursive,
|
||||
true,
|
||||
Number(delay),
|
||||
tagsEnabled && tags
|
||||
tagsEnabled && tags,
|
||||
savedSelectedItems
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -125,12 +166,25 @@ export default function RunnerResults({ collection }) {
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
setSelectedRequestItems([]);
|
||||
setConfigureMode(false);
|
||||
};
|
||||
|
||||
const cancelExecution = () => {
|
||||
dispatch(cancelRunnerExecution(runnerInfo.cancelTokenUid));
|
||||
};
|
||||
|
||||
const toggleConfigureMode = () => {
|
||||
dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tagsEnabled: false }));
|
||||
setConfigureMode(!configureMode);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(tagsEnabled) {
|
||||
setConfigureMode(false);
|
||||
}
|
||||
}, [tagsEnabled]);
|
||||
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
|
||||
const passedRequests = items.filter(allTestsPassed);
|
||||
const failedRequests = items.filter(anyTestFailed);
|
||||
@@ -142,59 +196,104 @@ export default function RunnerResults({ collection }) {
|
||||
|
||||
if (!items || !items.length) {
|
||||
return (
|
||||
<StyledWrapper className="px-4 pb-4">
|
||||
<div className="font-medium mt-6 title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
|
||||
</div>
|
||||
{isCollectionLoading ? <div className='my-1 danger'>Requests in this collection are still loading.</div> : null}
|
||||
<div className="mt-6">
|
||||
<label>Delay (in ms)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="block textbox mt-2 py-5"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={delay}
|
||||
onChange={(e) => setDelay(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<StyledWrapper className="pl-4 overflow-hidden h-full">
|
||||
<div className="flex overflow-hidden max-h-full h-full">
|
||||
<div className={`${configureMode ? 'w-1/2 pr-4' : 'w-full'}`}>
|
||||
<div className="font-medium mt-6 title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
|
||||
{isCollectionLoading && (
|
||||
<span className="ml-2 text-sm text-gray-500">
|
||||
(Loading...)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isCollectionLoading ? <div className='my-1 danger'>Requests in this collection are still loading.</div> : null}
|
||||
<div className="mt-6">
|
||||
<label>Delay (in ms)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="block textbox mt-2 py-5"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={delay}
|
||||
onChange={(e) => setDelay(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tags for the collection run */}
|
||||
<RunnerTags collectionUid={collection.uid} className='mb-6' />
|
||||
{/* Tags for the collection run */}
|
||||
<RunnerTags collectionUid={collection.uid} className='mb-6' />
|
||||
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" disabled={shouldDisableCollectionRun} onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
{/* Configure requests option */}
|
||||
<div className="flex flex-col border-b pb-6 mb-6 border-gray-200 dark:border-gray-700">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
id="filter-config"
|
||||
type="radio"
|
||||
name="filterMode"
|
||||
checked={configureMode}
|
||||
onChange={toggleConfigureMode}
|
||||
/>
|
||||
<label htmlFor="filter-config" className="block font-medium">Configure requests to run</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
|
||||
Reset
|
||||
</button>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-sm btn-secondary"
|
||||
disabled={shouldDisableCollectionRun || (configureMode && selectedRequestItems.length === 0) || isCollectionLoading}
|
||||
onClick={runCollection}
|
||||
>
|
||||
{configureMode && selectedRequestItems.length > 0
|
||||
? `Run ${selectedRequestItems.length} Selected Request${selectedRequestItems.length > 1 ? 's' : ''}`
|
||||
: "Run Collection"
|
||||
}
|
||||
</button>
|
||||
|
||||
<button className="submit btn btn-sm btn-close" onClick={resetRunner}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{configureMode && (
|
||||
<div className="w-1/2 border-l border-gray-200 dark:border-gray-700">
|
||||
<RunConfigurationPanel
|
||||
collection={collection}
|
||||
selectedItems={selectedRequestItems}
|
||||
setSelectedItems={setSelectedRequestItems}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 pb-4 flex flex-grow flex-col relative overflow-scroll">
|
||||
<div className="flex flex-row">
|
||||
<div className="font-medium my-6 title flex items-center">
|
||||
<StyledWrapper className="px-4 pb-4 flex flex-grow flex-col relative overflow-auto">
|
||||
<div className="flex items-center my-6 flex-row">
|
||||
<div className="font-medium title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
{runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid && (
|
||||
<button className="btn ml-6 my-4 btn-sm btn-danger" onClick={cancelExecution}>
|
||||
<button className="btn btn-sm btn-danger" onClick={cancelExecution}>
|
||||
Cancel Execution
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 h-[calc(100%_-_4.375rem)]">
|
||||
|
||||
<div className="flex gap-4 h-[calc(100vh_-_10rem)] overflow-hidden">
|
||||
<div
|
||||
className="flex flex-col flex-1 overflow-y-auto w-full"
|
||||
className={`flex flex-col overflow-y-auto ${selectedItem || (configureMode && !selectedItem && !runnerInfo.status === 'running') ? 'w-1/2' : 'w-full'}`}
|
||||
ref={runnerBodyRef}
|
||||
>
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
@@ -214,57 +313,59 @@ export default function RunnerResults({ collection }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{runnerInfo?.statusText ?
|
||||
{runnerInfo?.statusText ?
|
||||
<div className="pb-2 font-medium danger">
|
||||
{runnerInfo?.statusText}
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{allTestsPassed(item) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{item.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
{anyTestFailed(item) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : anyTestFailed(item) ? 'danger' : ''}`}
|
||||
>
|
||||
{item.displayName}
|
||||
</span>
|
||||
{item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
|
||||
) : item.responseReceived?.status ? (
|
||||
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
<span className="mr-1">{item.responseReceived?.status}</span>
|
||||
-
|
||||
<span>{item.responseReceived?.statusText}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(request failed)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{tagsEnabled && areTagsAdded && item?.tags?.length > 0 && (
|
||||
<div className="pl-7 text-xs text-gray-500">
|
||||
Tags: {item.tags.filter(t => tags.include.includes(t)).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
|
||||
: null}
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.preRequestTestResults
|
||||
? item.preRequestTestResults.map((result) => (
|
||||
{/* Items list */}
|
||||
<div className="overflow-y-auto flex-1">
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{allTestsPassed(item) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{item.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{anyTestFailed(item) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : anyTestFailed(item) ? 'danger' : ''}`}
|
||||
>
|
||||
{item.displayName}
|
||||
</span>
|
||||
{item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
|
||||
) : item.responseReceived?.status ? (
|
||||
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
<span className="mr-1">{item.responseReceived?.status}</span>
|
||||
-
|
||||
<span>{item.responseReceived?.statusText}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(request failed)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{tagsEnabled && areTagsAdded && item?.tags?.length > 0 && (
|
||||
<div className="pl-7 text-xs text-gray-500">
|
||||
Tags: {item.tags.filter(t => tags.include.includes(t)).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.preRequestTestResults
|
||||
? item.preRequestTestResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -282,9 +383,9 @@ export default function RunnerResults({ collection }) {
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.postResponseTestResults
|
||||
? item.postResponseTestResults.map((result) => (
|
||||
: null}
|
||||
{item.postResponseTestResults
|
||||
? item.postResponseTestResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -302,9 +403,9 @@ export default function RunnerResults({ collection }) {
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.testResults
|
||||
? item.testResults.map((result) => (
|
||||
: null}
|
||||
{item.testResults
|
||||
? item.testResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -322,30 +423,32 @@ export default function RunnerResults({ collection }) {
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.assertionResults?.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
: null}
|
||||
{item.assertionResults?.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{runnerInfo.status === 'ended' ? (
|
||||
<div className="mt-2 mb-4">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runAgain}>
|
||||
@@ -366,15 +469,15 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="flex items-center mb-4 font-medium">
|
||||
<span className="mr-2">{selectedItem.displayName}</span>
|
||||
<span>
|
||||
{allTestsPassed(selectedItem) ?
|
||||
{allTestsPassed(selectedItem) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{anyTestFailed(selectedItem) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
: null}
|
||||
{anyTestFailed(selectedItem) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
: null}
|
||||
</span>
|
||||
</div>
|
||||
<ResponsePane item={selectedItem} collection={collection} />
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.tooltip-mod {
|
||||
font-size: 11px !important;
|
||||
width: 150px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const SensitiveFieldWarning = ({ fieldName, warningMessage }) => {
|
||||
const tooltipId = `sensitive-field-warning-${fieldName}`;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconAlertTriangle id={tooltipId} className="text-amber-600 cursor-pointer" size={20} />
|
||||
<Tooltip
|
||||
anchorId={tooltipId}
|
||||
className="tooltip-mod max-w-lg"
|
||||
content={
|
||||
<div>
|
||||
<p>
|
||||
<span>{warningMessage}</span>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SensitiveFieldWarning;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import { IconDownload } from '@tabler/icons';
|
||||
import { IconDownload, IconLoader2 } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Bruno from 'components/Bruno';
|
||||
import exportBrunoCollection from 'utils/collections/export';
|
||||
@@ -8,10 +8,12 @@ import exportPostmanCollection from 'utils/exporters/postman-collection';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
import { findCollectionByUid, areItemsLoading } from 'utils/collections/index';
|
||||
|
||||
const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const isCollectionLoading = areItemsLoading(collection);
|
||||
|
||||
const handleExportBrunoCollection = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
|
||||
@@ -35,23 +37,49 @@ const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
>
|
||||
<StyledWrapper className="flex flex-col h-full w-[500px]">
|
||||
<div className="space-y-2">
|
||||
<div className="flex border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-500/10 items-center p-3 rounded-lg transition-colors cursor-pointer" onClick={handleExportBrunoCollection}>
|
||||
<div
|
||||
className={`flex border border-gray-200 dark:border-gray-600 items-center p-3 rounded-lg transition-colors ${
|
||||
isCollectionLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-500/10 cursor-pointer'
|
||||
}`}
|
||||
onClick={isCollectionLoading ? undefined : handleExportBrunoCollection}
|
||||
>
|
||||
<div className="mr-3 p-1 rounded-full">
|
||||
<Bruno width={28} />
|
||||
{isCollectionLoading ? (
|
||||
<IconLoader2 size={28} className="animate-spin" />
|
||||
) : (
|
||||
<Bruno width={28} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">Bruno Collection</div>
|
||||
<div className="text-xs">Export in Bruno format</div>
|
||||
<div className="text-xs">
|
||||
{isCollectionLoading ? 'Loading collection...' : 'Export in Bruno format'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-500/10 items-center p-3 rounded-lg transition-colors cursor-pointer" onClick={handleExportPostmanCollection}>
|
||||
<div
|
||||
className={`flex border border-gray-200 dark:border-gray-600 items-center p-3 rounded-lg transition-colors ${
|
||||
isCollectionLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-500/10 cursor-pointer'
|
||||
}`}
|
||||
onClick={isCollectionLoading ? undefined : handleExportPostmanCollection}
|
||||
>
|
||||
<div className="mr-3 p-1 rounded-full">
|
||||
<IconDownload size={28} strokeWidth={1} className="" />
|
||||
{isCollectionLoading ? (
|
||||
<IconLoader2 size={28} className="animate-spin" />
|
||||
) : (
|
||||
<IconDownload size={28} strokeWidth={1} className="" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">Postman Collection</div>
|
||||
<div className="text-xs">Export in Postman format</div>
|
||||
<div className="text-xs">
|
||||
{isCollectionLoading ? 'Loading collection...' : 'Export in Postman format'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { getAllVariables, getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { resolveInheritedAuth } from './utils/auth-utils';
|
||||
|
||||
const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
@@ -37,12 +37,11 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
const requestUrl =
|
||||
get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
const variables = getAllVariables(collection, item);
|
||||
|
||||
const interpolatedUrl = interpolateUrl({
|
||||
url: requestUrl,
|
||||
globalEnvironmentVariables,
|
||||
envVars,
|
||||
runtimeVariables: collection.runtimeVariables,
|
||||
processEnvVars: collection.processEnvVariables
|
||||
variables
|
||||
});
|
||||
|
||||
// interpolate the path params
|
||||
|
||||
@@ -69,24 +69,3 @@ export const interpolateBody = (body, variables = {}) => {
|
||||
|
||||
return interpolatedBody;
|
||||
};
|
||||
|
||||
export const createVariablesObject = ({
|
||||
globalEnvironmentVariables = {},
|
||||
collectionVars = {},
|
||||
allVariables = {},
|
||||
collection = {},
|
||||
runtimeVariables = {},
|
||||
processEnvVars = {}
|
||||
}) => {
|
||||
return {
|
||||
...globalEnvironmentVariables,
|
||||
...allVariables,
|
||||
...collectionVars,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { getAuthHeaders } from 'utils/codegenerator/auth';
|
||||
import { getAllVariables, getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import { interpolateHeaders, interpolateBody, createVariablesObject } from './interpolation';
|
||||
import { interpolateHeaders, interpolateBody } from './interpolation';
|
||||
|
||||
// Merge headers from collection, folders, and request
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
@@ -46,17 +46,7 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
|
||||
// Get HTTPSnippet dynamically so mocks can be applied in tests
|
||||
const { HTTPSnippet } = require('httpsnippet');
|
||||
|
||||
const allVariables = getAllVariables(collection, item);
|
||||
|
||||
// Create variables object for interpolation
|
||||
const variables = createVariablesObject({
|
||||
globalEnvironmentVariables: collection.globalEnvironmentVariables || {},
|
||||
collectionVars: collection.collectionVars || {},
|
||||
allVariables,
|
||||
collection,
|
||||
runtimeVariables: collection.runtimeVariables || {},
|
||||
processEnvVars: collection.processEnvVariables || {}
|
||||
});
|
||||
const variables = getAllVariables(collection, item);
|
||||
|
||||
const request = item.request;
|
||||
|
||||
|
||||
@@ -46,7 +46,10 @@ jest.mock('utils/codegenerator/auth', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('utils/collections/index', () => ({
|
||||
getAllVariables: jest.fn(() => ({
|
||||
getAllVariables: jest.fn((collection) => ({
|
||||
...collection?.globalEnvironmentVariables,
|
||||
...collection?.runtimeVariables,
|
||||
...collection?.processEnvVariables,
|
||||
baseUrl: 'https://api.example.com',
|
||||
apiKey: 'secret-key-123',
|
||||
userId: '12345'
|
||||
|
||||
@@ -61,13 +61,14 @@ const Collection = ({ collection, searchText }) => {
|
||||
};
|
||||
|
||||
const ensureCollectionIsMounted = () => {
|
||||
if (collection.mountStatus === 'unmounted') {
|
||||
dispatch(mountCollection({
|
||||
collectionUid: collection.uid,
|
||||
collectionPathname: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig
|
||||
}));
|
||||
if(collection.mountStatus === 'mounted'){
|
||||
return;
|
||||
}
|
||||
dispatch(mountCollection({
|
||||
collectionUid: collection.uid,
|
||||
collectionPathname: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig
|
||||
}));
|
||||
}
|
||||
|
||||
const hasSearchText = searchText && searchText?.trim()?.length;
|
||||
@@ -269,6 +270,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
ensureCollectionIsMounted();
|
||||
handleRun();
|
||||
}}
|
||||
>
|
||||
@@ -287,6 +289,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
ensureCollectionIsMounted();
|
||||
setShowShareCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -5,12 +5,12 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
height: 22px;
|
||||
padding: 0 1rem;
|
||||
height: 1.5rem;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
border-top: 1px solid ${(props) => props.theme.statusBar.border};
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.statusBar.color};
|
||||
font-size: 0.75rem;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
@@ -32,9 +32,7 @@ const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
@@ -43,13 +41,11 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
gap: 0.25rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.console-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -66,17 +62,13 @@ const StyledWrapper = styled.div`
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: ${(props) => props.theme.sidebar.dragbar};
|
||||
margin: 0 8px;
|
||||
opacity: 0.3;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.status-bar-version {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -71,18 +71,6 @@ const StatusBar = () => {
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
<ToolHint text="Cookies" toolhintId="Cookies" place="top" offset={10}>
|
||||
<button
|
||||
className="status-bar-button"
|
||||
data-trigger="cookies"
|
||||
onClick={() => setCookiesOpen(true)}
|
||||
tabIndex={0}
|
||||
aria-label="Open Cookies Settings"
|
||||
>
|
||||
<IconCookie size={16} strokeWidth={1.5} aria-hidden="true" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
<ToolHint text="Notifications" toolhintId="Notifications" place="top" offset={10}>
|
||||
<div className="status-bar-button">
|
||||
<Notifications />
|
||||
@@ -92,7 +80,20 @@ const StatusBar = () => {
|
||||
</div>
|
||||
|
||||
<div className="status-bar-section">
|
||||
<div className="status-bar-group">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className="status-bar-button"
|
||||
data-trigger="cookies"
|
||||
onClick={() => setCookiesOpen(true)}
|
||||
tabIndex={0}
|
||||
aria-label="Open Cookies"
|
||||
>
|
||||
<div className="console-button-content">
|
||||
<IconCookie size={16} strokeWidth={1.5} aria-hidden="true" />
|
||||
<span className="console-label">Cookies</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`status-bar-button ${errorCount > 0 ? 'has-errors' : ''}`}
|
||||
data-trigger="dev-tools"
|
||||
@@ -100,13 +101,13 @@ const StatusBar = () => {
|
||||
tabIndex={0}
|
||||
aria-label={`Open Dev Tools${errorCount > 0 ? ` (${errorCount} errors)` : ''}`}
|
||||
>
|
||||
<div className="console-button-content">
|
||||
<IconTool size={16} strokeWidth={1.5} aria-hidden="true" />
|
||||
<span className="console-label">Dev Tools</span>
|
||||
{errorCount > 0 && (
|
||||
<span className="error-count-inline">{errorCount}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="console-button-content">
|
||||
<IconTool size={16} strokeWidth={1.5} aria-hidden="true" />
|
||||
<span className="console-label">Dev Tools</span>
|
||||
{errorCount > 0 && (
|
||||
<span className="error-count-inline">{errorCount}</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="status-bar-divider"></div>
|
||||
|
||||
@@ -6,7 +6,7 @@ const StyledWrapper = styled.div`
|
||||
display: grid;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
padding: 0 1px;
|
||||
padding: 0 1.5px;
|
||||
|
||||
// for icon hover
|
||||
position: inherit;
|
||||
|
||||
@@ -89,7 +89,7 @@ const VariablesEditor = ({ collection }) => {
|
||||
const reactInspectorTheme = storedTheme === 'light' ? 'chromeLight' : 'chromeDark';
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 py-4 overflow-scroll">
|
||||
<StyledWrapper className="px-4 py-4 overflow-auto">
|
||||
<RuntimeVariables collection={collection} theme={reactInspectorTheme} />
|
||||
<EnvVariables collection={collection} theme={reactInspectorTheme} />
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const VARIABLE_NAME_REGEX = /\{\{([^}]+)\}\}/g;
|
||||
const ENV_VAR_REFERENCE_REGEX = /^\s*\{\{.*\}\}\s*$/;
|
||||
|
||||
export const useDetectSensitiveField = (collection) => {
|
||||
const envVars = useMemo(() => {
|
||||
if (!collection) {
|
||||
return [];
|
||||
}
|
||||
const activeEnv = collection?.environments?.find((env) => env.uid === collection.activeEnvironmentUid);
|
||||
if (!activeEnv || !Array.isArray(activeEnv.variables)) {
|
||||
return [];
|
||||
}
|
||||
return activeEnv.variables;
|
||||
}, [collection]);
|
||||
|
||||
// Checks if the value is a single environment variable reference (e.g., {{API_KEY}})
|
||||
const isEnvVarReference = (value) => {
|
||||
return typeof value === 'string' && ENV_VAR_REFERENCE_REGEX.test(value);
|
||||
};
|
||||
|
||||
// Extracts all variable names from a string (e.g., "Bearer {{TOKEN}}-{{SUFFIX}}" → ["TOKEN", "SUFFIX"])
|
||||
const extractVarNames = (value) => {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const matches = [];
|
||||
let match;
|
||||
while ((match = VARIABLE_NAME_REGEX.exec(value)) !== null) {
|
||||
matches.push(match[1].trim());
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
|
||||
// Checks if a variable is present and not marked as secret in the environment
|
||||
const isVarNotSecret = (varName, envVars = []) => {
|
||||
const found = envVars.find((v) => v.name === varName);
|
||||
return found && !found.secret;
|
||||
};
|
||||
|
||||
const isSensitive = (value) => {
|
||||
if (value && !isEnvVarReference(value)) {
|
||||
return {
|
||||
showWarning: true,
|
||||
warningMessage: 'Store sensitive info as a secret variable or in a .env file'
|
||||
};
|
||||
}
|
||||
|
||||
if (value && typeof value === 'string') {
|
||||
const varNames = extractVarNames(value);
|
||||
if (varNames.some((varName) => isVarNotSecret(varName, envVars))) {
|
||||
return {
|
||||
showWarning: true,
|
||||
warningMessage: 'Mark the environment variable as secret for better security.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// No warning needed
|
||||
return { showWarning: false };
|
||||
};
|
||||
|
||||
return {
|
||||
isSensitive
|
||||
};
|
||||
};
|
||||
@@ -24,7 +24,7 @@ import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { collectionAddOauth2CredentialsByUrl } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { collectionAddOauth2CredentialsByUrl, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { addLog } from 'providers/ReduxStore/slices/logs';
|
||||
|
||||
const useIpcEvents = () => {
|
||||
@@ -179,6 +179,10 @@ const useIpcEvents = () => {
|
||||
dispatch(collectionAddOauth2CredentialsByUrl(payload));
|
||||
});
|
||||
|
||||
const removeCollectionLoadingStateListener = ipcRenderer.on('main:collection-loading-state-updated', (val) => {
|
||||
dispatch(updateCollectionLoadingState(val));
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeCollectionTreeUpdateListener();
|
||||
removeOpenCollectionListener();
|
||||
@@ -199,6 +203,7 @@ const useIpcEvents = () => {
|
||||
removeGlobalEnvironmentsUpdatesListener();
|
||||
removeSnapshotHydrationListener();
|
||||
removeCollectionOauth2CredentialsUpdatesListener();
|
||||
removeCollectionLoadingStateListener();
|
||||
};
|
||||
}, [isElectron]);
|
||||
};
|
||||
|
||||
@@ -27,22 +27,25 @@ taskMiddleware.startListening({
|
||||
each(openRequestTasks, (task) => {
|
||||
if (collectionUid === task.collectionUid) {
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
const item = findItemInCollectionByPathname(collection, task.itemPathname);
|
||||
if (item) {
|
||||
listenerApi.dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
listenerApi.dispatch(hideHomePage());
|
||||
listenerApi.dispatch(
|
||||
removeTaskFromQueue({
|
||||
taskUid: task.uid
|
||||
})
|
||||
);
|
||||
if (collection && collection.mountStatus === 'mounted' && !collection.isLoading) {
|
||||
const item = findItemInCollectionByPathname(collection, task.itemPathname);
|
||||
if (item) {
|
||||
listenerApi.dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
listenerApi.dispatch(hideHomePage());
|
||||
}
|
||||
}
|
||||
|
||||
listenerApi.dispatch(
|
||||
removeTaskFromQueue({
|
||||
taskUid: task.uid
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const initialState = {
|
||||
isEnvironmentSettingsModalOpen: false,
|
||||
preferences: {
|
||||
request: {
|
||||
sslVerification: false,
|
||||
sslVerification: true,
|
||||
customCaCertificate: {
|
||||
enabled: false,
|
||||
filePath: null
|
||||
|
||||
@@ -38,7 +38,8 @@ import {
|
||||
setCollectionSecurityConfig,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
initRunRequestEvent
|
||||
initRunRequestEvent,
|
||||
updateRunnerConfiguration as _updateRunnerConfiguration
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
@@ -316,9 +317,9 @@ export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
|
||||
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
export const runCollectionFolder = (collectionUid, folderUid, recursive, delay, tags) => (dispatch, getState) => {
|
||||
export const runCollectionFolder = (collectionUid, folderUid, recursive, delay, tags, selectedRequestUids) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -346,6 +347,26 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay,
|
||||
})
|
||||
);
|
||||
|
||||
// to only include those requests in the specified order while preserving folder data
|
||||
if (selectedRequestUids && selectedRequestUids.length > 0) {
|
||||
const newItems = [];
|
||||
|
||||
selectedRequestUids.forEach((uid, index) => {
|
||||
const requestItem = findItemInCollection(collectionCopy, uid);
|
||||
if (requestItem) {
|
||||
const clonedRequest = cloneDeep(requestItem);
|
||||
clonedRequest.seq = index + 1;
|
||||
newItems.push(clonedRequest);
|
||||
}
|
||||
});
|
||||
|
||||
if (folder) {
|
||||
folder.items = newItems;
|
||||
} else {
|
||||
collectionCopy.items = newItems;
|
||||
}
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke(
|
||||
@@ -1064,7 +1085,7 @@ export const removeCollection = (collectionUid) => (dispatch, getState) => {
|
||||
}
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('renderer:remove-collection', collection.pathname)
|
||||
.invoke('renderer:remove-collection', collection.pathname, collectionUid)
|
||||
.then(() => {
|
||||
dispatch(closeAllCollectionTabs({ collectionUid }));
|
||||
})
|
||||
@@ -1373,3 +1394,11 @@ export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig
|
||||
ipcRenderer.invoke('renderer:show-in-folder', collectionPath).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const updateRunnerConfiguration = (collectionUid, selectedRequestItems, requestItemsOrder) => (dispatch) => {
|
||||
dispatch(_updateRunnerConfiguration({
|
||||
collectionUid,
|
||||
selectedRequestItems,
|
||||
requestItemsOrder
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -67,6 +67,12 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
updateCollectionLoadingState: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
if (collection) {
|
||||
collection.isLoading = action.payload.isLoading;
|
||||
}
|
||||
},
|
||||
setCollectionSecurityConfig: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
if (collection) {
|
||||
@@ -867,6 +873,43 @@ export const collectionsSlice = createSlice({
|
||||
enabled: enabled
|
||||
}));
|
||||
},
|
||||
setCollectionHeaders: (state, action) => {
|
||||
const { collectionUid, headers } = action.payload;
|
||||
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
collection.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
description: '',
|
||||
enabled: enabled
|
||||
}));
|
||||
},
|
||||
setFolderHeaders: (state, action) => {
|
||||
const { collectionUid, folderUid, headers } = action.payload;
|
||||
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const folder = findItemInCollection(collection, folderUid);
|
||||
if (!folder || !isItemAFolder(folder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
folder.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
description: '',
|
||||
enabled: enabled
|
||||
}));
|
||||
},
|
||||
addFormUrlEncodedParam: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -2232,6 +2275,7 @@ export const collectionsSlice = createSlice({
|
||||
collection.runnerResult = null;
|
||||
collection.runnerTags = { include: [], exclude: [] }
|
||||
collection.runnerTagsEnabled = false;
|
||||
collection.runnerConfiguration = null;
|
||||
}
|
||||
},
|
||||
updateRunnerTagsDetails: (state, action) => {
|
||||
@@ -2246,6 +2290,16 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRunnerConfiguration: (state, action) => {
|
||||
const { collectionUid, selectedRequestItems, requestItemsOrder } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (collection) {
|
||||
collection.runnerConfiguration = {
|
||||
selectedRequestItems: selectedRequestItems || [],
|
||||
requestItemsOrder: requestItemsOrder || []
|
||||
};
|
||||
}
|
||||
},
|
||||
updateRequestDocs: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -2413,6 +2467,7 @@ export const collectionsSlice = createSlice({
|
||||
export const {
|
||||
createCollection,
|
||||
updateCollectionMountStatus,
|
||||
updateCollectionLoadingState,
|
||||
setCollectionSecurityConfig,
|
||||
brunoConfigUpdateEvent,
|
||||
renameCollection,
|
||||
@@ -2454,6 +2509,8 @@ export const {
|
||||
deleteRequestHeader,
|
||||
moveRequestHeader,
|
||||
setRequestHeaders,
|
||||
setCollectionHeaders,
|
||||
setFolderHeaders,
|
||||
addFormUrlEncodedParam,
|
||||
updateFormUrlEncodedParam,
|
||||
deleteFormUrlEncodedParam,
|
||||
@@ -2516,6 +2573,7 @@ export const {
|
||||
runFolderEvent,
|
||||
resetCollectionRunner,
|
||||
updateRunnerTagsDetails,
|
||||
updateRunnerConfiguration,
|
||||
updateRequestDocs,
|
||||
updateFolderDocs,
|
||||
moveCollection,
|
||||
|
||||
@@ -299,6 +299,7 @@ const darkTheme = {
|
||||
|
||||
statusBar: {
|
||||
border: '#323233',
|
||||
color: 'rgb(169, 169, 169)'
|
||||
},
|
||||
console: {
|
||||
bg: '#1e1e1e',
|
||||
|
||||
@@ -300,6 +300,7 @@ const lightTheme = {
|
||||
|
||||
statusBar: {
|
||||
border: '#E9E9E9',
|
||||
color: 'rgb(100, 100, 100)'
|
||||
},
|
||||
console: {
|
||||
bg: '#f8f9fa',
|
||||
|
||||
@@ -37,6 +37,7 @@ const STATIC_API_HINTS = {
|
||||
'res.headers',
|
||||
'res.body',
|
||||
'res.responseTime',
|
||||
'res.url',
|
||||
'res.getStatus()',
|
||||
'res.getStatusText()',
|
||||
'res.getHeader(name)',
|
||||
@@ -48,6 +49,7 @@ const STATIC_API_HINTS = {
|
||||
'res.getSize().header',
|
||||
'res.getSize().body',
|
||||
'res.getSize().total',
|
||||
'res.getUrl()'
|
||||
],
|
||||
bru: [
|
||||
'bru',
|
||||
@@ -78,7 +80,17 @@ const STATIC_API_HINTS = {
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.interpolate(str)'
|
||||
'bru.interpolate(str)',
|
||||
'bru.cookies',
|
||||
'bru.cookies.jar()',
|
||||
'bru.cookies.jar().getCookie(url, name, callback)',
|
||||
'bru.cookies.jar().getCookies(url, callback)',
|
||||
'bru.cookies.jar().setCookie(url, name, value, callback)',
|
||||
'bru.cookies.jar().setCookie(url, cookieObject, callback)',
|
||||
'bru.cookies.jar().setCookies(url, cookiesArray, callback)',
|
||||
'bru.cookies.jar().clear(callback)',
|
||||
'bru.cookies.jar().deleteCookies(url, callback)',
|
||||
'bru.cookies.jar().deleteCookie(url, name, callback)',
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -137,6 +137,10 @@ export const findEnvironmentInCollectionByName = (collection, name) => {
|
||||
};
|
||||
|
||||
export const areItemsLoading = (folder) => {
|
||||
if (!folder || folder.isLoading) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let flattenedItems = flattenItems(folder.items);
|
||||
return flattenedItems?.reduce((isLoading, i) => {
|
||||
if (i?.loading) {
|
||||
|
||||
@@ -69,21 +69,12 @@ export const isValidUrl = (url) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const interpolateUrl = ({ url, globalEnvironmentVariables = {}, envVars, runtimeVariables, processEnvVars }) => {
|
||||
export const interpolateUrl = ({ url, variables }) => {
|
||||
if (!url || !url.length || typeof url !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
return interpolate(url, {
|
||||
...globalEnvironmentVariables,
|
||||
...envVars,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
});
|
||||
return interpolate(url, variables);
|
||||
};
|
||||
|
||||
export const interpolateUrlPathParams = (url, params) => {
|
||||
|
||||
@@ -77,11 +77,7 @@ describe('Url Utils - interpolateUrl, interpolateUrlPathParams', () => {
|
||||
const url = '{{host}}/api/:id/path?foo={{foo}}&bar={{bar}}&baz={{process.env.baz}}';
|
||||
const expectedUrl = 'https://example.com/api/:id/path?foo=foo_value&bar=bar_value&baz=baz_value';
|
||||
|
||||
const envVars = { host: 'https://example.com', foo: 'foo_value' };
|
||||
const runtimeVariables = { bar: 'bar_value' };
|
||||
const processEnvVars = { baz: 'baz_value' };
|
||||
|
||||
const result = interpolateUrl({ url, envVars, runtimeVariables, processEnvVars });
|
||||
const result = interpolateUrl({ url, variables: { host: 'https://example.com', foo: 'foo_value', bar: 'bar_value', 'process.env.baz': 'baz_value' } });
|
||||
|
||||
expect(result).toEqual(expectedUrl);
|
||||
});
|
||||
@@ -101,11 +97,7 @@ describe('Url Utils - interpolateUrl, interpolateUrlPathParams', () => {
|
||||
const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }];
|
||||
const expectedUrl = 'https://example.com/api/123/path?foo=foo_value&bar=bar_value&baz=baz_value';
|
||||
|
||||
const envVars = { host: 'https://example.com', foo: 'foo_value' };
|
||||
const runtimeVariables = { bar: 'bar_value' };
|
||||
const processEnvVars = { baz: 'baz_value' };
|
||||
|
||||
const intermediateResult = interpolateUrl({ url, envVars, runtimeVariables, processEnvVars });
|
||||
const intermediateResult = interpolateUrl({ url, variables: { host: 'https://example.com', foo: 'foo_value', bar: 'bar_value', 'process.env.baz': 'baz_value' } });
|
||||
const result = interpolateUrlPathParams(intermediateResult, params);
|
||||
|
||||
expect(result).toEqual(expectedUrl);
|
||||
|
||||
@@ -48,11 +48,12 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/filestore": "^0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "^1.8.3",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
@@ -68,7 +69,6 @@
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
}
|
||||
|
||||
@@ -5,15 +5,16 @@ 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');
|
||||
const { getEnvVars } = require('../utils/bru');
|
||||
const { isRequestTagsIncluded } = require("@usebruno/common")
|
||||
const makeJUnitOutput = require('../reporters/junit');
|
||||
const makeHtmlOutput = require('../reporters/html');
|
||||
const { rpad } = require('../utils/common');
|
||||
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
const { getOptions } = require('../utils/bru');
|
||||
const { parseDotEnv, parseEnvironment } = require('@usebruno/filestore');
|
||||
const constants = require('../constants');
|
||||
const { findItemInCollection, getAllRequestsInFolder, createCollectionJsonFromPathname, getCallStack } = require('../utils/collection');
|
||||
const { findItemInCollection, createCollectionJsonFromPathname, getCallStack } = require('../utils/collection');
|
||||
const { hasExecutableTestInScript } = require('../utils/request');
|
||||
const command = 'run [paths...]';
|
||||
const desc = 'Run one or more requests/folders';
|
||||
|
||||
@@ -346,7 +347,7 @@ const handler = async function (argv) {
|
||||
}
|
||||
|
||||
const envBruContent = fs.readFileSync(envFilePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
const envJson = bruToEnvJson(envBruContent);
|
||||
const envJson = parseEnvironment(envBruContent);
|
||||
envVars = getEnvVars(envJson);
|
||||
envVars.__name__ = envFile ? path.basename(envFilePath, '.bru') : env;
|
||||
}
|
||||
@@ -439,7 +440,7 @@ const handler = async function (argv) {
|
||||
};
|
||||
if (dotEnvExists) {
|
||||
const content = fs.readFileSync(dotEnvPath, 'utf8');
|
||||
const jsonData = dotenvToJson(content);
|
||||
const jsonData = parseDotEnv(content);
|
||||
|
||||
forOwn(jsonData, (value, key) => {
|
||||
processEnvVars[key] = value;
|
||||
@@ -467,10 +468,17 @@ const handler = async function (argv) {
|
||||
requestItems = getCallStack(resolvedPaths, collection, { recursive });
|
||||
|
||||
if (testsOnly) {
|
||||
requestItems = requestItems.filter((iter) => {
|
||||
const requestHasTests = iter.request?.tests;
|
||||
const requestHasActiveAsserts = iter.request?.assertions.some((x) => x.enabled) || false;
|
||||
return requestHasTests || requestHasActiveAsserts;
|
||||
requestItems = requestItems.filter((item) => {
|
||||
const requestHasTests = hasExecutableTestInScript(item.request?.tests);
|
||||
const requestHasActiveAsserts = item.request?.assertions.some((x) => x.enabled) || false;
|
||||
|
||||
const preRequestScript = item.request?.script?.req;
|
||||
const requestHasPreRequestTests = hasExecutableTestInScript(preRequestScript);
|
||||
|
||||
const postResponseScript = item.request?.script?.res;
|
||||
const requestHasPostResponseTests = hasExecutableTestInScript(postResponseScript);
|
||||
|
||||
return requestHasTests || requestHasActiveAsserts || requestHasPreRequestTests || requestHasPostResponseTests;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-he
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent, getSystemProxyEnvVariables } = require('../utils/proxy-util');
|
||||
const path = require('path');
|
||||
const { parseDataFromResponse } = require('../utils/common');
|
||||
const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies');
|
||||
const { getCookieStringForUrl, saveCookies } = require('../utils/cookies');
|
||||
const { createFormData } = require('../utils/form-data');
|
||||
const { getOAuth2Token } = require('./oauth2');
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
@@ -287,6 +287,10 @@ const runSingleRequest = async function (
|
||||
https_proxy,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
} else {
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Invalid system https_proxy');
|
||||
@@ -459,6 +463,7 @@ const runSingleRequest = async function (
|
||||
statusText: null,
|
||||
headers: null,
|
||||
data: null,
|
||||
url: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
|
||||
@@ -598,6 +603,7 @@ const runSingleRequest = async function (
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null,
|
||||
responseTime
|
||||
},
|
||||
error: null,
|
||||
@@ -626,6 +632,7 @@ const runSingleRequest = async function (
|
||||
statusText: null,
|
||||
headers: null,
|
||||
data: null,
|
||||
url: null,
|
||||
responseTime: 0
|
||||
},
|
||||
status: 'error',
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
const _ = require('lodash');
|
||||
const { bruToEnvJsonV2, bruToJsonV2, collectionBruToJson: _collectionBruToJson } = require('@usebruno/lang');
|
||||
const {
|
||||
parseRequest: _parseRequest,
|
||||
parseCollection: _parseCollection
|
||||
} = require('@usebruno/filestore');
|
||||
|
||||
const collectionBruToJson = (bru) => {
|
||||
try {
|
||||
const json = _collectionBruToJson(bru);
|
||||
const json = _parseCollection(bru);
|
||||
|
||||
const transformedJson = {
|
||||
request: {
|
||||
@@ -46,7 +49,7 @@ const collectionBruToJson = (bru) => {
|
||||
*/
|
||||
const bruToJson = (bru) => {
|
||||
try {
|
||||
const json = bruToJsonV2(bru);
|
||||
const json = _parseRequest(bru);
|
||||
|
||||
let requestType = _.get(json, 'meta.type');
|
||||
if (requestType === 'http') {
|
||||
@@ -88,14 +91,6 @@ const bruToJson = (bru) => {
|
||||
}
|
||||
};
|
||||
|
||||
const bruToEnvJson = (bru) => {
|
||||
try {
|
||||
return bruToEnvJsonV2(bru);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getEnvVars = (environment = {}) => {
|
||||
const variables = environment.variables;
|
||||
if (!variables || !variables.length) {
|
||||
@@ -119,7 +114,6 @@ const getOptions = () => {
|
||||
|
||||
module.exports = {
|
||||
bruToJson,
|
||||
bruToEnvJson,
|
||||
getEnvVars,
|
||||
getOptions,
|
||||
collectionBruToJson
|
||||
|
||||
@@ -2,9 +2,8 @@ const { get, each, find, compact } = require('lodash');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { jsonToBruV2, envJsonToBruV2, jsonToCollectionBru } = require('@usebruno/lang');
|
||||
const { sanitizeName } = require('./filesystem');
|
||||
const { bruToJson, collectionBruToJson } = require('./bru');
|
||||
const { parseRequest, parseCollection, parseFolder, stringifyCollection, stringifyFolder, stringifyEnvironment, stringifyRequest } = require('@usebruno/filestore');
|
||||
const constants = require('../constants');
|
||||
const chalk = require('chalk');
|
||||
|
||||
@@ -46,7 +45,7 @@ const createCollectionJsonFromPathname = (collectionPath) => {
|
||||
|
||||
// get the request item
|
||||
const bruContent = fs.readFileSync(filePath, 'utf8');
|
||||
const requestItem = bruToJson(bruContent);
|
||||
const requestItem = parseRequest(bruContent);
|
||||
currentDirItems.push({
|
||||
name: file,
|
||||
pathname: filePath,
|
||||
@@ -97,7 +96,7 @@ const getCollectionRoot = (dir) => {
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(collectionRootPath, 'utf8');
|
||||
return collectionBruToJson(content);
|
||||
return parseCollection(content);
|
||||
};
|
||||
|
||||
const getFolderRoot = (dir) => {
|
||||
@@ -108,7 +107,7 @@ const getFolderRoot = (dir) => {
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(folderRootPath, 'utf8');
|
||||
return collectionBruToJson(content);
|
||||
return parseFolder(content);
|
||||
};
|
||||
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
@@ -417,7 +416,7 @@ const createCollectionFromBrunoObject = async (collection, dirPath) => {
|
||||
|
||||
// Create collection.bru if root exists
|
||||
if (collection.root) {
|
||||
const collectionContent = await jsonToCollectionBru(collection.root);
|
||||
const collectionContent = await stringifyCollection(collection.root);
|
||||
fs.writeFileSync(path.join(dirPath, 'collection.bru'), collectionContent);
|
||||
}
|
||||
|
||||
@@ -427,7 +426,7 @@ const createCollectionFromBrunoObject = async (collection, dirPath) => {
|
||||
fs.mkdirSync(envDirPath, { recursive: true });
|
||||
|
||||
for (const env of collection.environments) {
|
||||
const content = await envJsonToBruV2(env);
|
||||
const content = await stringifyEnvironment(env);
|
||||
const filename = sanitizeName(`${env.name}.bru`);
|
||||
fs.writeFileSync(path.join(envDirPath, filename), content);
|
||||
}
|
||||
@@ -459,10 +458,7 @@ const processCollectionItems = async (items = [], currentPath) => {
|
||||
if (item.seq) {
|
||||
item.root.meta.seq = item.seq;
|
||||
}
|
||||
const folderContent = await jsonToCollectionBru(
|
||||
item.root,
|
||||
true
|
||||
);
|
||||
const folderContent = await stringifyFolder(item.root);
|
||||
safeWriteFileSync(folderBruFilePath, folderContent);
|
||||
}
|
||||
|
||||
@@ -506,7 +502,7 @@ const processCollectionItems = async (items = [], currentPath) => {
|
||||
};
|
||||
|
||||
// Convert to BRU format and write to file
|
||||
const content = await jsonToBruV2(bruJson);
|
||||
const content = await stringifyRequest(bruJson);
|
||||
safeWriteFileSync(path.join(currentPath, sanitizedFilename), content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +1 @@
|
||||
const { Cookie, CookieJar } = require('tough-cookie');
|
||||
const each = require('lodash/each');
|
||||
const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils;
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
const addCookieToJar = (setCookieHeader, requestUrl) => {
|
||||
const cookie = Cookie.parse(setCookieHeader, { loose: true });
|
||||
cookieJar.setCookieSync(cookie, requestUrl, {
|
||||
ignoreError: true // silently ignore things like parse errors and invalid domains
|
||||
});
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (url) => {
|
||||
return cookieJar.getCookiesSync(url, {
|
||||
secure: isPotentiallyTrustworthyOrigin(url)
|
||||
});
|
||||
};
|
||||
|
||||
const getCookieStringForUrl = (url) => {
|
||||
const cookies = getCookiesForUrl(url);
|
||||
|
||||
if (!Array.isArray(cookies) || !cookies.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now());
|
||||
|
||||
return validCookies.map((cookie) => cookie.cookieString()).join('; ');
|
||||
};
|
||||
|
||||
const getDomainsWithCookies = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const domainCookieMap = {};
|
||||
|
||||
cookieJar.store.getAllCookies((err, cookies) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
if (!domainCookieMap[cookie.domain]) {
|
||||
domainCookieMap[cookie.domain] = [cookie];
|
||||
} else {
|
||||
domainCookieMap[cookie.domain].push(cookie);
|
||||
}
|
||||
});
|
||||
|
||||
const domains = Object.keys(domainCookieMap);
|
||||
const domainsWithCookies = [];
|
||||
|
||||
each(domains, (domain) => {
|
||||
const cookies = domainCookieMap[domain];
|
||||
const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now());
|
||||
|
||||
if (validCookies.length) {
|
||||
domainsWithCookies.push({
|
||||
domain,
|
||||
cookies: validCookies,
|
||||
cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resolve(domainsWithCookies);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCookiesForDomain = (domain) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cookieJar.store.removeCookies(domain, null, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
let setCookieHeaders = [];
|
||||
if (headers['set-cookie']) {
|
||||
setCookieHeaders = Array.isArray(headers['set-cookie'])
|
||||
? headers['set-cookie']
|
||||
: [headers['set-cookie']];
|
||||
for (let setCookieHeader of setCookieHeaders) {
|
||||
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
|
||||
addCookieToJar(setCookieHeader, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addCookieToJar,
|
||||
getCookiesForUrl,
|
||||
getCookieStringForUrl,
|
||||
getDomainsWithCookies,
|
||||
deleteCookiesForDomain,
|
||||
saveCookies
|
||||
};
|
||||
module.exports = require('@usebruno/common').cookies;
|
||||
|
||||
44
packages/bruno-cli/src/utils/request.js
Normal file
44
packages/bruno-cli/src/utils/request.js
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
// Check for meaningful test() calls (not commented out or in strings)
|
||||
const hasExecutableTestInScript = (script) => {
|
||||
if (!script) return false;
|
||||
|
||||
// Remove single-line comments (// ...) and multi-line comments (/* ... */)
|
||||
let cleanScript = script
|
||||
.replace(/\/\/.*$/gm, '') // Remove line comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove block comments
|
||||
|
||||
// Remove string literals to avoid matching test() inside strings
|
||||
cleanScript = cleanScript
|
||||
.replace(/"(?:[^"\\]|\\.)*"/g, '""') // Remove double-quoted strings
|
||||
.replace(/'(?:[^'\\]|\\.)*'/g, "''") // Remove single-quoted strings
|
||||
.replace(/`(?:[^`\\]|\\.)*`/g, '``'); // Remove template literals
|
||||
|
||||
// Look for standalone test() calls (not object method calls like obj.test())
|
||||
// Find all test( occurrences and check they're not preceded by dots
|
||||
let hasValidTest = false;
|
||||
let searchFrom = 0;
|
||||
|
||||
while (true) {
|
||||
const index = cleanScript.indexOf('test', searchFrom);
|
||||
if (index === -1) break;
|
||||
|
||||
// Check if this looks like test( with optional whitespace
|
||||
const afterTest = cleanScript.substring(index + 4);
|
||||
if (/^\s*\(/.test(afterTest)) {
|
||||
// Found test( - check if it's not preceded by a dot
|
||||
if (index === 0 || cleanScript[index - 1] !== '.') {
|
||||
hasValidTest = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
searchFrom = index + 1;
|
||||
}
|
||||
|
||||
return hasValidTest;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
hasExecutableTestInScript
|
||||
};
|
||||
309
packages/bruno-cli/tests/utils/common.spec.js
Normal file
309
packages/bruno-cli/tests/utils/common.spec.js
Normal file
@@ -0,0 +1,309 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
const { hasExecutableTestInScript } = require('../../src/utils/request');
|
||||
|
||||
describe('hasExecutableTestInScript', () => {
|
||||
describe('should return true for valid test() calls', () => {
|
||||
it('should detect basic test calls', () => {
|
||||
const script = `
|
||||
test("should work", function() {
|
||||
expect(true).to.be.true;
|
||||
});
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect indented test calls', () => {
|
||||
const script = `
|
||||
if (true) {
|
||||
test("indented test", function() {
|
||||
expect(1).to.equal(1);
|
||||
});
|
||||
}
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect test calls with extra whitespace', () => {
|
||||
const script = `test ("with spaces", function() { });`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect test calls after assignments', () => {
|
||||
const script = `
|
||||
const result = test("assignment test", function() {
|
||||
expect("hello").to.be.a("string");
|
||||
});
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect test calls in conditionals', () => {
|
||||
const script = `
|
||||
if (condition) {
|
||||
test("conditional test", function() {
|
||||
expect(true).to.be.true;
|
||||
});
|
||||
}
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect test calls in arrays', () => {
|
||||
const script = `
|
||||
const tests = [
|
||||
test("array test", function() {
|
||||
expect(Array.isArray([])).to.be.true;
|
||||
})
|
||||
];
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect test calls in ternary operators', () => {
|
||||
const script = `
|
||||
const result = condition ? test("ternary test", function() {
|
||||
expect(true).to.be.true;
|
||||
}) : null;
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect test calls after semicolons', () => {
|
||||
const script = `
|
||||
const data = res.data; test("after semicolon", function() {
|
||||
expect(data).to.be.an("object");
|
||||
});
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect test calls in object values', () => {
|
||||
const script = `
|
||||
const config = {
|
||||
validation: test("object value test", function() {
|
||||
expect(true).to.be.true;
|
||||
})
|
||||
};
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect multiple test calls', () => {
|
||||
const script = `
|
||||
test("first test", function() {
|
||||
expect(1).to.equal(1);
|
||||
});
|
||||
|
||||
test("second test", function() {
|
||||
expect(2).to.equal(2);
|
||||
});
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect test calls at start of script', () => {
|
||||
const script = `test("at start", function() { expect(true).to.be.true; });`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return false for invalid test() calls', () => {
|
||||
it('should ignore commented out test calls with //', () => {
|
||||
const script = `
|
||||
// test("commented test", function() {
|
||||
// expect(true).to.be.true;
|
||||
// });
|
||||
console.log("no real tests here");
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore commented out test calls with /* */', () => {
|
||||
const script = `
|
||||
/* test("block commented test", function() {
|
||||
expect(true).to.be.true;
|
||||
}); */
|
||||
console.log("no real tests here");
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore test() in double-quoted strings', () => {
|
||||
const script = `
|
||||
console.log("This contains test() but should not match");
|
||||
console.log("Remember to test() your API");
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore test() in single-quoted strings', () => {
|
||||
const script = `
|
||||
console.log('Single quote test() should not match');
|
||||
const message = 'Use test() for validation';
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore test() in template literals', () => {
|
||||
const script = `
|
||||
console.log(\`Template literal test() should not match\`);
|
||||
const message = \`Remember to test() your code\`;
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore object method calls', () => {
|
||||
const script = `
|
||||
const obj = { test: function() { return "not a real test"; } };
|
||||
obj.test("This is a method call");
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore this.test() calls', () => {
|
||||
const script = `
|
||||
this.test("Another method call");
|
||||
this.test();
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore complex object chain calls', () => {
|
||||
const script = `
|
||||
api.client.test("Should not match");
|
||||
user.test.endpoint("Chained method");
|
||||
window.test("Should not match");
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore object methods in variables', () => {
|
||||
const script = `
|
||||
const validator = {
|
||||
test: function(value) { return value > 0; }
|
||||
};
|
||||
validator.test(42);
|
||||
|
||||
const tester = { test: () => "mock" };
|
||||
tester.test("method call");
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty scripts', () => {
|
||||
expect(hasExecutableTestInScript('')).toBe(false);
|
||||
expect(hasExecutableTestInScript(null)).toBe(false);
|
||||
expect(hasExecutableTestInScript(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for scripts with no test calls', () => {
|
||||
const script = `
|
||||
bru.setVar("userId", "12345");
|
||||
console.log("Setting up request");
|
||||
const data = res.data;
|
||||
bru.setVar("responseData", data);
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when test is part of other words', () => {
|
||||
const script = `
|
||||
const testing = "value";
|
||||
const protest = "demo";
|
||||
const fastest = "speed";
|
||||
console.log("contest results");
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle mixed scenarios correctly', () => {
|
||||
it('should return true when valid test exists among invalid ones', () => {
|
||||
const script = `
|
||||
// test("commented out");
|
||||
console.log("test() in string");
|
||||
obj.test("method call");
|
||||
|
||||
test("real test", function() {
|
||||
expect(true).to.be.true;
|
||||
});
|
||||
|
||||
api.client.test("another method");
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when only invalid tests exist', () => {
|
||||
const script = `
|
||||
// test("commented out test", function() {
|
||||
// expect(true).to.be.true;
|
||||
// });
|
||||
|
||||
console.log("test() inside string");
|
||||
console.log('test() in single quotes');
|
||||
console.log(\`test() in template\`);
|
||||
|
||||
const obj = { test: () => "mock" };
|
||||
obj.test("method call");
|
||||
this.test("another method");
|
||||
api.client.test("chained method");
|
||||
|
||||
bru.setVar("test", "variable name");
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle complex nested quotes correctly', () => {
|
||||
const script = `
|
||||
console.log("String with 'nested quotes' and test() call");
|
||||
console.log('String with "nested quotes" and test() call');
|
||||
|
||||
test("real test with \\"escaped quotes\\"", function() {
|
||||
expect(true).to.be.true;
|
||||
});
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle multi-line comments correctly', () => {
|
||||
const script = `
|
||||
/*
|
||||
* This is a multi-line comment with
|
||||
* test("commented test", function() {
|
||||
* expect(true).to.be.true;
|
||||
* });
|
||||
*/
|
||||
|
||||
test("real test", function() {
|
||||
expect(true).to.be.true;
|
||||
});
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle inline comments correctly', () => {
|
||||
const script = `
|
||||
const data = res.data; // test("inline comment")
|
||||
test("real test", function() { // this is a real test
|
||||
expect(data).to.be.an("object");
|
||||
});
|
||||
`;
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle test calls immediately after dots (edge case)', () => {
|
||||
const script = `
|
||||
// This should not match because it's after a dot
|
||||
console.test("should not match");
|
||||
|
||||
// But this should match because there's a space
|
||||
console. test("should match due to space");
|
||||
`;
|
||||
// Note: Our current implementation would consider the second one valid
|
||||
// because there's a space between the dot and test
|
||||
expect(hasExecutableTestInScript(script)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ module.exports = {
|
||||
'^.+\\.(ts|js)$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(lodash-es)/)',
|
||||
'/node_modules/(?!(lodash-es|is-ip|ip-regex|super-regex|function-timeout|time-span|convert-hrtime|clone-regexp|is-regexp)/)'
|
||||
],
|
||||
testEnvironment: 'node'
|
||||
};
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-jest": "^29.7.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"moment": "^2.29.4",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
|
||||
500
packages/bruno-common/src/cookies/index.ts
Normal file
500
packages/bruno-common/src/cookies/index.ts
Normal file
@@ -0,0 +1,500 @@
|
||||
import { Cookie, CookieJar } from 'tough-cookie';
|
||||
import each from 'lodash/each';
|
||||
import moment from 'moment';
|
||||
import { isPotentiallyTrustworthyOrigin } from '../utils';
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
const addCookieToJar = (setCookieHeader: string, requestUrl: string): void => {
|
||||
const cookie = Cookie.parse(setCookieHeader, { loose: true });
|
||||
if (!cookie) return;
|
||||
cookieJar.setCookieSync(cookie, requestUrl, {
|
||||
ignoreError: true
|
||||
});
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (url: string) => {
|
||||
return cookieJar.getCookiesSync(url, {
|
||||
secure: isPotentiallyTrustworthyOrigin(url)
|
||||
});
|
||||
};
|
||||
|
||||
const getCookieStringForUrl = (url: string): string => {
|
||||
const cookies = getCookiesForUrl(url);
|
||||
if (!Array.isArray(cookies) || !cookies.length) return '';
|
||||
|
||||
const validCookies = cookies.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now());
|
||||
return validCookies.map((cookie) => cookie.cookieString()).join('; ');
|
||||
};
|
||||
|
||||
const getDomainsWithCookies = (): Promise<Array<{ domain: string; cookies: Cookie[]; cookieString: string }>> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const domainCookieMap: Record<string, Cookie[]> = {};
|
||||
|
||||
(cookieJar as any).store.getAllCookies((err: Error, cookies: Cookie[]) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
// Handle null domain by skipping the cookie
|
||||
if (!cookie.domain) return;
|
||||
|
||||
if (!domainCookieMap[cookie.domain]) {
|
||||
domainCookieMap[cookie.domain] = [cookie];
|
||||
} else {
|
||||
domainCookieMap[cookie.domain].push(cookie);
|
||||
}
|
||||
});
|
||||
|
||||
const domains = Object.keys(domainCookieMap);
|
||||
const domainsWithCookies: Array<{ domain: string; cookies: Cookie[]; cookieString: string }> = [];
|
||||
|
||||
each(domains, (domain) => {
|
||||
const cookiesForDomain = domainCookieMap[domain];
|
||||
const validCookies = cookiesForDomain.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now());
|
||||
|
||||
if (validCookies.length) {
|
||||
domainsWithCookies.push({
|
||||
domain,
|
||||
cookies: validCookies,
|
||||
cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resolve(domainsWithCookies);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCookie = (domain: string, path: string, cookieKey: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(cookieJar as any).store.removeCookie(domain, path, cookieKey, (err: Error) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCookiesForDomain = (domain: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(cookieJar as any).store.removeCookies(domain, null, (err: Error) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateCookieObj = (cookieObj: any, oldCookie: Cookie) => {
|
||||
return {
|
||||
...cookieObj,
|
||||
path: oldCookie.path,
|
||||
key: oldCookie.key,
|
||||
domain: oldCookie.domain,
|
||||
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
|
||||
creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(),
|
||||
lastAccessed:
|
||||
oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid()
|
||||
? new Date(oldCookie.lastAccessed)
|
||||
: new Date()
|
||||
} as any;
|
||||
};
|
||||
|
||||
const createCookieObj = (cookieObj: any) => {
|
||||
return {
|
||||
...cookieObj,
|
||||
path: cookieObj.path,
|
||||
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
|
||||
creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(),
|
||||
lastAccessed:
|
||||
cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid()
|
||||
? new Date(cookieObj.lastAccessed)
|
||||
: new Date()
|
||||
} as any;
|
||||
};
|
||||
|
||||
const addCookieForDomain = (domain: string, cookieObj: any): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const cookie = new Cookie(createCookieObj(cookieObj));
|
||||
(cookieJar as any).store.putCookie(cookie, (err: Error) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const modifyCookieForDomain = (domain: string, oldCookieObj: any, cookieObj: any): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const oldCookie = new Cookie(createCookieObj(oldCookieObj));
|
||||
const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie));
|
||||
(cookieJar as any).store.updateCookie(oldCookie, newCookie, (removeErr: Error) => {
|
||||
if (removeErr) return reject(removeErr);
|
||||
resolve();
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const parseCookieString = (cookieStr: string): any | null => {
|
||||
try {
|
||||
const cookie = Cookie.parse(cookieStr);
|
||||
if (!cookie) return null;
|
||||
return {
|
||||
...cookie,
|
||||
expires: cookie.expires === 'Infinity' || (cookie.expires as any) === Infinity ? null : cookie.expires
|
||||
};
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const createCookieString = (cookieObj: any): string => {
|
||||
const cookie = new Cookie(createCookieObj(cookieObj));
|
||||
let cookieString = cookie.toString(); // tough-cookie omits domain
|
||||
|
||||
// Manually append domain if cookie is hostOnly but we still want Domain flag
|
||||
if (cookieObj.hostOnly && !cookieString.includes('Domain=')) {
|
||||
cookieString += `; Domain=${cookieObj.domain}`;
|
||||
}
|
||||
return cookieString;
|
||||
}
|
||||
|
||||
const saveCookies = (url: string, headers: any) => {
|
||||
if (headers['set-cookie']) {
|
||||
let setCookieHeaders = Array.isArray(headers['set-cookie'])
|
||||
? headers['set-cookie']
|
||||
: [headers['set-cookie']];
|
||||
for (let setCookieHeader of setCookieHeaders) {
|
||||
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
|
||||
addCookieToJar(setCookieHeader, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cookieJarWrapper = () => {
|
||||
return {
|
||||
|
||||
// Get the full cookie object for the given URL & name.
|
||||
getCookie: function (
|
||||
url: string,
|
||||
cookieName: string,
|
||||
callback?: (err: Error | null | undefined, cookie?: Cookie | null) => void
|
||||
) {
|
||||
if (!url || !cookieName) {
|
||||
const error = new Error('URL and cookie name are required');
|
||||
if (callback) return callback(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
return cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return callback(err);
|
||||
const cookie = cookies.find((c) => c.key === cookieName);
|
||||
callback(null, cookie || null);
|
||||
});
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<Cookie | null>((resolve, reject) => {
|
||||
cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return reject(err);
|
||||
const cookie = cookies.find((c) => c.key === cookieName);
|
||||
resolve(cookie || null);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// Get all cookies that would be sent to the given URL.
|
||||
getCookies: function (url: string, callback?: (err: Error | null | undefined, cookies?: Cookie[]) => void) {
|
||||
if (!url) {
|
||||
const error = new Error('URL is required');
|
||||
if (callback) return callback(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
return cookieJar.getCookies(url, callback);
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<Cookie[]>((resolve, reject) => {
|
||||
cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return reject(err);
|
||||
resolve(cookies);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setCookie: function (
|
||||
url: string,
|
||||
nameOrCookieObj: string | Record<string, any>,
|
||||
valueOrCallback?: string | ((err?: Error | undefined) => void),
|
||||
maybeCallback?: (err?: Error | undefined) => void
|
||||
) {
|
||||
// Determine the callback
|
||||
let callback: ((err?: Error | undefined) => void) | undefined;
|
||||
if (typeof maybeCallback === 'function') {
|
||||
callback = maybeCallback;
|
||||
} else if (typeof valueOrCallback === 'function') {
|
||||
callback = valueOrCallback as (err?: Error | undefined) => void;
|
||||
}
|
||||
|
||||
const executeSetCookie = () => {
|
||||
if (!url) throw new Error('URL is required');
|
||||
|
||||
// CASE 1: name/value pair provided
|
||||
if (typeof nameOrCookieObj === 'string') {
|
||||
const cookieName = nameOrCookieObj;
|
||||
const cookieValue = typeof valueOrCallback === 'string' ? valueOrCallback : '';
|
||||
|
||||
if (!cookieName) throw new Error('Cookie name is required');
|
||||
|
||||
const cookie = new Cookie({
|
||||
key: cookieName,
|
||||
value: cookieValue,
|
||||
domain: new URL(url).hostname,
|
||||
});
|
||||
|
||||
cookieJar.setCookieSync(cookie, url, { ignoreError: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// CASE 2: cookie object provided
|
||||
if (typeof nameOrCookieObj === 'object' && nameOrCookieObj !== null) {
|
||||
const obj = { ...(nameOrCookieObj as any) } as any;
|
||||
|
||||
if (!obj.key && obj.name) obj.key = obj.name;
|
||||
if (!obj.key) throw new Error('cookieObject.key (name) is required');
|
||||
|
||||
const base = {
|
||||
domain: new URL(url).hostname,
|
||||
...obj,
|
||||
} as any;
|
||||
|
||||
const processedCookie = createCookieObj(base);
|
||||
const cookie = new Cookie(processedCookie);
|
||||
cookieJar.setCookieSync(cookie, url, { ignoreError: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// If we reach here, arguments were invalid
|
||||
throw new Error('Invalid arguments passed to setCookie');
|
||||
};
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
try {
|
||||
executeSetCookie();
|
||||
callback(undefined);
|
||||
} catch (err) {
|
||||
callback(err as Error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
executeSetCookie();
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
setCookies: function (
|
||||
url: string,
|
||||
cookiesArray: any[],
|
||||
callback?: (err?: Error | undefined) => void
|
||||
) {
|
||||
const executeSetCookies = () => {
|
||||
if (!url) throw new Error('URL is required');
|
||||
if (!Array.isArray(cookiesArray)) {
|
||||
throw new Error('setCookies expects an array of cookie objects');
|
||||
}
|
||||
|
||||
for (const cookieObject of cookiesArray) {
|
||||
const obj = { ...(cookieObject as any) } as any;
|
||||
|
||||
if (!obj.key && obj.name) obj.key = obj.name;
|
||||
if (!obj.key) throw new Error('cookieObject.key (name) is required');
|
||||
|
||||
const base = {
|
||||
domain: new URL(url).hostname,
|
||||
...obj
|
||||
} as any;
|
||||
|
||||
const processedCookie = createCookieObj(base);
|
||||
const cookie = new Cookie(processedCookie);
|
||||
cookieJar.setCookieSync(cookie, url, { ignoreError: true });
|
||||
}
|
||||
};
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
try {
|
||||
executeSetCookies();
|
||||
callback(undefined);
|
||||
} catch (err) {
|
||||
callback(err as Error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
executeSetCookies();
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
clear: function (callback?: (err?: Error | undefined) => void) {
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
return (cookieJar as any).store.removeAllCookies(callback);
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
(cookieJar as any).store.removeAllCookies((err?: Error) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
deleteCookies: function (url: string, callback?: (err?: Error | undefined) => void) {
|
||||
if (!url) {
|
||||
const error = new Error('URL is required');
|
||||
if (callback) return callback(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
return cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return callback(err);
|
||||
if (!cookies || !cookies.length) return callback(undefined);
|
||||
|
||||
let pending = cookies.length;
|
||||
const done = (removeErr?: Error) => {
|
||||
if (removeErr) return callback(removeErr);
|
||||
if (--pending === 0) {
|
||||
callback(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
(cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return reject(err);
|
||||
if (!cookies || !cookies.length) return resolve();
|
||||
|
||||
let pending = cookies.length;
|
||||
const done = (removeErr?: Error) => {
|
||||
if (removeErr) return reject(removeErr);
|
||||
if (--pending === 0) {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
(cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
deleteCookie: function (url: string, cookieName: string, callback?: (err?: Error | undefined) => void) {
|
||||
if (!url || !cookieName) {
|
||||
const error = new Error('URL and cookie name are required');
|
||||
if (callback) return callback(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const executeDelete = (callback: (err?: Error) => void) => {
|
||||
cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Filter cookies matching key
|
||||
const matchingCookies = (cookies || []).filter((c) => c.key === cookieName);
|
||||
if (!matchingCookies.length) return callback(undefined);
|
||||
|
||||
const urlPath = new URL(url).pathname || '/';
|
||||
|
||||
// Prioritise a cookie whose path exactly matches the URL path
|
||||
let cookieToDelete = matchingCookies.find((c) => c.path === urlPath);
|
||||
|
||||
// If not found, fall back to the first matching cookie (most specific path first)
|
||||
if (!cookieToDelete) {
|
||||
// tough-cookie sorts cookies by path length desc, preserve that order
|
||||
cookieToDelete = matchingCookies[0];
|
||||
}
|
||||
|
||||
(cookieJar as any).store.removeCookie(
|
||||
cookieToDelete.domain,
|
||||
cookieToDelete.path,
|
||||
cookieToDelete.key,
|
||||
callback
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
return executeDelete(callback);
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
executeDelete((err?: Error) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
} as const;
|
||||
};
|
||||
|
||||
|
||||
const cookiesModule = {
|
||||
cookieJar,
|
||||
addCookieToJar,
|
||||
getCookiesForUrl,
|
||||
getCookieStringForUrl,
|
||||
getDomainsWithCookies,
|
||||
deleteCookie,
|
||||
deleteCookiesForDomain,
|
||||
addCookieForDomain,
|
||||
modifyCookieForDomain,
|
||||
parseCookieString,
|
||||
createCookieString,
|
||||
updateCookieObj,
|
||||
createCookieObj,
|
||||
jar: cookieJarWrapper,
|
||||
saveCookies
|
||||
};
|
||||
|
||||
export default cookiesModule;
|
||||
@@ -1,5 +1,6 @@
|
||||
export { mockDataFunctions } from './utils/faker-functions';
|
||||
export { default as interpolate } from './interpolate';
|
||||
export { default as isRequestTagsIncluded } from './tags';
|
||||
export { default as cookies } from './cookies';
|
||||
|
||||
export * as utils from './utils';
|
||||
@@ -1,5 +1,9 @@
|
||||
export {
|
||||
encodeUrl,
|
||||
parseQueryParams,
|
||||
buildQueryString
|
||||
buildQueryString,
|
||||
} from './url';
|
||||
|
||||
export {
|
||||
isPotentiallyTrustworthyOrigin
|
||||
} from './url/validation';
|
||||
@@ -1,4 +1,4 @@
|
||||
const { isPotentiallyTrustworthyOrigin } = require('./cookie-utils');
|
||||
import { isPotentiallyTrustworthyOrigin } from './validation';
|
||||
|
||||
describe('isPotentiallyTrustworthyOrigin', () => {
|
||||
describe('secure schemes', () => {
|
||||
@@ -130,4 +130,4 @@ describe('isPotentiallyTrustworthyOrigin', () => {
|
||||
expect(isPotentiallyTrustworthyOrigin('wss://localhost')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
67
packages/bruno-common/src/utils/url/validation.ts
Normal file
67
packages/bruno-common/src/utils/url/validation.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { isIPv4, isIPv6, isIP } from 'is-ip';
|
||||
|
||||
const hostNoBrackets = (host: string): string => {
|
||||
if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) {
|
||||
return host.substring(1, host.length - 1);
|
||||
}
|
||||
return host;
|
||||
};
|
||||
|
||||
const isLoopbackV4 = (address: string): boolean => {
|
||||
const octets = address.split('.');
|
||||
if (octets.length !== 4 || parseInt(octets[0], 10) !== 127) {
|
||||
return false;
|
||||
}
|
||||
return octets.every((octet) => {
|
||||
const n = parseInt(octet, 10);
|
||||
return !Number.isNaN(n) && n >= 0 && n <= 255;
|
||||
});
|
||||
};
|
||||
|
||||
const isLoopbackV6 = (address: string): boolean => address === '::1';
|
||||
|
||||
const isIpLoopback = (address: string): boolean => {
|
||||
if (isIPv4(address)) {
|
||||
return isLoopbackV4(address);
|
||||
}
|
||||
if (isIPv6(address)) {
|
||||
return isLoopbackV6(address);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isNormalizedLocalhostTLD = (host: string): boolean => host.toLowerCase().endsWith('.localhost');
|
||||
|
||||
const isLocalHostname = (host: string): boolean => {
|
||||
return host.toLowerCase() === 'localhost' || isNormalizedLocalhostTLD(host);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mirrors Chrome / Secure Contexts spec for "potentially trustworthy origins".
|
||||
*/
|
||||
const isPotentiallyTrustworthyOrigin = (urlString: string): boolean => {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(urlString);
|
||||
} catch {
|
||||
return false; // invalid URL or opaque origin
|
||||
}
|
||||
|
||||
const scheme = url.protocol.replace(':', '').toLowerCase();
|
||||
const hostname = hostNoBrackets(url.hostname).replace(/\.+$/, '');
|
||||
|
||||
// Secure schemes
|
||||
if (scheme === 'https' || scheme === 'wss' || scheme === 'file') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IP literals
|
||||
if (isIP(hostname)) {
|
||||
return isIpLoopback(hostname);
|
||||
}
|
||||
|
||||
// localhost / *.localhost
|
||||
return isLocalHostname(hostname);
|
||||
};
|
||||
|
||||
export { isPotentiallyTrustworthyOrigin };
|
||||
228
packages/bruno-common/tests/cookies/cookie-jar-wrapper.spec.js
Normal file
228
packages/bruno-common/tests/cookies/cookie-jar-wrapper.spec.js
Normal file
@@ -0,0 +1,228 @@
|
||||
const cookiesModule = require('../../src/cookies/index.ts').default;
|
||||
|
||||
describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
let jar;
|
||||
const testUrl = 'https://api.example.com';
|
||||
|
||||
beforeEach(() => {
|
||||
jar = cookiesModule.jar();
|
||||
// Clear all cookies before each test
|
||||
jar.clear();
|
||||
});
|
||||
|
||||
describe('Basic Cookie Operations', () => {
|
||||
test('setCookie and getCookie - name/value pair', async () => {
|
||||
const cookieName = 'authToken';
|
||||
const cookieValue = 'jwt123';
|
||||
|
||||
// Set a cookie
|
||||
await jar.setCookie(testUrl, cookieName, cookieValue);
|
||||
|
||||
// Get the cookie back
|
||||
const cookie = await jar.getCookie(testUrl, cookieName);
|
||||
expect(cookie.key).toBe(cookieName);
|
||||
expect(cookie.value).toBe(cookieValue);
|
||||
expect(cookie.domain).toBe('api.example.com');
|
||||
});
|
||||
|
||||
test('setCookie with cookie object', async () => {
|
||||
const cookieObj = {
|
||||
key: 'sessionId',
|
||||
value: 'abc123',
|
||||
path: '/api',
|
||||
httpOnly: true,
|
||||
secure: true
|
||||
};
|
||||
|
||||
await jar.setCookie(testUrl, cookieObj);
|
||||
|
||||
const cookie = await jar.getCookie(testUrl + '/api', 'sessionId');
|
||||
expect(cookie.key).toBe('sessionId');
|
||||
expect(cookie.value).toBe('abc123');
|
||||
expect(cookie.path).toBe('/api');
|
||||
expect(cookie.httpOnly).toBe(true);
|
||||
expect(cookie.secure).toBe(true);
|
||||
});
|
||||
|
||||
test('getCookie returns null for non-existent cookie', async () => {
|
||||
const cookie = await jar.getCookie(testUrl, 'nonexistent');
|
||||
expect(cookie).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Cookie Operations', () => {
|
||||
test('setCookies with array of cookie objects', async () => {
|
||||
const cookies = [
|
||||
{ key: 'cookie1', value: 'value1' },
|
||||
{ key: 'cookie2', value: 'value2' },
|
||||
{ key: 'cookie3', value: 'value3', httpOnly: true }
|
||||
];
|
||||
|
||||
await jar.setCookies(testUrl, cookies);
|
||||
|
||||
// Verify all cookies were set
|
||||
const retrievedCookies = await jar.getCookies(testUrl);
|
||||
expect(retrievedCookies).toHaveLength(3);
|
||||
|
||||
const cookieNames = retrievedCookies.map(c => c.key);
|
||||
expect(cookieNames).toContain('cookie1');
|
||||
expect(cookieNames).toContain('cookie2');
|
||||
expect(cookieNames).toContain('cookie3');
|
||||
});
|
||||
|
||||
test('getCookies returns all cookies for URL', async () => {
|
||||
// Set multiple cookies
|
||||
await jar.setCookie(testUrl, 'auth', 'token123');
|
||||
await jar.setCookie(testUrl, 'session', 'sess456');
|
||||
await jar.setCookie(testUrl, 'prefs', 'theme=dark');
|
||||
|
||||
const cookies = await jar.getCookies(testUrl);
|
||||
expect(cookies).toHaveLength(3);
|
||||
|
||||
const cookieMap = cookies.reduce((map, cookie) => {
|
||||
map[cookie.key] = cookie.value;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
expect(cookieMap.auth).toBe('token123');
|
||||
expect(cookieMap.session).toBe('sess456');
|
||||
expect(cookieMap.prefs).toBe('theme=dark');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cookie Deletion', () => {
|
||||
test('deleteCookie removes specific cookie', async () => {
|
||||
// Set two cookies
|
||||
await jar.setCookie(testUrl, 'keep', 'keepValue');
|
||||
await jar.setCookie(testUrl, 'remove', 'removeValue');
|
||||
|
||||
// Delete one cookie
|
||||
await jar.deleteCookie(testUrl, 'remove');
|
||||
|
||||
// Verify only one cookie remains
|
||||
const cookies = await jar.getCookies(testUrl);
|
||||
expect(cookies).toHaveLength(1);
|
||||
expect(cookies[0].key).toBe('keep');
|
||||
expect(cookies[0].value).toBe('keepValue');
|
||||
});
|
||||
|
||||
test('deleteCookies removes all cookies for URL', async () => {
|
||||
// Set multiple cookies
|
||||
await jar.setCookie(testUrl, 'cookie1', 'value1');
|
||||
await jar.setCookie(testUrl, 'cookie2', 'value2');
|
||||
|
||||
// Delete all cookies for the URL
|
||||
await jar.deleteCookies(testUrl);
|
||||
|
||||
// Verify no cookies remain
|
||||
const cookies = await jar.getCookies(testUrl);
|
||||
expect(cookies).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('clear removes all cookies from jar', async () => {
|
||||
// Set cookies for multiple URLs
|
||||
await jar.setCookie('https://site1.com', 'cookie1', 'value1');
|
||||
await jar.setCookie('https://site2.com', 'cookie2', 'value2');
|
||||
|
||||
// Clear entire jar
|
||||
await jar.clear();
|
||||
|
||||
// Verify no cookies remain for any URL
|
||||
const cookies1 = await jar.getCookies('https://site1.com');
|
||||
const cookies2 = await jar.getCookies('https://site2.com');
|
||||
|
||||
expect(cookies1).toHaveLength(0);
|
||||
expect(cookies2).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('setCookie handles missing URL', async () => {
|
||||
await expect(jar.setCookie('', 'name', 'value')).rejects.toThrow('URL is required');
|
||||
});
|
||||
|
||||
test('getCookie handles missing URL', async () => {
|
||||
await expect(jar.getCookie('', 'name')).rejects.toThrow('URL and cookie name are required');
|
||||
});
|
||||
|
||||
test('setCookies handles invalid input', async () => {
|
||||
await expect(jar.setCookies(testUrl, 'not-an-array')).rejects.toThrow('expects an array');
|
||||
});
|
||||
|
||||
test('setCookie handles missing cookie name in object', async () => {
|
||||
await expect(jar.setCookie(testUrl, { value: 'test' })).rejects.toThrow('key (name) is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world Usage Examples', () => {
|
||||
test('Authentication workflow example', async () => {
|
||||
const apiUrl = 'https://api.example.com';
|
||||
const authToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
|
||||
|
||||
// Simulate login - set auth cookie
|
||||
await jar.setCookie(apiUrl, 'authToken', authToken);
|
||||
|
||||
// Later in the session - retrieve auth token
|
||||
const cookie = await jar.getCookie(apiUrl, 'authToken');
|
||||
expect(cookie.value).toBe(authToken);
|
||||
|
||||
// Simulate logout - remove auth cookie
|
||||
await jar.deleteCookie(apiUrl, 'authToken');
|
||||
|
||||
// Verify cookie is gone
|
||||
const deletedCookie = await jar.getCookie(apiUrl, 'authToken');
|
||||
expect(deletedCookie).toBeNull();
|
||||
});
|
||||
|
||||
test('Session management with multiple cookies', async () => {
|
||||
const sessionUrl = 'https://app.example.com';
|
||||
|
||||
// Set session cookies
|
||||
const sessionCookies = [
|
||||
{ key: 'sessionId', value: 'sess_123', httpOnly: true },
|
||||
{ key: 'csrfToken', value: 'csrf_456' },
|
||||
{ key: 'userPrefs', value: JSON.stringify({ theme: 'dark', lang: 'en' }) }
|
||||
];
|
||||
|
||||
await jar.setCookies(sessionUrl, sessionCookies);
|
||||
|
||||
// Retrieve all session cookies
|
||||
const cookies = await jar.getCookies(sessionUrl);
|
||||
expect(cookies).toHaveLength(3);
|
||||
|
||||
// Find specific cookies
|
||||
const sessionCookie = cookies.find(c => c.key === 'sessionId');
|
||||
const csrfCookie = cookies.find(c => c.key === 'csrfToken');
|
||||
const prefsCookie = cookies.find(c => c.key === 'userPrefs');
|
||||
|
||||
expect(sessionCookie.value).toBe('sess_123');
|
||||
expect(sessionCookie.httpOnly).toBe(true);
|
||||
expect(csrfCookie.value).toBe('csrf_456');
|
||||
|
||||
const prefs = JSON.parse(prefsCookie.value);
|
||||
expect(prefs.theme).toBe('dark');
|
||||
expect(prefs.lang).toBe('en');
|
||||
});
|
||||
|
||||
test('Cookie path handling', async () => {
|
||||
const baseUrl = 'https://example.com';
|
||||
|
||||
// Set cookies with different paths
|
||||
await jar.setCookie(baseUrl, { key: 'global', value: 'global_val', path: '/' });
|
||||
await jar.setCookie(baseUrl, { key: 'api', value: 'api_val', path: '/api' });
|
||||
await jar.setCookie(baseUrl, { key: 'admin', value: 'admin_val', path: '/admin' });
|
||||
|
||||
const rootCookies = await jar.getCookies(baseUrl + '/');
|
||||
const globalCookie = rootCookies.find(c => c.key === 'global');
|
||||
expect(globalCookie).toBeTruthy();
|
||||
expect(globalCookie.value).toBe('global_val');
|
||||
|
||||
const apiCookies = await jar.getCookies(baseUrl + '/api/users');
|
||||
expect(apiCookies.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const apiCookieNames = apiCookies.map(c => c.key);
|
||||
expect(apiCookieNames).toContain('global');
|
||||
expect(apiCookieNames).toContain('api');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,13 +35,13 @@
|
||||
"@web/rollup-plugin-copy": "^0.5.1",
|
||||
"babel-jest": "^29.7.0",
|
||||
"rimraf": "^5.0.7",
|
||||
"rollup": "3.2.5",
|
||||
"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"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
"rollup": "3.29.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,13 @@ const replacements = {
|
||||
'pm\\.execution\\.skipRequest': 'bru.runner.skipRequest',
|
||||
'pm\\.execution\\.setNextRequest\\(null\\)': 'bru.runner.stopExecution()',
|
||||
'pm\\.execution\\.setNextRequest\\(\'null\'\\)': 'bru.runner.stopExecution()',
|
||||
// Cookie jar translations
|
||||
'pm\\.cookies\\.jar\\(\\)': 'bru.cookies.jar()',
|
||||
'pm\\.cookies\\.jar\\(\\)\\.get\\(': 'bru.cookies.jar().getCookie(',
|
||||
'pm\\.cookies\\.jar\\(\\)\\.set\\(': 'bru.cookies.jar().setCookie(',
|
||||
'pm\\.cookies\\.jar\\(\\)\\.unset\\(': 'bru.cookies.jar().deleteCookie(',
|
||||
'pm\\.cookies\\.jar\\(\\)\\.clear\\(': 'bru.cookies.jar().deleteCookies(',
|
||||
'pm\\.cookies\\.jar\\(\\)\\.getAll\\(': 'bru.cookies.jar().getCookies(',
|
||||
};
|
||||
|
||||
const extendedReplacements = Object.keys(replacements).reduce((acc, key) => {
|
||||
|
||||
@@ -14,6 +14,11 @@ function getMemberExpressionString(node) {
|
||||
return node.name;
|
||||
}
|
||||
|
||||
if (node.type === 'CallExpression') {
|
||||
const calleeStr = getMemberExpressionString(node.callee);
|
||||
return `${calleeStr}()`;
|
||||
}
|
||||
|
||||
// Handle member expressions
|
||||
if (node.type === 'MemberExpression') {
|
||||
const objectStr = getMemberExpressionString(node.object);
|
||||
@@ -89,6 +94,13 @@ const simpleTranslations = {
|
||||
'pm.response.size().body': 'res.getSize().body',
|
||||
'pm.response.size().header': 'res.getSize().header',
|
||||
'pm.response.size().total': 'res.getSize().total',
|
||||
'pm.cookies.jar': 'bru.cookies.jar',
|
||||
|
||||
'pm.cookies.jar().get': 'bru.cookies.jar().getCookie',
|
||||
'pm.cookies.jar().getAll': 'bru.cookies.jar().getCookies',
|
||||
'pm.cookies.jar().set': 'bru.cookies.jar().setCookie',
|
||||
'pm.cookies.jar().unset': 'bru.cookies.jar().deleteCookie',
|
||||
'pm.cookies.jar().clear': 'bru.cookies.jar().deleteCookies',
|
||||
|
||||
// Execution control
|
||||
'pm.execution.skipRequest': 'bru.runner.skipRequest',
|
||||
@@ -332,6 +344,9 @@ function translateCode(code) {
|
||||
// Preprocess the code to resolve all aliases
|
||||
preprocessAliases(ast);
|
||||
|
||||
// Handle cookie jar variable assignments and method renaming
|
||||
processCookieJarVariables(ast);
|
||||
|
||||
// Process all transformations in a single pass
|
||||
processTransformations(ast, transformedNodes);
|
||||
|
||||
@@ -610,6 +625,59 @@ function removeResolvedDeclarations(ast, symbolTable) {
|
||||
return changesMade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process cookie jar variable assignments and rename methods on those variables
|
||||
* @param {Object} ast - jscodeshift AST
|
||||
*/
|
||||
function processCookieJarVariables(ast) {
|
||||
// Map of Postman cookie jar method names to Bruno equivalents
|
||||
const cookieMethodMapping = {
|
||||
'get': 'getCookie',
|
||||
'getAll': 'getCookies',
|
||||
'set': 'setCookie',
|
||||
'unset': 'deleteCookie',
|
||||
'clear': 'deleteCookies'
|
||||
};
|
||||
|
||||
// Track variables that are assigned to cookie jar instances
|
||||
const cookieJarVariables = new Set();
|
||||
|
||||
// First pass: Find all variables assigned to cookie jar instances
|
||||
ast.find(j.VariableDeclarator).forEach(path => {
|
||||
if (path.value.init && path.value.init.type === 'CallExpression') {
|
||||
const initCall = path.value.init;
|
||||
|
||||
// Check if this is a cookie jar assignment
|
||||
if (initCall.callee.type === 'MemberExpression') {
|
||||
const calleeStr = getMemberExpressionString(initCall.callee);
|
||||
|
||||
if (calleeStr === 'pm.cookies.jar' || calleeStr === 'bru.cookies.jar') {
|
||||
if (path.value.id.type === 'Identifier') {
|
||||
cookieJarVariables.add(path.value.id.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Second pass: Rename method calls on cookie jar variables
|
||||
ast.find(j.CallExpression).forEach(path => {
|
||||
if (path.value.callee.type === 'MemberExpression' &&
|
||||
path.value.callee.object.type === 'Identifier' &&
|
||||
path.value.callee.property.type === 'Identifier') {
|
||||
|
||||
const varName = path.value.callee.object.name;
|
||||
const methodName = path.value.callee.property.name;
|
||||
|
||||
// If this is a method call on a cookie jar variable
|
||||
if (cookieJarVariables.has(varName) && cookieMethodMapping[methodName]) {
|
||||
const newMethodName = cookieMethodMapping[methodName];
|
||||
path.value.callee.property.name = newMethodName;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Postman's tests["..."] = ... syntax
|
||||
* @param {Object} ast - jscodeshift AST
|
||||
|
||||
@@ -16,8 +16,8 @@ describe('postmanTranslations - comment handling', () => {
|
||||
});
|
||||
|
||||
test('should comment non-translated pm commands', () => {
|
||||
const inputScript = "pm.test('random test', () => pm.cookies.get('cookieName'));";
|
||||
const expectedOutput = "// test('random test', () => pm.cookies.get('cookieName'));";
|
||||
const inputScript = "pm.test('random test', () => pm.globals.clear());";
|
||||
const expectedOutput = "// test('random test', () => pm.globals.clear());";
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
const { default: postmanTranslation } = require("../../../src/postman/postman-translations");
|
||||
|
||||
describe('postmanTranslations - cookie API conversions', () => {
|
||||
test('should convert pm.cookies.jar().get to bru.cookies.jar().getCookie', () => {
|
||||
const inputScript = `pm.cookies.jar().get('https://example.com', 'sessionId', (err, cookie) => {
|
||||
console.log(cookie);
|
||||
});`;
|
||||
|
||||
const expectedOutput = `bru.cookies.jar().getCookie('https://example.com', 'sessionId', (err, cookie) => {
|
||||
console.log(cookie);
|
||||
});`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert pm.cookies.jar().getAll to bru.cookies.jar().getCookies', () => {
|
||||
const inputScript = `pm.cookies.jar().getAll('https://example.com', (err, cookies) => {
|
||||
console.log(cookies);
|
||||
});`;
|
||||
|
||||
const expectedOutput = `bru.cookies.jar().getCookies('https://example.com', (err, cookies) => {
|
||||
console.log(cookies);
|
||||
});`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert pm.cookies.jar().set to bru.cookies.jar().setCookie', () => {
|
||||
const inputScript = `pm.cookies.jar().set('https://example.com', 'sessionId', 'abc123', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
const expectedOutput = `bru.cookies.jar().setCookie('https://example.com', 'sessionId', 'abc123', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert pm.cookies.jar().unset to bru.cookies.jar().deleteCookie', () => {
|
||||
const inputScript = `pm.cookies.jar().unset('https://example.com', 'sessionId', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
const expectedOutput = `bru.cookies.jar().deleteCookie('https://example.com', 'sessionId', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert pm.cookies.jar().clear to bru.cookies.jar().deleteCookies (behavior difference)', () => {
|
||||
const inputScript = `pm.cookies.jar().clear('https://example.com', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
const expectedOutput = `bru.cookies.jar().deleteCookies('https://example.com', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle multiple cookie operations in one script', () => {
|
||||
const inputScript = `
|
||||
pm.cookies.jar().set('https://api.example.com', 'auth', 'token123');
|
||||
const cookie = pm.cookies.jar().get('https://api.example.com', 'auth');
|
||||
pm.cookies.jar().getAll('https://api.example.com', (err, cookies) => {
|
||||
console.log('All cookies:', cookies);
|
||||
});
|
||||
pm.cookies.jar().unset('https://api.example.com', 'temp');
|
||||
pm.cookies.jar().clear('https://api.example.com');
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
bru.cookies.jar().setCookie('https://api.example.com', 'auth', 'token123');
|
||||
const cookie = bru.cookies.jar().getCookie('https://api.example.com', 'auth');
|
||||
bru.cookies.jar().getCookies('https://api.example.com', (err, cookies) => {
|
||||
console.log('All cookies:', cookies);
|
||||
});
|
||||
bru.cookies.jar().deleteCookie('https://api.example.com', 'temp');
|
||||
bru.cookies.jar().deleteCookies('https://api.example.com');
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert variable assignment and method calls on cookie jar variables', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.set('https://example.com', 'user', 'john');
|
||||
const userCookie = jar.get('https://example.com', 'user');
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.setCookie('https://example.com', 'user', 'john');
|
||||
const userCookie = jar.getCookie('https://example.com', 'user');
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert jar.get to jar.getCookie with callback', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.get('https://api.example.com', 'authToken', (error, cookie) => {
|
||||
if (error) {
|
||||
console.error('Error getting cookie:', error);
|
||||
} else {
|
||||
console.log('Retrieved cookie:', cookie);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.getCookie('https://api.example.com', 'authToken', (error, cookie) => {
|
||||
if (error) {
|
||||
console.error('Error getting cookie:', error);
|
||||
} else {
|
||||
console.log('Retrieved cookie:', cookie);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert jar.getAll to jar.getCookies with callback', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.getAll('https://api.example.com', (error, cookies) => {
|
||||
if (error) {
|
||||
console.error('Error getting cookies:', error);
|
||||
} else {
|
||||
console.log('All cookies:', cookies);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.getCookies('https://api.example.com', (error, cookies) => {
|
||||
if (error) {
|
||||
console.error('Error getting cookies:', error);
|
||||
} else {
|
||||
console.log('All cookies:', cookies);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert jar.set to jar.setCookie with cookie object', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.set('https://api.example.com', {
|
||||
key: 'sessionId',
|
||||
value: 'abc123',
|
||||
path: '/api',
|
||||
httpOnly: true,
|
||||
secure: true
|
||||
}, (error) => {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.setCookie('https://api.example.com', {
|
||||
key: 'sessionId',
|
||||
value: 'abc123',
|
||||
path: '/api',
|
||||
httpOnly: true,
|
||||
secure: true
|
||||
}, (error) => {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert jar.unset to jar.deleteCookie', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.unset('https://api.example.com', 'tempCookie', (error) => {
|
||||
if (error) {
|
||||
console.error('Failed to delete cookie:', error);
|
||||
} else {
|
||||
console.log('Cookie deleted successfully');
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.deleteCookie('https://api.example.com', 'tempCookie', (error) => {
|
||||
if (error) {
|
||||
console.error('Failed to delete cookie:', error);
|
||||
} else {
|
||||
console.log('Cookie deleted successfully');
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert jar.clear to jar.deleteCookies', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.clear('https://api.example.com', (error) => {
|
||||
if (error) {
|
||||
console.error('Failed to clear cookies:', error);
|
||||
} else {
|
||||
console.log('All cookies cleared for domain');
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.deleteCookies('https://api.example.com', (error) => {
|
||||
if (error) {
|
||||
console.error('Failed to clear cookies:', error);
|
||||
} else {
|
||||
console.log('All cookies cleared for domain');
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle complex cookie workflow with jar variable', () => {
|
||||
const inputScript = `
|
||||
const cookieJar = pm.cookies.jar();
|
||||
|
||||
// Set multiple cookies
|
||||
cookieJar.set('https://example.com', 'auth', 'token123');
|
||||
cookieJar.set('https://example.com', {
|
||||
key: 'preferences',
|
||||
value: JSON.stringify({theme: 'dark'}),
|
||||
path: '/'
|
||||
});
|
||||
|
||||
// Get specific cookie
|
||||
cookieJar.get('https://example.com', 'auth', (err, authCookie) => {
|
||||
console.log('Auth cookie:', authCookie);
|
||||
});
|
||||
|
||||
// Get all cookies
|
||||
cookieJar.getAll('https://example.com', (err, allCookies) => {
|
||||
console.log('Total cookies:', allCookies.length);
|
||||
});
|
||||
|
||||
// Clean up
|
||||
cookieJar.unset('https://example.com', 'temp');
|
||||
cookieJar.clear('https://example.com');
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const cookieJar = bru.cookies.jar();
|
||||
|
||||
// Set multiple cookies
|
||||
cookieJar.setCookie('https://example.com', 'auth', 'token123');
|
||||
cookieJar.setCookie('https://example.com', {
|
||||
key: 'preferences',
|
||||
value: JSON.stringify({theme: 'dark'}),
|
||||
path: '/'
|
||||
});
|
||||
|
||||
// Get specific cookie
|
||||
cookieJar.getCookie('https://example.com', 'auth', (err, authCookie) => {
|
||||
console.log('Auth cookie:', authCookie);
|
||||
});
|
||||
|
||||
// Get all cookies
|
||||
cookieJar.getCookies('https://example.com', (err, allCookies) => {
|
||||
console.log('Total cookies:', allCookies.length);
|
||||
});
|
||||
|
||||
// Clean up
|
||||
cookieJar.deleteCookie('https://example.com', 'temp');
|
||||
cookieJar.deleteCookies('https://example.com');
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle mixed jar variable and direct calls', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.get('https://api.com', 'session');
|
||||
|
||||
pm.cookies.jar().set('https://other.com', 'temp', 'value');
|
||||
|
||||
jar.getAll('https://api.com', (err, cookies) => {
|
||||
console.log(cookies);
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.getCookie('https://api.com', 'session');
|
||||
|
||||
bru.cookies.jar().setCookie('https://other.com', 'temp', 'value');
|
||||
|
||||
jar.getCookies('https://api.com', (err, cookies) => {
|
||||
console.log(cookies);
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
});
|
||||
@@ -32,12 +32,13 @@
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/filestore": "^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",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
"about-window": "^1.15.2",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "^1.8.3",
|
||||
@@ -64,7 +65,6 @@
|
||||
"nanoid": "3.3.8",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"uuid": "^9.0.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
@@ -72,7 +72,7 @@
|
||||
"dmg-license": "^1.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "33.2.1",
|
||||
"electron": "~37.2.6",
|
||||
"electron-builder": "25.1.8",
|
||||
"electron-devtools-installer": "^4.0.0"
|
||||
}
|
||||
|
||||
@@ -3,12 +3,18 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
const { hasBruExtension, isWSLPath, normalizeAndResolvePath, sizeInMB } = require('../utils/filesystem');
|
||||
const { bruToEnvJson, bruToJson, bruToJsonViaWorker, collectionBruToJson } = require('../bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
const {
|
||||
parseEnvironment,
|
||||
parseRequest,
|
||||
parseRequestViaWorker,
|
||||
parseCollection,
|
||||
parseFolder
|
||||
} = require('@usebruno/filestore');
|
||||
const { parseDotEnv } = require('@usebruno/filestore');
|
||||
|
||||
const { uuid } = require('../utils/common');
|
||||
const { getRequestUid } = require('../cache/requestUids');
|
||||
const { decryptString } = require('../utils/encryption');
|
||||
const { decryptStringSafe } = require('../utils/encryption');
|
||||
const { setDotEnvVars } = require('../store/process-env');
|
||||
const { setBrunoConfig } = require('../store/bruno-config');
|
||||
const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||
@@ -80,7 +86,7 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath)
|
||||
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = await bruToEnvJson(bruContent);
|
||||
file.data = await parseEnvironment(bruContent);
|
||||
file.data.name = basename.substring(0, basename.length - 4);
|
||||
file.data.uid = getRequestUid(pathname);
|
||||
|
||||
@@ -92,14 +98,15 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath)
|
||||
_.each(envSecrets, (secret) => {
|
||||
const variable = _.find(file.data.variables, (v) => v.name === secret.name);
|
||||
if (variable && secret.value) {
|
||||
variable.value = decryptString(secret.value);
|
||||
const decryptionResult = decryptStringSafe(secret.value);
|
||||
variable.value = decryptionResult.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error('Error processing environment file: ', err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,7 +122,7 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat
|
||||
};
|
||||
|
||||
const bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
file.data = await bruToEnvJson(bruContent);
|
||||
file.data = await parseEnvironment(bruContent);
|
||||
file.data.name = basename.substring(0, basename.length - 4);
|
||||
file.data.uid = getRequestUid(pathname);
|
||||
_.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));
|
||||
@@ -126,7 +133,8 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat
|
||||
_.each(envSecrets, (secret) => {
|
||||
const variable = _.find(file.data.variables, (v) => v.name === secret.name);
|
||||
if (variable && secret.value) {
|
||||
variable.value = decryptString(secret.value);
|
||||
const decryptionResult = decryptStringSafe(secret.value);
|
||||
variable.value = decryptionResult.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -160,7 +168,7 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
|
||||
}
|
||||
};
|
||||
|
||||
const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread) => {
|
||||
const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread, watcher) => {
|
||||
console.log(`watcher add: ${pathname}`);
|
||||
|
||||
if (isBrunoConfigFile(pathname, collectionPath)) {
|
||||
@@ -177,7 +185,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
|
||||
if (isDotEnvFile(pathname, collectionPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(pathname, 'utf8');
|
||||
const jsonData = dotenvToJson(content);
|
||||
const jsonData = parseDotEnv(content);
|
||||
|
||||
setDotEnvVars(collectionUid, jsonData);
|
||||
const payload = {
|
||||
@@ -209,7 +217,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = await collectionBruToJson(bruContent);
|
||||
file.data = await parseCollection(bruContent);
|
||||
|
||||
hydrateBruCollectionFileWithUuid(file.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
@@ -233,7 +241,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = await collectionBruToJson(bruContent);
|
||||
file.data = await parseCollection(bruContent);
|
||||
|
||||
hydrateBruCollectionFileWithUuid(file.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
@@ -245,6 +253,8 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
|
||||
}
|
||||
|
||||
if (hasBruExtension(pathname)) {
|
||||
watcher.addFileToProcessing(collectionUid, pathname);
|
||||
|
||||
const file = {
|
||||
meta: {
|
||||
collectionUid,
|
||||
@@ -258,14 +268,17 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
|
||||
// If worker thread is not used, we can directly parse the file
|
||||
if (!useWorkerThread) {
|
||||
try {
|
||||
file.data = await bruToJson(bruContent);
|
||||
file.data = await parseRequest(bruContent);
|
||||
file.partial = false;
|
||||
file.loading = false;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
watcher.markFileAsProcessed(win, collectionUid, pathname);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -278,7 +291,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
|
||||
type: 'http-request'
|
||||
};
|
||||
|
||||
const metaJson = await bruToJson(parseBruFileMeta(bruContent), true);
|
||||
const metaJson = parseBruFileMeta(bruContent);
|
||||
file.data = metaJson;
|
||||
file.partial = true;
|
||||
file.loading = false;
|
||||
@@ -295,7 +308,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
|
||||
// This is to update the file info in the UI
|
||||
file.data = await bruToJsonViaWorker(bruContent);
|
||||
file.data = await parseRequestViaWorker(bruContent);
|
||||
file.partial = false;
|
||||
file.loading = false;
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
@@ -314,6 +327,8 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
} finally {
|
||||
watcher.markFileAsProcessed(win, collectionUid, pathname);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -331,7 +346,7 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => {
|
||||
|
||||
if (fs.existsSync(folderBruFilePath)) {
|
||||
let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8');
|
||||
let folderBruData = await collectionBruToJson(folderBruFileContent);
|
||||
let folderBruData = await parseFolder(folderBruFileContent);
|
||||
name = folderBruData?.meta?.name || name;
|
||||
seq = folderBruData?.meta?.seq;
|
||||
}
|
||||
@@ -370,7 +385,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
if (isDotEnvFile(pathname, collectionPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(pathname, 'utf8');
|
||||
const jsonData = dotenvToJson(content);
|
||||
const jsonData = parseDotEnv(content);
|
||||
|
||||
setDotEnvVars(collectionUid, jsonData);
|
||||
const payload = {
|
||||
@@ -402,7 +417,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = await collectionBruToJson(bruContent);
|
||||
file.data = await parseCollection(bruContent);
|
||||
hydrateBruCollectionFileWithUuid(file.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'change', file);
|
||||
return;
|
||||
@@ -425,7 +440,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
try {
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
|
||||
file.data = await collectionBruToJson(bruContent);
|
||||
file.data = await parseCollection(bruContent);
|
||||
|
||||
hydrateBruCollectionFileWithUuid(file.data);
|
||||
win.webContents.send('main:collection-tree-updated', 'change', file);
|
||||
@@ -447,7 +462,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
||||
};
|
||||
|
||||
const bru = fs.readFileSync(pathname, 'utf8');
|
||||
file.data = await bruToJson(bru);
|
||||
file.data = await parseRequest(bru);
|
||||
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
win.webContents.send('main:collection-tree-updated', 'change', file);
|
||||
@@ -490,7 +505,7 @@ const unlinkDir = async (win, pathname, collectionUid, collectionPath) => {
|
||||
|
||||
if (fs.existsSync(folderBruFilePath)) {
|
||||
let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8');
|
||||
let folderBruData = await collectionBruToJson(folderBruFileContent);
|
||||
let folderBruData = await parseFolder(folderBruFileContent);
|
||||
name = folderBruData?.meta?.name || name;
|
||||
}
|
||||
|
||||
@@ -504,7 +519,10 @@ const unlinkDir = async (win, pathname, collectionUid, collectionPath) => {
|
||||
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
|
||||
};
|
||||
|
||||
const onWatcherSetupComplete = (win, watchPath) => {
|
||||
const onWatcherSetupComplete = (win, watchPath, collectionUid, watcher) => {
|
||||
// Mark discovery as complete
|
||||
watcher.completeCollectionDiscovery(win, collectionUid);
|
||||
|
||||
const UiStateSnapshotStore = new UiStateSnapshot();
|
||||
const collectionsSnapshotState = UiStateSnapshotStore.getCollections();
|
||||
const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == watchPath);
|
||||
@@ -514,6 +532,75 @@ const onWatcherSetupComplete = (win, watchPath) => {
|
||||
class CollectionWatcher {
|
||||
constructor() {
|
||||
this.watchers = {};
|
||||
this.loadingStates = {};
|
||||
}
|
||||
|
||||
// Initialize loading state tracking for a collection
|
||||
initializeLoadingState(collectionUid) {
|
||||
if (!this.loadingStates[collectionUid]) {
|
||||
this.loadingStates[collectionUid] = {
|
||||
isDiscovering: false, // Initial discovery phase
|
||||
isProcessing: false, // Processing discovered files
|
||||
pendingFiles: new Set(), // Files that need processing
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
startCollectionDiscovery(win, collectionUid) {
|
||||
this.initializeLoadingState(collectionUid);
|
||||
const state = this.loadingStates[collectionUid];
|
||||
|
||||
state.isDiscovering = true;
|
||||
state.pendingFiles.clear();
|
||||
|
||||
win.webContents.send('main:collection-loading-state-updated', {
|
||||
collectionUid,
|
||||
isLoading: true
|
||||
});
|
||||
}
|
||||
|
||||
addFileToProcessing(collectionUid, filepath) {
|
||||
this.initializeLoadingState(collectionUid);
|
||||
const state = this.loadingStates[collectionUid];
|
||||
state.pendingFiles.add(filepath);
|
||||
}
|
||||
|
||||
markFileAsProcessed(win, collectionUid, filepath) {
|
||||
if (!this.loadingStates[collectionUid]) return;
|
||||
|
||||
const state = this.loadingStates[collectionUid];
|
||||
state.pendingFiles.delete(filepath);
|
||||
|
||||
// If discovery is complete and no pending files, mark as not loading
|
||||
if (!state.isDiscovering && state.pendingFiles.size === 0 && state.isProcessing) {
|
||||
state.isProcessing = false;
|
||||
win.webContents.send('main:collection-loading-state-updated', {
|
||||
collectionUid,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
completeCollectionDiscovery(win, collectionUid) {
|
||||
if (!this.loadingStates[collectionUid]) return;
|
||||
|
||||
const state = this.loadingStates[collectionUid];
|
||||
state.isDiscovering = false;
|
||||
|
||||
// If there are pending files, start processing phase
|
||||
if (state.pendingFiles.size > 0) {
|
||||
state.isProcessing = true;
|
||||
} else {
|
||||
// No pending files, collection is fully loaded
|
||||
win.webContents.send('main:collection-loading-state-updated', {
|
||||
collectionUid,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cleanupLoadingState(collectionUid) {
|
||||
delete this.loadingStates[collectionUid];
|
||||
}
|
||||
|
||||
addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false, useWorkerThread) {
|
||||
@@ -521,6 +608,10 @@ class CollectionWatcher {
|
||||
this.watchers[watchPath].close();
|
||||
}
|
||||
|
||||
this.initializeLoadingState(collectionUid);
|
||||
|
||||
this.startCollectionDiscovery(win, collectionUid);
|
||||
|
||||
const ignores = brunoConfig?.ignore || [];
|
||||
setTimeout(() => {
|
||||
const watcher = chokidar.watch(watchPath, {
|
||||
@@ -546,8 +637,8 @@ class CollectionWatcher {
|
||||
|
||||
let startedNewWatcher = false;
|
||||
watcher
|
||||
.on('ready', () => onWatcherSetupComplete(win, watchPath))
|
||||
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread))
|
||||
.on('ready', () => onWatcherSetupComplete(win, watchPath, collectionUid, this))
|
||||
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread, this))
|
||||
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
|
||||
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
|
||||
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
|
||||
@@ -582,11 +673,15 @@ class CollectionWatcher {
|
||||
return this.watchers[watchPath];
|
||||
}
|
||||
|
||||
removeWatcher(watchPath, win) {
|
||||
removeWatcher(watchPath, win, collectionUid) {
|
||||
if (this.watchers[watchPath]) {
|
||||
this.watchers[watchPath].close();
|
||||
this.watchers[watchPath] = null;
|
||||
}
|
||||
|
||||
if (collectionUid) {
|
||||
this.cleanupLoadingState(collectionUid);
|
||||
}
|
||||
}
|
||||
|
||||
getWatcherByItemPath(itemPath) {
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const {
|
||||
bruToJsonV2,
|
||||
jsonToBruV2,
|
||||
bruToEnvJsonV2,
|
||||
envJsonToBruV2,
|
||||
collectionBruToJson: _collectionBruToJson,
|
||||
jsonToCollectionBru: _jsonToCollectionBru
|
||||
} = require('@usebruno/lang');
|
||||
const BruParserWorker = require('./workers');
|
||||
|
||||
const bruParserWorker = new BruParserWorker();
|
||||
|
||||
const collectionBruToJson = async (data, parsed = false) => {
|
||||
try {
|
||||
const json = parsed ? data : _collectionBruToJson(data);
|
||||
|
||||
const transformedJson = {
|
||||
request: {
|
||||
headers: _.get(json, 'headers', []),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
script: _.get(json, 'script', {}),
|
||||
vars: _.get(json, 'vars', {}),
|
||||
tests: _.get(json, 'tests', '')
|
||||
},
|
||||
settings: _.get(json, 'settings', {}),
|
||||
docs: _.get(json, 'docs', '')
|
||||
};
|
||||
|
||||
// add meta if it exists
|
||||
// this is only for folder bru file
|
||||
// in the future, all of this will be replaced by standard bru lang
|
||||
const sequence = _.get(json, 'meta.seq');
|
||||
if (json?.meta) {
|
||||
transformedJson.meta = {
|
||||
name: json.meta.name,
|
||||
};
|
||||
|
||||
if (sequence) {
|
||||
transformedJson.meta.seq = Number(sequence);
|
||||
}
|
||||
}
|
||||
|
||||
return transformedJson;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const jsonToCollectionBru = async (json, isFolder) => {
|
||||
try {
|
||||
const collectionBruJson = {
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
script: {
|
||||
req: _.get(json, 'request.script.req', ''),
|
||||
res: _.get(json, 'request.script.res', '')
|
||||
},
|
||||
vars: {
|
||||
req: _.get(json, 'request.vars.req', []),
|
||||
res: _.get(json, 'request.vars.res', [])
|
||||
},
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
docs: _.get(json, 'docs', '')
|
||||
};
|
||||
|
||||
// add meta if it exists
|
||||
// this is only for folder bru file
|
||||
// in the future, all of this will be replaced by standard bru lang
|
||||
const sequence = _.get(json, 'meta.seq');
|
||||
if (json?.meta) {
|
||||
collectionBruJson.meta = {
|
||||
name: json.meta.name,
|
||||
};
|
||||
|
||||
if (sequence) {
|
||||
collectionBruJson.meta.seq = Number(sequence);
|
||||
}
|
||||
}
|
||||
|
||||
return _jsonToCollectionBru(collectionBruJson);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const bruToEnvJson = async (bru) => {
|
||||
try {
|
||||
const json = bruToEnvJsonV2(bru);
|
||||
|
||||
// the app env format requires each variable to have a type
|
||||
// this need to be evaluated and safely removed
|
||||
// i don't see it being used in schema validation
|
||||
if (json && json.variables && json.variables.length) {
|
||||
_.each(json.variables, (v) => (v.type = 'text'));
|
||||
}
|
||||
|
||||
return json;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const envJsonToBru = async (json) => {
|
||||
try {
|
||||
const bru = envJsonToBruV2(json);
|
||||
return bru;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The transformer function for converting a BRU file to JSON.
|
||||
*
|
||||
* We map the json response from the bru lang and transform it into the DSL
|
||||
* format that the app uses
|
||||
*
|
||||
* @param {string} data The BRU file content.
|
||||
* @returns {object} The JSON representation of the BRU file.
|
||||
*/
|
||||
const bruToJson = (data, parsed = false) => {
|
||||
try {
|
||||
const json = parsed ? data : bruToJsonV2(data);
|
||||
|
||||
let requestType = _.get(json, 'meta.type');
|
||||
if (requestType === 'http') {
|
||||
requestType = 'http-request';
|
||||
} else if (requestType === 'graphql') {
|
||||
requestType = 'graphql-request';
|
||||
} else {
|
||||
requestType = 'http-request';
|
||||
}
|
||||
|
||||
const sequence = _.get(json, 'meta.seq');
|
||||
const transformedJson = {
|
||||
type: requestType,
|
||||
name: _.get(json, 'meta.name'),
|
||||
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
||||
settings: _.get(json, 'settings', {}),
|
||||
tags: _.get(json, 'meta.tags', []),
|
||||
request: {
|
||||
method: _.upperCase(_.get(json, 'http.method')),
|
||||
url: _.get(json, 'http.url'),
|
||||
params: _.get(json, 'params', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
body: _.get(json, 'body', {}),
|
||||
script: _.get(json, 'script', {}),
|
||||
vars: _.get(json, 'vars', {}),
|
||||
assertions: _.get(json, 'assertions', []),
|
||||
tests: _.get(json, 'tests', ''),
|
||||
docs: _.get(json, 'docs', '')
|
||||
}
|
||||
};
|
||||
|
||||
transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');
|
||||
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
|
||||
return transformedJson;
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
};
|
||||
|
||||
const bruToJsonViaWorker = async (data) => {
|
||||
try {
|
||||
const json = await bruParserWorker?.bruToJson(data);
|
||||
return bruToJson(json, true);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The transformer function for converting a JSON to BRU file.
|
||||
*
|
||||
* We map the json response from the app and transform it into the DSL
|
||||
* format that the bru lang understands
|
||||
*
|
||||
* @param {object} json The JSON representation of the BRU file.
|
||||
* @returns {string} The BRU file content.
|
||||
*/
|
||||
const jsonToBru = async (json) => {
|
||||
let type = _.get(json, 'type');
|
||||
if (type === 'http-request') {
|
||||
type = 'http';
|
||||
} else if (type === 'graphql-request') {
|
||||
type = 'graphql';
|
||||
} else {
|
||||
type = 'http';
|
||||
}
|
||||
|
||||
const sequence = _.get(json, 'seq');
|
||||
const bruJson = {
|
||||
meta: {
|
||||
name: _.get(json, 'name'),
|
||||
type: type,
|
||||
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
||||
tags: _.get(json, 'tags', []),
|
||||
},
|
||||
http: {
|
||||
method: _.lowerCase(_.get(json, 'request.method')),
|
||||
url: _.get(json, 'request.url'),
|
||||
auth: _.get(json, 'request.auth.mode', 'none'),
|
||||
body: _.get(json, 'request.body.mode', 'none')
|
||||
},
|
||||
params: _.get(json, 'request.params', []),
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
body: _.get(json, 'request.body', {}),
|
||||
script: _.get(json, 'request.script', {}),
|
||||
vars: {
|
||||
req: _.get(json, 'request.vars.req', []),
|
||||
res: _.get(json, 'request.vars.res', [])
|
||||
},
|
||||
assertions: _.get(json, 'request.assertions', []),
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
settings: _.get(json, 'settings', {}),
|
||||
docs: _.get(json, 'request.docs', '')
|
||||
};
|
||||
|
||||
const bru = jsonToBruV2(bruJson);
|
||||
return bru;
|
||||
};
|
||||
|
||||
const jsonToBruViaWorker = async (json) => {
|
||||
let type = _.get(json, 'type');
|
||||
if (type === 'http-request') {
|
||||
type = 'http';
|
||||
} else if (type === 'graphql-request') {
|
||||
type = 'graphql';
|
||||
} else {
|
||||
type = 'http';
|
||||
}
|
||||
|
||||
const sequence = _.get(json, 'seq');
|
||||
const bruJson = {
|
||||
meta: {
|
||||
name: _.get(json, 'name'),
|
||||
type: type,
|
||||
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
||||
tags: _.get(json, 'tags', [])
|
||||
},
|
||||
http: {
|
||||
method: _.lowerCase(_.get(json, 'request.method')),
|
||||
url: _.get(json, 'request.url'),
|
||||
auth: _.get(json, 'request.auth.mode', 'none'),
|
||||
body: _.get(json, 'request.body.mode', 'none')
|
||||
},
|
||||
params: _.get(json, 'request.params', []),
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
body: _.get(json, 'request.body', {}),
|
||||
script: _.get(json, 'request.script', {}),
|
||||
vars: {
|
||||
req: _.get(json, 'request.vars.req', []),
|
||||
res: _.get(json, 'request.vars.res', [])
|
||||
},
|
||||
assertions: _.get(json, 'request.assertions', []),
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
settings: _.get(json, 'settings', {}),
|
||||
docs: _.get(json, 'request.docs', '')
|
||||
};
|
||||
|
||||
const bru = await bruParserWorker?.jsonToBru(bruJson)
|
||||
return bru;
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
bruToJson,
|
||||
bruToJsonViaWorker,
|
||||
jsonToBru,
|
||||
bruToEnvJson,
|
||||
envJsonToBru,
|
||||
collectionBruToJson,
|
||||
jsonToCollectionBru,
|
||||
jsonToBruViaWorker
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
const { sizeInMB } = require("../../utils/filesystem");
|
||||
const WorkerQueue = require("../../workers");
|
||||
const path = require("path");
|
||||
|
||||
const getSize = (data) => {
|
||||
return sizeInMB(typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : Buffer.byteLength(JSON.stringify(data), 'utf8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lanes are used to determine which worker queue to use based on the size of the data.
|
||||
*
|
||||
* The first lane is for smaller files (<0.1MB), the second lane is for larger files (>=0.1MB).
|
||||
* This helps with parsing performance.
|
||||
*/
|
||||
const LANES = [{
|
||||
maxSize: 0.005
|
||||
},{
|
||||
maxSize: 0.1
|
||||
},{
|
||||
maxSize: 1
|
||||
},{
|
||||
maxSize: 10
|
||||
},{
|
||||
maxSize: 100
|
||||
}];
|
||||
|
||||
class BruParserWorker {
|
||||
constructor() {
|
||||
this.workerQueues = LANES?.map(lane => ({
|
||||
maxSize: lane?.maxSize,
|
||||
workerQueue: new WorkerQueue()
|
||||
}));
|
||||
}
|
||||
|
||||
getWorkerQueue(size) {
|
||||
// Find the first queue that can handle the given size
|
||||
// or fallback to the last queue for largest files
|
||||
const queueForSize = this.workerQueues.find((queue) =>
|
||||
queue.maxSize >= size
|
||||
);
|
||||
|
||||
return queueForSize?.workerQueue ?? this.workerQueues.at(-1).workerQueue;
|
||||
}
|
||||
|
||||
async enqueueTask({data, scriptFile }) {
|
||||
const size = getSize(data);
|
||||
const workerQueue = this.getWorkerQueue(size);
|
||||
return workerQueue.enqueue({
|
||||
data,
|
||||
priority: size,
|
||||
scriptPath: path.join(__dirname, `./scripts/${scriptFile}.js`)
|
||||
});
|
||||
}
|
||||
|
||||
async bruToJson(data) {
|
||||
return this.enqueueTask({ data, scriptFile: `bru-to-json` });
|
||||
}
|
||||
|
||||
async jsonToBru(data) {
|
||||
return this.enqueueTask({ data, scriptFile: `json-to-bru` });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BruParserWorker;
|
||||
@@ -1,16 +0,0 @@
|
||||
const { parentPort } = require('worker_threads');
|
||||
const {
|
||||
bruToJsonV2,
|
||||
} = require('@usebruno/lang');
|
||||
|
||||
parentPort.on('message', (workerData) => {
|
||||
try {
|
||||
const bru = workerData;
|
||||
const json = bruToJsonV2(bru);
|
||||
parentPort.postMessage(json);
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
parentPort.postMessage({ error: error?.message });
|
||||
}
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
const { parentPort } = require('worker_threads');
|
||||
const {
|
||||
jsonToBruV2,
|
||||
} = require('@usebruno/lang');
|
||||
|
||||
parentPort.on('message', (workerData) => {
|
||||
try {
|
||||
const json = workerData;
|
||||
const bru = jsonToBruV2(json);
|
||||
parentPort.postMessage(bru);
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
parentPort.postMessage({ error: error?.message });
|
||||
}
|
||||
});
|
||||
@@ -5,7 +5,18 @@ const fsExtra = require('fs-extra');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { ipcMain, shell, dialog, app } = require('electron');
|
||||
const { envJsonToBru, bruToJson, jsonToBru, jsonToBruViaWorker, collectionBruToJson, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru');
|
||||
const {
|
||||
parseRequest,
|
||||
stringifyRequest,
|
||||
parseRequestViaWorker,
|
||||
stringifyRequestViaWorker,
|
||||
parseCollection,
|
||||
stringifyCollection,
|
||||
parseFolder,
|
||||
stringifyFolder,
|
||||
parseEnvironment,
|
||||
stringifyEnvironment
|
||||
} = require('@usebruno/filestore');
|
||||
const brunoConverters = require('@usebruno/converters');
|
||||
const { postmanToBruno } = brunoConverters;
|
||||
|
||||
@@ -225,10 +236,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
};
|
||||
}
|
||||
|
||||
const content = await jsonToCollectionBru(
|
||||
folderRoot,
|
||||
true // isFolder
|
||||
);
|
||||
const content = await stringifyFolder(folderRoot);
|
||||
await writeFile(folderBruFilePath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@@ -238,7 +246,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
try {
|
||||
const collectionBruFilePath = path.join(collectionPathname, 'collection.bru');
|
||||
|
||||
const content = await jsonToCollectionBru(collectionRoot);
|
||||
const content = await stringifyCollection(collectionRoot);
|
||||
await writeFile(collectionBruFilePath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@@ -256,7 +264,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(`${request.filename}.bru is not a valid filename`);
|
||||
}
|
||||
validatePathIsInsideCollection(pathname, lastOpenedCollections);
|
||||
const content = await jsonToBruViaWorker(request);
|
||||
const content = await stringifyRequestViaWorker(request);
|
||||
await writeFile(pathname, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@@ -270,7 +278,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(`path: ${pathname} does not exist`);
|
||||
}
|
||||
|
||||
const content = await jsonToBruViaWorker(request);
|
||||
const content = await stringifyRequestViaWorker(request);
|
||||
await writeFile(pathname, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@@ -288,7 +296,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
throw new Error(`path: ${pathname} does not exist`);
|
||||
}
|
||||
|
||||
const content = await jsonToBruViaWorker(request);
|
||||
const content = await stringifyRequestViaWorker(request);
|
||||
await writeFile(pathname, content);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -318,7 +326,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
|
||||
}
|
||||
|
||||
const content = await envJsonToBru(environment);
|
||||
const content = await stringifyEnvironment(environment);
|
||||
|
||||
await writeFile(envFilePath, content);
|
||||
} catch (error) {
|
||||
@@ -343,7 +351,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
|
||||
}
|
||||
|
||||
const content = await envJsonToBru(environment);
|
||||
const content = await stringifyEnvironment(environment);
|
||||
await writeFile(envFilePath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@@ -402,7 +410,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
let folderBruFileJsonContent;
|
||||
if (fs.existsSync(folderBruFilePath)) {
|
||||
const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8');
|
||||
folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent);
|
||||
folderBruFileJsonContent = await parseFolder(oldFolderBruFileContent);
|
||||
folderBruFileJsonContent.meta.name = newName;
|
||||
} else {
|
||||
folderBruFileJsonContent = {
|
||||
@@ -412,7 +420,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
};
|
||||
}
|
||||
|
||||
const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true);
|
||||
const folderBruFileContent = await stringifyFolder(folderBruFileJsonContent);
|
||||
await writeFile(folderBruFilePath, folderBruFileContent);
|
||||
|
||||
return;
|
||||
@@ -424,9 +432,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(itemPath, 'utf8');
|
||||
const jsonData = await bruToJson(data);
|
||||
const jsonData = parseRequest(data);
|
||||
jsonData.name = newName;
|
||||
const content = await jsonToBru(jsonData);
|
||||
const content = stringifyRequest(jsonData);
|
||||
await writeFile(itemPath, content);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@@ -452,7 +460,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
let folderBruFileJsonContent;
|
||||
if (fs.existsSync(folderBruFilePath)) {
|
||||
const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8');
|
||||
folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent);
|
||||
folderBruFileJsonContent = await parseFolder(oldFolderBruFileContent);
|
||||
folderBruFileJsonContent.meta.name = newName;
|
||||
} else {
|
||||
folderBruFileJsonContent = {
|
||||
@@ -462,7 +470,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
};
|
||||
}
|
||||
|
||||
const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true);
|
||||
const folderBruFileContent = await stringifyFolder(folderBruFileJsonContent);
|
||||
await writeFile(folderBruFilePath, folderBruFileContent);
|
||||
|
||||
const bruFilesAtSource = await searchForBruFiles(oldPath);
|
||||
@@ -503,11 +511,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
// update name in file and save new copy, then delete old copy
|
||||
const data = await fs.promises.readFile(oldPath, 'utf8'); // Use async read
|
||||
const jsonData = await bruToJsonViaWorker(data);
|
||||
const jsonData = parseRequest(data);
|
||||
jsonData.name = newName;
|
||||
moveRequestUid(oldPath, newPath);
|
||||
|
||||
const content = await jsonToBruViaWorker(jsonData);
|
||||
const content = stringifyRequest(jsonData);
|
||||
await fs.promises.unlink(oldPath);
|
||||
await writeFile(newPath, content);
|
||||
|
||||
@@ -538,7 +546,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
if (!fs.existsSync(pathname)) {
|
||||
fs.mkdirSync(pathname);
|
||||
const folderBruFilePath = path.join(pathname, 'folder.bru');
|
||||
const content = await jsonToCollectionBru(folderBruJsonData, true); // isFolder flag
|
||||
const content = await stringifyFolder(folderBruJsonData);
|
||||
await writeFile(folderBruFilePath, content);
|
||||
} else {
|
||||
return Promise.reject(new Error('The directory already exists'));
|
||||
@@ -585,10 +593,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:remove-collection', async (event, collectionPath) => {
|
||||
ipcMain.handle('renderer:remove-collection', async (event, collectionPath, collectionUid) => {
|
||||
if (watcher && mainWindow) {
|
||||
console.log(`watcher stopWatching: ${collectionPath}`);
|
||||
watcher.removeWatcher(collectionPath, mainWindow);
|
||||
watcher.removeWatcher(collectionPath, mainWindow, collectionUid);
|
||||
lastOpenedCollections.remove(collectionPath);
|
||||
}
|
||||
});
|
||||
@@ -611,7 +619,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
items.forEach(async (item) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.bru`);
|
||||
const content = await jsonToBruViaWorker(item);
|
||||
const content = await stringifyRequestViaWorker(item);
|
||||
const filePath = path.join(currentPath, sanitizedFilename);
|
||||
safeWriteFileSync(filePath, content);
|
||||
}
|
||||
@@ -623,10 +631,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
if (item?.root?.meta?.name) {
|
||||
const folderBruFilePath = path.join(folderPath, 'folder.bru');
|
||||
item.root.meta.seq = item.seq;
|
||||
const folderContent = await jsonToCollectionBru(
|
||||
item.root,
|
||||
true // isFolder
|
||||
);
|
||||
const folderContent = await stringifyFolder(item.root);
|
||||
safeWriteFileSync(folderBruFilePath, folderContent);
|
||||
}
|
||||
|
||||
@@ -650,7 +655,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
|
||||
environments.forEach(async (env) => {
|
||||
const content = await envJsonToBru(env);
|
||||
const content = await stringifyEnvironment(env);
|
||||
let sanitizedEnvFilename = sanitizeName(`${env.name}.bru`);
|
||||
const filePath = path.join(envDirPath, sanitizedEnvFilename);
|
||||
safeWriteFileSync(filePath, content);
|
||||
@@ -681,7 +686,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// Write the Bruno configuration to a file
|
||||
await writeFile(path.join(collectionPath, 'bruno.json'), stringifiedBrunoConfig);
|
||||
|
||||
const collectionContent = await jsonToCollectionBru(collection.root);
|
||||
const collectionContent = await stringifyCollection(collection.root);
|
||||
await writeFile(path.join(collectionPath, 'collection.bru'), collectionContent);
|
||||
|
||||
const { size, filesCount } = await getCollectionStats(collectionPath);
|
||||
@@ -711,7 +716,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
const parseCollectionItems = (items = [], currentPath) => {
|
||||
items.forEach(async (item) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
const content = await jsonToBruViaWorker(item);
|
||||
const content = await stringifyRequestViaWorker(item);
|
||||
const filePath = path.join(currentPath, item.filename);
|
||||
safeWriteFileSync(filePath, content);
|
||||
}
|
||||
@@ -721,7 +726,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
// If folder has a root element, then I should write its folder.bru file
|
||||
if (item.root) {
|
||||
const folderContent = await jsonToCollectionBru(item.root, true);
|
||||
const folderContent = await stringifyFolder(item.root);
|
||||
folderContent.name = item.name;
|
||||
if (folderContent) {
|
||||
const bruFolderPath = path.join(folderPath, `folder.bru`);
|
||||
@@ -740,7 +745,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
// If initial folder has a root element, then I should write its folder.bru file
|
||||
if (itemFolder.root) {
|
||||
const folderContent = await jsonToCollectionBru(itemFolder.root, true);
|
||||
const folderContent = await stringifyFolder(itemFolder.root);
|
||||
if (folderContent) {
|
||||
const bruFolderPath = path.join(collectionPath, `folder.bru`);
|
||||
safeWriteFileSync(bruFolderPath, folderContent);
|
||||
@@ -767,7 +772,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
};
|
||||
if (fs.existsSync(folderRootPath)) {
|
||||
const bru = fs.readFileSync(folderRootPath, 'utf8');
|
||||
folderBruJsonData = await collectionBruToJson(bru);
|
||||
folderBruJsonData = await parseCollection(bru);
|
||||
if (!folderBruJsonData?.meta) {
|
||||
folderBruJsonData.meta = {
|
||||
name: path.basename(item.pathname),
|
||||
@@ -779,12 +784,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
folderBruJsonData.meta.seq = item.seq;
|
||||
}
|
||||
const content = await jsonToCollectionBru(folderBruJsonData);
|
||||
const content = await stringifyFolder(folderBruJsonData);
|
||||
await writeFile(folderRootPath, content);
|
||||
} else {
|
||||
if (fs.existsSync(item.pathname)) {
|
||||
const itemToSave = transformRequestToSaveToFilesystem(item);
|
||||
const content = await jsonToBruViaWorker(itemToSave);
|
||||
const content = await stringifyRequestViaWorker(itemToSave);
|
||||
await writeFile(item.pathname, content);
|
||||
}
|
||||
}
|
||||
@@ -1065,14 +1070,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
};
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
const metaJson = await bruToJson(parseBruFileMeta(bruContent), true);
|
||||
const metaJson = parseBruFileMeta(bruContent);
|
||||
file.data = metaJson;
|
||||
file.loading = true;
|
||||
file.partial = true;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
file.data = await bruToJsonViaWorker(bruContent);
|
||||
file.data = await parseRequestViaWorker(bruContent);
|
||||
file.partial = false;
|
||||
file.loading = true;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
@@ -1089,7 +1094,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
};
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
const metaJson = await bruToJson(parseBruFileMeta(bruContent), true);
|
||||
const metaJson = parseRequest(parseBruFileMeta(bruContent));
|
||||
file.data = metaJson;
|
||||
file.partial = true;
|
||||
file.loading = false;
|
||||
@@ -1140,14 +1145,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
};
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
const metaJson = await bruToJson(parseBruFileMeta(bruContent), true);
|
||||
const metaJson = parseRequest(parseBruFileMeta(bruContent));
|
||||
file.data = metaJson;
|
||||
file.loading = true;
|
||||
file.partial = true;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
hydrateRequestWithUuid(file.data, pathname);
|
||||
mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
|
||||
file.data = bruToJson(bruContent);
|
||||
file.data = parseRequest(bruContent);
|
||||
file.partial = false;
|
||||
file.loading = true;
|
||||
file.size = sizeInMB(fileStats?.size);
|
||||
@@ -1164,7 +1169,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
};
|
||||
let bruContent = fs.readFileSync(pathname, 'utf8');
|
||||
const metaJson = await bruToJson(parseBruFileMeta(bruContent), true);
|
||||
const metaJson = parseRequest(parseBruFileMeta(bruContent));
|
||||
file.data = metaJson;
|
||||
file.partial = true;
|
||||
file.loading = false;
|
||||
|
||||
@@ -472,6 +472,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
@@ -584,6 +587,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
|
||||
const domainsWithCookiesPost = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPost)));
|
||||
}
|
||||
return scriptResult;
|
||||
};
|
||||
@@ -891,6 +897,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
scriptType: 'test',
|
||||
error: testError
|
||||
});
|
||||
|
||||
const domainsWithCookiesTest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest)));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -901,6 +910,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
dataBuffer: response.dataBuffer.toString('base64'),
|
||||
size: Buffer.byteLength(response.dataBuffer),
|
||||
duration: responseTime ?? 0,
|
||||
url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null,
|
||||
timeline: response.timeline
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -1093,6 +1103,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
error: preRequestError
|
||||
});
|
||||
|
||||
const domainsWithCookiesPreRequest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPreRequest)));
|
||||
|
||||
if (preRequestError) {
|
||||
throw preRequestError;
|
||||
}
|
||||
@@ -1208,13 +1221,14 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
data: response.data,
|
||||
responseTime: response.responseTime,
|
||||
timeline: response.timeline,
|
||||
url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null
|
||||
},
|
||||
...eventData
|
||||
});
|
||||
} catch (error) {
|
||||
// Skip further processing if request was cancelled
|
||||
if (axios.isCancel(error)) {
|
||||
throw Promise.reject(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error?.response) {
|
||||
@@ -1248,7 +1262,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
await executeRequestOnFailHandler(request, error);
|
||||
|
||||
// if it's not a network error, don't continue
|
||||
throw Promise.reject(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1280,6 +1294,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
error: postResponseError
|
||||
});
|
||||
|
||||
const domainsWithCookiesPostResponse = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPostResponse)));
|
||||
|
||||
if (postResponseScriptResult?.nextRequestName !== undefined) {
|
||||
nextRequestName = postResponseScriptResult.nextRequestName;
|
||||
}
|
||||
@@ -1384,6 +1401,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
scriptType: 'test',
|
||||
error: testError
|
||||
});
|
||||
|
||||
const domainsWithCookiesTest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest)));
|
||||
}
|
||||
} catch (error) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
|
||||
@@ -15,11 +15,17 @@ const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
const { http_proxy, https_proxy, no_proxy } = systemProxyVars || {};
|
||||
mainWindow.webContents.send('main:load-system-proxy-env', { http_proxy, https_proxy, no_proxy });
|
||||
|
||||
// load global environments
|
||||
const globalEnvironments = globalEnvironmentsStore.getGlobalEnvironments();
|
||||
let activeGlobalEnvironmentUid = globalEnvironmentsStore.getActiveGlobalEnvironmentUid();
|
||||
activeGlobalEnvironmentUid = globalEnvironments?.find(env => env?.uid == activeGlobalEnvironmentUid) ? activeGlobalEnvironmentUid : null;
|
||||
mainWindow.webContents.send('main:load-global-environments', { globalEnvironments, activeGlobalEnvironmentUid });
|
||||
try {
|
||||
// load global environments
|
||||
const globalEnvironments = globalEnvironmentsStore.getGlobalEnvironments();
|
||||
let activeGlobalEnvironmentUid = globalEnvironmentsStore.getActiveGlobalEnvironmentUid();
|
||||
activeGlobalEnvironmentUid = globalEnvironments?.find(env => env?.uid == activeGlobalEnvironmentUid) ? activeGlobalEnvironmentUid : null;
|
||||
mainWindow.webContents.send('main:load-global-environments', { globalEnvironments, activeGlobalEnvironmentUid });
|
||||
}
|
||||
catch(error) {
|
||||
console.error("Error occured while fetching global environements!");
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
// reload last opened collections
|
||||
const lastOpened = lastOpenedCollections.getAll();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
const Store = require('electron-store');
|
||||
const { encryptString } = require('../utils/encryption');
|
||||
const { encryptStringSafe } = require('../utils/encryption');
|
||||
|
||||
/**
|
||||
* Sample secrets store file
|
||||
@@ -33,7 +33,7 @@ class EnvironmentSecretsStore {
|
||||
if (v.secret) {
|
||||
envVars.push({
|
||||
name: v.name,
|
||||
value: encryptString(v.value)
|
||||
value: encryptStringSafe(v.value).value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
const Store = require('electron-store');
|
||||
const { encryptString, decryptString } = require('../utils/encryption');
|
||||
const { encryptStringSafe, decryptStringSafe } = require('../utils/encryption');
|
||||
|
||||
class GlobalEnvironmentsStore {
|
||||
constructor() {
|
||||
@@ -14,7 +14,7 @@ class GlobalEnvironmentsStore {
|
||||
return globalEnvironments?.map(env => {
|
||||
const variables = env.variables?.map(v => ({
|
||||
...v,
|
||||
value: v?.secret ? encryptString(v.value) : v?.value
|
||||
value: v?.secret ? encryptStringSafe(v.value).value : v?.value
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
@@ -28,7 +28,7 @@ class GlobalEnvironmentsStore {
|
||||
return globalEnvironments?.map(env => {
|
||||
const variables = env.variables?.map(v => ({
|
||||
...v,
|
||||
value: v?.secret ? decryptString(v.value) : v?.value
|
||||
value: v?.secret ? decryptStringSafe(v.value).value : v?.value
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const _ = require('lodash');
|
||||
const Store = require('electron-store');
|
||||
const { uuid, safeStringifyJSON, safeParseJSON } = require('../utils/common');
|
||||
const { encryptString, decryptString } = require('../utils/encryption');
|
||||
const { encryptStringSafe, decryptStringSafe } = require('../utils/encryption');
|
||||
|
||||
/**
|
||||
* Sample secrets store file
|
||||
@@ -119,7 +119,8 @@ class Oauth2Store {
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
|
||||
let credentials = oauth2DataForCollection?.credentials?.find(c => (c?.url == url) && (c?.credentialsId == credentialsId));
|
||||
if (!credentials?.data) return null;
|
||||
let decryptedCredentialsData = safeParseJSON(decryptString(credentials?.data));
|
||||
const decryptionResult = decryptStringSafe(credentials?.data);
|
||||
const decryptedCredentialsData = safeParseJSON(decryptionResult.value);
|
||||
return decryptedCredentialsData;
|
||||
} catch (err) {
|
||||
console.log('error retrieving oauth2 credentials from cache', err);
|
||||
@@ -128,7 +129,8 @@ class Oauth2Store {
|
||||
|
||||
updateCredentialsForCollection({ collectionUid, url, credentialsId, credentials = {} }) {
|
||||
try {
|
||||
let encryptedCredentialsData = encryptString(safeStringifyJSON(credentials));
|
||||
const encryptionResult = encryptStringSafe(safeStringifyJSON(credentials));
|
||||
const encryptedCredentialsData = encryptionResult.value;
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
|
||||
let filteredCredentials = oauth2DataForCollection?.credentials?.filter(c => (c?.url !== url) || (c?.credentialsId !== credentialsId));
|
||||
if (!filteredCredentials) filteredCredentials = [];
|
||||
|
||||
@@ -10,7 +10,7 @@ const { get, merge } = require('lodash');
|
||||
|
||||
const defaultPreferences = {
|
||||
request: {
|
||||
sslVerification: false,
|
||||
sslVerification: true,
|
||||
customCaCertificate: {
|
||||
enabled: false,
|
||||
filePath: null
|
||||
@@ -27,7 +27,7 @@ const defaultPreferences = {
|
||||
codeFontSize: 14
|
||||
},
|
||||
proxy: {
|
||||
mode: 'system',
|
||||
mode: 'off',
|
||||
protocol: 'http',
|
||||
hostname: '',
|
||||
port: null,
|
||||
@@ -132,7 +132,7 @@ const savePreferences = async (newPreferences) => {
|
||||
|
||||
const preferencesUtil = {
|
||||
shouldVerifyTls: () => {
|
||||
return get(getPreferences(), 'request.sslVerification', false);
|
||||
return get(getPreferences(), 'request.sslVerification', true);
|
||||
},
|
||||
shouldUseCustomCaCertificate: () => {
|
||||
return get(getPreferences(), 'request.customCaCertificate.enabled', false);
|
||||
|
||||
@@ -237,12 +237,47 @@ const parseBruFileMeta = (data) => {
|
||||
metaJson[key] = isNaN(value) ? value : Number(value);
|
||||
}
|
||||
});
|
||||
return { meta: metaJson };
|
||||
|
||||
// Transform to the format expected by bruno-app
|
||||
let requestType = metaJson.type;
|
||||
if (requestType === 'http') {
|
||||
requestType = 'http-request';
|
||||
} else if (requestType === 'graphql') {
|
||||
requestType = 'graphql-request';
|
||||
} else {
|
||||
requestType = 'http-request';
|
||||
}
|
||||
|
||||
const sequence = metaJson.seq;
|
||||
const transformedJson = {
|
||||
type: requestType,
|
||||
name: metaJson.name,
|
||||
seq: !isNaN(sequence) ? Number(sequence) : 1,
|
||||
settings: {},
|
||||
tags: metaJson.tags || [],
|
||||
request: {
|
||||
method: '',
|
||||
url: '',
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: { mode: 'none' },
|
||||
body: { mode: 'none' },
|
||||
script: {},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: '',
|
||||
docs: ''
|
||||
}
|
||||
};
|
||||
|
||||
return transformedJson;
|
||||
} else {
|
||||
console.log('No "meta" block found in the file.');
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error reading file:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,197 +1 @@
|
||||
const { Cookie, CookieJar } = require('tough-cookie');
|
||||
const each = require('lodash/each');
|
||||
const moment = require('moment');
|
||||
const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils;
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
const addCookieToJar = (setCookieHeader, requestUrl) => {
|
||||
const cookie = Cookie.parse(setCookieHeader, { loose: true });
|
||||
cookieJar.setCookieSync(cookie, requestUrl, {
|
||||
ignoreError: true // silently ignore things like parse errors and invalid domains
|
||||
});
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (url) => {
|
||||
return cookieJar.getCookiesSync(url, {
|
||||
secure: isPotentiallyTrustworthyOrigin(url)
|
||||
});
|
||||
};
|
||||
|
||||
const getCookieStringForUrl = (url) => {
|
||||
const cookies = getCookiesForUrl(url);
|
||||
|
||||
if (!Array.isArray(cookies) || !cookies.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now());
|
||||
|
||||
return validCookies.map((cookie) => cookie.cookieString()).join('; ');
|
||||
};
|
||||
|
||||
const getDomainsWithCookies = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const domainCookieMap = {};
|
||||
|
||||
cookieJar.store.getAllCookies((err, cookies) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
if (!domainCookieMap[cookie.domain]) {
|
||||
domainCookieMap[cookie.domain] = [cookie];
|
||||
} else {
|
||||
domainCookieMap[cookie.domain].push(cookie);
|
||||
}
|
||||
});
|
||||
|
||||
const domains = Object.keys(domainCookieMap);
|
||||
const domainsWithCookies = [];
|
||||
|
||||
each(domains, (domain) => {
|
||||
const cookies = domainCookieMap[domain];
|
||||
const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now());
|
||||
|
||||
if (validCookies.length) {
|
||||
domainsWithCookies.push({
|
||||
domain,
|
||||
cookies: validCookies,
|
||||
cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resolve(domainsWithCookies);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCookie = (domain, path, cookieKey) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cookieJar.store.removeCookie(domain, path, cookieKey, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCookiesForDomain = (domain) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cookieJar.store.removeCookies(domain, null, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateCookieObj = (cookieObj, oldCookie) => {
|
||||
return {
|
||||
...cookieObj,
|
||||
// Preserve immutable properties from old cookie
|
||||
path: oldCookie.path,
|
||||
key: oldCookie.key,
|
||||
domain: oldCookie.domain,
|
||||
// Handle other mutable properties
|
||||
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
|
||||
creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(),
|
||||
lastAccessed:
|
||||
oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid()
|
||||
? new Date(oldCookie.lastAccessed)
|
||||
: new Date()
|
||||
};
|
||||
};
|
||||
|
||||
const createCookieObj = (cookieObj) => {
|
||||
return {
|
||||
...cookieObj,
|
||||
path: cookieObj.path || '/',
|
||||
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
|
||||
creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(),
|
||||
lastAccessed:
|
||||
cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid()
|
||||
? new Date(cookieObj.lastAccessed)
|
||||
: new Date()
|
||||
};
|
||||
};
|
||||
|
||||
const addCookieForDomain = (domain, cookieObj) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const cookie = new Cookie(createCookieObj(cookieObj));
|
||||
cookieJar.store.putCookie(cookie, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const modifyCookieForDomain = (domain, oldCookieObj, cookieObj) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const oldCookie = new Cookie(createCookieObj(oldCookieObj));
|
||||
const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie));
|
||||
cookieJar.store.updateCookie(oldCookie, newCookie, (removeErr) => {
|
||||
if (removeErr) {
|
||||
return reject(removeErr);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const parseCookieString = (cookieStr) => {
|
||||
try {
|
||||
const cookie = Cookie.parse(cookieStr);
|
||||
if (!cookie) return null;
|
||||
|
||||
return {
|
||||
...cookie,
|
||||
expires: cookie.expires === Infinity ? null : cookie.expires
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const createCookieString = (cookieObj) => {
|
||||
const cookie = new Cookie(createCookieObj(cookieObj));
|
||||
|
||||
// cookie.toString() omits the domain
|
||||
let cookieString = cookie.toString();
|
||||
|
||||
// Manually append domain and hostOnly if they exist
|
||||
if (cookieObj.hostOnly && !cookieString.includes('Domain=')) {
|
||||
cookieString += `; Domain=${cookieObj.domain}`;
|
||||
}
|
||||
|
||||
return cookieString;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
addCookieToJar,
|
||||
getCookiesForUrl,
|
||||
getCookieStringForUrl,
|
||||
getDomainsWithCookies,
|
||||
deleteCookie,
|
||||
deleteCookiesForDomain,
|
||||
addCookieForDomain,
|
||||
modifyCookieForDomain,
|
||||
parseCookieString,
|
||||
createCookieString,
|
||||
updateCookieObj,
|
||||
createCookieObj
|
||||
};
|
||||
module.exports = require('@usebruno/common').cookies;
|
||||
|
||||
@@ -54,11 +54,16 @@ function aes256Decrypt(data) {
|
||||
return decrypted;
|
||||
} catch (err) {
|
||||
// If decryption fails, fall back to old key derivation
|
||||
const { key: oldKey, iv: oldIv } = deriveKeyAndIv(rawKey, 32, 16);
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', oldKey, oldIv);
|
||||
let decrypted = decipher.update(data, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
try {
|
||||
const { key: oldKey, iv: oldIv } = deriveKeyAndIv(rawKey, 32, 16);
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', oldKey, oldIv);
|
||||
const decrypted = decipher.update(data, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
} catch (fallbackErr) {
|
||||
console.error('AES256 decryption failed with both methods:', err, fallbackErr);
|
||||
throw new Error('AES256 decryption failed: ' + fallbackErr.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,16 +78,21 @@ function safeStorageEncrypt(str) {
|
||||
return encryptedString;
|
||||
}
|
||||
function safeStorageDecrypt(str) {
|
||||
// Convert the hexadecimal string to a buffer
|
||||
const encryptedStringBuffer = Buffer.from(str, 'hex');
|
||||
try {
|
||||
// Convert the hexadecimal string to a buffer
|
||||
const encryptedStringBuffer = Buffer.from(str, 'hex');
|
||||
|
||||
// Decrypt the buffer
|
||||
const decryptedStringBuffer = safeStorage.decryptString(encryptedStringBuffer);
|
||||
// Decrypt the buffer
|
||||
const decryptedStringBuffer = safeStorage.decryptString(encryptedStringBuffer);
|
||||
|
||||
// Convert the decrypted buffer to a string
|
||||
const decryptedString = decryptedStringBuffer.toString();
|
||||
// Convert the decrypted buffer to a string
|
||||
const decryptedString = decryptedStringBuffer.toString();
|
||||
|
||||
return decryptedString;
|
||||
return decryptedString;
|
||||
} catch (err) {
|
||||
console.error('SafeStorage decryption failed:', err);
|
||||
throw new Error('SafeStorage decryption failed: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function encryptString(str) {
|
||||
@@ -142,7 +152,29 @@ function decryptString(str) {
|
||||
}
|
||||
}
|
||||
|
||||
function decryptStringSafe(str) {
|
||||
try {
|
||||
const result = decryptString(str);
|
||||
return { success: true, value: result };
|
||||
} catch (err) {
|
||||
console.error('Decryption failed:', err.message);
|
||||
return { success: false, error: err.message, value: '' };
|
||||
}
|
||||
}
|
||||
|
||||
function encryptStringSafe(str) {
|
||||
try {
|
||||
const result = encryptString(str);
|
||||
return { success: true, value: result };
|
||||
} catch (err) {
|
||||
console.error('Encryption failed:', err.message);
|
||||
return { success: false, error: err.message, value: '' };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
encryptString,
|
||||
decryptString
|
||||
encryptStringSafe,
|
||||
decryptString,
|
||||
decryptStringSafe
|
||||
};
|
||||
|
||||
@@ -368,6 +368,9 @@ function setupProxyAgents({
|
||||
{ proxy: https_proxy,...tlsOptions },
|
||||
timeline
|
||||
);
|
||||
} else {
|
||||
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
|
||||
requestConfig.httpsAgent = new TimelineHttpsAgent(tlsOptions, timeline);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Invalid system https_proxy');
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
const { Worker } = require('worker_threads');
|
||||
|
||||
class WorkerQueue {
|
||||
constructor() {
|
||||
this.queue = [];
|
||||
this.isProcessing = false;
|
||||
this.workers = {};
|
||||
}
|
||||
|
||||
async getWorkerForScriptPath(scriptPath) {
|
||||
if (!this.workers) this.workers = {};
|
||||
let worker = this.workers[scriptPath];
|
||||
if (!worker || worker.threadId === -1) {
|
||||
this.workers[scriptPath] = worker = new Worker(scriptPath);
|
||||
}
|
||||
return worker;
|
||||
}
|
||||
|
||||
async enqueue(task) {
|
||||
const { priority, scriptPath, data } = task;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.queue.push({ priority, scriptPath, data, resolve, reject });
|
||||
this.queue?.sort((taskX, taskY) => taskX?.priority - taskY?.priority);
|
||||
this.processQueue();
|
||||
});
|
||||
}
|
||||
|
||||
async processQueue() {
|
||||
if (this.isProcessing || this.queue.length === 0){
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessing = true;
|
||||
const { scriptPath, data, resolve, reject } = this.queue.shift();
|
||||
|
||||
try {
|
||||
const result = await this.runWorker({ scriptPath, data });
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
this.processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
async runWorker({ scriptPath, data }) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let worker = await this.getWorkerForScriptPath(scriptPath);
|
||||
worker.postMessage(data);
|
||||
worker.on('message', (data) => {
|
||||
if (data?.error) {
|
||||
reject(new Error(data?.error));
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
worker.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
worker.on('exit', (code) => {
|
||||
reject(new Error(`stopped with ${code} exit code`));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WorkerQueue;
|
||||
@@ -11,22 +11,35 @@ describe('parseBruFileMeta', () => {
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
meta: {
|
||||
name: '0.2_mb',
|
||||
type: 'http',
|
||||
seq: 1,
|
||||
},
|
||||
type: 'http-request',
|
||||
name: '0.2_mb',
|
||||
seq: 1,
|
||||
settings: {},
|
||||
tags: [],
|
||||
request: {
|
||||
method: '',
|
||||
url: '',
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: { mode: 'none' },
|
||||
body: { mode: 'none' },
|
||||
script: {},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: '',
|
||||
docs: ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('returns undefined for missing meta block', () => {
|
||||
test('returns null for missing meta block', () => {
|
||||
const data = `someOtherBlock {
|
||||
key: value
|
||||
}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('handles empty meta block gracefully', () => {
|
||||
@@ -34,7 +47,26 @@ describe('parseBruFileMeta', () => {
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({ meta: {} });
|
||||
expect(result).toEqual({
|
||||
type: 'http-request',
|
||||
name: undefined,
|
||||
seq: 1,
|
||||
settings: {},
|
||||
tags: [],
|
||||
request: {
|
||||
method: '',
|
||||
url: '',
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: { mode: 'none' },
|
||||
body: { mode: 'none' },
|
||||
script: {},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: '',
|
||||
docs: ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('ignores invalid lines in meta block', () => {
|
||||
@@ -47,10 +79,24 @@ describe('parseBruFileMeta', () => {
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
meta: {
|
||||
name: '0.2_mb',
|
||||
seq: 1,
|
||||
},
|
||||
type: 'http-request',
|
||||
name: '0.2_mb',
|
||||
seq: 1,
|
||||
settings: {},
|
||||
tags: [],
|
||||
request: {
|
||||
method: '',
|
||||
url: '',
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: { mode: 'none' },
|
||||
body: { mode: 'none' },
|
||||
script: {},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: '',
|
||||
docs: ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +105,7 @@ describe('parseBruFileMeta', () => {
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('handles missing colon gracefully', () => {
|
||||
@@ -71,9 +117,24 @@ describe('parseBruFileMeta', () => {
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
meta: {
|
||||
seq: 1,
|
||||
},
|
||||
type: 'http-request',
|
||||
name: undefined,
|
||||
seq: 1,
|
||||
settings: {},
|
||||
tags: [],
|
||||
request: {
|
||||
method: '',
|
||||
url: '',
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: { mode: 'none' },
|
||||
body: { mode: 'none' },
|
||||
script: {},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: '',
|
||||
docs: ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,16 +143,30 @@ describe('parseBruFileMeta', () => {
|
||||
numValue: 1234
|
||||
floatValue: 12.34
|
||||
strValue: some_text
|
||||
seq: 5
|
||||
}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
meta: {
|
||||
numValue: 1234,
|
||||
floatValue: 12.34,
|
||||
strValue: 'some_text',
|
||||
},
|
||||
type: 'http-request',
|
||||
name: undefined,
|
||||
seq: 5,
|
||||
settings: {},
|
||||
tags: [],
|
||||
request: {
|
||||
method: '',
|
||||
url: '',
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: { mode: 'none' },
|
||||
body: { mode: 'none' },
|
||||
script: {},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: '',
|
||||
docs: ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,7 +179,7 @@ describe('parseBruFileMeta', () => {
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('handles syntax error in meta block 2', () => {
|
||||
@@ -116,6 +191,98 @@ describe('parseBruFileMeta', () => {
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('handles graphql type correctly', () => {
|
||||
const data = `meta {
|
||||
name: graphql_query
|
||||
type: graphql
|
||||
seq: 2
|
||||
}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'graphql-request',
|
||||
name: 'graphql_query',
|
||||
seq: 2,
|
||||
settings: {},
|
||||
tags: [],
|
||||
request: {
|
||||
method: '',
|
||||
url: '',
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: { mode: 'none' },
|
||||
body: { mode: 'none' },
|
||||
script: {},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: '',
|
||||
docs: ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('handles unknown type correctly', () => {
|
||||
const data = `meta {
|
||||
name: unknown_request
|
||||
type: unknown
|
||||
seq: 3
|
||||
}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'http-request',
|
||||
name: 'unknown_request',
|
||||
seq: 3,
|
||||
settings: {},
|
||||
tags: [],
|
||||
request: {
|
||||
method: '',
|
||||
url: '',
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: { mode: 'none' },
|
||||
body: { mode: 'none' },
|
||||
script: {},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: '',
|
||||
docs: ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('handles missing seq gracefully', () => {
|
||||
const data = `meta {
|
||||
name: no_seq_request
|
||||
type: http
|
||||
}`;
|
||||
|
||||
const result = parseBruFileMeta(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'http-request',
|
||||
name: 'no_seq_request',
|
||||
seq: 1, // Default fallback
|
||||
settings: {},
|
||||
tags: [],
|
||||
request: {
|
||||
method: '',
|
||||
url: '',
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: { mode: 'none' },
|
||||
body: { mode: 'none' },
|
||||
script: {},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: '',
|
||||
docs: ''
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
const { encryptString, decryptString } = require('../../src/utils/encryption');
|
||||
const { encryptString, decryptString, encryptStringSafe, decryptStringSafe } = require('../../src/utils/encryption');
|
||||
|
||||
// We can only unit test aes 256 fallback as safeStorage is only available
|
||||
// in the main process
|
||||
@@ -45,3 +45,69 @@ describe('Encryption and Decryption Tests', () => {
|
||||
expect(() => decryptString(invalidAlgo)).toThrow('Decrypt failed: Invalid algo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Safe Encryption and Decryption Tests', () => {
|
||||
it('should encrypt and decrypt successfully using encryptStringSafe and decryptStringSafe', () => {
|
||||
const plaintext = 'bruno is awesome';
|
||||
const encryptionResult = encryptStringSafe(plaintext);
|
||||
const decryptionResult = decryptStringSafe(encryptionResult.value);
|
||||
|
||||
expect(encryptionResult.success).toBe(true);
|
||||
expect(decryptionResult.success).toBe(true);
|
||||
expect(decryptionResult.value).toBe(plaintext);
|
||||
});
|
||||
|
||||
it('should handle empty strings in encryptStringSafe', () => {
|
||||
const result = encryptStringSafe('');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.value).toBe('');
|
||||
});
|
||||
|
||||
it('should handle empty strings in decryptStringSafe', () => {
|
||||
const result = decryptStringSafe('');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.value).toBe('');
|
||||
});
|
||||
|
||||
it('should handle invalid string format in decryptStringSafe', () => {
|
||||
const result = decryptStringSafe('garbage');
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Decrypt failed: unrecognized string format');
|
||||
expect(result.value).toBe('');
|
||||
});
|
||||
|
||||
it('should handle invalid algorithm in decryptStringSafe', () => {
|
||||
const invalidAlgo = '$99:abcdefg';
|
||||
const result = decryptStringSafe(invalidAlgo);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Decrypt failed: Invalid algo');
|
||||
expect(result.value).toBe('');
|
||||
});
|
||||
|
||||
it('should handle malformed encrypted string in decryptStringSafe', () => {
|
||||
const malformedString = '$01:invalid-hex-string';
|
||||
const result = decryptStringSafe(malformedString);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('AES256 decryption failed');
|
||||
expect(result.value).toBe('');
|
||||
});
|
||||
|
||||
it('should handle special characters in encryptStringSafe and decryptStringSafe', () => {
|
||||
const specialText = 'bruno@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||
const encryptionResult = encryptStringSafe(specialText);
|
||||
const decryptionResult = decryptStringSafe(encryptionResult.value);
|
||||
|
||||
expect(encryptionResult.success).toBe(true);
|
||||
expect(decryptionResult.success).toBe(true);
|
||||
expect(decryptionResult.value).toBe(specialText);
|
||||
});
|
||||
|
||||
it('decrypt-safe should not throw error for invalid inputs', () => {
|
||||
expect(() => decryptStringSafe(null)).not.toThrow();
|
||||
expect(() => decryptStringSafe(undefined)).not.toThrow();
|
||||
expect(() => decryptStringSafe('garbage')).not.toThrow();
|
||||
expect(() => decryptStringSafe(123456789)).not.toThrow();
|
||||
expect(() => decryptStringSafe('aes256:')).not.toThrow();
|
||||
expect(() => decryptStringSafe('aes256:invalid_base64')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
5
packages/bruno-filestore/.gitignore
vendored
Normal file
5
packages/bruno-filestore/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
*.log
|
||||
dist
|
||||
coverage
|
||||
22
packages/bruno-filestore/LICENSE.md
Normal file
22
packages/bruno-filestore/LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 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.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user