Files
superset2/tests/unit_tests/test_subdirectory_url_for.py
Joe Li 28b845ead4 fix(subdirectory): close adversarial-review gaps in helpers, backend models, and FAB links
Adversarial review surfaced six classes of subdirectory-deployment gaps not
covered by the existing TDD scaffold. Each is fixed where it lives, with
pinning tests added beside the change:

Helpers
- navigationUtils: drop `//` from the navigation safety regex so
  `openInNewTab('//evil.com')` can no longer open a cross-origin tab
- pathUtils.stripAppRoot: greedy strip so an upstream `/superset/superset/x`
  payload survives one strip + react-router basename re-prepend
- RedirectWarning.isAllowedScheme: explicit `//` guard; the `new URL(...)`
  catch branch was silently allowing protocol-relative URLs through
- SupersetClientClass.getUrl: implement the runtime appRoot dedupe the
  project memory was already documenting. Flips the contract test from
  pinning the doubled shape under a misleading name to asserting single-
  prefix emission with segment-boundary + bare-root coverage

Frontend literals and sinks
- loggerMiddleware: `/superset/log/` -> `/log/` (matches the live route
  after `Superset.route_base = ""`); updated three test fixtures
- DatasetPanel: raw `window.open(explore_url)` -> `openInNewTab` with null guard
- RedirectWarning: raw `window.location.href = targetUrl` -> `redirect()`
  so the helpers' validation applies

Backend literals and sinks
- Slice.explore_json_url: `/superset/explore_json` -> `/explore_json`
- Database.sql_url: `/superset/sql/<id>/` (route no longer exists) ->
  `/sqllab/?dbid=<id>` (the live SQL Lab deep-link)
- tasks/async_queries.result_url: same `/superset/` strip
- initialization Home menu: hardcoded `href="/superset/welcome/"` ->
  `f"{app_root}/welcome/"` so it works under any application_root

FAB list-view raw HTML
- dashboard_link / slice_link render raw `<a href=...>` strings, which do
  not receive SCRIPT_NAME at render time. Migrated both to `url_for`
  (`Superset.dashboard` / `ExploreView.root`) so subdir deployments emit
  single-prefix hrefs. The model properties themselves keep their
  router-relative shape for frontend callers using ensureAppRoot

Tests
- test_subdirectory_url_for.py grew from 7 to 11 cases pinning
  Slice.explore_json_url, Database.sql_url, dashboard_link, and slice_link
  under SCRIPT_NAME=/superset
- 82 helper Jest tests + 71 touched component tests green; pre-commit clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:39:01 -07:00

233 lines
9.2 KiB
Python

# 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.
"""url_for emission for Superset.* endpoints under subdirectory deployments.
Flask-AppBuilder's BaseView auto-derives `route_base` from the class name,
which used to mount every `@expose` route on `Superset` under `/superset/...`.
Combined with `AppRootMiddleware` stripping `/superset` from `PATH_INFO` and
setting `SCRIPT_NAME=/superset`, werkzeug's `MapAdapter.build` produced
doubled URLs (`/superset/superset/...`) on every `url_for(..., _external=True)`
call into that namespace — and the routes themselves became unreachable at
request time because the in-rule `/superset` prefix no longer matched the
post-strip PATH_INFO.
`Superset.route_base = ""` mounts the routes at the root so the appRoot
applies exactly once (via SCRIPT_NAME / basename). These tests pin both
branches: no SCRIPT_NAME and SCRIPT_NAME=`/superset`.
"""
from flask import url_for
def test_dashboard_permalink_url_has_no_route_prefix_without_script_name(
app_context: None,
) -> None:
"""Under root deployment, the permalink route lives at /dashboard/p/<key>/.
The auto-derived `/superset` prefix on the `Superset` view class is gone;
the route is now mounted at the root path so url_for returns a single,
prefix-free URL.
"""
from flask import current_app
with current_app.test_request_context("/"):
url = url_for("Superset.dashboard_permalink", key="abc123")
assert url == "/dashboard/p/abc123/"
def test_dashboard_permalink_url_carries_single_script_name_prefix(
app_context: None,
) -> None:
"""Under subdirectory deployment, url_for emits exactly one prefix.
AppRootMiddleware sets SCRIPT_NAME=/superset on every inbound request
once APPLICATION_ROOT is configured. url_for prepends SCRIPT_NAME to the
rule, so the emitted URL is `/superset/dashboard/p/<key>/` — a single
prefix, not the previous `/superset/superset/dashboard/p/<key>/`.
"""
from flask import current_app
with current_app.test_request_context(
"/",
environ_overrides={"SCRIPT_NAME": "/superset"},
):
url = url_for("Superset.dashboard_permalink", key="abc123")
assert url == "/superset/dashboard/p/abc123/"
def test_welcome_url_carries_single_script_name_prefix(
app_context: None,
) -> None:
"""Spot-check a second route to confirm the fix is not endpoint-specific.
The `brand.path` regression in the QA findings traced to
`url_for("Superset.welcome", _external=True)` returning the doubled
`/superset/superset/welcome/`. Pinning a single-prefix expectation for
welcome guards against a regression that reintroduces the auto-derived
route_base on the Superset class.
"""
from flask import current_app
with current_app.test_request_context(
"/",
environ_overrides={"SCRIPT_NAME": "/superset"},
):
url = url_for("Superset.welcome")
assert url == "/superset/welcome/"
def test_dashboard_permalink_external_url_is_single_prefixed(
app_context: None,
) -> None:
"""`url_for(..., _external=True)` is the shape the permalink API serves.
Pin the external (scheme://host included) variant explicitly — that is
the value that ends up on the user's clipboard via
`superset/dashboards/permalink/api.py` and must carry one application
root segment, not two.
"""
from flask import current_app
with current_app.test_request_context(
"/",
environ_overrides={"SCRIPT_NAME": "/superset"},
):
url = url_for("Superset.dashboard_permalink", key="abc123", _external=True)
assert url.endswith("/superset/dashboard/p/abc123/")
assert "/superset/superset/" not in url
def test_explore_permalink_url_is_single_prefixed(app_context: None) -> None:
"""ExplorePermalinkView previously hard-coded route_base = "/superset".
The `superset/explore/permalink/api.py` endpoint serves the clipboard URL
via `url_for("ExplorePermalinkView.permalink", _external=True)`. Under
SCRIPT_NAME=/superset that combination doubled to
`/superset/superset/explore/p/<key>/`. Mirroring the Superset.route_base=""
decision pins the single-prefix shape and prevents the doubled emission
from regressing.
"""
from flask import current_app
with current_app.test_request_context(
"/",
environ_overrides={"SCRIPT_NAME": "/superset"},
):
url = url_for("ExplorePermalinkView.permalink", key="abc123", _external=True)
assert url.endswith("/superset/explore/p/abc123/")
assert "/superset/superset/" not in url
def test_tag_views_urls_are_single_prefixed(app_context: None) -> None:
"""TagModelView and TaggedObjectsModelView previously hard-coded
`route_base = "/superset/tags"` / `/superset/all_entities"`.
Mirroring the Superset.route_base="" decision, the rule is now
`/tags/` / `/all_entities/` and `url_for` under SCRIPT_NAME=/superset
must carry the prefix exactly once.
"""
from flask import current_app
with current_app.test_request_context(
"/",
environ_overrides={"SCRIPT_NAME": "/superset"},
):
assert url_for("TagModelView.list") == "/superset/tags/"
assert url_for("TaggedObjectsModelView.list") == "/superset/all_entities/"
def test_dashboard_model_url_has_no_route_prefix() -> None:
"""`Dashboard.url` / `Dashboard.get_url` previously hard-coded
`/superset/dashboard/<id>/` as a string literal.
After `Superset.route_base = ""` the backend rule is `/dashboard/<id>/`,
so the literal was wrong on both root deployments (no matching rule) and
on subdirectory deployments (downstream callers re-applied the app root
and produced doubled paths in DashboardList row hrefs, ultimately leading
to the user-visible discard-edit 404). The method now returns a
prefix-free relative URL that downstream `ensureAppRoot`-aware helpers
can prepend exactly once.
"""
from superset.models.dashboard import Dashboard
assert Dashboard.get_url(11) == "/dashboard/11/"
assert Dashboard.get_url(11, "births") == "/dashboard/births/"
def test_slice_explore_json_url_has_no_route_prefix(app_context: None) -> None:
"""`Slice.explore_json_url` previously baked `/superset/explore_json` into
the path. After `Superset.route_base = ""` the rule is `/explore_json/`,
so the literal pointed at a non-existent route. Pin the prefix-free shape
so downstream `ensureAppRoot` callers prepend the application root once.
"""
from superset.models.slice import Slice
slc = Slice(id=42)
assert slc.explore_json_url.startswith("/explore_json/")
assert "/superset/explore_json" not in slc.explore_json_url
def test_database_sql_url_resolves_to_live_sqllab_route() -> None:
"""`Database.sql_url` previously returned `/superset/sql/<id>/` — a route
that was removed when SQL Lab moved to its own blueprint at `/sqllab/`.
The property now deep-links to SQL Lab via the `dbid` query parameter so
it resolves under any application_root.
"""
from superset.models.core import Database
db = Database(database_name="db", id=7)
assert db.sql_url == "/sqllab/?dbid=7"
def test_dashboard_link_emits_script_name_under_subdirectory(
app_context: None,
) -> None:
"""`dashboard_link` renders raw HTML for the FAB list-view "title" column.
Flask does not auto-apply SCRIPT_NAME to string fragments, so a literal
`/dashboard/<id>/` href routed outside the application_root under
subdirectory deployment. Pin that the rendered href now carries the
SCRIPT_NAME prefix exactly once.
"""
from flask import current_app
from superset.models.dashboard import Dashboard
dash = Dashboard(id=5, slug=None, dashboard_title="t")
with current_app.test_request_context(
"/", environ_overrides={"SCRIPT_NAME": "/superset"}
):
html = str(dash.dashboard_link())
assert 'href="/superset/dashboard/5/"' in html
assert "/superset/superset/" not in html
def test_slice_link_emits_script_name_under_subdirectory(
app_context: None,
) -> None:
"""Mirror of `dashboard_link` for `Slice.slice_link`."""
from flask import current_app
from superset.models.slice import Slice
slc = Slice(id=9, slice_name="chart")
with current_app.test_request_context(
"/", environ_overrides={"SCRIPT_NAME": "/superset"}
):
html = str(slc.slice_link)
assert 'href="/superset/explore/?slice_id=9"' in html
assert "/superset/superset/" not in html