diff --git a/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py b/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py index 88dfa393c6f..6fe39c1cc3d 100644 --- a/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py +++ b/tests/unit_tests/dashboards/commands/importers/v1/utils_test.py @@ -123,6 +123,121 @@ def test_update_native_filter_config_scope_excluded(): } +def test_update_native_filter_config_preserves_rootpath_and_remaps_excluded(): + """ + Regression guard for #19944: a native filter's ``scope`` has two parts that + must both survive an export/import roundtrip: + + - ``rootPath`` controls *which* dashboard sections (tabs/rows) the filter + applies to. It uses position keys (``ROOT_ID``, ``TAB-xxx``), not chart + IDs, so ``update_id_refs`` must leave it untouched. + - ``excluded`` is a list of chart IDs the filter does NOT apply to within + its rootPath. Those IDs must be remapped to destination-env IDs. + + The original bug report — "filters are automatically applied to all charts, + even if a different scoping was defined before the export" — describes a + rootPath silently being collapsed back to ``["ROOT_ID"]`` (i.e. "apply + everywhere"). This test pins the post-refactor contract: the import path + must not mutate or drop ``rootPath``. + """ + from superset.commands.dashboard.importers.v1.utils import update_id_refs + + config: dict[str, Any] = { + "position": { + "CHART1": { + "id": "CHART1", + "meta": {"chartId": 101, "uuid": "uuid1"}, + "type": "CHART", + }, + "CHART2": { + "id": "CHART2", + "meta": {"chartId": 102, "uuid": "uuid2"}, + "type": "CHART", + }, + "CHART3": { + "id": "CHART3", + "meta": {"chartId": 103, "uuid": "uuid3"}, + "type": "CHART", + }, + }, + "metadata": { + "native_filter_configuration": [ + { + "id": "NATIVE_FILTER-region", + "name": "Region", + "scope": { + # Filter applies only to charts under TAB-revenue, + # except chart 102 which is explicitly excluded. + "rootPath": ["TAB-revenue"], + "excluded": [102], + }, + }, + { + "id": "NATIVE_FILTER-product", + "name": "Product", + "scope": { + # Different filter, different rootPath; must not be + # cross-contaminated with the first filter's scope. + "rootPath": ["TAB-inventory", "TAB-revenue"], + "excluded": [101, 103], + }, + }, + ], + }, + } + chart_ids = {"uuid1": 1, "uuid2": 2, "uuid3": 3} + dataset_info: dict[str, dict[str, Any]] = {} + + fixed = update_id_refs(config, chart_ids, dataset_info) + filters = fixed["metadata"]["native_filter_configuration"] + + # rootPath uses position keys, not chart IDs — must pass through unchanged. + assert filters[0]["scope"]["rootPath"] == ["TAB-revenue"] + assert filters[1]["scope"]["rootPath"] == ["TAB-inventory", "TAB-revenue"] + + # excluded uses chart IDs — must be remapped to destination-env IDs. + assert filters[0]["scope"]["excluded"] == [2] + assert filters[1]["scope"]["excluded"] == [1, 3] + + +def test_update_native_filter_config_default_rootpath_preserved(): + """ + The "apply everywhere" default — ``rootPath: ["ROOT_ID"]`` — must also + survive untouched. A regression that special-cased this value (e.g. by + deleting it) would silently change "apply everywhere" into "apply nowhere" + on import, since downstream consumers treat a missing rootPath as empty + rather than as the default. + """ + from superset.commands.dashboard.importers.v1.utils import update_id_refs + + config: dict[str, Any] = { + "position": { + "CHART1": { + "id": "CHART1", + "meta": {"chartId": 101, "uuid": "uuid1"}, + "type": "CHART", + }, + }, + "metadata": { + "native_filter_configuration": [ + { + "id": "NATIVE_FILTER-global", + "name": "Global", + "scope": {"rootPath": ["ROOT_ID"], "excluded": []}, + } + ], + }, + } + chart_ids = {"uuid1": 1} + dataset_info: dict[str, dict[str, Any]] = {} + + fixed = update_id_refs(config, chart_ids, dataset_info) + scope = fixed["metadata"]["native_filter_configuration"][0]["scope"] + + assert scope["rootPath"] == ["ROOT_ID"] + assert scope["excluded"] == [] + + def test_update_id_refs_cross_filter_chart_configuration_key_and_excluded_mapping(): from superset.commands.dashboard.importers.v1.utils import update_id_refs