fix(ui): correct “modified” indicator state across collection, folder, request, and presets/auth tabs (#3386) (#8027)

* fix: 3296 Folder-level No Auth inheritance is ignored; requests still use Collection Auth
This commit is contained in:
sharan-bruno
2026-06-08 16:57:18 +05:30
committed by GitHub
parent b9d8bdf2ec
commit 2d4d4e4037
41 changed files with 1182 additions and 356 deletions

204
package-lock.json generated
View File

@@ -5027,7 +5027,7 @@
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz",
"integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -5045,7 +5045,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz",
"integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
@@ -5062,7 +5062,7 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -5080,7 +5080,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/@babel/helper-globals": {
@@ -5160,7 +5160,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz",
"integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -5235,7 +5235,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz",
"integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
@@ -5278,7 +5278,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz",
"integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -5295,7 +5295,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz",
"integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5311,7 +5311,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz",
"integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5327,7 +5327,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz",
"integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -5345,7 +5345,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz",
"integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -5380,7 +5380,7 @@
"version": "7.21.0-placeholder-for-preset-env.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -5479,7 +5479,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz",
"integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5495,7 +5495,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz",
"integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5677,7 +5677,7 @@
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
"integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
@@ -5694,7 +5694,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz",
"integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5710,7 +5710,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz",
"integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -5728,7 +5728,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz",
"integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.25.9",
@@ -5746,7 +5746,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz",
"integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5762,7 +5762,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz",
"integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5794,7 +5794,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz",
"integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.25.9",
@@ -5811,7 +5811,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz",
"integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -5832,7 +5832,7 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -5842,7 +5842,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz",
"integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -5859,7 +5859,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz",
"integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5875,7 +5875,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz",
"integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -5892,7 +5892,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz",
"integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5908,7 +5908,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz",
"integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -5925,7 +5925,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz",
"integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5941,7 +5941,7 @@
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz",
"integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5957,7 +5957,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz",
"integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5989,7 +5989,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz",
"integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -6006,7 +6006,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz",
"integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.25.9",
@@ -6024,7 +6024,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz",
"integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6040,7 +6040,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz",
"integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6056,7 +6056,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz",
"integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6072,7 +6072,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz",
"integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6088,7 +6088,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz",
"integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.25.9",
@@ -6121,7 +6121,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz",
"integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.25.9",
@@ -6140,7 +6140,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz",
"integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.25.9",
@@ -6157,7 +6157,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz",
"integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -6174,7 +6174,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz",
"integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6205,7 +6205,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz",
"integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6221,7 +6221,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz",
"integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.25.9",
@@ -6239,7 +6239,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz",
"integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -6256,7 +6256,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz",
"integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6288,7 +6288,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz",
"integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6320,7 +6320,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz",
"integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -6338,7 +6338,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz",
"integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6423,7 +6423,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz",
"integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -6440,7 +6440,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz",
"integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -6457,7 +6457,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz",
"integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6473,7 +6473,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz",
"integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6489,7 +6489,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz",
"integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -6506,7 +6506,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz",
"integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6522,7 +6522,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz",
"integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6538,7 +6538,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz",
"integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6573,7 +6573,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz",
"integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6589,7 +6589,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz",
"integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -6606,7 +6606,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz",
"integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -6623,7 +6623,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz",
"integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -6640,7 +6640,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz",
"integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.26.0",
@@ -6741,7 +6741,7 @@
"version": "0.1.6-no-external-plugins",
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
"integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
@@ -11111,8 +11111,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rspack/binding-darwin-x64": {
"version": "1.1.8",
@@ -11126,8 +11125,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rspack/binding-linux-arm64-gnu": {
"version": "1.1.8",
@@ -11141,8 +11139,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rspack/binding-linux-arm64-musl": {
"version": "1.1.8",
@@ -11156,8 +11153,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rspack/binding-linux-x64-gnu": {
"version": "1.1.8",
@@ -11171,8 +11167,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rspack/binding-linux-x64-musl": {
"version": "1.1.8",
@@ -11186,8 +11181,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rspack/binding-win32-arm64-msvc": {
"version": "1.1.8",
@@ -11201,8 +11195,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rspack/binding-win32-ia32-msvc": {
"version": "1.1.8",
@@ -11216,8 +11209,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rspack/binding-win32-x64-msvc": {
"version": "1.1.8",
@@ -11231,8 +11223,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rspack/core": {
"version": "1.1.8",
@@ -12562,6 +12553,7 @@
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.10.4",
@@ -12581,6 +12573,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -12593,6 +12586,7 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1",
@@ -12607,6 +12601,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/jest-dom": {
@@ -12687,6 +12682,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": {
@@ -12978,6 +12974,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
@@ -13000,6 +12997,7 @@
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/linkify-it": "*",
@@ -13010,6 +13008,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
@@ -14501,6 +14500,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"dequal": "^2.0.3"
@@ -14926,7 +14926,7 @@
"version": "0.4.12",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz",
"integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.6",
@@ -14941,7 +14941,7 @@
"version": "0.10.6",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz",
"integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.2",
@@ -14955,7 +14955,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz",
"integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.3"
@@ -16812,7 +16812,7 @@
"version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
"integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.24.2"
@@ -18042,6 +18042,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-converter": {
@@ -21615,7 +21616,7 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@@ -24030,6 +24031,7 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
@@ -25654,7 +25656,7 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/path-scurry": {
@@ -28047,14 +28049,14 @@
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
"integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/regenerate-unicode-properties": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz",
"integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2"
@@ -28067,7 +28069,7 @@
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
"integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.4"
@@ -28077,7 +28079,7 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz",
"integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2",
@@ -28095,14 +28097,14 @@
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
"integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/regjsparser": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz",
"integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==",
"devOptional": true,
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"jsesc": "~3.0.2"
@@ -28115,7 +28117,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -28307,7 +28309,7 @@
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
@@ -30565,7 +30567,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -31759,7 +31761,7 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -31820,7 +31822,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
"integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -31830,7 +31832,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
"integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"unicode-canonical-property-names-ecmascript": "^2.0.0",
@@ -31844,7 +31846,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz",
"integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -31854,7 +31856,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
"integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"

View File

@@ -75,13 +75,13 @@ const AuthMode = ({ collection }) => {
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
<div className="inline-flex items-center cursor-pointer auth-mode-selector" data-testid="auth-mode-selector">
<MenuDropdown
items={menuItems}
placement="bottom-end"
selectedItemId={authMode}
>
<div className="flex items-center justify-center auth-mode-label select-none">
<div className="flex items-center justify-center auth-mode-label select-none" data-testid="auth-mode-label">
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
</div>
</MenuDropdown>

View File

@@ -5,10 +5,11 @@ import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { get } from 'lodash';
import Button from 'ui/Button';
import { DEFAULT_PRESET_REQUEST_TYPE, PRESET_REQUEST_TYPES } from 'utils/common/constants';
const PresetsSettings = ({ collection }) => {
const dispatch = useDispatch();
const initialPresets = { requestType: 'http', requestUrl: '' };
const initialPresets = { requestType: DEFAULT_PRESET_REQUEST_TYPE, requestUrl: '' };
// Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig
const currentPresets = collection.draft?.brunoConfig
@@ -47,12 +48,13 @@ const PresetsSettings = ({ collection }) => {
<div className="flex items-center">
<input
id="http"
data-testid="presets-request-type-http"
className="cursor-pointer"
type="radio"
name="requestType"
onChange={handleRequestTypeChange}
value="http"
checked={(currentPresets.requestType || 'http') === 'http'}
value={PRESET_REQUEST_TYPES.HTTP}
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.HTTP}
/>
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
HTTP
@@ -60,12 +62,13 @@ const PresetsSettings = ({ collection }) => {
<input
id="graphql"
data-testid="presets-request-type-graphql"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={handleRequestTypeChange}
value="graphql"
checked={(currentPresets.requestType || 'http') === 'graphql'}
value={PRESET_REQUEST_TYPES.GRAPHQL}
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.GRAPHQL}
/>
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
GraphQL
@@ -73,12 +76,13 @@ const PresetsSettings = ({ collection }) => {
<input
id="grpc"
data-testid="presets-request-type-grpc"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={handleRequestTypeChange}
value="grpc"
checked={(currentPresets.requestType || 'http') === 'grpc'}
value={PRESET_REQUEST_TYPES.GRPC}
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.GRPC}
/>
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
gRPC
@@ -86,12 +90,13 @@ const PresetsSettings = ({ collection }) => {
<input
id="ws"
data-testid="presets-request-type-ws"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={handleRequestTypeChange}
value="ws"
checked={(currentPresets.requestType || 'http') === 'ws'}
value={PRESET_REQUEST_TYPES.WS}
checked={(currentPresets.requestType || DEFAULT_PRESET_REQUEST_TYPE) === PRESET_REQUEST_TYPES.WS}
/>
<label htmlFor="ws" className="ml-1 cursor-pointer select-none">
WebSocket
@@ -106,6 +111,7 @@ const PresetsSettings = ({ collection }) => {
<div className="flex items-center flex-grow input-container h-full">
<input
id="request-url"
data-testid="presets-request-url"
type="text"
name="requestUrl"
placeholder="Request URL"
@@ -123,7 +129,7 @@ const PresetsSettings = ({ collection }) => {
</div>
<div className="mt-6">
<Button type="button" size="sm" onClick={handleSave}>
<Button type="button" size="sm" data-testid="presets-save-btn" onClick={handleSave}>
Save
</Button>
</div>

View File

@@ -15,6 +15,7 @@ import StyledWrapper from './StyledWrapper';
import Vars from './Vars/index';
import StatusDot from 'components/StatusDot';
import Overview from './Overview/index';
import { DEFAULT_PRESET_REQUEST_TYPE } from 'utils/common/constants';
const CollectionSettings = ({ collection }) => {
const dispatch = useDispatch();
@@ -60,7 +61,7 @@ const CollectionSettings = ({ collection }) => {
? get(collection, 'draft.brunoConfig.protobuf', {})
: get(collection, 'brunoConfig.protobuf', {});
const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', {}) : get(collection, 'brunoConfig.presets', {});
const hasPresets = presets && presets.requestUrl !== '';
const hasPresets = presets && ((presets.requestType && presets.requestType !== DEFAULT_PRESET_REQUEST_TYPE) || (presets.requestUrl && presets.requestUrl !== ''));
const getTabPanel = (tab) => {
switch (tab) {

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import get from 'lodash/get';
import StyledWrapper from './StyledWrapper';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
@@ -18,8 +18,9 @@ import OAuth1 from 'components/RequestPane/Auth/OAuth1';
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';
import { humanizeRequestAuthMode } from 'utils/collections/index';
import Button from 'ui/Button';
import { getEffectiveAuthSource } from 'utils/auth';
const GrantTypeComponentMap = ({ collection, folder, updateFolderAuth }) => {
const dispatch = useDispatch();
@@ -52,41 +53,6 @@ const Auth = ({ collection, folder }) => {
let request = get(folderRoot, 'request', {});
const authMode = get(folderRoot, 'request.auth.mode');
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
// Get path from collection to current folder
const folderTreePath = getTreePathFromCollectionToItem(collection, folder);
// Check parent folders to find closest auth configuration
// Skip the last item which is the current folder
for (let i = 0; i < folderTreePath.length - 1; i++) {
const parentFolder = folderTreePath[i];
if (parentFolder.type === 'folder') {
const parentFolderRoot = parentFolder?.draft || parentFolder?.root;
const folderAuth = get(parentFolderRoot, 'request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
name: parentFolder.name,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const handleSave = () => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
};
@@ -98,6 +64,11 @@ const Auth = ({ collection, folder }) => {
});
};
const inheritedSource = useMemo(
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, folder) : null),
[authMode, folder, collection]
);
const getAuthView = () => {
switch (authMode) {
case 'basic': {
@@ -202,12 +173,11 @@ const Auth = ({ collection, folder }) => {
);
}
case 'inherit': {
const source = getEffectiveAuthSource();
return (
<>
<div className="flex flex-row w-full mt-2 gap-2">
<div>Auth inherited from {source.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
<div>Auth inherited from {inheritedSource.name}: </div>
<div className="inherit-mode-text" data-testid="inherited-auth-mode">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
</div>
</>
);

View File

@@ -81,14 +81,15 @@ const AuthMode = ({ collection, folder }) => {
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
<div className="inline-flex items-center cursor-pointer auth-mode-selector" data-testid="auth-mode-selector">
<MenuDropdown
items={menuItems}
placement="bottom-end"
selectedItemId={authMode}
showTickMark={true}
data-testid="auth-mode-dropdown"
>
<div className="flex items-center justify-center auth-mode-label select-none">
<div className="flex items-center justify-center auth-mode-label select-none" data-testid="auth-mode-label">
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
</MenuDropdown>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import classnames from 'classnames';
import { updatedFolderSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
@@ -10,7 +10,7 @@ import Vars from './Vars';
import Documentation from './Documentation';
import Auth from './Auth';
import StatusDot from 'components/StatusDot';
import get from 'lodash/get';
import { hasEffectiveAuth } from 'utils/auth';
const FolderSettings = ({ collection, folder }) => {
const dispatch = useDispatch();
@@ -31,8 +31,11 @@ const FolderSettings = ({ collection, folder }) => {
const responseVars = folderRoot?.request?.vars?.res || [];
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const auth = get(folderRoot, 'request.auth.mode');
const hasAuth = auth && auth !== 'none';
const folderAuthMode = folder?.draft?.request?.auth?.mode ?? folder?.root?.request?.auth?.mode;
const hasAuth = useMemo(
() => hasEffectiveAuth(collection, folder),
[folder, folderAuthMode, collection]
);
const setTab = (tab) => {
dispatch(
@@ -95,7 +98,7 @@ const FolderSettings = ({ collection, folder }) => {
</div>
<div className={getTabClassname('auth')} role="tab" data-testid="folder-settings-tab-auth" onClick={() => setTab('auth')}>
Auth
{hasAuth && <StatusDot />}
{hasAuth && <StatusDot dataTestId="auth" />}
</div>
<div className={getTabClassname('docs')} role="tab" data-testid="folder-settings-tab-docs" onClick={() => setTab('docs')}>
Docs

View File

@@ -81,14 +81,15 @@ const AuthMode = ({ item, collection }) => {
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
<div className="inline-flex items-center cursor-pointer auth-mode-selector" data-testid="auth-mode-selector">
<MenuDropdown
items={menuItems}
placement="bottom-end"
selectedItemId={authMode}
showTickMark={true}
data-testid="auth-mode-dropdown"
>
<div className="flex items-center justify-center auth-mode-label select-none">
<div className="flex items-center justify-center auth-mode-label select-none" data-testid="auth-mode-label">
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
</div>
</MenuDropdown>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import get from 'lodash/get';
import AwsV4Auth from './AwsV4Auth';
import BearerAuth from './BearerAuth';
@@ -15,22 +15,11 @@ import ApiKeyAuth from './ApiKeyAuth';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections';
import OAuth2 from './OAuth2/index';
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
const getTreePathFromCollectionToItem = (collection, _item) => {
let path = [];
let item = findItemInCollection(collection, _item?.uid);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item?.uid);
}
return path;
};
import { getEffectiveAuthSource } from 'utils/auth';
const Auth = ({ item, collection }) => {
const dispatch = useDispatch();
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
// Create a request object to pass to the auth components
const request = item.draft
@@ -42,34 +31,10 @@ const Auth = ({ item, collection }) => {
return dispatch(saveRequest(item.uid, collection.uid));
};
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
// Check folders in reverse to find the closest auth configuration
for (let i of [...requestTreePath].reverse()) {
if (i.type === 'folder') {
const folderAuth = get(i, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
name: i.name,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const inheritedSource = useMemo(
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, item) : null),
[authMode, item, collection]
);
const getAuthView = () => {
switch (authMode) {
@@ -104,12 +69,11 @@ const Auth = ({ item, collection }) => {
return <ApiKeyAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
}
case 'inherit': {
const source = getEffectiveAuthSource();
return (
<>
<div className="flex flex-row w-full gap-2">
<div>Auth inherited from {source.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
<div>Auth inherited from {inheritedSource.name}: </div>
<div className="inherit-mode-text" data-testid="inherited-auth-mode">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
</div>
</>
);

View File

@@ -24,10 +24,12 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
import Documentation from 'components/Documentation/index';
import useGraphqlSchema from '../GraphQLSchemaActions/useGraphqlSchema';
import { findEnvironmentInCollection } from 'utils/collections';
import { hasEffectiveAuth } from 'utils/auth';
import HeightBoundContainer from 'ui/HeightBoundContainer';
import Settings from 'components/RequestPane/Settings';
import ResponsiveTabs from 'ui/ResponsiveTabs';
import AuthMode from '../Auth/AuthMode/index';
import StatusDot from 'components/StatusDot';
const TAB_CONFIG = [
{ key: 'query', label: 'Query' },
@@ -172,7 +174,20 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
[dispatch, item.uid]
);
const allTabs = useMemo(() => TAB_CONFIG.map(({ key, label }) => ({ key, label })), []);
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
const hasAuth = useMemo(
() => hasEffectiveAuth(collection, item),
[item, itemAuthMode, collection]
);
const allTabs = useMemo(
() => TAB_CONFIG.map(({ key, label }) => ({
key,
label,
indicator: key === 'auth' && hasAuth ? <StatusDot dataTestId="auth" /> : null
})),
[hasAuth]
);
const handlePrettify = useCallback(() => {
if (queryEditorRef.current?.beautifyRequestBody) {

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import GrpcAuthMode from './GrpcAuthMode';
@@ -9,32 +9,32 @@ import OAuth2 from '../../Auth/OAuth2/index';
import WsseAuth from '../../Auth/WsseAuth';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections';
import { getTreePathFromCollectionToItem } from 'utils/collections/index';
import { getEffectiveAuthSource } from 'utils/auth';
import { updateRequestAuthMode, updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
// List of auth modes supported by gRPC
// Note: Only header-based auth modes work with gRPC
// Complex auth modes like AWS Sig v4, Digest, and NTLM require axios interceptors
// and cannot be supported in gRPC requests as of now
const supportedGrpcAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'wsse', 'none', 'inherit'];
import { AUTH_MODES_GRPC } from 'utils/common/constants';
const GrpcAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
const request = item.draft
? get(item, 'draft.request', {})
: get(item, 'request', {});
const inheritedSource = useMemo(
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, item) : null),
[authMode, item, collection]
);
const save = () => {
return saveRequest(item.uid, collection.uid);
};
// Reset to 'none' if current auth mode is not supported by gRPC
useEffect(() => {
if (authMode && !supportedGrpcAuthModes.includes(authMode)) {
if (authMode && !AUTH_MODES_GRPC.includes(authMode)) {
dispatch(
updateRequestAuthMode({
itemUid: item.uid,
@@ -45,35 +45,6 @@ const GrpcAuth = ({ item, collection }) => {
}
}, [authMode, collection.uid, dispatch, item.uid]);
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
// Check folders in reverse to find the closest auth configuration
for (let i of [...requestTreePath].reverse()) {
if (i.type === 'folder') {
const folderAuth = get(i, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
name: i.name,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const getAuthView = () => {
switch (authMode) {
case 'none': {
@@ -95,15 +66,13 @@ const GrpcAuth = ({ item, collection }) => {
return <WsseAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
}
case 'inherit': {
const source = getEffectiveAuthSource();
// Only show inherited auth if it's one of the supported types
if (source && supportedGrpcAuthModes.includes(source.auth?.mode)) {
if (inheritedSource && AUTH_MODES_GRPC.includes(inheritedSource.auth?.mode)) {
return (
<>
<div className="flex flex-row w-full gap-2">
<div>Auth inherited from {source.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
<div>Auth inherited from {inheritedSource.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
</div>
</>
);

View File

@@ -12,6 +12,8 @@ import Documentation from 'components/Documentation/index';
import { getPropertyFromDraftOrRequest } from 'utils/collections/index';
import ResponsiveTabs from 'ui/ResponsiveTabs';
import StyledWrapper from './StyledWrapper';
import { hasEffectiveAuth } from 'utils/auth';
import { AUTH_MODES_GRPC } from 'utils/common/constants';
const GrpcRequestPane = ({ item, collection, handleRun }) => {
const dispatch = useDispatch();
@@ -53,8 +55,11 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
const body = getPropertyFromDraftOrRequest(item, 'request.body');
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
const auth = getPropertyFromDraftOrRequest(item, 'request.auth');
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
const hasAuth = useMemo(
() => hasEffectiveAuth(collection, item, AUTH_MODES_GRPC),
[item, itemAuthMode, collection]
);
const activeHeadersLength = headers.filter((header) => header.enabled).length;
const grpcMessagesCount = body?.grpc?.length || 0;
@@ -88,7 +93,7 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
{
key: 'auth',
label: 'Auth',
indicator: auth?.mode && auth.mode !== 'none' ? <StatusDot type="default" /> : null
indicator: hasAuth ? <StatusDot type="default" dataTestId="auth" /> : null
},
{
key: 'docs',
@@ -96,7 +101,7 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
indicator: docs && docs.length > 0 ? <StatusDot type="default" /> : null
}
];
}, [grpcMessagesCount, isClientStreaming, activeHeadersLength, auth?.mode, docs]);
}, [grpcMessagesCount, isClientStreaming, activeHeadersLength, hasAuth, docs]);
// Initialize tab to 'body' if no tab is currently set
useEffect(() => {

View File

@@ -18,6 +18,7 @@ import StatusDot from 'components/StatusDot';
import ResponsiveTabs from 'ui/ResponsiveTabs';
import HeightBoundContainer from 'ui/HeightBoundContainer';
import AuthMode from '../Auth/AuthMode/index';
import { hasEffectiveAuth } from 'utils/auth';
const TAB_CONFIG = [
{ key: 'params', label: 'Params' },
@@ -54,7 +55,6 @@ const HttpRequestPane = ({ item, collection }) => {
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const requestPaneTab = focusedTab?.requestPaneTab;
const getProperty = useCallback(
(key) => (item.draft ? get(item, `draft.${key}`, []) : get(item, key, [])),
[item.draft, item]
@@ -86,6 +86,12 @@ const HttpRequestPane = ({ item, collection }) => {
[dispatch, item.uid]
);
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
const hasAuth = useMemo(
() => hasEffectiveAuth(collection, item),
[item, itemAuthMode, collection]
);
const indicators = useMemo(() => {
const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;
const hasTestError = item.testScriptErrorMessage;
@@ -94,7 +100,7 @@ const HttpRequestPane = ({ item, collection }) => {
params: activeCounts.params > 0 ? <sup className="font-medium">{activeCounts.params}</sup> : null,
body: body.mode !== 'none' ? <StatusDot /> : null,
headers: activeCounts.headers > 0 ? <sup className="font-medium">{activeCounts.headers}</sup> : null,
auth: auth.mode !== 'none' ? <StatusDot /> : null,
auth: hasAuth ? <StatusDot dataTestId="auth" /> : null,
vars: activeCounts.vars > 0 ? <sup className="font-medium">{activeCounts.vars}</sup> : null,
script: (script.req || script.res) ? (hasScriptError ? <StatusDot type="error" /> : <StatusDot />) : null,
assert: activeCounts.assertions > 0 ? <sup className="font-medium">{activeCounts.assertions}</sup> : null,
@@ -102,7 +108,7 @@ const HttpRequestPane = ({ item, collection }) => {
docs: docs?.length > 0 ? <StatusDot /> : null,
settings: tags?.length > 0 ? <StatusDot /> : null
};
}, [activeCounts, body.mode, auth.mode, script, item.preRequestScriptErrorMessage, item.postResponseScriptErrorMessage, item.testScriptErrorMessage, tests, docs, tags]);
}, [activeCounts, body.mode, hasAuth, script, item.preRequestScriptErrorMessage, item.postResponseScriptErrorMessage, item.testScriptErrorMessage, tests, docs, tags]);
const allTabs = useMemo(
() => TAB_CONFIG.map(({ key, label }) => ({ key, label, indicator: indicators[key] })),

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import BearerAuth from '../../Auth/BearerAuth';
@@ -6,16 +6,15 @@ import BasicAuth from '../../Auth/BasicAuth';
import ApiKeyAuth from '../../Auth/ApiKeyAuth';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections';
import { getTreePathFromCollectionToItem } from 'utils/collections/index';
import { getEffectiveAuthSource } from 'utils/auth';
import { updateRequestAuthMode, updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
const supportedAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'none', 'inherit'];
import { AUTH_MODES_WS } from 'utils/common/constants';
const WSAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
const request = item.draft
? get(item, 'draft.request', {})
@@ -25,9 +24,14 @@ const WSAuth = ({ item, collection }) => {
return saveRequest(item.uid, collection.uid);
};
const inheritedSource = useMemo(
() => (authMode === 'inherit' ? getEffectiveAuthSource(collection, item) : null),
[authMode, item, collection]
);
// Reset to 'none' if current auth mode is not supported
useEffect(() => {
if (authMode && !supportedAuthModes.includes(authMode)) {
if (authMode && !AUTH_MODES_WS.includes(authMode)) {
dispatch(updateRequestAuthMode({
itemUid: item.uid,
collectionUid: collection.uid,
@@ -36,35 +40,6 @@ const WSAuth = ({ item, collection }) => {
}
}, [authMode, collection.uid, dispatch, item.uid]);
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
// Check folders in reverse to find the closest auth configuration
for (let i of [...requestTreePath].reverse()) {
if (i.type === 'folder') {
const folderAuth = get(i, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
name: i.name,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const getAuthView = () => {
switch (authMode) {
case 'none': {
@@ -91,26 +66,24 @@ const WSAuth = ({ item, collection }) => {
);
}
case 'inherit': {
const source = getEffectiveAuthSource();
// Check if inherited auth is OAuth1/OAuth2 - not supported for WebSockets
if (source?.auth?.mode === 'oauth1' || source?.auth?.mode === 'oauth2') {
if (inheritedSource?.auth?.mode === 'oauth1' || inheritedSource?.auth?.mode === 'oauth2') {
return (
<>
<div className="flex flex-row w-full mt-2 gap-2">
{source.auth.mode === 'oauth1' ? 'OAuth 1.0' : 'OAuth 2'} not <strong>yet</strong> supported by WebSockets. Using no auth instead.
{inheritedSource.auth.mode === 'oauth1' ? 'OAuth 1.0' : 'OAuth 2'} not <strong>yet</strong> supported by WebSockets. Using no auth instead.
</div>
</>
);
}
// Only show inherited auth if it's one of the supported types
if (source && supportedAuthModes.includes(source.auth?.mode)) {
if (inheritedSource && AUTH_MODES_WS.includes(inheritedSource.auth?.mode)) {
return (
<>
<div className="flex flex-row w-full gap-2">
<div> Auth inherited from {source.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
<div> Auth inherited from {inheritedSource.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(inheritedSource.auth?.mode)}</div>
</div>
</>
);

View File

@@ -20,6 +20,8 @@ import StyledWrapper from './StyledWrapper';
import WSAuth from './WSAuth';
import WSAuthMode from './WSAuth/WSAuthMode';
import WSSettingsPane from '../WSSettingsPane/index';
import { hasEffectiveAuth } from 'utils/auth';
import { AUTH_MODES_WS } from 'utils/common/constants';
const WSRequestPane = ({ item, collection, handleRun }) => {
const dispatch = useDispatch();
@@ -102,8 +104,11 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
const auth = getPropertyFromDraftOrRequest(item, 'request.auth');
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
const hasAuth = useMemo(
() => hasEffectiveAuth(collection, item, AUTH_MODES_WS),
[item, itemAuthMode, collection]
);
const activeHeadersLength = headers.filter((header) => header.enabled).length;
const allTabs = useMemo(() => {
@@ -121,7 +126,7 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
{
key: 'auth',
label: 'Auth',
indicator: auth.mode !== 'none' ? <StatusDot type="default" /> : null
indicator: hasAuth ? <StatusDot type="default" dataTestId="auth" /> : null
},
{
key: 'settings',
@@ -134,7 +139,7 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
indicator: docs && docs.length > 0 ? <StatusDot type="default" /> : null
}
];
}, [activeHeadersLength, auth.mode, docs]);
}, [activeHeadersLength, hasAuth, docs]);
const tabPanel = useMemo(() => {
switch (requestPaneTab) {

View File

@@ -53,7 +53,12 @@ const Timeline = ({ collection, item }) => {
useTrackScroll({ ref: wrapperRef, selector: null, onChange: setScroll, initialValue: scroll });
const [activeFilter, setActiveFilter] = useState('all');
const authSource = getEffectiveAuthSource(collection, item);
// Get the effective auth source if auth mode is inherit
const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode;
const authSource = useMemo(
() => getEffectiveAuthSource(collection, item),
[item, itemAuthMode, collection]
);
const isGrpcRequest = item.type === 'grpc-request' || item.type === 'ws-request';
const entries = useMemo(

View File

@@ -1,11 +1,12 @@
import React from 'react';
import DotIcon from 'components/Icons/Dot';
const StatusDot = ({ type = 'default' }) => (
const StatusDot = ({ type = 'default', dataTestId = null }) => (
<sup
className={`ml-[.125rem] opacity-80 font-medium ${
type === 'error' ? 'text-red-500' : ''
}`}
data-testid={dataTestId ? `status-dot-${dataTestId}` : 'status-dot'}
>
<DotIcon width="10" />
</sup>

View File

@@ -2,6 +2,7 @@ import { get } from 'lodash';
import {
getTreePathFromCollectionToItem
} from 'utils/collections/index';
import { AUTH_MODES } from 'utils/common/constants';
// Resolve inherited auth by traversing up the folder hierarchy
export const resolveInheritedAuth = (item, collection) => {
@@ -25,8 +26,9 @@ export const resolveInheritedAuth = (item, collection) => {
const collectionAuth = get(collectionRoot, 'request.auth', { mode: 'none' });
let effectiveAuth = collectionAuth;
// Check folders in reverse to find the closest auth configuration
for (let i of [...requestTreePath].reverse()) {
// Walk ancestor folders from deepest up; pick the first one with a concrete auth mode (skip 'none'/'inherit').
for (let idx = requestTreePath.length - 1; idx >= 0; idx--) {
const i = requestTreePath[idx];
if (i.type === 'folder') {
const folderAuth = i?.draft ? get(i, 'draft.request.auth') : get(i, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
@@ -41,3 +43,51 @@ export const resolveInheritedAuth = (item, collection) => {
auth: effectiveAuth
};
};
export const getEffectiveAuthSource = (collection, item) => {
const authMode = item?.draft
? get(item, 'draft.request.auth.mode')
: (get(item, 'request.auth.mode') ?? get(item, 'root.request.auth.mode'));
if (authMode !== AUTH_MODES.INHERIT) return null;
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
for (let idx = requestTreePath.length - 1; idx >= 0; idx--) {
const i = requestTreePath[idx];
if (i?.uid === item?.uid) continue;
if (i?.type !== 'folder') continue;
const folderAuth = i?.draft ? get(i, 'draft.request.auth') : get(i, 'root.request.auth');
if (!folderAuth || !folderAuth.mode) continue;
if (folderAuth.mode === AUTH_MODES.INHERIT) continue;
effectiveSource = {
type: 'folder',
name: i.name,
auth: folderAuth
};
break;
}
return effectiveSource;
};
// Returns true when an item actually has auth applied — resolves `inherit` up
// the chain, then checks that the effective mode is set, not 'none', and (if a
// supportedModes list is passed) is one the protocol can apply.
export const hasEffectiveAuth = (collection, item, supportedModes) => {
const auth = item?.draft
? get(item, 'draft.request.auth')
: (get(item, 'request.auth') ?? get(item, 'root.request.auth'));
const mode = auth?.mode === AUTH_MODES.INHERIT
? getEffectiveAuthSource(collection, item)?.auth?.mode
: auth?.mode;
if (!mode || mode === AUTH_MODES.NONE) return false;
if (supportedModes && !supportedModes.includes(mode)) return false;
return true;
};

View File

@@ -1,13 +1,21 @@
import { resolveInheritedAuth } from './index';
import { getEffectiveAuthSource, resolveInheritedAuth } from './index';
jest.mock('utils/collections/index', () => ({
// General path finder: walks the collection.items tree until it finds the
// item with the matching uid and returns the full path to it.
getTreePathFromCollectionToItem: (collection, item) => {
const itemUid = item.uid;
if (itemUid === 'r1') {
return [collection.items[0], collection.items[0].items[0]];
}
return [];
const findPath = (items, targetUid, path = []) => {
for (const i of items || []) {
const next = [...path, i];
if (i.uid === targetUid) return next;
if (i.items) {
const found = findPath(i.items, targetUid, next);
if (found) return found;
}
}
return null;
};
return findPath(collection.items, item?.uid) || [];
}
}));
@@ -17,7 +25,7 @@ const buildCollection = () => {
uid: 'c1',
root: {
request: {
auth: { mode: 'bearer', bearer: { token: 'COLLECTION' } }
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
}
},
items: [
@@ -64,7 +72,7 @@ describe('auth-utils.resolveInheritedAuth', () => {
const resolved = resolveInheritedAuth(item, collection);
expect(resolved.auth.mode).toBe('bearer');
expect(resolved.auth.bearer.token).toBe('COLLECTION');
expect(resolved.auth.bearer.token).toBe('COLLECTION_LEVEL_TOKEN');
});
it('should return original request when mode is not inherit', () => {
@@ -77,3 +85,211 @@ describe('auth-utils.resolveInheritedAuth', () => {
expect(resolved.auth.basic.username).toBe('override');
});
});
describe('auth-utils.getEffectiveAuthSource', () => {
it('returns null when the request mode is not inherit', () => {
const collection = buildCollection();
const item = collection.items[0].items[0]; // r1
item.request.auth = { mode: 'bearer', bearer: { token: 'MOCK_REQUEST_OWN_TOKEN_STRING' } };
expect(getEffectiveAuthSource(collection, item)).toBeNull();
});
it('returns null when the request has no auth configured', () => {
const collection = buildCollection();
const item = collection.items[0].items[0];
item.request.auth = undefined;
expect(getEffectiveAuthSource(collection, item)).toBeNull();
});
it('returns the nearest configured folder when request inherits and folder has auth', () => {
const collection = buildCollection();
const item = collection.items[0].items[0]; // r1, mode 'inherit'
const source = getEffectiveAuthSource(collection, item);
expect(source).toEqual({
type: 'folder',
name: 'Folder',
auth: { mode: 'basic', basic: { username: 'user', password: 'pass' } }
});
});
it('falls back to the collection when no ancestor folder has configured auth', () => {
const collection = buildCollection();
// make the folder also inherit so the walk falls through to the collection
collection.items[0].root.request.auth = { mode: 'inherit' };
const item = collection.items[0].items[0];
const source = getEffectiveAuthSource(collection, item);
expect(source).toEqual({
type: 'collection',
name: 'Collection',
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
});
});
it('skips the item itself when the item is a folder in inherit mode', () => {
// Build a parent → child folder chain; child is the item under test.
const collection = {
uid: 'c1',
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
items: [
{
uid: 'parent',
type: 'folder',
name: 'Parent',
root: { request: { auth: { mode: 'basic', basic: { username: 'p', password: 'p' } } } },
items: [
{
uid: 'child',
type: 'folder',
name: 'Child',
root: { request: { auth: { mode: 'inherit' } } },
items: []
}
]
}
]
};
const child = collection.items[0].items[0];
const source = getEffectiveAuthSource(collection, child);
expect(source).toEqual({
type: 'folder',
name: 'Parent',
auth: { mode: 'basic', basic: { username: 'p', password: 'p' } }
});
});
it('prefers the draft mode when item.draft exists', () => {
const collection = buildCollection();
const item = collection.items[0].items[0];
item.request.auth = { mode: 'bearer' }; // saved is not inherit
item.draft = { request: { auth: { mode: 'inherit' } } }; // draft is inherit
const source = getEffectiveAuthSource(collection, item);
expect(source).toEqual({
type: 'folder',
name: 'Folder',
auth: { mode: 'basic', basic: { username: 'user', password: 'pass' } }
});
});
it('resolves correctly when both draft and saved auth are inherit on a folder whose parent is also a folder', () => {
const collection = {
uid: 'c1',
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
items: [
{
uid: 'parent',
type: 'folder',
name: 'Parent',
root: { request: { auth: { mode: 'basic', basic: { username: 'p', password: 'p' } } } },
items: [
{
uid: 'child',
type: 'folder',
name: 'Child',
root: { request: { auth: { mode: 'inherit' } } }, // saved: inherit
draft: { request: { auth: { mode: 'inherit' } } }, // draft: inherit
items: []
}
]
}
]
};
const child = collection.items[0].items[0];
const source = getEffectiveAuthSource(collection, child);
expect(source).toEqual({
type: 'folder',
name: 'Parent',
auth: { mode: 'basic', basic: { username: 'p', password: 'p' } }
});
});
it('handles a folder item without draft using its root.request.auth.mode', () => {
// The folder's mode is read from root.request.auth.mode when no draft exists.
const collection = {
uid: 'c1',
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
items: [
{
uid: 'folder-inherit',
type: 'folder',
name: 'FolderInherit',
root: { request: { auth: { mode: 'inherit' } } },
items: []
}
]
};
const folder = collection.items[0];
const source = getEffectiveAuthSource(collection, folder);
expect(source).toEqual({
type: 'collection',
name: 'Collection',
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
});
});
it('handles a folder item with draft by reading its draft.request.auth.mode (not the saved root mode)', () => {
// Saved mode is 'basic' (would return null since not inherit), but draft is 'inherit'
// so the walk should run and resolve to the collection.
const collection = {
uid: 'c1',
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
items: [
{
uid: 'folder-draft-inherit',
type: 'folder',
name: 'FolderDraftInherit',
root: { request: { auth: { mode: 'basic', basic: { username: 'saved', password: 'saved' } } } },
draft: { request: { auth: { mode: 'inherit' } } },
items: []
}
]
};
const folder = collection.items[0];
const source = getEffectiveAuthSource(collection, folder);
expect(source).toEqual({
type: 'collection',
name: 'Collection',
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
});
});
it('skips ancestor folders whose auth.mode is itself "inherit"', () => {
// Parent folder also inherits — walk should continue past it to collection.
const collection = {
uid: 'c1',
root: { request: { auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } } } },
items: [
{
uid: 'parent',
type: 'folder',
name: 'Parent',
root: { request: { auth: { mode: 'inherit' } } },
items: [
{
uid: 'r1',
type: 'request',
name: 'Request',
request: { auth: { mode: 'inherit' } }
}
]
}
]
};
const item = collection.items[0].items[0];
const source = getEffectiveAuthSource(collection, item);
expect(source).toEqual({
type: 'collection',
name: 'Collection',
auth: { mode: 'bearer', bearer: { token: 'COLLECTION_LEVEL_TOKEN' } }
});
});
});

View File

@@ -1,3 +1,47 @@
export const REQUEST_TYPES = ['http-request', 'graphql-request', 'grpc-request', 'ws-request'];
export const DEFAULT_COLLECTION_FORMAT = 'yml';
export const PRESET_REQUEST_TYPES = {
HTTP: 'http',
GRAPHQL: 'graphql',
GRPC: 'grpc',
WS: 'ws'
};
export const DEFAULT_PRESET_REQUEST_TYPE = PRESET_REQUEST_TYPES.HTTP;
export const AUTH_MODES = {
AWSV4: 'awsv4',
BASIC: 'basic',
BEARER: 'bearer',
DIGEST: 'digest',
NTLM: 'ntlm',
OAUTH1: 'oauth1',
OAUTH2: 'oauth2',
WSSE: 'wsse',
APIKEY: 'apikey',
NONE: 'none',
INHERIT: 'inherit'
};
// Auth modes supported by WS protocol.
export const AUTH_MODES_WS = [
AUTH_MODES.BASIC,
AUTH_MODES.BEARER,
AUTH_MODES.APIKEY,
AUTH_MODES.OAUTH2,
AUTH_MODES.NONE,
AUTH_MODES.INHERIT
];
// Auth modes supported by GRPC protocol
export const AUTH_MODES_GRPC = [
AUTH_MODES.BASIC,
AUTH_MODES.BEARER,
AUTH_MODES.APIKEY,
AUTH_MODES.OAUTH2,
AUTH_MODES.WSSE,
AUTH_MODES.NONE,
AUTH_MODES.INHERIT
];

View File

@@ -2,7 +2,7 @@ import { toOpenCollectionAuth, toOpenCollectionHeaders, toOpenCollectionScripts,
import { toOpenCollectionEnvironments } from "./environment";
import { toOpenCollectionFolder } from "./folder";
import { toOpenCollectionItems } from "./items";
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, ClientCertificate, CollectionConfig, OpenCollection, PemCertificate, Pkcs12Certificate, Protobuf } from "./types";
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, BrunoPresets, ClientCertificate, CollectionConfig, OpenCollection, PemCertificate, Pkcs12Certificate, Protobuf } from "./types";
const toOpenCollectionConfig = (brunoConfig: BrunoConfig | undefined): CollectionConfig | undefined => {
if (!brunoConfig) {
@@ -147,10 +147,7 @@ export const brunoToOpenCollection = (collection: BrunoCollection): OpenCollecti
const brunoExtension: {
ignore?: string[];
presets?: {
requestType?: string;
requestUrl?: string;
};
presets?: BrunoPresets;
} = {};
if ((collection.brunoConfig as BrunoConfig)?.ignore?.length) {

View File

@@ -1,5 +1,5 @@
import { OpenCollection } from "@opencollection/types";
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, PemCertificate, Pkcs12Certificate } from "./types";
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, BrunoPresets, PemCertificate, Pkcs12Certificate } from "./types";
import { fromOpenCollectionAuth, fromOpenCollectionHeaders, fromOpenCollectionScripts, fromOpenCollectionVariables } from "./common";
import { uuid } from "../common";
import { fromOpenCollectionItems } from "./items";
@@ -9,10 +9,7 @@ import { fromOpenCollectionEnvironments } from "./environment";
const fromOpenCollectionConfig = (oc: OpenCollection): BrunoConfig => {
const brunoExtension = oc.extensions?.bruno as {
ignore?: string[];
presets?: {
requestType?: string;
requestUrl?: string;
};
presets?: BrunoPresets;
} | undefined;
const ignoreList = brunoExtension && Array.isArray(brunoExtension.ignore)

View File

@@ -168,15 +168,17 @@ export type {
WebSocketMessage as BrunoWsMessage
} from '@usebruno/schema-types/requests/websocket';
export interface BrunoPresets {
requestType?: string;
requestUrl?: string;
}
export interface BrunoConfig {
version?: string;
name?: string;
type?: string;
ignore?: string[];
presets?: {
requestType?: string;
requestUrl?: string;
};
presets?: BrunoPresets;
protobuf?: {
protoFiles?: { path: string }[];
importPaths?: { path: string; enabled?: boolean }[];

View File

@@ -809,7 +809,7 @@ const mergeAuth = (collection, request, requestTreePath) => {
const folderRoot = i?.draft || i?.root;
const folderAuth = get(folderRoot, 'request.auth');
// Only consider folders that have a valid auth mode
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
effectiveAuth = folderAuth;
lastFolderWithAuth = i;
}

View File

@@ -7,6 +7,7 @@ import { toBrunoVariables } from './common/variables';
import { toBrunoPostResponseVariables } from './common/actions';
import { toBrunoScripts } from './common/scripts';
import { ensureString } from '../../utils';
import type { BrunoPresetsExtension } from '../../types';
interface ParsedCollection {
collectionRoot: FolderRoot;
@@ -32,11 +33,11 @@ const parseCollection = (ymlString: string): ParsedCollection => {
// presets
if (brunoExtension?.presets) {
const presets = brunoExtension.presets as any;
const presets = brunoExtension.presets as BrunoPresetsExtension;
if (presets.request) {
brunoConfig.presets = {
requestType: presets.request.type || [],
requestUrl: presets.request.url || []
requestType: presets.request.type || '',
requestUrl: presets.request.url || ''
};
}
}

View File

@@ -19,15 +19,9 @@ const parseFolder = (ymlString: string): FolderRoot => {
name: ensureString(info?.name, 'Untitled Folder'),
seq: info?.seq || 1
},
request: null,
docs: null
};
// request defaults
if (ocFolder.request) {
folderRoot.request = {
request: {
headers: [],
auth: null,
auth: toBrunoAuth(ocFolder.request?.auth),
script: {
req: null,
res: null
@@ -37,40 +31,39 @@ const parseFolder = (ymlString: string): FolderRoot => {
res: []
},
tests: null
};
},
docs: null
};
if (ocFolder.request) {
const folderRequest = folderRoot.request!;
// headers
const headers = toBrunoHttpHeaders(ocFolder.request.headers);
if (headers) {
folderRoot.request.headers = headers;
}
// auth
const auth = toBrunoAuth(ocFolder.request.auth);
if (auth) {
folderRoot.request.auth = auth;
folderRequest.headers = headers;
}
// variables
const variables = toBrunoVariables(ocFolder.request.variables);
const postResponseVars = toBrunoPostResponseVariables((ocFolder.request as any).actions);
folderRoot.request.vars = {
folderRequest.vars = {
req: variables.req,
res: postResponseVars
};
// scripts
const scripts = toBrunoScripts(ocFolder.request.scripts);
if (scripts?.script && folderRoot.request.script) {
if (scripts?.script && folderRequest.script) {
if (scripts.script.req) {
folderRoot.request.script.req = scripts.script.req;
folderRequest.script.req = scripts.script.req;
}
if (scripts.script.res) {
folderRoot.request.script.res = scripts.script.res;
folderRequest.script.res = scripts.script.res;
}
}
if (scripts?.tests) {
folderRoot.request.tests = scripts.tests;
folderRequest.tests = scripts.tests;
}
}

View File

@@ -0,0 +1,7 @@
opencollection: 1.0.0
info:
name: My Collection
extensions:
bruno:
presets:
request: {}

View File

@@ -0,0 +1,3 @@
opencollection: 1.0.0
info:
name: My Collection

View File

@@ -0,0 +1,6 @@
opencollection: 1.0.0
info:
name: My Collection
extensions:
bruno:
presets: {}

View File

@@ -0,0 +1,24 @@
opencollection: 1.0.0
info:
name: md8
config:
proxy:
inherit: true
config:
protocol: http
hostname: ""
port: ""
auth:
username: ""
password: ""
bypassProxy: ""
bundled: false
extensions:
bruno:
ignore:
- node_modules
- .git
presets:
request:
type: graphql

View File

@@ -0,0 +1,25 @@
opencollection: 1.0.0
info:
name: md8
config:
proxy:
inherit: true
config:
protocol: http
hostname: ""
port: ""
auth:
username: ""
password: ""
bypassProxy: ""
bundled: false
extensions:
bruno:
ignore:
- node_modules
- .git
presets:
request:
type: http
url: https://example.com/graphql

View File

@@ -0,0 +1,9 @@
opencollection: 1.0.0
info:
name: My Collection
extensions:
bruno:
presets:
request:
type: graphql
url: https://example.com/graphql

View File

@@ -0,0 +1,63 @@
import fs from 'node:fs';
import path from 'node:path';
import parseCollection from '../parseCollection';
const loadFixture = (name) =>
fs.readFileSync(path.join(__dirname, 'fixtures', 'presets', `${name}.yml`), 'utf8');
describe('yml parseCollection - presets', () => {
it('parses presets when request.type and request.url are present', () => {
const { brunoConfig } = parseCollection(loadFixture('with-type-and-url'));
expect(brunoConfig.presets).toEqual({
requestType: 'graphql',
requestUrl: 'https://example.com/graphql'
});
});
it('defaults requestType and requestUrl to empty strings (not arrays) when request fields are missing', () => {
const { brunoConfig } = parseCollection(loadFixture('empty-request'));
expect(brunoConfig.presets).toEqual({
requestType: '',
requestUrl: ''
});
expect(Array.isArray(brunoConfig.presets.requestType)).toBe(false);
expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
});
it('does not set presets when the extension has no presets block', () => {
const { brunoConfig } = parseCollection(loadFixture('no-presets'));
expect(brunoConfig.presets).toBeUndefined();
});
it('does not set presets when presets exists but request key is absent', () => {
const { brunoConfig } = parseCollection(loadFixture('no-request-key'));
expect(brunoConfig.presets).toBeUndefined();
});
it('parses a realistic collection with only request.type set (no url) — defaults url to empty string', () => {
const { brunoConfig } = parseCollection(loadFixture('type-only-realistic'));
expect(brunoConfig.presets).toEqual({
requestType: 'graphql',
requestUrl: ''
});
expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
expect(brunoConfig.ignore).toEqual(['node_modules', '.git']);
});
it('parses a realistic collection with request.type http and request.url set', () => {
const { brunoConfig } = parseCollection(loadFixture('url-only-realistic'));
expect(brunoConfig.presets).toEqual({
requestType: 'http',
requestUrl: 'https://example.com/graphql'
});
expect(Array.isArray(brunoConfig.presets.requestType)).toBe(false);
expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
expect(brunoConfig.ignore).toEqual(['node_modules', '.git']);
});
});

View File

@@ -20,3 +20,10 @@ export interface WorkerTask {
export interface Lane {
maxSize: number;
}
export interface BrunoPresetsExtension {
request?: {
type?: string;
url?: string;
};
}

View File

@@ -0,0 +1,113 @@
import { test, expect } from '../../../playwright';
import { buildCommonLocators, closeAllCollections, createCollection, createFolder, selectAuthMode } from '../../utils/page';
import { AUTH_MODE_LABELS } from '../../utils/constants';
test.describe('Effective auth mode resolution', () => {
test.afterEach(async ({ page }) => {
await closeAllCollections(page);
});
test('Nested folder with Inherit should pick up its immediate parent folder, not a grandparent', async ({ page, createTmpDir }) => {
const collectionName = 'effective-auth-mode-collection';
const locators = buildCommonLocators(page);
await test.step('Create a collection', async () => {
await createCollection(page, collectionName, await createTmpDir());
});
await test.step('Create folder-1 inside the collection and set auth type for folder-1 as Bearer Token', async () => {
await createFolder(page, 'folder-1', collectionName, true);
await locators.sidebar.folder('folder-1').dblclick();
await locators.paneTabs.folderSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.BEARER);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Create folder-2 inside folder-1 and set auth type for folder-2 as Basic Auth', async () => {
await createFolder(page, 'folder-2', 'folder-1', false);
await locators.sidebar.folder('folder-2').dblclick();
await locators.paneTabs.folderSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.BASIC);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Create folder-3 inside folder-2 and set auth type for folder-3 as Inherit', async () => {
await createFolder(page, 'folder-3', 'folder-2', false);
await locators.sidebar.folder('folder-3').dblclick();
await locators.paneTabs.folderSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.INHERIT);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Verify folder-3 should inherit auth from folder-2', async () => {
await expect(page.getByText('Auth inherited from folder-2:')).toBeVisible();
await expect(locators.auth.inheritedMode()).toHaveText(AUTH_MODE_LABELS.BASIC);
});
});
test('Child folder with Inherit should pick up parent folder set to No Auth (not fall through to collection)', async ({ page, createTmpDir }) => {
const collectionName = 'no-auth-inherit-collection';
const locators = buildCommonLocators(page);
await test.step('Create a collection', async () => {
await createCollection(page, collectionName, await createTmpDir());
});
await test.step('Set auth type for the collection as Basic Auth', async () => {
await locators.paneTabs.collectionSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.BASIC);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Create folder-1 inside the collection and set auth type for folder-1 as No Auth', async () => {
await createFolder(page, 'folder-1', collectionName, true);
await locators.sidebar.folder('folder-1').dblclick();
await locators.paneTabs.folderSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.NONE);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Create folder-2 inside folder-1 and set auth type for folder-2 as Inherit', async () => {
await createFolder(page, 'folder-2', 'folder-1', false);
await locators.sidebar.folder('folder-2').dblclick();
await locators.paneTabs.folderSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.INHERIT);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Verify folder-2 should inherit No Auth from folder-1 (not fall through to the collection)', async () => {
await expect(page.getByText('Auth inherited from folder-1:')).toBeVisible();
await expect(locators.auth.inheritedMode()).toHaveText(AUTH_MODE_LABELS.NONE);
});
});
test('Auth dropdown shows No Auth as the selected option after picking it', async ({ page, createTmpDir }) => {
const collectionName = 'no-auth-dropdown-collection';
const locators = buildCommonLocators(page);
await test.step('Create a collection', async () => {
await createCollection(page, collectionName, await createTmpDir());
});
await test.step('Create folder-1 inside the collection and set auth type for folder-1 as No Auth', async () => {
await createFolder(page, 'folder-1', collectionName, true);
await locators.sidebar.folder('folder-1').dblclick();
await locators.paneTabs.folderSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.NONE);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Verify the auth mode selector shows No Auth as the current mode', async () => {
await expect(locators.auth.modeSelector()).toContainText(AUTH_MODE_LABELS.NONE);
});
await test.step('Reopen the dropdown and verify No Auth is highlighted as the selected option', async () => {
await page.locator('.auth-mode-label').first().click();
// Bruno marks the selected dropdown item with the `dropdown-item-active` class.
const noAuthItem = locators.auth.dropdownItem('none');
await expect(noAuthItem).toBeVisible();
await expect(noAuthItem).toHaveClass(/dropdown-item-active/);
await expect(noAuthItem).toContainText(AUTH_MODE_LABELS.NONE);
});
});
});

View File

@@ -0,0 +1,95 @@
import { test, expect } from '../../../playwright';
import {
buildCommonLocators,
closeAllCollections,
createCollection,
createFolder,
createRequest,
openRequest,
saveRequest,
selectAuthMode,
selectRequestPaneTab,
selectResponsePaneTab,
sendRequest,
typeIntoField
} from '../../utils/page';
import { AUTH_MODE_LABELS } from '../../utils/constants';
test.afterEach(async ({ page }) => {
await closeAllCollections(page);
});
test('Request inherits No Auth from the folder — collection Bearer Token is overridden', async ({ page, createTmpDir }) => {
const collectionName = 'folder-no-auth-inheritance';
const locators = buildCommonLocators(page);
await test.step('Create a collection', async () => {
await createCollection(page, collectionName, await createTmpDir());
});
await test.step('Set auth type for the collection as Bearer Token', async () => {
await locators.paneTabs.collectionSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.BEARER);
await typeIntoField(page, 'Token', 'your_secret_token');
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Create folder-1 inside the collection and set auth type for folder-1 as No Auth', async () => {
await createFolder(page, 'folder-1', collectionName, true);
await locators.sidebar.folder('folder-1').dblclick();
await locators.paneTabs.folderSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.NONE);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Create an HTTP request inside folder-1 and set auth type for the request as Inherit', async () => {
const requestName = 'http-request-1';
await createRequest(page, requestName, 'folder-1', {
inFolder: true,
requestType: 'http',
method: 'GET',
url: 'https://testbench-sanity.usebruno.com/api/auth/bearer/protected'
});
await openRequest(page, collectionName, requestName);
await selectRequestPaneTab(page, 'Auth');
await selectAuthMode(page, AUTH_MODE_LABELS.INHERIT);
await saveRequest(page);
});
await test.step('Send the request and open the Timeline tab', async () => {
await sendRequest(page);
await selectResponsePaneTab(page, 'Timeline');
});
await test.step('Verify the response status code is 401 Unauthorized', async () => {
await expect(locators.response.statusCode()).toContainText('401');
});
await test.step('Open the latest timeline entry and verify no Authorization header was sent', async () => {
const timelineItem = locators.timeline.lastItem();
await locators.timeline.itemHeader(timelineItem).click();
await expect(timelineItem).toContainText('No Headers found');
await locators.timeline.clearButton().click();
});
await test.step('Change folder-1 auth type to Bearer Token with the expected token', async () => {
await locators.sidebar.folder('folder-1').dblclick();
await locators.paneTabs.folderSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.BEARER);
await typeIntoField(page, 'Token', 'your_secret_token');
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Send the request again and verify the response status code is 200', async () => {
await openRequest(page, collectionName, 'http-request-1');
await sendRequest(page);
await selectResponsePaneTab(page, 'Timeline');
await expect(locators.response.statusCode()).toContainText('200');
});
await test.step('Open the latest timeline entry and verify the Bearer token was sent', async () => {
const timelineItem = locators.timeline.lastItem();
await locators.timeline.itemHeader(timelineItem).click();
await expect(timelineItem).toContainText('Bearer your_secret_token');
});
});

View File

@@ -0,0 +1,147 @@
import { test, expect } from '../../../playwright';
import {
buildCommonLocators,
closeAllCollections,
createCollection,
createFolder,
createRequest,
openRequest,
saveRequest,
selectAuthMode,
selectRequestPaneTab
} from '../../utils/page';
import { AUTH_MODE_LABELS } from '../../utils/constants';
test.describe('Modified indicator for auth tab', () => {
test.afterEach(async ({ page }) => {
await closeAllCollections(page);
});
test('Folder Auth tab indicator dot reflects effective auth (shows on inherit, hides on No Auth)', async ({ page, createTmpDir }) => {
const collectionName = 'modified-indicator-collection';
const locators = buildCommonLocators(page);
await test.step('Create a collection', async () => {
await createCollection(page, collectionName, await createTmpDir());
});
await test.step('Set auth type for the collection as Bearer Token', async () => {
await locators.paneTabs.collectionSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.BEARER);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Verify the collection auth mode shows Bearer Token', async () => {
await expect(locators.auth.modeSelector()).toContainText(AUTH_MODE_LABELS.BEARER);
});
await test.step('Create folder-1 inside the collection and set auth type for folder-1 as Inherit', async () => {
await createFolder(page, 'folder-1', collectionName, true);
await locators.sidebar.folder('folder-1').dblclick();
await locators.paneTabs.folderSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.INHERIT);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Verify folder-1 inherits Bearer Token from the collection', async () => {
await expect(page.getByText('Auth inherited from Collection:')).toBeVisible();
await expect(locators.auth.inheritedMode()).toHaveText(AUTH_MODE_LABELS.BEARER);
});
await test.step('Verify the Auth tab shows the status dot for folder-1 (inheriting Bearer Token)', async () => {
await expect(
locators.paneTabs.folderSettingsTab('auth').getByTestId('status-dot-auth')
).toBeVisible();
});
await test.step('Change folder-1 auth type to No Auth', async () => {
await selectAuthMode(page, AUTH_MODE_LABELS.NONE);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Verify the Auth tab does NOT show the status dot for folder-1 (No Auth)', async () => {
await expect(
locators.paneTabs.folderSettingsTab('auth').getByTestId('status-dot-auth')
).toBeHidden();
});
});
const requestProtocolCases = [
{ protocol: 'HTTP', requestType: 'http' as const, requestName: 'http-request-1', url: 'https://example.com/api' },
{ protocol: 'gRPC', requestType: 'grpc' as const, requestName: 'grpc-request-1', url: 'grpc://localhost:50051' },
{ protocol: 'WebSocket', requestType: 'ws' as const, requestName: 'ws-request-1', url: 'ws://localhost:8080' },
{ protocol: 'GraphQL', requestType: 'graphql' as const, requestName: 'graphql-request-1', url: 'https://example.com/graphql' }
];
for (const { protocol, requestType, requestName, url } of requestProtocolCases) {
test(`${protocol} request inheriting auth from its folder shows the modified indicator dot`, async ({ page, createTmpDir }) => {
const collectionName = `${protocol.toLowerCase()}-inherit-indicator-collection`;
const locators = buildCommonLocators(page);
await test.step('Create a collection', async () => {
await createCollection(page, collectionName, await createTmpDir());
});
await test.step('Set auth type for the collection as Bearer Token', async () => {
await locators.paneTabs.collectionSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.BEARER);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('Create folder-1 inside the collection and set auth type for folder-1 as Basic Auth', async () => {
await createFolder(page, 'folder-1', collectionName, true);
await locators.sidebar.folder('folder-1').dblclick();
await locators.paneTabs.folderSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.BASIC);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step(`Create a ${protocol} request inside folder-1 and set auth type for the request as Inherit`, async () => {
await createRequest(page, requestName, 'folder-1', { inFolder: true, requestType, url });
await openRequest(page, collectionName, requestName);
await selectRequestPaneTab(page, 'Auth');
await selectAuthMode(page, AUTH_MODE_LABELS.INHERIT);
await saveRequest(page);
});
await test.step(`Verify the ${protocol} request Auth tab shows the status dot (inheriting Basic Auth from folder-1)`, async () => {
await expect(
locators.paneTabs.responsiveTab('auth').getByTestId('status-dot-auth')
).toBeVisible();
});
await test.step(`Change the ${protocol} request auth type to No Auth and verify the dot disappears`, async () => {
await selectAuthMode(page, AUTH_MODE_LABELS.NONE);
await saveRequest(page);
await expect(
locators.paneTabs.responsiveTab('auth').getByTestId('status-dot-auth')
).toBeHidden();
});
await test.step(`Change the ${protocol} request auth type to Basic Auth and verify the dot appears`, async () => {
await selectAuthMode(page, AUTH_MODE_LABELS.BASIC);
await saveRequest(page);
await expect(
locators.paneTabs.responsiveTab('auth').getByTestId('status-dot-auth')
).toBeVisible();
});
await test.step('Change folder-1 auth type to No Auth', async () => {
await locators.sidebar.folder('folder-1').dblclick();
await locators.paneTabs.folderSettingsTab('auth').click();
await selectAuthMode(page, AUTH_MODE_LABELS.NONE);
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step(`Set the ${protocol} request auth back to Inherit and verify the dot is hidden (folder is No Auth)`, async () => {
await openRequest(page, collectionName, requestName);
await selectRequestPaneTab(page, 'Auth');
await selectAuthMode(page, AUTH_MODE_LABELS.INHERIT);
await saveRequest(page);
await expect(
locators.paneTabs.responsiveTab('auth').getByTestId('status-dot-auth')
).toBeHidden();
});
});
}
});

View File

@@ -0,0 +1,70 @@
import type { Locator } from '@playwright/test';
import { test, expect } from '../../playwright';
import { buildCommonLocators, closeAllCollections, createCollection } from '../utils/page';
test.describe('Presets status dot in collection settings', () => {
test.afterEach(async ({ page }) => {
await closeAllCollections(page);
});
test('Presets dot is hidden on a fresh collection and stays visible once a preset has been saved (even at defaults)', async ({
page,
createTmpDir
}) => {
const collectionName = 'test-presets-indicator';
const locators = buildCommonLocators(page);
let presetsTab: Locator;
await test.step('Create a fresh collection (opens collection settings tab)', async () => {
await createCollection(page, collectionName, await createTmpDir());
});
await test.step('Open the Presets sub-tab', async () => {
presetsTab = locators.paneTabs.collectionSettingsTab('presets');
// visibility of the Presets sub-tab implies the collection settings tab is open
await expect(presetsTab).toBeVisible();
await presetsTab.click();
});
await test.step('Verify default state: HTTP selected and request URL is empty', async () => {
await expect(locators.presets.requestType('http')).toBeChecked();
await expect(locators.presets.requestType('graphql')).not.toBeChecked();
await expect(locators.presets.requestType('grpc')).not.toBeChecked();
await expect(locators.presets.requestType('ws')).not.toBeChecked();
await expect(locators.presets.requestUrl()).toHaveValue('');
});
await test.step('Verify Presets dot is NOT visible when HTTP is selected and URL is empty', async () => {
await expect(presetsTab.getByTestId('status-dot')).toBeHidden();
});
await test.step('Select gRPC request type and save', async () => {
await locators.presets.requestType('grpc').check();
await locators.presets.save().click();
});
await test.step('Verify Presets dot appears when a non-default request type is selected', async () => {
await expect(presetsTab.getByTestId('status-dot')).toBeVisible();
});
await test.step('Switch back to HTTP and set a request URL, then save', async () => {
await locators.presets.requestType('http').check();
await locators.presets.requestUrl().fill('https://example.com');
await locators.presets.save().click();
});
await test.step('Verify Presets dot remains visible when request URL is set', async () => {
await expect(presetsTab.getByTestId('status-dot')).toBeVisible();
});
await test.step('Clear the request URL with HTTP selected, then save (returns to default values)', async () => {
await locators.presets.requestUrl().fill('');
await expect(locators.presets.requestType('http')).toBeChecked();
await locators.presets.save().click();
});
await test.step('Verify Presets dot is hidden after returning to defaults', async () => {
await expect(presetsTab.getByTestId('status-dot')).not.toBeVisible({ timeout: 5000 });
});
});
});

View File

@@ -0,0 +1,13 @@
export const AUTH_MODE_LABELS = {
AWSV4: 'AWS Sig v4',
BASIC: 'Basic Auth',
BEARER: 'Bearer Token',
DIGEST: 'Digest Auth',
NTLM: 'NTLM Auth',
OAUTH1: 'OAuth 1.0',
OAUTH2: 'OAuth 2.0',
WSSE: 'WSSE Auth',
APIKEY: 'API Key',
INHERIT: 'Inherit',
NONE: 'No Auth'
} as const;

View File

@@ -0,0 +1 @@
export * from './auth';

View File

@@ -97,7 +97,17 @@ export const buildCommonLocators = (page: Page) => ({
},
oauth2: {
grantTypeDropdown: () => page.getByTestId('grant-type-dropdown')
}
},
modeSelector: () => page.getByTestId('auth-mode-selector'),
modeLabel: () => page.getByTestId('auth-mode-label'),
inheritedMode: () => page.getByTestId('inherited-auth-mode'),
dropdownItem: (id: string) => page.getByTestId(`auth-mode-dropdown-${id}`)
},
presets: {
requestType: (type: 'http' | 'graphql' | 'grpc' | 'ws') =>
page.getByTestId(`presets-request-type-${type}`),
requestUrl: () => page.getByTestId('presets-request-url'),
save: () => page.getByTestId('presets-save-btn')
},
tags: {
input: () => page.getByTestId('tag-input').getByRole('textbox'),
@@ -119,6 +129,12 @@ export const buildCommonLocators = (page: Page) => ({
codeLine: () => page.locator('.response-pane .editor-container .CodeMirror-line'),
jsonTreeLine: () => page.locator('.response-pane .object-content')
},
timeline: {
items: () => page.locator('.timeline-item'),
lastItem: () => page.locator('.timeline-item').last(),
itemHeader: (item: Locator) => item.locator('.oauth-request-item-header'),
clearButton: () => page.getByRole('button', { name: 'Clear Timeline' })
},
plusMenu: {
button: () => page.getByTestId('collections-header-add-menu'),
createCollection: () => page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }),