mirror of
https://github.com/apache/superset.git
synced 2026-05-16 05:15:16 +00:00
Compare commits
9 Commits
feat/glyph
...
chat-proto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c85661f4fd | ||
|
|
a06e6ea19b | ||
|
|
ee9eec25f9 | ||
|
|
ffa32414ef | ||
|
|
407321e394 | ||
|
|
2b71d964cc | ||
|
|
f02e5b7e83 | ||
|
|
5fa9657528 | ||
|
|
d853930840 |
0
extensions/chat/PUT_FILES_HERE.txt
Normal file
0
extensions/chat/PUT_FILES_HERE.txt
Normal file
266
superset-frontend/package-lock.json
generated
266
superset-frontend/package-lock.json
generated
@@ -224,7 +224,7 @@
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"babel-jest": "^30.0.2",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
@@ -17027,16 +17027,16 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "30.3.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
|
||||
"integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==",
|
||||
"version": "30.4.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz",
|
||||
"integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/transform": "30.3.0",
|
||||
"@jest/transform": "30.4.1",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"babel-plugin-istanbul": "^7.0.1",
|
||||
"babel-preset-jest": "30.3.0",
|
||||
"babel-preset-jest": "30.4.0",
|
||||
"chalk": "^4.1.2",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"slash": "^3.0.0"
|
||||
@@ -17048,6 +17048,85 @@
|
||||
"@babel/core": "^7.11.0 || ^8.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/@jest/pattern": {
|
||||
"version": "30.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz",
|
||||
"integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"jest-regex-util": "30.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/@jest/schemas": {
|
||||
"version": "30.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz",
|
||||
"integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.34.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/@jest/transform": {
|
||||
"version": "30.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz",
|
||||
"integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.27.4",
|
||||
"@jest/types": "30.4.1",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"babel-plugin-istanbul": "^7.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jest-haste-map": "30.4.1",
|
||||
"jest-regex-util": "30.4.0",
|
||||
"jest-util": "30.4.1",
|
||||
"pirates": "^4.0.7",
|
||||
"slash": "^3.0.0",
|
||||
"write-file-atomic": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/@jest/types": {
|
||||
"version": "30.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz",
|
||||
"integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/pattern": "30.4.0",
|
||||
"@jest/schemas": "30.4.1",
|
||||
"@types/istanbul-lib-coverage": "^2.0.6",
|
||||
"@types/istanbul-reports": "^3.0.4",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chalk": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/@sinclair/typebox": {
|
||||
"version": "0.34.49",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz",
|
||||
"integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -17065,6 +17144,105 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/jest-haste-map": {
|
||||
"version": "30.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz",
|
||||
"integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.4.1",
|
||||
"@types/node": "*",
|
||||
"anymatch": "^3.1.3",
|
||||
"fb-watchman": "^2.0.2",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jest-regex-util": "30.4.0",
|
||||
"jest-util": "30.4.1",
|
||||
"jest-worker": "30.4.1",
|
||||
"picomatch": "^4.0.3",
|
||||
"walker": "^1.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "^2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/jest-regex-util": {
|
||||
"version": "30.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz",
|
||||
"integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/jest-util": {
|
||||
"version": "30.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz",
|
||||
"integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.4.1",
|
||||
"@types/node": "*",
|
||||
"chalk": "^4.1.2",
|
||||
"ci-info": "^4.2.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/jest-worker": {
|
||||
"version": "30.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz",
|
||||
"integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@ungap/structured-clone": "^1.3.0",
|
||||
"jest-util": "30.4.1",
|
||||
"merge-stream": "^2.0.0",
|
||||
"supports-color": "^8.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/jest-worker/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest/node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
@@ -17132,9 +17310,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-jest-hoist": {
|
||||
"version": "30.3.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz",
|
||||
"integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==",
|
||||
"version": "30.4.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz",
|
||||
"integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -17288,13 +17466,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-jest": {
|
||||
"version": "30.3.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz",
|
||||
"integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==",
|
||||
"version": "30.4.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz",
|
||||
"integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"babel-plugin-jest-hoist": "30.3.0",
|
||||
"babel-plugin-jest-hoist": "30.4.0",
|
||||
"babel-preset-current-node-syntax": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -28917,6 +29095,58 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jest-config/node_modules/babel-jest": {
|
||||
"version": "30.3.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
|
||||
"integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/transform": "30.3.0",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"babel-plugin-istanbul": "^7.0.1",
|
||||
"babel-preset-jest": "30.3.0",
|
||||
"chalk": "^4.1.2",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"slash": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.11.0 || ^8.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/babel-plugin-jest-hoist": {
|
||||
"version": "30.3.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz",
|
||||
"integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/babel__core": "^7.20.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/babel-preset-jest": {
|
||||
"version": "30.3.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz",
|
||||
"integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"babel-plugin-jest-hoist": "30.3.0",
|
||||
"babel-preset-current-node-syntax": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.11.0 || ^8.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/brace-expansion": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||
@@ -50174,7 +50404,7 @@
|
||||
"version": "0.20.4",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.2.2",
|
||||
"@ant-design/icons": "^6.2.3",
|
||||
"@apache-superset/core": "*",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
@@ -50206,7 +50436,7 @@
|
||||
"react-js-cron": "^5.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
@@ -50275,9 +50505,9 @@
|
||||
}
|
||||
},
|
||||
"packages/superset-ui-core/node_modules/@ant-design/icons": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.2.tgz",
|
||||
"integrity": "sha512-zlJtE7AMbG12TeYVPhtBXwNpFInNy8mjLzcIm+0BPw16/b8ODG87YJ1G37VIF5VFscdgfsf6EweAFPTobu/3iQ==",
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.3.tgz",
|
||||
"integrity": "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^8.0.1",
|
||||
|
||||
@@ -305,7 +305,7 @@
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"babel-jest": "^30.0.2",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.2.2",
|
||||
"@ant-design/icons": "^6.2.3",
|
||||
"@apache-superset/core": "*",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
@@ -56,7 +56,7 @@
|
||||
"react-js-cron": "^5.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
||||
@@ -113,7 +113,7 @@ const EstimateQueryCostButton = ({
|
||||
modalBody={renderModalBody()}
|
||||
triggerNode={
|
||||
<Button
|
||||
color="primary"
|
||||
color="default"
|
||||
variant="text"
|
||||
style={{ height: 32, padding: '4px 15px' }}
|
||||
onClick={onClickHandler}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import { logging } from '@apache-superset/core/utils';
|
||||
import type { common as core } from '@apache-superset/core';
|
||||
import ExtensionsLoader from './ExtensionsLoader';
|
||||
@@ -111,3 +112,33 @@ test('handles initialization errors gracefully', async () => {
|
||||
errorSpy.mockRestore();
|
||||
appendChildSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('logs success after initializeExtensions completes', async () => {
|
||||
const loader = ExtensionsLoader.getInstance();
|
||||
const infoSpy = jest.spyOn(logging, 'info').mockImplementation();
|
||||
jest.spyOn(SupersetClient, 'get').mockResolvedValue({
|
||||
json: { result: [] },
|
||||
} as any);
|
||||
|
||||
await loader.initializeExtensions();
|
||||
|
||||
expect(infoSpy).toHaveBeenCalledWith('Extensions initialized successfully.');
|
||||
|
||||
infoSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('logs error when initializeExtensions fails', async () => {
|
||||
const loader = ExtensionsLoader.getInstance();
|
||||
const errorSpy = jest.spyOn(logging, 'error').mockImplementation();
|
||||
const fetchError = new Error('Network error');
|
||||
jest.spyOn(SupersetClient, 'get').mockRejectedValue(fetchError);
|
||||
|
||||
await loader.initializeExtensions();
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Error setting up extensions:',
|
||||
fetchError,
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -34,6 +34,8 @@ class ExtensionsLoader {
|
||||
|
||||
private extensionIndex: Map<string, Extension> = new Map();
|
||||
|
||||
private initializationPromise: Promise<void> | null = null;
|
||||
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
private constructor() {
|
||||
// Private constructor for singleton pattern
|
||||
@@ -54,16 +56,27 @@ class ExtensionsLoader {
|
||||
* Initializes extensions by fetching the list from the API and loading each one.
|
||||
* @throws Error if initialization fails.
|
||||
*/
|
||||
public async initializeExtensions(): Promise<void> {
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: '/api/v1/extensions/',
|
||||
});
|
||||
const extensions: Extension[] = response.json.result;
|
||||
await Promise.all(
|
||||
extensions.map(async extension => {
|
||||
await this.initializeExtension(extension);
|
||||
}),
|
||||
);
|
||||
public initializeExtensions(): Promise<void> {
|
||||
if (this.initializationPromise) {
|
||||
return this.initializationPromise;
|
||||
}
|
||||
this.initializationPromise = (async () => {
|
||||
try {
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: '/api/v1/extensions/',
|
||||
});
|
||||
const extensions: Extension[] = response.json.result;
|
||||
await Promise.all(
|
||||
extensions.map(async extension => {
|
||||
await this.initializeExtension(extension);
|
||||
}),
|
||||
);
|
||||
logging.info('Extensions initialized successfully.');
|
||||
} catch (error) {
|
||||
logging.error('Error setting up extensions:', error);
|
||||
}
|
||||
})();
|
||||
return this.initializationPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
import { render, waitFor } from 'spec/helpers/testing-library';
|
||||
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
|
||||
import { logging } from '@apache-superset/core/utils';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import ExtensionsStartup from './ExtensionsStartup';
|
||||
import ExtensionsLoader from './ExtensionsLoader';
|
||||
@@ -192,14 +191,12 @@ test('only initializes once even with multiple renders', async () => {
|
||||
loader.initializeExtensions = originalInitialize;
|
||||
});
|
||||
|
||||
test('initializes ExtensionsLoader and logs success when EnableExtensions feature flag is enabled', async () => {
|
||||
test('initializes ExtensionsLoader when EnableExtensions feature flag is enabled', async () => {
|
||||
// Ensure feature flag is enabled
|
||||
mockIsFeatureEnabled.mockImplementation(
|
||||
(flag: FeatureFlag) => flag === FeatureFlag.EnableExtensions,
|
||||
);
|
||||
|
||||
const infoSpy = jest.spyOn(logging, 'info').mockImplementation();
|
||||
|
||||
// Mock the initializeExtensions method to succeed
|
||||
const originalInitialize = ExtensionsLoader.prototype.initializeExtensions;
|
||||
ExtensionsLoader.prototype.initializeExtensions = jest
|
||||
@@ -220,15 +217,10 @@ test('initializes ExtensionsLoader and logs success when EnableExtensions featur
|
||||
expect(
|
||||
ExtensionsLoader.prototype.initializeExtensions,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
// Verify success message was logged
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
'Extensions initialized successfully.',
|
||||
);
|
||||
});
|
||||
|
||||
// Restore original method
|
||||
ExtensionsLoader.prototype.initializeExtensions = originalInitialize;
|
||||
infoSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('does not initialize ExtensionsLoader when EnableExtensions feature flag is disabled', async () => {
|
||||
@@ -259,38 +251,36 @@ test('does not initialize ExtensionsLoader when EnableExtensions feature flag is
|
||||
initializeSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('logs error when ExtensionsLoader initialization fails', async () => {
|
||||
test('continues rendering children even when ExtensionsLoader initialization fails', async () => {
|
||||
// Ensure feature flag is enabled
|
||||
mockIsFeatureEnabled.mockReturnValue(true);
|
||||
|
||||
const errorSpy = jest.spyOn(logging, 'error').mockImplementation();
|
||||
|
||||
// Mock the initializeExtensions method to throw an error
|
||||
// Mock the initializeExtensions method to reject — ExtensionsLoader handles
|
||||
// its own error logging internally
|
||||
const originalInitialize = ExtensionsLoader.prototype.initializeExtensions;
|
||||
ExtensionsLoader.prototype.initializeExtensions = jest
|
||||
.fn()
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Test initialization error');
|
||||
});
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
render(<ExtensionsStartup />, {
|
||||
useRedux: true,
|
||||
initialState: mockInitialState,
|
||||
});
|
||||
const { container } = render(
|
||||
<ExtensionsStartup>
|
||||
<div data-testid="child" />
|
||||
</ExtensionsStartup>,
|
||||
{
|
||||
useRedux: true,
|
||||
initialState: mockInitialState,
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify feature flag was checked
|
||||
expect(mockIsFeatureEnabled).toHaveBeenCalledWith(
|
||||
FeatureFlag.EnableExtensions,
|
||||
);
|
||||
// Verify error was logged
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Error setting up extensions:',
|
||||
expect.any(Error),
|
||||
);
|
||||
expect(
|
||||
container.querySelector('[data-testid="child"]'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Restore original method
|
||||
ExtensionsLoader.prototype.initializeExtensions = originalInitialize;
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -82,17 +82,7 @@ const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
|
||||
|
||||
const setup = async () => {
|
||||
if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
|
||||
try {
|
||||
await ExtensionsLoader.getInstance().initializeExtensions();
|
||||
supersetCore.utils.logging.info(
|
||||
'Extensions initialized successfully.',
|
||||
);
|
||||
} catch (error) {
|
||||
supersetCore.utils.logging.error(
|
||||
'Error setting up extensions:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
await ExtensionsLoader.getInstance().initializeExtensions();
|
||||
}
|
||||
setInitialized(true);
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ else:
|
||||
from flask import Flask, Response
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from superset.extensions.cache_middleware import ExtensionCacheMiddleware
|
||||
from superset.extensions.local_extensions_watcher import (
|
||||
start_local_extensions_watcher_thread,
|
||||
)
|
||||
@@ -66,7 +67,6 @@ def create_app(
|
||||
or app.config["APPLICATION_ROOT"],
|
||||
)
|
||||
if app_root != "/":
|
||||
app.wsgi_app = AppRootMiddleware(app.wsgi_app, app_root)
|
||||
# If not set, manually configure options that depend on the
|
||||
# value of app_root so things work out of the box
|
||||
if not app.config["STATIC_ASSETS_PREFIX"]:
|
||||
@@ -77,6 +77,13 @@ def create_app(
|
||||
app_initializer = app.config.get("APP_INITIALIZER", SupersetAppInitializer)(app)
|
||||
app_initializer.init_app()
|
||||
|
||||
# Must be applied before AppRootMiddleware so the path prefix
|
||||
# is stripped before the extension asset path regex runs.
|
||||
app.wsgi_app = ExtensionCacheMiddleware(app.wsgi_app)
|
||||
|
||||
if app_root != "/":
|
||||
app.wsgi_app = AppRootMiddleware(app.wsgi_app, app_root)
|
||||
|
||||
# Set up LOCAL_EXTENSIONS file watcher when in debug mode
|
||||
if app.debug:
|
||||
start_local_extensions_watcher_thread(app)
|
||||
|
||||
@@ -27,9 +27,13 @@ from sqlalchemy.exc import MultipleResultsFound
|
||||
from sqlalchemy.sql.visitors import VisitableType
|
||||
|
||||
from superset import db, security_manager
|
||||
from superset.commands.dataset.exceptions import DatasetForbiddenDataURI
|
||||
from superset.commands.dataset.exceptions import (
|
||||
DatasetAccessDeniedError,
|
||||
DatasetForbiddenDataURI,
|
||||
)
|
||||
from superset.commands.exceptions import ImportFailedError
|
||||
from superset.connectors.sqla.models import SqlaTable
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
from superset.models.core import Database
|
||||
from superset.sql.parse import Table
|
||||
from superset.utils import json
|
||||
@@ -172,6 +176,12 @@ def import_dataset( # noqa: C901
|
||||
if dataset.id is None:
|
||||
db.session.flush()
|
||||
|
||||
if not ignore_permissions:
|
||||
try:
|
||||
security_manager.raise_for_access(datasource=dataset)
|
||||
except SupersetSecurityException as ex:
|
||||
raise DatasetAccessDeniedError() from ex
|
||||
|
||||
try:
|
||||
table_exists = dataset.database.has_table(
|
||||
Table(dataset.table_name, dataset.schema, dataset.catalog),
|
||||
|
||||
@@ -58,7 +58,11 @@ class QueryDAO(BaseDAO[Query]):
|
||||
|
||||
@staticmethod
|
||||
def stop_query(client_id: str) -> None:
|
||||
query = db.session.query(Query).filter_by(client_id=client_id).one_or_none()
|
||||
query = (
|
||||
db.session.query(Query)
|
||||
.filter(Query.client_id == client_id, Query.user_id == get_user_id())
|
||||
.one_or_none()
|
||||
)
|
||||
if not query:
|
||||
raise QueryNotFoundException(f"Query with client_id {client_id} not found")
|
||||
|
||||
|
||||
@@ -225,4 +225,9 @@ class ExtensionsRestApi(BaseApi):
|
||||
if not mimetype:
|
||||
mimetype = "application/octet-stream"
|
||||
|
||||
return send_file(BytesIO(chunk), mimetype=mimetype)
|
||||
response = send_file(BytesIO(chunk), mimetype=mimetype)
|
||||
# Chunk filenames include a content hash, so they are immutable.
|
||||
response.cache_control.max_age = 31536000
|
||||
response.cache_control.public = True
|
||||
response.cache_control.immutable = True
|
||||
return response
|
||||
|
||||
73
superset/extensions/cache_middleware.py
Normal file
73
superset/extensions/cache_middleware.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from types import TracebackType
|
||||
from typing import Callable, Iterable, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed.wsgi import StartResponse, WSGIApplication, WSGIEnvironment
|
||||
|
||||
# Matches only the static asset endpoint: /api/v1/extensions/<publisher>/<name>/<file>
|
||||
# Does not match the list (/), get (/<publisher>/<name>), or info (/_info) endpoints.
|
||||
_ASSET_PATH_RE = re.compile(r"^/api/v1/extensions/[^/]+/[^/]+/[^/]+$")
|
||||
|
||||
|
||||
class ExtensionCacheMiddleware:
|
||||
"""Strip 'Cookie' from the Vary header on extension asset responses.
|
||||
|
||||
Flask's session interface appends Vary: Cookie unconditionally after every
|
||||
after_request hook runs, so it cannot be removed at the view layer. This
|
||||
middleware intercepts the WSGI response at the lowest level, after all
|
||||
Flask processing is complete.
|
||||
"""
|
||||
|
||||
def __init__(self, wsgi_app: WSGIApplication) -> None:
|
||||
self.wsgi_app = wsgi_app
|
||||
|
||||
def __call__(
|
||||
self, environ: WSGIEnvironment, start_response: StartResponse
|
||||
) -> Iterable[bytes]:
|
||||
path = environ.get("PATH_INFO", "")
|
||||
if not _ASSET_PATH_RE.match(path):
|
||||
return self.wsgi_app(environ, start_response)
|
||||
|
||||
def patched_start_response(
|
||||
status: str,
|
||||
response_headers: list[tuple[str, str]],
|
||||
exc_info: (
|
||||
tuple[type[BaseException], BaseException, TracebackType]
|
||||
| tuple[None, None, None]
|
||||
| None
|
||||
) = None,
|
||||
) -> Callable[[bytes], object]:
|
||||
new_headers = []
|
||||
for name, value in response_headers:
|
||||
if name.lower() == "vary":
|
||||
parts = [
|
||||
v.strip()
|
||||
for v in value.split(",")
|
||||
if v.strip().lower() != "cookie"
|
||||
]
|
||||
if parts:
|
||||
new_headers.append((name, ", ".join(parts)))
|
||||
else:
|
||||
new_headers.append((name, value))
|
||||
return start_response(status, new_headers, exc_info)
|
||||
|
||||
return self.wsgi_app(environ, patched_start_response)
|
||||
@@ -29,8 +29,7 @@ BLOCKLIST = {
|
||||
# sqlite creates a local DB, which allows mapping server's filesystem
|
||||
re.compile(r"sqlite(?:\+[^\s]*)?$"),
|
||||
# shillelagh allows opening local files (eg, 'SELECT * FROM "csv:///etc/passwd"')
|
||||
re.compile(r"shillelagh$"),
|
||||
re.compile(r"shillelagh\+apsw$"),
|
||||
re.compile(r"shillelagh(?:\+[^\s]*)?$"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ DATABASE_KEYS = [
|
||||
"allow_cvas",
|
||||
"allow_dml",
|
||||
"allow_run_async",
|
||||
"allows_cost_estimate",
|
||||
"allows_subquery",
|
||||
"backend",
|
||||
"database_name",
|
||||
|
||||
@@ -72,11 +72,30 @@ from superset.security.analytics_db_safety import check_sqlalchemy_uri
|
||||
True,
|
||||
"shillelagh cannot be used as a data source for security reasons.",
|
||||
),
|
||||
("shillelagh+:///home/superset/bad.db", False, None),
|
||||
(
|
||||
"shillelagh+:///home/superset/bad.db",
|
||||
True,
|
||||
"shillelagh cannot be used as a data source for security reasons.",
|
||||
),
|
||||
(
|
||||
"shillelagh+something:///home/superset/bad.db",
|
||||
False,
|
||||
None,
|
||||
True,
|
||||
"shillelagh cannot be used as a data source for security reasons.",
|
||||
),
|
||||
(
|
||||
"shillelagh+csv:///etc/passwd",
|
||||
True,
|
||||
"shillelagh cannot be used as a data source for security reasons.",
|
||||
),
|
||||
(
|
||||
"shillelagh+json:///etc/passwd",
|
||||
True,
|
||||
"shillelagh cannot be used as a data source for security reasons.",
|
||||
),
|
||||
(
|
||||
"shillelagh+gsheets:///",
|
||||
True,
|
||||
"shillelagh cannot be used as a data source for security reasons.",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -279,3 +279,49 @@ def test_query_dao_stop_query(
|
||||
QueryDAO.stop_query(query_obj.client_id)
|
||||
query = db.session.query(Query).one()
|
||||
assert query.status == QueryStatus.STOPPED
|
||||
|
||||
|
||||
def test_query_dao_stop_query_wrong_user(
|
||||
mocker: MockerFixture, app: Any, session: Session
|
||||
) -> None:
|
||||
"""A user cannot stop a query that belongs to a different user."""
|
||||
from superset import db
|
||||
from superset.common.db_query_status import QueryStatus
|
||||
from superset.models.core import Database
|
||||
from superset.models.sql_lab import Query
|
||||
|
||||
engine = db.session.get_bind()
|
||||
Query.metadata.create_all(engine) # pylint: disable=no-member
|
||||
|
||||
database = Database(database_name="my_database", sqlalchemy_uri="sqlite://")
|
||||
|
||||
query_obj = Query(
|
||||
client_id="foo",
|
||||
database=database,
|
||||
tab_name="test_tab",
|
||||
sql_editor_id="test_editor_id",
|
||||
sql="select * from bar",
|
||||
select_sql="select * from bar",
|
||||
executed_sql="select * from bar",
|
||||
limit=100,
|
||||
select_as_cta=False,
|
||||
rows=100,
|
||||
error_message="none",
|
||||
results_key="abc",
|
||||
status=QueryStatus.RUNNING,
|
||||
user_id=1,
|
||||
)
|
||||
|
||||
db.session.add(database)
|
||||
db.session.add(query_obj)
|
||||
|
||||
# Simulate a different user (user 2) attempting to stop user 1's query
|
||||
mocker.patch("superset.daos.query.get_user_id", return_value=2)
|
||||
|
||||
from superset.daos.query import QueryDAO
|
||||
|
||||
with pytest.raises(QueryNotFoundException):
|
||||
QueryDAO.stop_query(query_obj.client_id)
|
||||
|
||||
query = db.session.query(Query).one()
|
||||
assert query.status == QueryStatus.RUNNING
|
||||
|
||||
@@ -31,6 +31,7 @@ from sqlalchemy.orm.session import Session
|
||||
|
||||
from superset import db, security_manager
|
||||
from superset.commands.dataset.exceptions import (
|
||||
DatasetAccessDeniedError,
|
||||
DatasetForbiddenDataURI,
|
||||
)
|
||||
from superset.commands.dataset.importers.v1.utils import (
|
||||
@@ -744,6 +745,44 @@ def test_import_dataset_without_owner_permission(
|
||||
mock_can_access.assert_called_with("can_write", "Dataset")
|
||||
|
||||
|
||||
def test_import_dataset_access_check(
|
||||
mocker: MockerFixture,
|
||||
session: Session,
|
||||
) -> None:
|
||||
"""
|
||||
Test that import_dataset raises DatasetAccessDeniedError when the user does not
|
||||
have datasource-level access to the target dataset.
|
||||
"""
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
|
||||
mocker.patch.object(security_manager, "can_access", return_value=True)
|
||||
mocker.patch.object(
|
||||
security_manager,
|
||||
"raise_for_access",
|
||||
side_effect=SupersetSecurityException(
|
||||
SupersetError(
|
||||
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
|
||||
message="User does not have access to this datasource",
|
||||
level=ErrorLevel.ERROR,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
engine = db.session.get_bind()
|
||||
SqlaTable.metadata.create_all(engine) # pylint: disable=no-member
|
||||
|
||||
database = Database(database_name="my_database", sqlalchemy_uri="sqlite://")
|
||||
db.session.add(database)
|
||||
db.session.flush()
|
||||
|
||||
config = copy.deepcopy(dataset_fixture)
|
||||
config["database_id"] = database.id
|
||||
|
||||
with pytest.raises(DatasetAccessDeniedError):
|
||||
import_dataset(config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"allowed_urls, data_uri, expected, exception_class",
|
||||
[
|
||||
|
||||
156
tests/unit_tests/extensions/test_cache_middleware.py
Normal file
156
tests/unit_tests/extensions/test_cache_middleware.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from typing import Any, Callable
|
||||
|
||||
from superset.extensions.cache_middleware import ExtensionCacheMiddleware
|
||||
|
||||
ResponseHeaders = list[tuple[str, str]]
|
||||
|
||||
|
||||
def make_wsgi_app(
|
||||
status: str = "200 OK",
|
||||
headers: ResponseHeaders | None = None,
|
||||
) -> Callable[..., Any]:
|
||||
"""Returns a minimal WSGI app that calls start_response with the given headers."""
|
||||
|
||||
def app(environ, start_response): # noqa: ARG001
|
||||
start_response(status, headers or [])
|
||||
return [b"body"]
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def call_middleware(
|
||||
path: str,
|
||||
upstream_headers: ResponseHeaders,
|
||||
) -> ResponseHeaders:
|
||||
"""Run middleware for a given path, return headers passed to start_response."""
|
||||
captured: list[ResponseHeaders] = []
|
||||
|
||||
def start_response(status, headers, exc_info=None): # noqa: ARG001
|
||||
captured.append(headers)
|
||||
|
||||
wsgi_app = make_wsgi_app(headers=upstream_headers)
|
||||
middleware = ExtensionCacheMiddleware(wsgi_app)
|
||||
environ = {"PATH_INFO": path}
|
||||
list(middleware(environ, start_response))
|
||||
|
||||
return captured[0]
|
||||
|
||||
|
||||
# --- Path matching ---
|
||||
|
||||
|
||||
def test_asset_path_is_intercepted() -> None:
|
||||
headers = call_middleware(
|
||||
"/api/v1/extensions/acme/my-ext/main.js",
|
||||
[("Vary", "Accept-Encoding, Cookie")],
|
||||
)
|
||||
vary = dict(headers).get("Vary", "")
|
||||
assert "Cookie" not in vary
|
||||
|
||||
|
||||
def test_list_endpoint_is_not_intercepted() -> None:
|
||||
upstream = [("Vary", "Accept-Encoding, Cookie")]
|
||||
headers = call_middleware("/api/v1/extensions/", upstream)
|
||||
assert headers == upstream
|
||||
|
||||
|
||||
def test_get_endpoint_is_not_intercepted() -> None:
|
||||
upstream = [("Vary", "Accept-Encoding, Cookie")]
|
||||
headers = call_middleware("/api/v1/extensions/acme/my-ext", upstream)
|
||||
assert headers == upstream
|
||||
|
||||
|
||||
def test_info_endpoint_is_not_intercepted() -> None:
|
||||
upstream = [("Vary", "Accept-Encoding, Cookie")]
|
||||
headers = call_middleware("/api/v1/extensions/_info", upstream)
|
||||
assert headers == upstream
|
||||
|
||||
|
||||
def test_unrelated_path_is_not_intercepted() -> None:
|
||||
upstream = [("Vary", "Accept-Encoding, Cookie")]
|
||||
headers = call_middleware("/api/v1/dashboard/", upstream)
|
||||
assert headers == upstream
|
||||
|
||||
|
||||
# --- Vary stripping logic ---
|
||||
|
||||
|
||||
def test_strips_cookie_from_vary() -> None:
|
||||
headers = call_middleware(
|
||||
"/api/v1/extensions/acme/my-ext/chunk.js",
|
||||
[("Vary", "Accept-Encoding, Cookie")],
|
||||
)
|
||||
assert dict(headers)["Vary"] == "Accept-Encoding"
|
||||
|
||||
|
||||
def test_strips_cookie_case_insensitive() -> None:
|
||||
headers = call_middleware(
|
||||
"/api/v1/extensions/acme/my-ext/chunk.js",
|
||||
[("Vary", "Accept-Encoding, COOKIE")],
|
||||
)
|
||||
assert dict(headers)["Vary"] == "Accept-Encoding"
|
||||
|
||||
|
||||
def test_removes_vary_header_when_cookie_is_only_value() -> None:
|
||||
headers = call_middleware(
|
||||
"/api/v1/extensions/acme/my-ext/chunk.js",
|
||||
[("Vary", "Cookie")],
|
||||
)
|
||||
assert "Vary" not in dict(headers)
|
||||
|
||||
|
||||
def test_multiple_vary_headers_all_stripped() -> None:
|
||||
"""Some middleware stacks emit multiple separate Vary headers."""
|
||||
headers = call_middleware(
|
||||
"/api/v1/extensions/acme/my-ext/chunk.js",
|
||||
[("Vary", "Cookie"), ("Vary", "Accept-Encoding, Cookie")],
|
||||
)
|
||||
vary_values = [v for k, v in headers if k == "Vary"]
|
||||
assert all("Cookie" not in v for v in vary_values)
|
||||
assert vary_values == ["Accept-Encoding"]
|
||||
|
||||
|
||||
def test_non_vary_headers_are_preserved() -> None:
|
||||
headers = call_middleware(
|
||||
"/api/v1/extensions/acme/my-ext/chunk.wasm",
|
||||
[
|
||||
("Content-Type", "application/wasm"),
|
||||
("Cache-Control", "public, max-age=31536000, immutable"),
|
||||
("Vary", "Accept-Encoding, Cookie"),
|
||||
],
|
||||
)
|
||||
d = dict(headers)
|
||||
assert d["Content-Type"] == "application/wasm"
|
||||
assert d["Cache-Control"] == "public, max-age=31536000, immutable"
|
||||
|
||||
|
||||
def test_vary_without_cookie_is_unchanged() -> None:
|
||||
headers = call_middleware(
|
||||
"/api/v1/extensions/acme/my-ext/chunk.js",
|
||||
[("Vary", "Accept-Encoding")],
|
||||
)
|
||||
assert dict(headers)["Vary"] == "Accept-Encoding"
|
||||
|
||||
|
||||
def test_no_vary_header_produces_no_vary() -> None:
|
||||
headers = call_middleware(
|
||||
"/api/v1/extensions/acme/my-ext/chunk.js",
|
||||
[("Content-Type", "application/javascript")],
|
||||
)
|
||||
assert "Vary" not in dict(headers)
|
||||
Reference in New Issue
Block a user