mirror of
https://github.com/apache/superset.git
synced 2026-06-11 18:49:15 +00:00
Compare commits
7 Commits
fix/chart-
...
mcp-chart-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30ebc94b2f | ||
|
|
053f1292a4 | ||
|
|
8b85cdbd4a | ||
|
|
c8d6f09aa9 | ||
|
|
0f65ec35ca | ||
|
|
d694de8856 | ||
|
|
62fae8c493 |
@@ -157,14 +157,31 @@ def validate_chart_dataset(
|
||||
)
|
||||
|
||||
|
||||
def generate_explore_link(dataset_id: int | str, form_data: Dict[str, Any]) -> str:
|
||||
"""Generate an explore link for the given dataset and form data."""
|
||||
def generate_explore_link(
|
||||
dataset_id: int | str,
|
||||
form_data: Dict[str, Any],
|
||||
prefer_permalink: bool = True,
|
||||
) -> str:
|
||||
"""Generate an explore link for the given dataset and form data.
|
||||
|
||||
Prefers a durable explore permalink (DB-backed key-value store, does not
|
||||
expire) over an ephemeral form_data_key (Redis cache, expires in ~24h).
|
||||
Falls back to the form_data_key approach if permalink creation fails, then
|
||||
to a plain dataset URL as a last resort.
|
||||
|
||||
Set ``prefer_permalink=False`` for callers that depend on a ``form_data_key``
|
||||
in the returned URL (e.g. preview flows that extract and re-cache the key);
|
||||
this skips the permalink path and returns an ``/explore/?form_data_key=...``
|
||||
URL directly.
|
||||
"""
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from superset.commands.exceptions import CommandException
|
||||
from superset.commands.explore.form_data.parameters import CommandParameters
|
||||
from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
|
||||
from superset.daos.dataset import DatasetDAO
|
||||
from superset.exceptions import SupersetException
|
||||
from superset.explore.permalink.exceptions import ExplorePermalinkCreateFailedError
|
||||
from superset.mcp_service.commands.create_form_data import (
|
||||
MCPCreateFormDataCommand,
|
||||
)
|
||||
@@ -200,7 +217,27 @@ def generate_explore_link(dataset_id: int | str, form_data: Dict[str, Any]) -> s
|
||||
"datasource": f"{numeric_dataset_id}__table",
|
||||
}
|
||||
|
||||
# Try to create form_data in cache using MCP-specific CreateFormDataCommand
|
||||
# Try durable permalink first (DB-backed key-value store, does not expire).
|
||||
# CreateExplorePermalinkCommand wraps its internal failures (encode/create/
|
||||
# SQLAlchemy errors) into ExplorePermalinkCreateFailedError, so catch only
|
||||
# those expected modes here — letting programming errors (TypeError, etc.)
|
||||
# surface instead of being silently masked by the form_data_key fallback.
|
||||
# Callers that need a form_data_key URL opt out via prefer_permalink=False.
|
||||
if prefer_permalink:
|
||||
try:
|
||||
state = {"formData": form_data_with_datasource}
|
||||
permalink_key = CreateExplorePermalinkCommand(state=state).run()
|
||||
return f"{base_url}/explore/p/{permalink_key}/"
|
||||
except (
|
||||
ExplorePermalinkCreateFailedError,
|
||||
SQLAlchemyError,
|
||||
) as permalink_e:
|
||||
logger.debug(
|
||||
"Permalink generation failed, falling back to form_data_key: %s",
|
||||
permalink_e,
|
||||
)
|
||||
|
||||
# Fall back to ephemeral form_data_key (Redis-backed cache)
|
||||
cmd_params = CommandParameters(
|
||||
datasource_type=DatasourceType.TABLE,
|
||||
datasource_id=numeric_dataset_id,
|
||||
@@ -208,23 +245,18 @@ def generate_explore_link(dataset_id: int | str, form_data: Dict[str, Any]) -> s
|
||||
tab_id=None,
|
||||
form_data=json.dumps(form_data_with_datasource),
|
||||
)
|
||||
|
||||
# Create the form_data cache entry and get the key
|
||||
form_data_key = MCPCreateFormDataCommand(cmd_params).run()
|
||||
|
||||
# Return URL with just the form_data_key
|
||||
return f"{base_url}/explore/?form_data_key={form_data_key}"
|
||||
|
||||
except (
|
||||
CommandException,
|
||||
SupersetException,
|
||||
SQLAlchemyError,
|
||||
KeyError,
|
||||
ValueError,
|
||||
AttributeError,
|
||||
TypeError,
|
||||
) as e:
|
||||
# Fallback to basic explore URL with numeric ID if available
|
||||
# Fallback to basic explore URL with numeric ID if available. Only the
|
||||
# expected failure modes of dataset lookup / form_data creation are caught
|
||||
# here; programming errors propagate to the tool handler so they aren't
|
||||
# silently masked behind a fallback URL.
|
||||
logger.debug("Explore link generation fallback due to: %s", e)
|
||||
if numeric_dataset_id is not None:
|
||||
return (
|
||||
|
||||
@@ -527,7 +527,9 @@ async def generate_chart( # noqa: C901
|
||||
# Generate explore link with cached form_data for preview-only mode
|
||||
from superset.mcp_service.chart.chart_utils import generate_explore_link
|
||||
|
||||
explore_url = generate_explore_link(request.dataset_id, form_data)
|
||||
explore_url = generate_explore_link(
|
||||
request.dataset_id, form_data, prefer_permalink=False
|
||||
)
|
||||
await ctx.debug("Generated explore link: explore_url=%s" % (explore_url,))
|
||||
|
||||
# Extract form_data_key from the explore URL
|
||||
|
||||
@@ -240,8 +240,11 @@ def update_chart_preview( # noqa: C901
|
||||
"api_version": "v1",
|
||||
}
|
||||
|
||||
# Generate new explore link with updated form_data
|
||||
explore_url = generate_explore_link(request.dataset_id, new_form_data)
|
||||
# Generate new explore link with updated form_data. This preview flow
|
||||
# extracts and re-caches the form_data_key, so force that URL shape.
|
||||
explore_url = generate_explore_link(
|
||||
request.dataset_id, new_form_data, prefer_permalink=False
|
||||
)
|
||||
|
||||
# Extract new form_data_key from the explore URL
|
||||
new_form_data_key = extract_form_data_key_from_url(explore_url)
|
||||
|
||||
@@ -30,7 +30,9 @@ from superset_core.mcp.decorators import tool, ToolAnnotations
|
||||
|
||||
from superset.extensions import event_logger
|
||||
from superset.mcp_service.auth import has_dataset_access
|
||||
from superset.mcp_service.chart.chart_helpers import extract_form_data_key_from_url
|
||||
from superset.mcp_service.chart.chart_helpers import (
|
||||
extract_form_data_key_from_url,
|
||||
)
|
||||
from superset.mcp_service.chart.chart_utils import (
|
||||
generate_explore_link as generate_url,
|
||||
get_table_chart_type_label,
|
||||
@@ -40,6 +42,9 @@ from superset.mcp_service.chart.compile import validate_and_compile
|
||||
from superset.mcp_service.chart.schemas import (
|
||||
GenerateExploreLinkRequest,
|
||||
)
|
||||
from superset.mcp_service.utils.url_utils import (
|
||||
extract_permalink_key_from_url,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -135,6 +140,7 @@ async def generate_explore_link(
|
||||
return {
|
||||
"url": "",
|
||||
"form_data": {},
|
||||
"permalink_key": None,
|
||||
"form_data_key": None,
|
||||
"chart_type_label": None,
|
||||
"error": (
|
||||
@@ -154,6 +160,7 @@ async def generate_explore_link(
|
||||
return {
|
||||
"url": "",
|
||||
"form_data": {},
|
||||
"permalink_key": None,
|
||||
"form_data_key": None,
|
||||
"chart_type_label": None,
|
||||
"error": (
|
||||
@@ -178,6 +185,7 @@ async def generate_explore_link(
|
||||
return {
|
||||
"url": default_url,
|
||||
"form_data": {},
|
||||
"permalink_key": None,
|
||||
"form_data_key": None,
|
||||
"chart_type_label": None,
|
||||
"error": None,
|
||||
@@ -209,7 +217,7 @@ async def generate_explore_link(
|
||||
# Add datasource to form_data for consistency with generate_chart
|
||||
# Only set if not already present to avoid overwriting
|
||||
if "datasource" not in form_data:
|
||||
form_data["datasource"] = f"{request.dataset_id}__table"
|
||||
form_data["datasource"] = f"{dataset.id}__table"
|
||||
|
||||
await ctx.debug(
|
||||
"Form data generated with keys: %s, has_viz_type=%s, has_datasource=%s"
|
||||
@@ -248,6 +256,7 @@ async def generate_explore_link(
|
||||
return {
|
||||
"url": "",
|
||||
"form_data": form_data,
|
||||
"permalink_key": None,
|
||||
"form_data_key": None,
|
||||
"chart_type_label": None,
|
||||
"error": error_payload,
|
||||
@@ -262,19 +271,23 @@ async def generate_explore_link(
|
||||
dataset_id=request.dataset_id, form_data=form_data
|
||||
)
|
||||
|
||||
# Extract form_data_key from the explore URL
|
||||
form_data_key = extract_form_data_key_from_url(explore_url)
|
||||
# Extract permalink_key (durable) or fall back to form_data_key (ephemeral)
|
||||
permalink_key = extract_permalink_key_from_url(explore_url)
|
||||
form_data_key = (
|
||||
extract_form_data_key_from_url(explore_url) if not permalink_key else None
|
||||
)
|
||||
|
||||
await ctx.report_progress(4, 4, "URL generation complete")
|
||||
await ctx.info(
|
||||
"Explore link generated successfully: url_length=%s, dataset_id=%s, "
|
||||
"form_data_key=%s"
|
||||
% (len(explore_url or ""), request.dataset_id, form_data_key)
|
||||
"permalink_key=%s, form_data_key=%s"
|
||||
% (len(explore_url or ""), request.dataset_id, permalink_key, form_data_key)
|
||||
)
|
||||
|
||||
return {
|
||||
"url": explore_url,
|
||||
"form_data": form_data,
|
||||
"permalink_key": permalink_key,
|
||||
"form_data_key": form_data_key,
|
||||
"chart_type_label": get_table_chart_type_label(form_data.get("viz_type")),
|
||||
"error": None,
|
||||
@@ -293,6 +306,7 @@ async def generate_explore_link(
|
||||
return {
|
||||
"url": "",
|
||||
"form_data": {},
|
||||
"permalink_key": None,
|
||||
"form_data_key": None,
|
||||
"chart_type_label": None,
|
||||
"error": f"Failed to generate explore link: {str(e)}",
|
||||
|
||||
@@ -57,6 +57,22 @@ def get_superset_base_url() -> str:
|
||||
return default_url
|
||||
|
||||
|
||||
def extract_permalink_key_from_url(url: str | None) -> str | None:
|
||||
"""Extract the permalink key from an explore permalink URL.
|
||||
|
||||
Matches the /explore/p/<key>/ pattern produced by
|
||||
CreateExplorePermalinkCommand. Returns the key, or None if the URL
|
||||
does not follow that pattern.
|
||||
"""
|
||||
if not url:
|
||||
return None
|
||||
path = urlparse(url).path
|
||||
parts = [p for p in path.split("/") if p]
|
||||
if len(parts) >= 3 and parts[-3] == "explore" and parts[-2] == "p":
|
||||
return parts[-1]
|
||||
return None
|
||||
|
||||
|
||||
def get_mcp_service_url() -> str:
|
||||
"""
|
||||
Get the MCP service base URL where screenshot endpoints are served.
|
||||
|
||||
@@ -1176,13 +1176,25 @@ class TestGenerateExploreLink:
|
||||
self, mock_command, mock_get_base_url
|
||||
) -> None:
|
||||
"""Test generate_explore_link creates form_data_key when dataset exists"""
|
||||
from superset.explore.permalink.exceptions import (
|
||||
ExplorePermalinkCreateFailedError,
|
||||
)
|
||||
|
||||
mock_get_base_url.return_value = "http://localhost:9001"
|
||||
mock_command.return_value.run.return_value = "test_form_data_key"
|
||||
|
||||
# Mock dataset exists
|
||||
# Mock dataset exists; force the durable-permalink path to fail so the
|
||||
# function falls back to the ephemeral form_data_key.
|
||||
mock_dataset = type("Dataset", (), {"id": 123})()
|
||||
with patch(
|
||||
"superset.daos.dataset.DatasetDAO.find_by_id", return_value=mock_dataset
|
||||
with (
|
||||
patch(
|
||||
"superset.daos.dataset.DatasetDAO.find_by_id", return_value=mock_dataset
|
||||
),
|
||||
patch(
|
||||
"superset.commands.explore.permalink.create."
|
||||
"CreateExplorePermalinkCommand.run",
|
||||
side_effect=ExplorePermalinkCreateFailedError("permalink unavailable"),
|
||||
),
|
||||
):
|
||||
result = generate_explore_link(123, {"viz_type": "table"})
|
||||
|
||||
@@ -1191,6 +1203,36 @@ class TestGenerateExploreLink:
|
||||
)
|
||||
mock_command.assert_called_once()
|
||||
|
||||
@patch("superset.mcp_service.chart.chart_utils.get_superset_base_url")
|
||||
@patch("superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand")
|
||||
def test_generate_explore_link_prefer_permalink_false(
|
||||
self, mock_command, mock_get_base_url
|
||||
) -> None:
|
||||
"""prefer_permalink=False skips the permalink path and returns a
|
||||
form_data_key URL, so preview callers that re-parse the key keep working."""
|
||||
mock_get_base_url.return_value = "http://localhost:9001"
|
||||
mock_command.return_value.run.return_value = "test_form_data_key"
|
||||
|
||||
mock_dataset = type("Dataset", (), {"id": 123})()
|
||||
with (
|
||||
patch(
|
||||
"superset.daos.dataset.DatasetDAO.find_by_id", return_value=mock_dataset
|
||||
),
|
||||
patch(
|
||||
"superset.commands.explore.permalink.create."
|
||||
"CreateExplorePermalinkCommand.run"
|
||||
) as mock_permalink,
|
||||
):
|
||||
result = generate_explore_link(
|
||||
123, {"viz_type": "table"}, prefer_permalink=False
|
||||
)
|
||||
|
||||
assert (
|
||||
result == "http://localhost:9001/explore/?form_data_key=test_form_data_key"
|
||||
)
|
||||
mock_command.assert_called_once()
|
||||
mock_permalink.assert_not_called()
|
||||
|
||||
@patch("superset.mcp_service.chart.chart_utils.get_superset_base_url")
|
||||
def test_generate_explore_link_exception_handling(self, mock_get_base_url) -> None:
|
||||
"""Test generate_explore_link handles SQLAlchemy exceptions gracefully"""
|
||||
|
||||
@@ -49,6 +49,13 @@ generate_explore_link_module = importlib.import_module(
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PERMALINK_PATCH = (
|
||||
"superset.commands.explore.permalink.create.CreateExplorePermalinkCommand.run"
|
||||
)
|
||||
_FORM_DATA_PATCH = (
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mcp_server():
|
||||
@@ -100,6 +107,17 @@ def mock_webdriver_baseurl(app_context):
|
||||
current_app.config["WEBDRIVER_BASEURL_USER_FRIENDLY"] = original_value
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_permalink_creation():
|
||||
"""Create durable permalink by default.
|
||||
|
||||
Override in individual tests that need fallback-to-form_data_key or
|
||||
fallback-to-basic-URL behaviour by patching _PERMALINK_PATCH to raise.
|
||||
"""
|
||||
with patch(_PERMALINK_PATCH, return_value="test_permalink_key"):
|
||||
yield
|
||||
|
||||
|
||||
def _mock_dataset(id: int = 1) -> Mock:
|
||||
"""Create a mock dataset object with columns and db_engine_spec."""
|
||||
from superset.utils.core import ColumnSpec, GenericDataType
|
||||
@@ -132,15 +150,11 @@ class TestGenerateExploreLink:
|
||||
"""Comprehensive tests for generate_explore_link MCP tool."""
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_table_explore_link_minimal(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Test generating explore link for minimal table chart."""
|
||||
mock_create_form_data.return_value = "test_form_data_key_123"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
|
||||
config = TableChartConfig(
|
||||
@@ -156,21 +170,18 @@ class TestGenerateExploreLink:
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=test_form_data_key_123"
|
||||
== "http://localhost:9001/explore/p/test_permalink_key/"
|
||||
)
|
||||
assert result.data["permalink_key"] == "test_permalink_key"
|
||||
assert result.data["form_data_key"] is None
|
||||
assert result.data["chart_type_label"] == "table chart"
|
||||
mock_create_form_data.assert_called_once()
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_table_explore_link_with_features(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Test generating explore link for table chart with features."""
|
||||
mock_create_form_data.return_value = "comprehensive_key_456"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=5)
|
||||
|
||||
config = TableChartConfig(
|
||||
@@ -196,21 +207,18 @@ class TestGenerateExploreLink:
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=comprehensive_key_456"
|
||||
== "http://localhost:9001/explore/p/test_permalink_key/"
|
||||
)
|
||||
assert result.data["permalink_key"] == "test_permalink_key"
|
||||
assert result.data["form_data_key"] is None
|
||||
assert result.data["chart_type_label"] == "table chart"
|
||||
mock_create_form_data.assert_called_once()
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_ag_grid_table_explore_link_label(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
) -> None:
|
||||
"""Test generating explore link reports AG Grid table label."""
|
||||
mock_create_form_data.return_value = "ag_grid_key_123"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
|
||||
config = TableChartConfig(
|
||||
@@ -229,15 +237,11 @@ class TestGenerateExploreLink:
|
||||
assert result.data["chart_type_label"] == "interactive table chart"
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_line_chart_explore_link(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Test generating explore link for line chart."""
|
||||
mock_create_form_data.return_value = "line_chart_key_789"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=3)
|
||||
|
||||
config = XYChartConfig(
|
||||
@@ -263,21 +267,14 @@ class TestGenerateExploreLink:
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=line_chart_key_789"
|
||||
== "http://localhost:9001/explore/p/test_permalink_key/"
|
||||
)
|
||||
assert result.data["chart_type_label"] is None
|
||||
mock_create_form_data.assert_called_once()
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_bar_chart_explore_link(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
):
|
||||
async def test_generate_bar_chart_explore_link(self, mock_find_dataset, mcp_server):
|
||||
"""Test generating explore link for bar chart."""
|
||||
mock_create_form_data.return_value = "bar_chart_key_abc"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=7)
|
||||
|
||||
config = XYChartConfig(
|
||||
@@ -298,20 +295,15 @@ class TestGenerateExploreLink:
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=bar_chart_key_abc"
|
||||
== "http://localhost:9001/explore/p/test_permalink_key/"
|
||||
)
|
||||
mock_create_form_data.assert_called_once()
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_area_chart_explore_link(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Test generating explore link for area chart."""
|
||||
mock_create_form_data.return_value = "area_chart_key_def"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=2)
|
||||
|
||||
config = XYChartConfig(
|
||||
@@ -335,20 +327,15 @@ class TestGenerateExploreLink:
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=area_chart_key_def"
|
||||
== "http://localhost:9001/explore/p/test_permalink_key/"
|
||||
)
|
||||
mock_create_form_data.assert_called_once()
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_scatter_chart_explore_link(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Test generating explore link for scatter chart."""
|
||||
mock_create_form_data.return_value = "scatter_chart_key_ghi"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=4)
|
||||
|
||||
config = XYChartConfig(
|
||||
@@ -370,22 +357,71 @@ class TestGenerateExploreLink:
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=scatter_chart_key_ghi"
|
||||
== "http://localhost:9001/explore/p/test_permalink_key/"
|
||||
)
|
||||
|
||||
@patch(_PERMALINK_PATCH)
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(_FORM_DATA_PATCH)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_permalink_fails_fallback_to_form_data_key(
|
||||
self,
|
||||
mock_create_form_data,
|
||||
mock_find_dataset,
|
||||
mock_create_permalink,
|
||||
mcp_server,
|
||||
):
|
||||
"""When permalink creation fails, fall back to ephemeral form_data_key URL."""
|
||||
from superset.explore.permalink.exceptions import (
|
||||
ExplorePermalinkCreateFailedError,
|
||||
)
|
||||
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
mock_create_permalink.side_effect = ExplorePermalinkCreateFailedError(
|
||||
"DB unavailable"
|
||||
)
|
||||
mock_create_form_data.return_value = "fallback_form_data_key"
|
||||
|
||||
config = TableChartConfig(
|
||||
chart_type="table", columns=[ColumnRef(name="test_col")]
|
||||
)
|
||||
request = GenerateExploreLinkRequest(dataset_id="1", config=config)
|
||||
|
||||
async with Client(mcp_server) as client:
|
||||
result = await client.call_tool(
|
||||
"generate_explore_link", {"request": request.model_dump()}
|
||||
)
|
||||
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=fallback_form_data_key"
|
||||
)
|
||||
assert result.data["form_data_key"] == "fallback_form_data_key"
|
||||
assert result.data["permalink_key"] is None
|
||||
mock_create_form_data.assert_called_once()
|
||||
|
||||
@patch(_PERMALINK_PATCH)
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@patch(_FORM_DATA_PATCH)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_cache_failure_fallback(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
async def test_generate_explore_link_both_fail_fallback_to_basic_url(
|
||||
self,
|
||||
mock_create_form_data,
|
||||
mock_find_dataset,
|
||||
mock_create_permalink,
|
||||
mcp_server,
|
||||
):
|
||||
"""Test fallback when form_data cache creation fails."""
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
"""When both permalink and form_data_key fail, fall back to basic URL."""
|
||||
from superset.commands.exceptions import CommandException
|
||||
from superset.explore.permalink.exceptions import (
|
||||
ExplorePermalinkCreateFailedError,
|
||||
)
|
||||
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
mock_create_permalink.side_effect = ExplorePermalinkCreateFailedError(
|
||||
"DB unavailable"
|
||||
)
|
||||
mock_create_form_data.side_effect = CommandException("Cache storage failed")
|
||||
|
||||
config = TableChartConfig(
|
||||
@@ -398,28 +434,31 @@ class TestGenerateExploreLink:
|
||||
"generate_explore_link", {"request": request.model_dump()}
|
||||
)
|
||||
|
||||
# Should fallback to basic URL format
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?datasource_type=table&datasource_id=1"
|
||||
)
|
||||
|
||||
@patch(_PERMALINK_PATCH)
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@patch(_FORM_DATA_PATCH)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_database_lock_fallback(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self,
|
||||
mock_create_form_data,
|
||||
mock_find_dataset,
|
||||
mock_create_permalink,
|
||||
mcp_server,
|
||||
):
|
||||
"""Test fallback when database is locked."""
|
||||
"""When permalink fails with SQLAlchemy error, fall back to form_data_key."""
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
mock_find_dataset.return_value = _mock_dataset(id=5)
|
||||
mock_create_form_data.side_effect = OperationalError(
|
||||
mock_create_permalink.side_effect = OperationalError(
|
||||
"database is locked", None, None
|
||||
)
|
||||
mock_create_form_data.return_value = "lock_fallback_key"
|
||||
|
||||
config = XYChartConfig(
|
||||
chart_type="xy",
|
||||
@@ -434,23 +473,18 @@ class TestGenerateExploreLink:
|
||||
"generate_explore_link", {"request": request.model_dump()}
|
||||
)
|
||||
|
||||
# Should fallback to basic dataset URL
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?datasource_type=table&datasource_id=5"
|
||||
== "http://localhost:9001/explore/?form_data_key=lock_fallback_key"
|
||||
)
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_with_many_columns(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Test generating explore link with many columns."""
|
||||
mock_create_form_data.return_value = "many_columns_key"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
|
||||
# Create 15 columns
|
||||
@@ -474,20 +508,15 @@ class TestGenerateExploreLink:
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=many_columns_key"
|
||||
== "http://localhost:9001/explore/p/test_permalink_key/"
|
||||
)
|
||||
mock_create_form_data.assert_called_once()
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_with_many_filters(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Test generating explore link with many filters."""
|
||||
mock_create_form_data.return_value = "many_filters_key"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
|
||||
# Create 12 filters
|
||||
@@ -517,20 +546,15 @@ class TestGenerateExploreLink:
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=many_filters_key"
|
||||
== "http://localhost:9001/explore/p/test_permalink_key/"
|
||||
)
|
||||
mock_create_form_data.assert_called_once()
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_explore_link_url_format_consistency(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Test that all generated URLs follow consistent format."""
|
||||
mock_create_form_data.return_value = "consistency_test_key"
|
||||
"""Test that all generated URLs follow consistent permalink format."""
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
|
||||
configs = [
|
||||
@@ -569,23 +593,19 @@ class TestGenerateExploreLink:
|
||||
"generate_explore_link", {"request": request.model_dump()}
|
||||
)
|
||||
|
||||
# All URLs should follow the same format
|
||||
# All URLs should follow the same permalink format
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=consistency_test_key"
|
||||
== "http://localhost:9001/explore/p/test_permalink_key/"
|
||||
)
|
||||
assert result.data["error"] is None
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_dataset_id_types(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Test explore link generation with different dataset_id formats."""
|
||||
mock_create_form_data.return_value = "dataset_test_key"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
|
||||
config = TableChartConfig(
|
||||
@@ -604,19 +624,15 @@ class TestGenerateExploreLink:
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=dataset_test_key"
|
||||
== "http://localhost:9001/explore/p/test_permalink_key/"
|
||||
)
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_complex_configuration(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Test explore link generation with complex chart configuration."""
|
||||
mock_create_form_data.return_value = "complex_config_key"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=10)
|
||||
|
||||
config = XYChartConfig(
|
||||
@@ -648,22 +664,30 @@ class TestGenerateExploreLink:
|
||||
assert result.data["error"] is None
|
||||
assert (
|
||||
result.data["url"]
|
||||
== "http://localhost:9001/explore/?form_data_key=complex_config_key"
|
||||
== "http://localhost:9001/explore/p/test_permalink_key/"
|
||||
)
|
||||
mock_create_form_data.assert_called_once()
|
||||
|
||||
@patch(_PERMALINK_PATCH)
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@patch(_FORM_DATA_PATCH)
|
||||
@pytest.mark.asyncio
|
||||
async def test_fallback_url_different_datasets(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self,
|
||||
mock_create_form_data,
|
||||
mock_find_dataset,
|
||||
mock_create_permalink,
|
||||
mcp_server,
|
||||
):
|
||||
"""Test fallback URLs are correct for different dataset IDs."""
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
"""When both fallbacks fail, basic URL uses the correct dataset_id."""
|
||||
from superset.commands.exceptions import CommandException
|
||||
from superset.explore.permalink.exceptions import (
|
||||
ExplorePermalinkCreateFailedError,
|
||||
)
|
||||
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
mock_create_permalink.side_effect = ExplorePermalinkCreateFailedError(
|
||||
"Always fail for fallback testing"
|
||||
)
|
||||
mock_create_form_data.side_effect = CommandException(
|
||||
"Always fail for fallback testing"
|
||||
)
|
||||
@@ -680,7 +704,10 @@ class TestGenerateExploreLink:
|
||||
)
|
||||
|
||||
# Should fallback to basic URL with correct dataset_id
|
||||
expected_url = f"http://localhost:9001/explore/?datasource_type=table&datasource_id={dataset_id}"
|
||||
expected_url = (
|
||||
f"http://localhost:9001/explore/?datasource_type=table"
|
||||
f"&datasource_id={dataset_id}"
|
||||
)
|
||||
assert result.data["error"] is None
|
||||
assert result.data["url"] == expected_url
|
||||
|
||||
@@ -720,22 +747,21 @@ class TestGenerateExploreLink:
|
||||
assert result.data["url"] == ""
|
||||
assert result.data["form_data"] == {}
|
||||
assert result.data["form_data_key"] is None
|
||||
assert result.data["permalink_key"] is None
|
||||
assert result.data["chart_type_label"] is None
|
||||
assert "Invalid config structure" in result.data["error"]
|
||||
finally:
|
||||
# Restore original function
|
||||
explore_module.map_config_to_form_data = original_func
|
||||
|
||||
@patch(_PERMALINK_PATCH)
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_returns_form_data_key(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
async def test_generate_explore_link_returns_permalink_key(
|
||||
self, mock_find_dataset, mock_create_permalink, mcp_server
|
||||
):
|
||||
"""Test that form_data_key is properly extracted from URL."""
|
||||
mock_create_form_data.return_value = "extracted_form_key_xyz"
|
||||
"""Test that permalink_key is properly extracted from the durable URL."""
|
||||
mock_create_permalink.return_value = "extracted_permalink_xyz"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
|
||||
config = TableChartConfig(
|
||||
@@ -749,19 +775,19 @@ class TestGenerateExploreLink:
|
||||
)
|
||||
|
||||
assert result.data["error"] is None
|
||||
assert result.data["form_data_key"] == "extracted_form_key_xyz"
|
||||
assert "form_data_key=extracted_form_key_xyz" in result.data["url"]
|
||||
assert result.data["permalink_key"] == "extracted_permalink_xyz"
|
||||
assert result.data["form_data_key"] is None
|
||||
assert "extracted_permalink_xyz" in result.data["url"]
|
||||
assert result.data["url"] == (
|
||||
"http://localhost:9001/explore/p/extracted_permalink_xyz/"
|
||||
)
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_explore_link_returns_form_data(
|
||||
self, mock_create_form_data, mock_find_dataset, mcp_server
|
||||
self, mock_find_dataset, mcp_server
|
||||
):
|
||||
"""Test that form_data dict is returned for external rendering."""
|
||||
mock_create_form_data.return_value = "form_data_test_key"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=1)
|
||||
|
||||
config = XYChartConfig(
|
||||
@@ -806,6 +832,7 @@ class TestGenerateExploreLink:
|
||||
assert result.data["url"] == ""
|
||||
assert result.data["form_data"] == {}
|
||||
assert result.data["form_data_key"] is None
|
||||
assert result.data["permalink_key"] is None
|
||||
assert result.data["chart_type_label"] is None
|
||||
assert "Dataset not found: 99999" in result.data["error"]
|
||||
assert "list_datasets" in result.data["error"]
|
||||
@@ -833,6 +860,7 @@ class TestGenerateExploreLink:
|
||||
)
|
||||
assert result.data["form_data"] == {}
|
||||
assert result.data["form_data_key"] is None
|
||||
assert result.data["permalink_key"] is None
|
||||
assert result.data["chart_type_label"] is None
|
||||
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@@ -853,6 +881,7 @@ class TestGenerateExploreLink:
|
||||
assert result.data["url"] == ""
|
||||
assert result.data["form_data"] == {}
|
||||
assert result.data["form_data_key"] is None
|
||||
assert result.data["permalink_key"] is None
|
||||
assert result.data["chart_type_label"] is None
|
||||
assert "Dataset not found: 99999" in result.data["error"]
|
||||
|
||||
@@ -879,6 +908,7 @@ class TestGenerateExploreLink:
|
||||
assert result.data["url"] == ""
|
||||
assert result.data["form_data"] == {}
|
||||
assert result.data["form_data_key"] is None
|
||||
assert result.data["permalink_key"] is None
|
||||
assert result.data["chart_type_label"] is None
|
||||
assert "Dataset not found" in result.data["error"]
|
||||
|
||||
@@ -895,19 +925,14 @@ class TestGenerateExploreLinkColumnNormalization:
|
||||
"superset.mcp_service.chart.validation.dataset_validator.DatasetValidator._get_dataset_context"
|
||||
)
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_xy_chart_x_axis_normalized_in_form_data(
|
||||
self,
|
||||
mock_create_form_data,
|
||||
mock_find_dataset,
|
||||
mock_get_context,
|
||||
mcp_server,
|
||||
):
|
||||
"""x-axis column name in wrong case is normalized in form_data."""
|
||||
mock_create_form_data.return_value = "norm_test_key_1"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=18)
|
||||
mock_get_context.return_value = DatasetContext(
|
||||
id=18,
|
||||
@@ -942,19 +967,14 @@ class TestGenerateExploreLinkColumnNormalization:
|
||||
"superset.mcp_service.chart.validation.dataset_validator.DatasetValidator._get_dataset_context"
|
||||
)
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_column_normalized_in_form_data(
|
||||
self,
|
||||
mock_create_form_data,
|
||||
mock_find_dataset,
|
||||
mock_get_context,
|
||||
mcp_server,
|
||||
):
|
||||
"""Filter column name in wrong case is normalized in adhoc_filters."""
|
||||
mock_create_form_data.return_value = "norm_test_key_2"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=18)
|
||||
mock_get_context.return_value = DatasetContext(
|
||||
id=18,
|
||||
@@ -1001,19 +1021,14 @@ class TestGenerateExploreLinkColumnNormalization:
|
||||
"superset.mcp_service.chart.validation.dataset_validator.DatasetValidator._get_dataset_context"
|
||||
)
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_normalization_fallback_when_dataset_not_found(
|
||||
self,
|
||||
mock_create_form_data,
|
||||
mock_find_dataset,
|
||||
mock_get_context,
|
||||
mcp_server,
|
||||
):
|
||||
"""When dataset context is unavailable, original names pass through."""
|
||||
mock_create_form_data.return_value = "norm_test_key_3"
|
||||
mock_find_dataset.return_value = _mock_dataset(id=99)
|
||||
mock_get_context.return_value = None
|
||||
|
||||
@@ -1046,21 +1061,19 @@ class TestGenerateExploreLinkValidation:
|
||||
"""
|
||||
return
|
||||
|
||||
@patch(_PERMALINK_PATCH)
|
||||
@patch.object(generate_explore_link_module, "validate_and_compile")
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_validation_failure_returns_structured_error(
|
||||
self,
|
||||
mock_create_form_data,
|
||||
mock_find_dataset,
|
||||
mock_validate,
|
||||
mock_create_permalink,
|
||||
mcp_server,
|
||||
):
|
||||
"""Non-existent column → structured ChartGenerationError with suggestions,
|
||||
and MCPCreateFormDataCommand must NOT be called (no cache write)."""
|
||||
and CreateExplorePermalinkCommand must NOT be called (no cache write)."""
|
||||
from superset.mcp_service.chart.compile import CompileResult
|
||||
from superset.mcp_service.common.error_schemas import ChartGenerationError
|
||||
|
||||
@@ -1094,29 +1107,28 @@ class TestGenerateExploreLinkValidation:
|
||||
|
||||
assert result.data["url"] == ""
|
||||
assert result.data["form_data_key"] is None
|
||||
assert result.data["permalink_key"] is None
|
||||
assert result.data["chart_type_label"] is None
|
||||
error = result.data["error"]
|
||||
assert isinstance(error, dict)
|
||||
assert error["error_code"] == "CHART_VALIDATION_FAILED"
|
||||
assert "sum_boys" in error["suggestions"]
|
||||
mock_create_form_data.assert_not_called()
|
||||
mock_create_permalink.assert_not_called()
|
||||
|
||||
@patch(_PERMALINK_PATCH)
|
||||
@patch.object(
|
||||
generate_explore_link_module, "has_dataset_access", return_value=False
|
||||
)
|
||||
@patch("superset.daos.dataset.DatasetDAO.find_by_id")
|
||||
@patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand.run"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_dataset_access_denied_short_circuits(
|
||||
self,
|
||||
mock_create_form_data,
|
||||
mock_find_dataset,
|
||||
unused_access_mock,
|
||||
mock_create_permalink,
|
||||
mcp_server,
|
||||
):
|
||||
"""has_dataset_access=False blocks the tool before any cache write."""
|
||||
"""has_dataset_access=False blocks the tool before any permalink write."""
|
||||
mock_find_dataset.return_value = _mock_dataset(id=3)
|
||||
|
||||
config = TableChartConfig(
|
||||
@@ -1133,4 +1145,4 @@ class TestGenerateExploreLinkValidation:
|
||||
assert result.data["chart_type_label"] is None
|
||||
# Surface as "not found" rather than leaking that the dataset exists.
|
||||
assert "Dataset not found" in result.data["error"]
|
||||
mock_create_form_data.assert_not_called()
|
||||
mock_create_permalink.assert_not_called()
|
||||
|
||||
46
tests/unit_tests/mcp_service/test_url_utils.py
Normal file
46
tests/unit_tests/mcp_service/test_url_utils.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# 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 superset.mcp_service.utils.url_utils import extract_permalink_key_from_url
|
||||
|
||||
|
||||
def test_extract_permalink_key_from_url_with_trailing_slash():
|
||||
url = "http://localhost:8088/explore/p/abc123/"
|
||||
assert extract_permalink_key_from_url(url) == "abc123"
|
||||
|
||||
|
||||
def test_extract_permalink_key_from_url_without_trailing_slash():
|
||||
url = "http://localhost:8088/explore/p/abc123"
|
||||
assert extract_permalink_key_from_url(url) == "abc123"
|
||||
|
||||
|
||||
def test_extract_permalink_key_from_url_no_match():
|
||||
url = "http://localhost:8088/explore/?form_data_key=abc123"
|
||||
assert extract_permalink_key_from_url(url) is None
|
||||
|
||||
|
||||
def test_extract_permalink_key_from_url_none():
|
||||
assert extract_permalink_key_from_url(None) is None
|
||||
|
||||
|
||||
def test_extract_permalink_key_from_url_empty():
|
||||
assert extract_permalink_key_from_url("") is None
|
||||
|
||||
|
||||
def test_extract_permalink_key_from_url_with_path_prefix():
|
||||
url = "https://example.com/superset/explore/p/xyz789/"
|
||||
assert extract_permalink_key_from_url(url) == "xyz789"
|
||||
Reference in New Issue
Block a user