Files
superset2/tests/unit_tests/mcp_service/dashboard/test_layout_validation.py
T

402 lines
11 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.
"""Tests for MCP dashboard layout validation."""
from copy import deepcopy
from pathlib import Path
from typing import Any
import yaml
from superset.mcp_service.dashboard.layout_validation import (
validate_dashboard_layout,
)
def _grid_layout() -> dict[str, Any]:
return {
"DASHBOARD_VERSION_KEY": "v2",
"ROOT_ID": {
"children": ["GRID_ID"],
"id": "ROOT_ID",
"type": "ROOT",
},
"GRID_ID": {
"children": ["ROW-1"],
"id": "GRID_ID",
"parents": ["ROOT_ID"],
"type": "GRID",
},
"ROW-1": {
"children": ["CHART-1"],
"id": "ROW-1",
"meta": {},
"parents": ["ROOT_ID", "GRID_ID"],
"type": "ROW",
},
"CHART-1": {
"children": [],
"id": "CHART-1",
"meta": {"chartId": 1},
"parents": ["ROOT_ID", "GRID_ID", "ROW-1"],
"type": "CHART",
},
}
def test_valid_layout() -> None:
assert validate_dashboard_layout(_grid_layout(), {1}) is None
def test_valid_empty_grid() -> None:
layout = _grid_layout()
layout["GRID_ID"]["children"] = []
del layout["ROW-1"]
del layout["CHART-1"]
assert validate_dashboard_layout(layout, set()) is None
def test_accepts_decimal_string_chart_id() -> None:
layout = _grid_layout()
layout["CHART-1"]["meta"]["chartId"] = "1"
assert validate_dashboard_layout(layout, {1}) is None
def test_valid_top_level_tabs_with_reserved_nodes() -> None:
layout = {
"DASHBOARD_VERSION_KEY": "v2",
"ROOT_ID": {
"children": ["TABS-1"],
"id": "ROOT_ID",
"type": "ROOT",
},
"GRID_ID": {
"children": [],
"id": "GRID_ID",
"parents": ["ROOT_ID"],
"type": "GRID",
},
"HEADER_ID": {
"id": "HEADER_ID",
"meta": {"text": "Tabbed dashboard"},
"type": "HEADER",
},
"TABS-1": {
"children": ["TAB-1"],
"id": "TABS-1",
"meta": {},
"parents": ["ROOT_ID"],
"type": "TABS",
},
"TAB-1": {
"children": ["CHART-1"],
"id": "TAB-1",
"meta": {"text": "Overview"},
"parents": ["ROOT_ID", "TABS-1"],
"type": "TAB",
},
"CHART-1": {
"id": "CHART-1",
"meta": {"chartId": 1},
"parents": ["ROOT_ID", "TABS-1", "TAB-1"],
"type": "CHART",
},
}
assert validate_dashboard_layout(layout, {1}) is None
def test_rejects_unreachable_chart() -> None:
layout = _grid_layout()
layout["GRID_ID"]["children"] = []
error = validate_dashboard_layout(layout, {1})
assert error == "Layout component ROW-1 is unreachable from ROOT_ID."
def test_rejects_missing_child_reference() -> None:
layout = _grid_layout()
del layout["ROW-1"]
error = validate_dashboard_layout(layout, {1})
assert error == "Layout references missing component ROW-1."
def test_rejects_cycle() -> None:
layout = _grid_layout()
layout["GRID_ID"]["children"] = []
layout["ROW-1"]["children"] = ["COLUMN-1"]
layout["ROW-1"]["parents"] = ["COLUMN-1"]
layout["COLUMN-1"] = {
"children": ["ROW-1"],
"id": "COLUMN-1",
"meta": {},
"parents": ["ROW-1"],
"type": "COLUMN",
}
error = validate_dashboard_layout(layout, {1})
assert error == "Layout contains a cycle at ROW-1."
def test_accepts_stale_parents_metadata() -> None:
layout = _grid_layout()
layout["CHART-1"]["parents"] = ["ROOT_ID", "GRID_ID"]
assert validate_dashboard_layout(layout, {1}) is None
def test_accepts_missing_parents_metadata() -> None:
layout = _grid_layout()
del layout["GRID_ID"]["parents"]
del layout["CHART-1"]["parents"]
assert validate_dashboard_layout(layout, {1}) is None
def test_accepts_saved_example_layout_with_stale_parents() -> None:
fixture_path = (
Path(__file__).parents[4]
/ "superset"
/ "examples"
/ "video_game_sales"
/ "dashboard.yaml"
)
with fixture_path.open(encoding="utf-8") as fixture:
layout = yaml.safe_load(fixture)["position"]
chart_ids = {
component["meta"]["chartId"]
for component in layout.values()
if isinstance(component, dict) and component.get("type") == "CHART"
}
assert validate_dashboard_layout(layout, chart_ids) is None
def test_rejects_component_in_invalid_parent() -> None:
layout = _grid_layout()
layout["ROW-1"]["type"] = "TABS"
error = validate_dashboard_layout(layout, {1})
assert error == "Layout component CHART-1 cannot be a child of ROW-1."
def test_rejects_unsupported_component_type() -> None:
layout = _grid_layout()
layout["ROW-1"]["type"] = "UNKNOWN"
error = validate_dashboard_layout(layout, {1})
assert error == "Layout component ROW-1 has unsupported type."
def test_rejects_layout_that_hides_associated_chart() -> None:
layout = deepcopy(_grid_layout())
layout["CHART-1"]["meta"]["chartId"] = 2
error = validate_dashboard_layout(layout, {1, 2})
assert error == "Layout would hide dashboard charts: [1]."
def test_rejects_chart_not_associated_with_dashboard() -> None:
error = validate_dashboard_layout(_grid_layout(), set())
assert error == "Layout references charts not associated with the dashboard: [1]."
def test_rejects_empty_root() -> None:
layout = _grid_layout()
layout["ROOT_ID"]["children"] = []
assert validate_dashboard_layout(layout, {1}) == (
"ROOT_ID must contain exactly one GRID or TABS component."
)
def test_rejects_empty_tabs() -> None:
layout = _grid_layout()
layout["ROOT_ID"]["children"] = ["TABS-1"]
layout["TABS-1"] = {
"children": [],
"id": "TABS-1",
"meta": {},
"parents": ["ROOT_ID"],
"type": "TABS",
}
assert validate_dashboard_layout(layout, {1}) == (
"Tabs component TABS-1 must contain at least one tab."
)
def test_rejects_non_component_top_level_value() -> None:
layout = _grid_layout()
layout["BROKEN"] = None
assert validate_dashboard_layout(layout, {1}) == (
"Layout value BROKEN must be a component object."
)
def test_rejects_invalid_version() -> None:
layout = _grid_layout()
layout["DASHBOARD_VERSION_KEY"] = None
assert validate_dashboard_layout(layout, {1}) == (
"DASHBOARD_VERSION_KEY must be the string 'v2'."
)
def test_rejects_missing_renderer_metadata() -> None:
layout = _grid_layout()
del layout["ROW-1"]["meta"]
assert validate_dashboard_layout(layout, {1}) == (
"Layout component ROW-1.meta must be an object."
)
def test_rejects_dynamic_component() -> None:
layout = _grid_layout()
layout["CHART-1"]["type"] = "DYNAMIC"
layout["CHART-1"]["meta"] = {"componentKey": "unknown"}
assert validate_dashboard_layout(layout, set()) == (
"Layout component CHART-1 uses DYNAMIC, which cannot be safely "
"validated by the server."
)
def test_rejects_malformed_string_chart_id() -> None:
layout = _grid_layout()
layout["CHART-1"]["meta"]["chartId"] = "1.0"
assert validate_dashboard_layout(layout, {1}) == (
"Chart component CHART-1 must have a positive integer or "
"decimal-string chartId."
)
def test_rejects_leading_zero_string_chart_id() -> None:
# ``remove_chart_from_dashboard`` cleans json_metadata by ``str(chart_id)``,
# so accepting "001" here would let a chart be detached while stale "001"
# references survive in expanded_slices and timed_refresh_immune_slices.
layout = _grid_layout()
layout["CHART-1"]["meta"]["chartId"] = "001"
assert validate_dashboard_layout(layout, {1}) == (
"Chart component CHART-1 must have a positive integer or "
"decimal-string chartId."
)
def test_rejects_oversized_string_chart_id() -> None:
# Guards CPython's integer string conversion limit: an unbounded int()
# would raise ValueError out of the tool instead of returning an error.
layout = _grid_layout()
layout["CHART-1"]["meta"]["chartId"] = "9" * 10_000
assert validate_dashboard_layout(layout, {1}) == (
"Chart component CHART-1 must have a positive integer or "
"decimal-string chartId."
)
def test_rejects_nesting_beyond_frontend_depth_limit() -> None:
# isValidChild.ts caps COLUMN > ROW at a parent depth of three. Nesting
# ROW > COLUMN > ROW > COLUMN > ROW pushes the innermost COLUMN past it.
layout = _grid_layout()
layout["ROW-1"]["children"] = ["COLUMN-1"]
layout["COLUMN-1"] = {
"children": ["ROW-2"],
"id": "COLUMN-1",
"meta": {},
"type": "COLUMN",
}
layout["ROW-2"] = {
"children": ["COLUMN-2"],
"id": "ROW-2",
"meta": {},
"type": "ROW",
}
layout["COLUMN-2"] = {
"children": ["ROW-3"],
"id": "COLUMN-2",
"meta": {},
"type": "COLUMN",
}
layout["ROW-3"] = {
"children": ["CHART-1"],
"id": "ROW-3",
"meta": {},
"type": "ROW",
}
assert validate_dashboard_layout(layout, {1}) == (
"Layout component ROW-3 is nested too deeply under COLUMN-2."
)
def test_accepts_maximum_supported_nesting_depth() -> None:
# The deepest arrangement isValidChild.ts documents as valid:
# root > grid > row > column > row > chart.
layout = _grid_layout()
layout["ROW-1"]["children"] = ["COLUMN-1"]
layout["COLUMN-1"] = {
"children": ["ROW-2"],
"id": "COLUMN-1",
"meta": {},
"type": "COLUMN",
}
layout["ROW-2"] = {
"children": ["CHART-1"],
"id": "ROW-2",
"meta": {},
"type": "ROW",
}
assert validate_dashboard_layout(layout, {1}) is None
def test_accepts_tabs_without_consuming_depth() -> None:
# TABS and TAB render children at their own depth, so a tab-wrapped row
# must remain valid at the depth its enclosing container already had.
layout = _grid_layout()
layout["GRID_ID"]["children"] = ["TABS-1"]
layout["TABS-1"] = {
"children": ["TAB-1"],
"id": "TABS-1",
"meta": {},
"type": "TABS",
}
layout["TAB-1"] = {
"children": ["ROW-1"],
"id": "TAB-1",
"meta": {},
"type": "TAB",
}
assert validate_dashboard_layout(layout, {1}) is None