This is an automated email from the ASF dual-hosted git repository.

elizabeth pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new b35b1d7633 fix: add subdirectory deployment support for app icon and 
reports urls (#35098)
b35b1d7633 is described below

commit b35b1d763348725afa8c91295f05a080ceb6e0f9
Author: Elizabeth Thompson <[email protected]>
AuthorDate: Mon Dec 8 16:06:08 2025 -0800

    fix: add subdirectory deployment support for app icon and reports urls 
(#35098)
    
    Co-authored-by: Claude <[email protected]>
    Co-authored-by: Daniel Gaspar <[email protected]>
---
 .../src/SqlLab/components/QueryTable/index.tsx     |   3 +-
 .../src/SqlLab/components/ResultSet/index.tsx      |   4 +-
 .../DatasourceEditor/DatasourceEditor.jsx          |   3 +-
 .../src/explore/components/controls/ViewQuery.tsx  |   5 +-
 .../src/features/databases/DatabaseModal/index.tsx |   3 +-
 superset-frontend/src/features/home/EmptyState.tsx |   3 +-
 superset-frontend/src/features/home/RightMenu.tsx  |   4 +-
 .../src/pages/SavedQueryList/index.tsx             |  15 +-
 superset-frontend/src/utils/pathUtils.test.ts      | 160 +++++++++++++++++++++
 superset-frontend/src/utils/pathUtils.ts           |  16 +++
 superset/app.py                                    |  16 +++
 superset/utils/urls.py                             |  19 ++-
 .../test_subdirectory_deployments.py               | 101 +++++++++++++
 13 files changed, 332 insertions(+), 20 deletions(-)

diff --git a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx 
b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
index a0b03e9ccc..7e08b45cfe 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
@@ -41,6 +41,7 @@ import {
 import { fDuration, extendedDayjs } from '@superset-ui/core/utils/dates';
 import { SqlLabRootState } from 'src/SqlLab/types';
 import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
+import { makeUrl } from 'src/utils/pathUtils';
 import ResultSet from '../ResultSet';
 import HighlightedSql from '../HighlightedSql';
 import { StaticPosition, StyledTooltip } from './styles';
@@ -68,7 +69,7 @@ interface QueryTableProps {
 }
 
 const openQuery = (id: number) => {
-  const url = `/sqllab?queryId=${id}`;
+  const url = makeUrl(`/sqllab?queryId=${id}`);
   window.open(url);
 };
 
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx 
b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index 4df11f4ca0..8198bd2587 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -85,8 +85,8 @@ import { Icons } from '@superset-ui/core/components/Icons';
 import { findPermission } from 'src/utils/findPermission';
 import { StreamingExportModal } from 'src/components/StreamingExportModal';
 import { useStreamingExport } from 
'src/components/StreamingExportModal/useStreamingExport';
-import { ensureAppRoot } from 'src/utils/pathUtils';
 import { useConfirmModal } from 'src/hooks/useConfirmModal';
+import { makeUrl } from 'src/utils/pathUtils';
 import ExploreCtasResultsButton from '../ExploreCtasResultsButton';
 import ExploreResultsButton from '../ExploreResultsButton';
 import HighlightedSql from '../HighlightedSql';
@@ -314,7 +314,7 @@ const ResultSet = ({
   };
 
   const getExportCsvUrl = (clientId: string) =>
-    ensureAppRoot(`/api/v1/sqllab/export/${clientId}/`);
+    makeUrl(`/api/v1/sqllab/export/${clientId}/`);
 
   const handleCloseStreamingModal = () => {
     cancelExport();
diff --git 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.jsx
 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.jsx
index 30d5a82428..56be62634f 100644
--- 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.jsx
+++ 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.jsx
@@ -72,6 +72,7 @@ import {
 } from 'src/database/actions';
 import Mousetrap from 'mousetrap';
 import { clearDatasetCache } from 'src/utils/cachedSupersetGet';
+import { makeUrl } from 'src/utils/pathUtils';
 import { DatabaseSelector } from '../../../DatabaseSelector';
 import CollectionTable from '../CollectionTable';
 import Fieldset from '../Fieldset';
@@ -787,7 +788,7 @@ class DatasourceEditor extends PureComponent {
       autorun: true,
       isDataset: true,
     });
-    return `/sqllab/?${queryParams.toString()}`;
+    return makeUrl(`/sqllab/?${queryParams.toString()}`);
   }
 
   openOnSqlLab() {
diff --git a/superset-frontend/src/explore/components/controls/ViewQuery.tsx 
b/superset-frontend/src/explore/components/controls/ViewQuery.tsx
index 20ef5450e0..86f51d97a7 100644
--- a/superset-frontend/src/explore/components/controls/ViewQuery.tsx
+++ b/superset-frontend/src/explore/components/controls/ViewQuery.tsx
@@ -39,6 +39,7 @@ import {
 import { CopyToClipboard } from 'src/components';
 import { RootState } from 'src/dashboard/types';
 import { findPermission } from 'src/utils/findPermission';
+import { makeUrl } from 'src/utils/pathUtils';
 import CodeSyntaxHighlighter, {
   SupportedLanguage,
   preloadLanguages,
@@ -137,7 +138,9 @@ const ViewQuery: FC<ViewQueryProps> = props => {
       if (domEvent.metaKey || domEvent.ctrlKey) {
         domEvent.preventDefault();
         window.open(
-          
`/sqllab?datasourceKey=${datasource}&sql=${encodeURIComponent(currentSQL)}`,
+          makeUrl(
+            
`/sqllab?datasourceKey=${datasource}&sql=${encodeURIComponent(currentSQL)}`,
+          ),
           '_blank',
         );
       } else {
diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx 
b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
index ee713591de..da3d432dc7 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
@@ -33,6 +33,7 @@ import { CheckboxChangeEvent } from 
'@superset-ui/core/components/Checkbox/types
 
 import { useHistory } from 'react-router-dom';
 import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
+import { makeUrl } from 'src/utils/pathUtils';
 import Tabs from '@superset-ui/core/components/Tabs';
 import {
   Button,
@@ -1736,7 +1737,7 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
         onClick={() => {
           setLoading(true);
           fetchAndSetDB();
-          redirectURL(`/sqllab?db=true`);
+          redirectURL(makeUrl(`/sqllab?db=true`));
         }}
       >
         {t('Query data in SQL Lab')}
diff --git a/superset-frontend/src/features/home/EmptyState.tsx 
b/superset-frontend/src/features/home/EmptyState.tsx
index 34c8ac5857..35a7050fa1 100644
--- a/superset-frontend/src/features/home/EmptyState.tsx
+++ b/superset-frontend/src/features/home/EmptyState.tsx
@@ -24,6 +24,7 @@ import { TableTab } from 'src/views/CRUD/types';
 import { t } from '@superset-ui/core';
 import { styled } from '@apache-superset/core/ui';
 import { navigateTo } from 'src/utils/navigationUtils';
+import { makeUrl } from 'src/utils/pathUtils';
 import { WelcomeTable } from './types';
 
 const EmptyContainer = styled.div`
@@ -58,7 +59,7 @@ const REDIRECTS = {
   create: {
     [WelcomeTable.Charts]: '/chart/add',
     [WelcomeTable.Dashboards]: '/dashboard/new',
-    [WelcomeTable.SavedQueries]: '/sqllab?new=true',
+    [WelcomeTable.SavedQueries]: makeUrl('/sqllab?new=true'),
   },
   viewAll: {
     [WelcomeTable.Charts]: '/chart/list',
diff --git a/superset-frontend/src/features/home/RightMenu.tsx 
b/superset-frontend/src/features/home/RightMenu.tsx
index 6222df3d04..fda352a0ce 100644
--- a/superset-frontend/src/features/home/RightMenu.tsx
+++ b/superset-frontend/src/features/home/RightMenu.tsx
@@ -33,7 +33,7 @@ import {
   TelemetryPixel,
 } from '@superset-ui/core/components';
 import type { ItemType, MenuItem } from '@superset-ui/core/components/Menu';
-import { ensureAppRoot } from 'src/utils/pathUtils';
+import { ensureAppRoot, makeUrl } from 'src/utils/pathUtils';
 import { findPermission } from 'src/utils/findPermission';
 import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
 import {
@@ -201,7 +201,7 @@ const RightMenu = ({
     },
     {
       label: t('SQL query'),
-      url: '/sqllab?new=true',
+      url: makeUrl('/sqllab?new=true'),
       icon: <Icons.SearchOutlined data-test={`menu-item-${t('SQL query')}`} />,
       perm: 'can_sqllab',
       view: 'Superset',
diff --git a/superset-frontend/src/pages/SavedQueryList/index.tsx 
b/superset-frontend/src/pages/SavedQueryList/index.tsx
index a66848c46c..9057ec34b2 100644
--- a/superset-frontend/src/pages/SavedQueryList/index.tsx
+++ b/superset-frontend/src/pages/SavedQueryList/index.tsx
@@ -65,6 +65,7 @@ import copyTextToClipboard from 'src/utils/copy';
 import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
 import SavedQueryPreviewModal from 
'src/features/queries/SavedQueryPreviewModal';
 import { findPermission } from 'src/utils/findPermission';
+import { makeUrl } from 'src/utils/pathUtils';
 
 const PAGE_SIZE = 25;
 const PASSWORDS_NEEDED_MESSAGE = t(
@@ -222,7 +223,7 @@ function SavedQueryList({
     name: t('Query'),
     buttonStyle: 'primary',
     onClick: () => {
-      history.push('/sqllab?new=true');
+      history.push(makeUrl('/sqllab?new=true'));
     },
   });
 
@@ -231,7 +232,9 @@ function SavedQueryList({
   // Action methods
   const openInSqlLab = (id: number, openInNewWindow: boolean) => {
     copyTextToClipboard(() =>
-      Promise.resolve(`${window.location.origin}/sqllab?savedQueryId=${id}`),
+      Promise.resolve(
+        `${window.location.origin}${makeUrl(`/sqllab?savedQueryId=${id}`)}`,
+      ),
     )
       .then(() => {
         addSuccessToast(t('Link Copied!'));
@@ -240,9 +243,9 @@ function SavedQueryList({
         addDangerToast(t('Sorry, your browser does not support copying.'));
       });
     if (openInNewWindow) {
-      window.open(`/sqllab?savedQueryId=${id}`);
+      window.open(makeUrl(`/sqllab?savedQueryId=${id}`));
     } else {
-      history.push(`/sqllab?savedQueryId=${id}`);
+      history.push(makeUrl(`/sqllab?savedQueryId=${id}`));
     }
   };
 
@@ -335,7 +338,9 @@ function SavedQueryList({
           row: {
             original: { id, label },
           },
-        }: any) => <Link to={`/sqllab?savedQueryId=${id}`}>{label}</Link>,
+        }: any) => (
+          <Link to={makeUrl(`/sqllab?savedQueryId=${id}`)}>{label}</Link>
+        ),
         id: 'label',
       },
       {
diff --git a/superset-frontend/src/utils/pathUtils.test.ts 
b/superset-frontend/src/utils/pathUtils.test.ts
new file mode 100644
index 0000000000..bf24053e67
--- /dev/null
+++ b/superset-frontend/src/utils/pathUtils.test.ts
@@ -0,0 +1,160 @@
+/**
+ * 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.
+ */
+
+afterEach(() => {
+  // Clean up the DOM
+  document.body.innerHTML = '';
+  jest.resetModules();
+});
+
+test('ensureAppRoot should add application root prefix to path with default 
root', async () => {
+  document.body.innerHTML = '';
+  jest.resetModules();
+
+  const { ensureAppRoot } = await import('./pathUtils');
+  expect(ensureAppRoot('/sqllab')).toBe('/sqllab');
+  expect(ensureAppRoot('/api/v1/chart')).toBe('/api/v1/chart');
+});
+
+test('ensureAppRoot should add application root prefix to path with custom 
subdirectory', async () => {
+  const customData = {
+    common: {
+      application_root: '/superset/',
+    },
+  };
+  document.body.innerHTML = `<div id="app" 
data-bootstrap='${JSON.stringify(customData)}'></div>`;
+  jest.resetModules();
+
+  // Import getBootstrapData first to initialize the cache
+  await import('./getBootstrapData');
+  const { ensureAppRoot } = await import('./pathUtils');
+
+  expect(ensureAppRoot('/sqllab')).toBe('/superset/sqllab');
+  expect(ensureAppRoot('/api/v1/chart')).toBe('/superset/api/v1/chart');
+});
+
+test('ensureAppRoot should handle paths without leading slash', async () => {
+  const customData = {
+    common: {
+      application_root: '/superset/',
+    },
+  };
+  document.body.innerHTML = `<div id="app" 
data-bootstrap='${JSON.stringify(customData)}'></div>`;
+  jest.resetModules();
+
+  await import('./getBootstrapData');
+  const { ensureAppRoot } = await import('./pathUtils');
+
+  expect(ensureAppRoot('sqllab')).toBe('/superset/sqllab');
+  expect(ensureAppRoot('api/v1/chart')).toBe('/superset/api/v1/chart');
+});
+
+test('ensureAppRoot should handle paths with query strings', async () => {
+  const customData = {
+    common: {
+      application_root: '/superset/',
+    },
+  };
+  document.body.innerHTML = `<div id="app" 
data-bootstrap='${JSON.stringify(customData)}'></div>`;
+  jest.resetModules();
+
+  await import('./getBootstrapData');
+  const { ensureAppRoot } = await import('./pathUtils');
+
+  expect(ensureAppRoot('/sqllab?new=true')).toBe('/superset/sqllab?new=true');
+  expect(ensureAppRoot('/api/v1/chart/export/123/')).toBe(
+    '/superset/api/v1/chart/export/123/',
+  );
+});
+
+test('makeUrl should create URL with default application root', async () => {
+  document.body.innerHTML = '';
+  jest.resetModules();
+
+  const { makeUrl } = await import('./pathUtils');
+  expect(makeUrl('/sqllab')).toBe('/sqllab');
+  expect(makeUrl('/api/v1/chart')).toBe('/api/v1/chart');
+});
+
+test('makeUrl should create URL with subdirectory prefix', async () => {
+  const customData = {
+    common: {
+      application_root: '/superset/',
+    },
+  };
+  document.body.innerHTML = `<div id="app" 
data-bootstrap='${JSON.stringify(customData)}'></div>`;
+  jest.resetModules();
+
+  await import('./getBootstrapData');
+  const { makeUrl } = await import('./pathUtils');
+
+  expect(makeUrl('/sqllab')).toBe('/superset/sqllab');
+  expect(makeUrl('/sqllab?new=true')).toBe('/superset/sqllab?new=true');
+  expect(makeUrl('/api/v1/sqllab/export/123/')).toBe(
+    '/superset/api/v1/sqllab/export/123/',
+  );
+});
+
+test('makeUrl should handle paths without leading slash', async () => {
+  const customData = {
+    common: {
+      application_root: '/superset/',
+    },
+  };
+  document.body.innerHTML = `<div id="app" 
data-bootstrap='${JSON.stringify(customData)}'></div>`;
+  jest.resetModules();
+
+  await import('./getBootstrapData');
+  const { makeUrl } = await import('./pathUtils');
+
+  expect(makeUrl('sqllab?queryId=123')).toBe('/superset/sqllab?queryId=123');
+});
+
+test('makeUrl should work with different subdirectory paths', async () => {
+  const customData = {
+    common: {
+      application_root: '/my-app/superset/',
+    },
+  };
+  document.body.innerHTML = `<div id="app" 
data-bootstrap='${JSON.stringify(customData)}'></div>`;
+  jest.resetModules();
+
+  await import('./getBootstrapData');
+  const { makeUrl } = await import('./pathUtils');
+
+  expect(makeUrl('/sqllab')).toBe('/my-app/superset/sqllab');
+  expect(makeUrl('/dashboard/list')).toBe('/my-app/superset/dashboard/list');
+});
+
+test('makeUrl should handle URLs with anchors', async () => {
+  const customData = {
+    common: {
+      application_root: '/superset/',
+    },
+  };
+  document.body.innerHTML = `<div id="app" 
data-bootstrap='${JSON.stringify(customData)}'></div>`;
+  jest.resetModules();
+
+  await import('./getBootstrapData');
+  const { makeUrl } = await import('./pathUtils');
+
+  expect(makeUrl('/dashboard/123#anchor')).toBe(
+    '/superset/dashboard/123#anchor',
+  );
+});
diff --git a/superset-frontend/src/utils/pathUtils.ts 
b/superset-frontend/src/utils/pathUtils.ts
index db4b94d69b..0e6cac429f 100644
--- a/superset-frontend/src/utils/pathUtils.ts
+++ b/superset-frontend/src/utils/pathUtils.ts
@@ -26,3 +26,19 @@ import { applicationRoot } from 'src/utils/getBootstrapData';
 export function ensureAppRoot(path: string): string {
   return `${applicationRoot()}${path.startsWith('/') ? path : `/${path}`}`;
 }
+
+/**
+ * Creates a URL with the proper application root prefix for subdirectory 
deployments.
+ * Use this when constructing URLs for navigation, API calls, or file 
downloads.
+ *
+ * @param path - The path to convert to a full URL (e.g., '/sqllab', 
'/api/v1/chart/123')
+ * @returns The path prefixed with the application root (e.g., 
'/superset/sqllab')
+ *
+ * @example
+ * // In a subdirectory deployment at /superset
+ * makeUrl('/sqllab?new=true') // returns '/superset/sqllab?new=true'
+ * makeUrl('/api/v1/chart/export/123/') // returns 
'/superset/api/v1/chart/export/123/'
+ */
+export function makeUrl(path: string): string {
+  return ensureAppRoot(path);
+}
diff --git a/superset/app.py b/superset/app.py
index 54f1b79bae..cf89171a57 100644
--- a/superset/app.py
+++ b/superset/app.py
@@ -68,6 +68,22 @@ def create_app(
             # value of app_root so things work out of the box
             if not app.config["STATIC_ASSETS_PREFIX"]:
                 app.config["STATIC_ASSETS_PREFIX"] = app_root
+            # Prefix APP_ICON path with subdirectory root for subdirectory 
deployments
+            if (
+                app.config.get("APP_ICON", "").startswith("/static/")
+                and app_root != "/"
+            ):
+                app.config["APP_ICON"] = f"{app_root}{app.config['APP_ICON']}"
+                # Also update theme tokens for subdirectory deployments
+                for theme_key in ("THEME_DEFAULT", "THEME_DARK"):
+                    theme = app.config[theme_key]
+                    token = theme.get("token", {})
+                    # Update brandLogoUrl if it points to /static/
+                    if token.get("brandLogoUrl", "").startswith("/static/"):
+                        token["brandLogoUrl"] = 
f"{app_root}{token['brandLogoUrl']}"
+                    # Update brandLogoHref if it's the default "/"
+                    if token.get("brandLogoHref") == "/":
+                        token["brandLogoHref"] = app_root
             if app.config["APPLICATION_ROOT"] == "/":
                 app.config["APPLICATION_ROOT"] = app_root
 
diff --git a/superset/utils/urls.py b/superset/utils/urls.py
index 9e672eb944..ef92b7de37 100644
--- a/superset/utils/urls.py
+++ b/superset/utils/urls.py
@@ -15,7 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 import urllib
-from contextlib import nullcontext
 from typing import Any
 from urllib.parse import urlparse
 
@@ -33,13 +32,21 @@ def headless_url(path: str, user_friendly: bool = False) -> 
str:
 
 
 def get_url_path(view: str, user_friendly: bool = False, **kwargs: Any) -> str:
-    if has_request_context():
-        request_context = nullcontext
+    in_request_context = has_request_context()
+
+    # When already in a request context, Flask's url_for respects SCRIPT_NAME 
from
+    # the WSGI environment, so the prefix is already included. Only add 
APPLICATION_ROOT
+    # prefix when creating a new request context.
+    if in_request_context:
+        url = url_for(view, **kwargs)
     else:
-        request_context = app.test_request_context
+        with app.test_request_context():
+            url = url_for(view, **kwargs)
+            app_root = app.config.get("APPLICATION_ROOT", "/")
+            if app_root != "/" and not url.startswith(app_root):
+                url = app_root.rstrip("/") + url
 
-    with request_context():
-        return headless_url(url_for(view, **kwargs), 
user_friendly=user_friendly)
+    return headless_url(url, user_friendly=user_friendly)
 
 
 def modify_url_query(url: str, **kwargs: Any) -> str:
diff --git a/tests/integration_tests/test_subdirectory_deployments.py 
b/tests/integration_tests/test_subdirectory_deployments.py
new file mode 100644
index 0000000000..c767628853
--- /dev/null
+++ b/tests/integration_tests/test_subdirectory_deployments.py
@@ -0,0 +1,101 @@
+# 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.
+"""Tests for subdirectory deployment features."""
+
+from unittest.mock import MagicMock
+
+from werkzeug.test import EnvironBuilder
+
+from superset.app import AppRootMiddleware
+from tests.integration_tests.base_tests import SupersetTestCase
+
+
+class TestSubdirectoryDeployments(SupersetTestCase):
+    """Test subdirectory deployment features including middleware."""
+
+    def setUp(self):
+        super().setUp()
+
+    # AppRootMiddleware tests (core subdirectory deployment functionality)
+
+    def test_app_root_middleware_path_handling(self):
+        """Test middleware correctly handles path prefixes."""
+        # Create a mock WSGI app
+        mock_app = MagicMock()
+        mock_app.return_value = [b"response"]
+
+        middleware = AppRootMiddleware(mock_app, "/superset")
+
+        # Test with correct prefix
+        environ = EnvironBuilder("/superset/dashboard").get_environ()
+        start_response = MagicMock()
+
+        result = list(middleware(environ, start_response))
+
+        # Should call the wrapped app
+        mock_app.assert_called_once()
+        called_environ = mock_app.call_args[0][0]
+
+        # PATH_INFO should be stripped of prefix
+        assert called_environ["PATH_INFO"] == "/dashboard"
+        # SCRIPT_NAME should be set to the prefix
+        assert called_environ["SCRIPT_NAME"] == "/superset"
+        assert result == [b"response"]
+
+    def test_app_root_middleware_wrong_path_returns_404(self):
+        """Test middleware returns 404 for incorrect paths."""
+        # Create a mock WSGI app
+        mock_app = MagicMock()
+
+        middleware = AppRootMiddleware(mock_app, "/superset")
+
+        # Test with incorrect prefix
+        environ = EnvironBuilder("/wrong/path").get_environ()
+        start_response = MagicMock()
+
+        list(middleware(environ, start_response))
+
+        # Should not call the wrapped app
+        mock_app.assert_not_called()
+
+        # Should return 404 response
+        start_response.assert_called_once()
+        status = start_response.call_args[0][0]
+        assert "404" in status
+
+    def test_app_root_middleware_root_path_handling(self):
+        """Test middleware handles root path correctly."""
+        # Create a mock WSGI app
+        mock_app = MagicMock()
+        mock_app.return_value = [b"response"]
+
+        middleware = AppRootMiddleware(mock_app, "/superset")
+
+        # Test with exact prefix path
+        environ = EnvironBuilder("/superset").get_environ()
+        start_response = MagicMock()
+
+        list(middleware(environ, start_response))
+
+        # Should call the wrapped app
+        mock_app.assert_called_once()
+        called_environ = mock_app.call_args[0][0]
+
+        # PATH_INFO should be empty
+        assert called_environ["PATH_INFO"] == ""
+        # SCRIPT_NAME should be set to the prefix
+        assert called_environ["SCRIPT_NAME"] == "/superset"

Reply via email to