Files
superset2/tests/unit_tests/commands/dashboard/export_test.py
2026-06-22 03:50:29 -07:00

819 lines
30 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.
from __future__ import annotations
import uuid
from typing import Any
from unittest.mock import MagicMock, patch
import yaml
from superset.utils import json
def _make_mock_dashboard(json_metadata: dict[str, Any]) -> MagicMock:
dashboard = MagicMock()
dashboard.dashboard_title = "Test Dashboard"
dashboard.theme = None
dashboard.slices = []
dashboard.tags = []
dashboard.export_to_dict.return_value = {
"position_json": json.dumps(
{
"DASHBOARD_VERSION_KEY": "v2",
"ROOT_ID": {"children": ["GRID_ID"], "id": "ROOT_ID", "type": "ROOT"},
"GRID_ID": {
"children": [],
"id": "GRID_ID",
"parents": ["ROOT_ID"],
"type": "GRID",
},
"HEADER_ID": {
"id": "HEADER_ID",
"meta": {"text": "Test Dashboard"},
"type": "HEADER",
},
}
),
"json_metadata": json.dumps(json_metadata),
}
return dashboard
def test_file_content_replaces_dataset_id_with_uuid_in_display_controls():
"""
_file_content must replace datasetId with datasetUuid in chart_customization_config
targets, mirroring what it already does for native_filter_configuration.
"""
from superset.commands.dashboard.export import ExportDashboardsCommand
dataset_uuid = str(uuid.uuid4())
mock_dashboard = _make_mock_dashboard(
{
"native_filter_configuration": [],
"chart_customization_config": [
{
"id": "CUSTOMIZATION-abc",
"type": "CHART_CUSTOMIZATION",
"targets": [{"datasetId": 99, "column": {"name": "col"}}],
},
{
"id": "CUSTOMIZATION-divider",
"type": "CHART_CUSTOMIZATION_DIVIDER",
"targets": [],
},
],
}
)
mock_dataset = MagicMock()
mock_dataset.uuid = dataset_uuid
with (
patch(
"superset.commands.dashboard.export.DatasetDAO.find_by_id",
return_value=mock_dataset,
),
patch(
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
return_value=False,
),
):
content = ExportDashboardsCommand._file_content(mock_dashboard)
result = yaml.safe_load(content)
customizations = result["metadata"]["chart_customization_config"]
# datasetUuid must be added; datasetId preserved for backward compat
target = customizations[0]["targets"][0]
assert target["datasetUuid"] == dataset_uuid
assert target["datasetId"] == 99
# Dividers with no targets must be unaffected
assert customizations[1]["targets"] == []
def test_export_yields_dataset_files_for_display_controls():
"""
_export must yield dataset files for datasets referenced by display controls.
The _export generator has a second pass over json_metadata (separate from
_file_content) whose job is to emit dataset YAML files into the bundle.
Without this, the round-trip fails: the UUID is in the dashboard YAML but
the dataset file is absent from the ZIP.
"""
from superset.commands.dashboard.export import ExportDashboardsCommand
dataset_id = 42
mock_dashboard = _make_mock_dashboard(
{
"native_filter_configuration": [],
"chart_customization_config": [
{
"id": "CUSTOMIZATION-abc",
"type": "CHART_CUSTOMIZATION",
"targets": [{"datasetId": dataset_id}],
},
],
}
)
mock_dataset = MagicMock()
sentinel_file = ("datasets/my_dataset.yaml", lambda: "dataset_content")
mock_datasets_cmd = MagicMock()
mock_datasets_cmd.run.return_value = iter([sentinel_file])
with (
patch(
"superset.commands.dashboard.export.DatasetDAO.find_by_id",
return_value=mock_dataset,
),
patch(
"superset.commands.dashboard.export.ExportDatasetsCommand",
return_value=mock_datasets_cmd,
) as mock_datasets_cls,
patch(
"superset.commands.dashboard.export.ExportChartsCommand"
) as mock_charts_cls,
patch(
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
return_value=False,
),
):
mock_charts_cls.return_value.run.return_value = iter([])
results = list(ExportDashboardsCommand._export(mock_dashboard))
mock_datasets_cls.assert_called_once_with([dataset_id])
mock_datasets_cmd.run.assert_called_once()
filenames = [name for name, _ in results]
assert "datasets/my_dataset.yaml" in filenames
def test_file_content_null_chart_customization_config_does_not_raise():
"""
When chart_customization_config is explicitly null in metadata,
_file_content must not raise — the `or []` guard handles it.
"""
from superset.commands.dashboard.export import ExportDashboardsCommand
mock_dashboard = _make_mock_dashboard(
{
"native_filter_configuration": [],
"chart_customization_config": None,
}
)
with patch(
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
return_value=False,
):
content = ExportDashboardsCommand._file_content(mock_dashboard)
result = yaml.safe_load(content)
assert result["metadata"]["chart_customization_config"] is None
def test_file_content_includes_roles_for_dashboard_with_role_restrictions():
"""
Regression guard for #21000: dashboards restricted via DASHBOARD_RBAC must
have their role assignments included in the exported YAML. Without this,
importing the bundle into another environment recreates the dashboard with
no role restriction — silently turning a restricted dashboard into a
publicly accessible one.
The export bundle is the canonical source of truth for migrating
dashboards across environments; dropping roles silently is a security
regression (a "least privilege" dashboard becomes "all privileges" on
import). The user, not the export pipeline, should decide whether to
strip roles before sharing a bundle.
We assert against role *names* rather than IDs because role IDs are
environment-local; the import side resolves names back to the destination
environment's roles.
"""
from superset.commands.dashboard.export import ExportDashboardsCommand
role_alpha = MagicMock()
role_alpha.name = "Finance"
role_beta = MagicMock()
role_beta.name = "Executives"
mock_dashboard = _make_mock_dashboard({"native_filter_configuration": []})
mock_dashboard.roles = [role_alpha, role_beta]
with patch(
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
return_value=False,
):
content = ExportDashboardsCommand._file_content(mock_dashboard)
result = yaml.safe_load(content)
assert "roles" in result, (
"Dashboard export must include role names; without them, importing "
"into a fresh environment loses the role-based access restriction "
"and the dashboard becomes accessible to all roles by default."
)
assert sorted(result["roles"]) == ["Executives", "Finance"]
def test_file_content_omits_roles_field_when_dashboard_has_no_roles():
"""
A dashboard with no role restrictions must not emit an empty ``roles: []``
key. Older bundles in the wild were written without the key at all, and
the import side treats "missing" as "no restriction"; emitting an empty
list could trip importers that distinguish the two states.
"""
from superset.commands.dashboard.export import ExportDashboardsCommand
mock_dashboard = _make_mock_dashboard({"native_filter_configuration": []})
mock_dashboard.roles = []
with patch(
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
return_value=False,
):
content = ExportDashboardsCommand._file_content(mock_dashboard)
result = yaml.safe_load(content)
# Strict: the key must be absent (not an empty list). The import side
# treats "missing" as "no restriction"; emitting an empty list could
# trip importers that distinguish the two states.
assert "roles" not in result
def test_position_json_chart_id_is_stable_across_environments() -> None:
"""
Regression for #32972: dashboard export must produce stable output that does
not vary with env-local integer chartIds.
The export format includes a ``meta.chartId`` field inside each ``CHART-*``
position entry. Historically that integer was the database auto-increment
primary key from the source environment. When a bundle is imported into a
different environment the importer (``update_id_refs``) rewrites those IDs to
the destination-env primary keys, so a second export from the destination
would serialize the new env-local integers — the same logical chart produced
different ``chartId`` values in each environment.
The exporter now derives ``meta.chartId`` from the (stable) ``meta.uuid``
instead, so this test asserts that two exports of the same logical dashboard
agree on ``chartId`` even when the underlying chart has a different integer
primary key in each environment.
"""
from superset.commands.dashboard.export import ExportDashboardsCommand
chart_uuid = "812bc377-ac09-475a-8d34-a63f7f087bd7"
# ------------------------------------------------------------------ #
# Source environment: chart has auto-increment id = 392 #
# ------------------------------------------------------------------ #
source_position = {
"DASHBOARD_VERSION_KEY": "v2",
"ROOT_ID": {"children": ["GRID_ID"], "id": "ROOT_ID", "type": "ROOT"},
"GRID_ID": {
"children": ["CHART-srcAAA"],
"id": "GRID_ID",
"parents": ["ROOT_ID"],
"type": "GRID",
},
"HEADER_ID": {
"id": "HEADER_ID",
"meta": {"text": "My Dashboard"},
"type": "HEADER",
},
"CHART-srcAAA": {
"children": [],
"id": "CHART-srcAAA",
"meta": {
"chartId": 392, # source-env integer primary key
"height": 20,
"sliceName": "My Wonderful Chart",
"uuid": chart_uuid,
"width": 4,
},
"parents": ["ROOT_ID", "GRID_ID"],
"type": "CHART",
},
}
src_dashboard = MagicMock()
src_dashboard.dashboard_title = "My Dashboard"
src_dashboard.theme = None
src_dashboard.slices = []
src_dashboard.tags = []
src_dashboard.roles = []
src_dashboard.export_to_dict.return_value = {
"position_json": json.dumps(source_position),
"json_metadata": json.dumps({"native_filter_configuration": []}),
}
with patch(
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
return_value=False,
):
first_export_content = ExportDashboardsCommand._file_content(src_dashboard)
first_export = yaml.safe_load(first_export_content)
first_chart_positions = [
node
for node in first_export["position"].values()
if isinstance(node, dict) and node.get("type") == "CHART"
]
assert first_chart_positions, "Export must contain at least one CHART position node"
first_chart_meta = first_chart_positions[0]["meta"]
# uuid must be present — it is the stable cross-environment identifier
assert first_chart_meta.get("uuid") == chart_uuid, (
"meta.uuid must be present in exported position_json; "
"it is the stable identifier that survives re-import."
)
# ------------------------------------------------------------------ #
# Destination environment: same chart receives a different id = 1001 #
# (This is what happens after import — update_id_refs rewrites the #
# chartId field inside position_json to the destination-env integer.) #
# ------------------------------------------------------------------ #
dest_position = {
k: (
{
**v,
"meta": {
**v["meta"],
"chartId": 1001, # destination-env integer primary key
},
}
if isinstance(v, dict)
and v.get("type") == "CHART"
and v.get("meta", {}).get("uuid") == chart_uuid
else v
)
for k, v in source_position.items()
}
dest_dashboard = MagicMock()
dest_dashboard.dashboard_title = "My Dashboard"
dest_dashboard.theme = None
dest_dashboard.slices = []
dest_dashboard.tags = []
dest_dashboard.roles = []
dest_dashboard.export_to_dict.return_value = {
"position_json": json.dumps(dest_position),
"json_metadata": json.dumps({"native_filter_configuration": []}),
}
with patch(
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
return_value=False,
):
second_export_content = ExportDashboardsCommand._file_content(dest_dashboard)
second_export = yaml.safe_load(second_export_content)
second_chart_positions = [
node
for node in second_export["position"].values()
if isinstance(node, dict) and node.get("type") == "CHART"
]
assert second_chart_positions, "Second export must contain at least one CHART node"
second_chart_meta = second_chart_positions[0]["meta"]
# uuid must survive the re-import round-trip unchanged
assert second_chart_meta.get("uuid") == chart_uuid, (
"meta.uuid must be stable across export → import → re-export; "
"UUIDs are the cross-environment identity, not integer IDs."
)
# ------------------------------------------------------------------ #
# THE INVARIANT: chartId must be stable across environments. #
# #
# The source-env export and the destination-env export (whose #
# position_json carries a different env-local integer after import) #
# must agree on chartId, because it is derived from the stable #
# meta.uuid rather than the env-local primary key. #
# ------------------------------------------------------------------ #
first_chart_id = first_chart_meta.get("chartId")
second_chart_id = second_chart_meta.get("chartId")
assert first_chart_id == second_chart_id, (
f"meta.chartId must be stable across environments, but the source-env "
f"export produced chartId={first_chart_id!r} while the destination-env "
f"export (after re-import with remapped IDs) produced "
f"chartId={second_chart_id!r}. "
"Env-local integer IDs must not leak into the export format (issue "
"#32972); chartId is derived from meta.uuid so the export stays "
"environment-independent."
)
def _export_with_chart(
chart_uuid: str,
chart_id: int,
json_metadata: dict[str, Any],
) -> dict[str, Any]:
"""Export a single-chart dashboard and return the parsed YAML payload."""
from superset.commands.dashboard.export import ExportDashboardsCommand
position = {
"DASHBOARD_VERSION_KEY": "v2",
"ROOT_ID": {"children": ["GRID_ID"], "id": "ROOT_ID", "type": "ROOT"},
"GRID_ID": {
"children": ["CHART-aaa"],
"id": "GRID_ID",
"parents": ["ROOT_ID"],
"type": "GRID",
},
"CHART-aaa": {
"children": [],
"id": "CHART-aaa",
"meta": {
"chartId": chart_id,
"height": 20,
"sliceName": "Chart",
"uuid": chart_uuid,
"width": 4,
},
"parents": ["ROOT_ID", "GRID_ID"],
"type": "CHART",
},
}
dashboard = MagicMock()
dashboard.dashboard_title = "Test Dashboard"
dashboard.theme = None
dashboard.slices = []
dashboard.tags = []
dashboard.roles = []
dashboard.export_to_dict.return_value = {
"position_json": json.dumps(position),
"json_metadata": json.dumps(json_metadata),
}
with patch(
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
return_value=False,
):
content = ExportDashboardsCommand._file_content(dashboard)
return yaml.safe_load(content)
def test_orphan_chart_gets_uuid_derived_chart_id() -> None:
"""
A chart in ``model.slices`` that is NOT referenced in ``position_json`` is
appended via ``append_charts`` (which writes the env-local ``chart.id``).
``_stabilize_chart_ids`` runs afterwards, so the appended node must end up
with a UUID-derived ``chartId`` rather than the env-local integer.
"""
from superset.commands.dashboard.export import (
ExportDashboardsCommand,
stable_chart_id,
)
chart_uuid = "812bc377-ac09-475a-8d34-a63f7f087bd7"
env_local_id = 392
orphan = MagicMock()
orphan.id = env_local_id
orphan.uuid = chart_uuid
orphan.slice_name = "Orphan Chart"
# position_json references no charts, so ``orphan`` is appended as an orphan.
mock_dashboard = _make_mock_dashboard({"native_filter_configuration": []})
mock_dashboard.slices = [orphan]
with patch(
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
return_value=False,
):
content = ExportDashboardsCommand._file_content(mock_dashboard)
result = yaml.safe_load(content)
chart_nodes = [
node
for node in result["position"].values()
if isinstance(node, dict) and node.get("type") == "CHART"
]
assert chart_nodes, "Orphan chart must be appended to the position tree"
chart_meta = chart_nodes[0]["meta"]
assert chart_meta["uuid"] == chart_uuid
# The env-local id must have been stabilized away, not serialized verbatim.
assert chart_meta["chartId"] == stable_chart_id(chart_uuid)
assert chart_meta["chartId"] != env_local_id
def test_stabilize_chart_ids_skips_invalid_uuid() -> None:
"""A malformed meta.uuid must not abort the whole dashboard export."""
result = _export_with_chart(
"not-a-valid-uuid",
392,
{"native_filter_configuration": []},
)
chart_nodes = [
node
for node in result["position"].values()
if isinstance(node, dict) and node.get("type") == "CHART"
]
# Export still succeeds; the unstabilizable node keeps its original chartId.
assert chart_nodes
assert chart_nodes[0]["meta"]["chartId"] == 392
def test_stabilize_chart_ids_remaps_native_filter_scope() -> None:
"""Native filter scope.excluded / chartsInScope must track the stabilized id."""
from superset.commands.dashboard.export import stable_chart_id
chart_uuid = "812bc377-ac09-475a-8d34-a63f7f087bd7"
new_id = stable_chart_id(chart_uuid)
result = _export_with_chart(
chart_uuid,
392,
{
"native_filter_configuration": [
{
"id": "NATIVE_FILTER-1",
"scope": {"rootPath": ["ROOT_ID"], "excluded": [392]},
"chartsInScope": [392],
}
]
},
)
native_filter = result["metadata"]["native_filter_configuration"][0]
assert native_filter["scope"]["excluded"] == [new_id]
assert native_filter["chartsInScope"] == [new_id]
def test_stabilize_chart_ids_remaps_cross_filter_configuration() -> None:
"""global_chart_configuration and chart_configuration must be remapped."""
from superset.commands.dashboard.export import stable_chart_id
chart_uuid = "812bc377-ac09-475a-8d34-a63f7f087bd7"
new_id = stable_chart_id(chart_uuid)
result = _export_with_chart(
chart_uuid,
392,
{
"native_filter_configuration": [],
"global_chart_configuration": {
"scope": {"rootPath": ["ROOT_ID"], "excluded": [392]},
"chartsInScope": [392],
},
"chart_configuration": {
"392": {
"id": 392,
"crossFilters": {
"scope": {"rootPath": ["ROOT_ID"], "excluded": [392]},
"chartsInScope": [392],
},
}
},
},
)
metadata = result["metadata"]
assert metadata["global_chart_configuration"]["scope"]["excluded"] == [new_id]
assert metadata["global_chart_configuration"]["chartsInScope"] == [new_id]
# chart_configuration is re-keyed and its inner id / scopes are remapped.
assert str(new_id) in metadata["chart_configuration"]
chart_config = metadata["chart_configuration"][str(new_id)]
assert chart_config["id"] == new_id
assert chart_config["crossFilters"]["scope"]["excluded"] == [new_id]
assert chart_config["crossFilters"]["chartsInScope"] == [new_id]
def test_stable_chart_id_is_deterministic_and_in_range() -> None:
"""
stable_chart_id must derive a stable, environment-independent integer from a
chart UUID. The same UUID always yields the same id, and the id stays within
the positive signed 32-bit range so it can stand in for a database
auto-increment primary key without colliding with the sign bit.
"""
from superset.commands.dashboard.export import (
_STABLE_CHART_ID_MODULO,
stable_chart_id,
)
chart_uuid = "812bc377-ac09-475a-8d34-a63f7f087bd7"
# Deterministic: repeated derivations of the same UUID agree.
assert stable_chart_id(chart_uuid) == stable_chart_id(chart_uuid)
# Distinct UUIDs map to distinct ids (no accidental collapse).
other_uuid = "00000000-0000-4000-8000-000000000001"
assert stable_chart_id(chart_uuid) != stable_chart_id(other_uuid)
# Bounded to [1, _STABLE_CHART_ID_MODULO] for any UUID, including one whose
# high bits are all set (boundary case). Static inputs keep the test
# deterministic.
high_bits_uuid = "ffffffff-ffff-4fff-bfff-ffffffffffff"
for candidate in (chart_uuid, other_uuid, high_bits_uuid):
derived = stable_chart_id(candidate)
assert 1 <= derived <= _STABLE_CHART_ID_MODULO
def test_stabilize_chart_ids_remaps_default_filters() -> None:
"""
default_filters is a JSON *string* keyed by env-local chart id. The exporter
must parse it, remap the top-level keys to the stabilized ids, and re-emit it
as a JSON string so the bundle never leaks the source-env integer.
"""
from superset.commands.dashboard.export import stable_chart_id
chart_uuid = "812bc377-ac09-475a-8d34-a63f7f087bd7"
new_id = stable_chart_id(chart_uuid)
result = _export_with_chart(
chart_uuid,
392,
{
"native_filter_configuration": [],
"default_filters": json.dumps({"392": {"__time_range": "No filter"}}),
},
)
# default_filters round-trips as a JSON string keyed by the stabilized id.
default_filters = json.loads(result["metadata"]["default_filters"])
assert str(new_id) in default_filters
assert "392" not in default_filters
assert default_filters[str(new_id)] == {"__time_range": "No filter"}
def test_stabilize_chart_ids_remaps_timed_refresh_immune_slices() -> None:
"""
timed_refresh_immune_slices is a flat list of env-local chart ids. Each entry
must be remapped to the stabilized id so the immune list keeps pointing at the
same logical charts after a cross-environment round-trip.
"""
from superset.commands.dashboard.export import stable_chart_id
chart_uuid = "812bc377-ac09-475a-8d34-a63f7f087bd7"
new_id = stable_chart_id(chart_uuid)
result = _export_with_chart(
chart_uuid,
392,
{
"native_filter_configuration": [],
"timed_refresh_immune_slices": [392],
},
)
assert result["metadata"]["timed_refresh_immune_slices"] == [new_id]
def test_stabilize_chart_ids_remaps_filter_scopes_keys_and_immune() -> None:
"""
filter_scopes is a dict keyed by env-local chart id, whose values hold nested
per-column ``immune`` lists of chart ids. The exporter must remap BOTH the
top-level keys AND the nested immune arrays to the stabilized ids.
"""
from superset.commands.dashboard.export import stable_chart_id
chart_uuid = "812bc377-ac09-475a-8d34-a63f7f087bd7"
new_id = stable_chart_id(chart_uuid)
result = _export_with_chart(
chart_uuid,
392,
{
"native_filter_configuration": [],
"filter_scopes": {
"392": {
"region": {
"scope": ["ROOT_ID"],
"immune": [392],
}
}
},
},
)
filter_scopes = result["metadata"]["filter_scopes"]
# Top-level key remapped to the stabilized id (and the old key is gone).
assert str(new_id) in filter_scopes
assert "392" not in filter_scopes
# Nested immune array remapped too.
assert filter_scopes[str(new_id)]["region"]["immune"] == [new_id]
def test_stabilize_chart_ids_remaps_expanded_slices() -> None:
"""
expanded_slices is a dict keyed by env-local chart id. The exporter must
re-key it to the stabilized ids while preserving the values.
"""
from superset.commands.dashboard.export import stable_chart_id
chart_uuid = "812bc377-ac09-475a-8d34-a63f7f087bd7"
new_id = stable_chart_id(chart_uuid)
result = _export_with_chart(
chart_uuid,
392,
{
"native_filter_configuration": [],
"expanded_slices": {"392": True},
},
)
expanded_slices = result["metadata"]["expanded_slices"]
assert str(new_id) in expanded_slices
assert "392" not in expanded_slices
assert expanded_slices[str(new_id)] is True
def test_file_content_missing_dataset_preserves_dataset_id() -> None:
"""
When DatasetDAO.find_by_id returns None for a display control target,
datasetId is preserved (dual-write: it was never popped) and no
datasetUuid is added — the target is not silently emptied.
"""
from superset.commands.dashboard.export import ExportDashboardsCommand
mock_dashboard = _make_mock_dashboard(
{
"chart_customization_config": [
{
"id": "CUSTOMIZATION-orphan",
"type": "CHART_CUSTOMIZATION",
"targets": [{"datasetId": 9999}],
},
],
}
)
with (
patch(
"superset.commands.dashboard.export.DatasetDAO.find_by_id",
return_value=None,
),
patch(
"superset.commands.dashboard.export.feature_flag_manager.is_feature_enabled",
return_value=False,
),
):
content = ExportDashboardsCommand._file_content(mock_dashboard)
result = yaml.safe_load(content)
target = result["metadata"]["chart_customization_config"][0]["targets"][0]
assert target["datasetId"] == 9999
assert "datasetUuid" not in target
def test_stabilize_chart_ids_resolves_id_collisions() -> None:
"""
Two charts whose UUIDs reduce to the same derived id must still get distinct
stabilized ids, so the chart-keyed metadata remaps (filter_scopes,
chart_configuration, …) never silently overwrite one another. The forced
collision is simulated by stubbing ``stable_chart_id`` to a constant.
"""
from superset.commands.dashboard.export import _stabilize_chart_ids
uuid_a = "00000000-0000-4000-8000-00000000000a"
uuid_b = "00000000-0000-4000-8000-00000000000b"
payload: dict[str, Any] = {
"position": {
"CHART-a": {
"type": "CHART",
"meta": {"chartId": 1, "uuid": uuid_a},
},
"CHART-b": {
"type": "CHART",
"meta": {"chartId": 2, "uuid": uuid_b},
},
},
"metadata": {
"expanded_slices": {"1": True, "2": False},
},
}
with patch(
"superset.commands.dashboard.export.stable_chart_id",
return_value=100,
):
_stabilize_chart_ids(payload)
id_a = payload["position"]["CHART-a"]["meta"]["chartId"]
id_b = payload["position"]["CHART-b"]["meta"]["chartId"]
# Distinct ids despite the collision; one keeps the derived value, the other
# is deterministically probed forward.
assert id_a != id_b
assert {id_a, id_b} == {100, 101}
# Neither expanded_slices entry was dropped by a key clash.
assert set(payload["metadata"]["expanded_slices"]) == {str(id_a), str(id_b)}