# #region TestMigrationEngine [C:2] [TYPE Module] # # @PURPOSE: Unit tests for MigrationEngine's cross-filter patching algorithms. # @LAYER Domain # @RELATION BINDS_TO -> [MigrationEngine] # import json import os from pathlib import Path import pytest import sys import tempfile import zipfile import yaml backend_dir = str(Path(__file__).parent.parent.parent.resolve()) if backend_dir not in sys.path: sys.path.insert(0, backend_dir) from src.core.migration_engine import MigrationEngine # --- Fixtures --- # #region MockMappingService [C:2] [TYPE Class] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Deterministic mapping service double for native filter ID remapping scenarios. # @INVARIANT: Returns mappings only for requested UUID keys present in seeded map. class MockMappingService: """Mock that simulates IdMappingService.get_remote_ids_batch.""" def __init__(self, mappings: dict): self.mappings = mappings def get_remote_ids_batch(self, env_id, resource_type, uuids): # @INVARIANT_VIOLATION: resource_type parameter is silently ignored. Lookups are UUID-only; chart and dataset UUIDs share the same namespace. Tests that mix resource types will produce false positives. result = {} for uuid in uuids: if uuid in self.mappings: result[uuid] = self.mappings[uuid] return result # #endregion MockMappingService # #region _write_dashboard_yaml [C:2] [TYPE Function] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Serialize dashboard metadata into YAML fixture with json_metadata payload for patch tests. def _write_dashboard_yaml(dir_path: Path, metadata: dict) -> Path: """Helper: writes a dashboard YAML file with json_metadata.""" file_path = dir_path / "dash.yaml" with open(file_path, "w") as f: yaml.dump({"json_metadata": json.dumps(metadata)}, f) return file_path # --- _patch_dashboard_metadata tests --- # #endregion _write_dashboard_yaml # #region test_patch_dashboard_metadata_replaces_chart_ids [C:2] [TYPE Function] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Verify native filter target chartId values are remapped via mapping service results. def test_patch_dashboard_metadata_replaces_chart_ids(): """Verifies that chartId values are replaced using the mapping service.""" mock_service = MockMappingService({"uuid-chart-A": 999}) engine = MigrationEngine(mock_service) metadata = {"native_filter_configuration": [{"targets": [{"chartId": 42}]}]} with tempfile.TemporaryDirectory() as td: fp = _write_dashboard_yaml(Path(td), metadata) source_map = {42: "uuid-chart-A"} engine._patch_dashboard_metadata(fp, "target-env", source_map) with open(fp) as f: data = yaml.safe_load(f) result = json.loads(data["json_metadata"]) assert ( result["native_filter_configuration"][0]["targets"][0]["chartId"] == 999 ) # #endregion test_patch_dashboard_metadata_replaces_chart_ids # #region test_patch_dashboard_metadata_replaces_dataset_ids [C:2] [TYPE Function] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Verify native filter target datasetId values are remapped via mapping service results. def test_patch_dashboard_metadata_replaces_dataset_ids(): """Verifies that datasetId values are replaced using the mapping service.""" mock_service = MockMappingService({"uuid-ds-B": 500}) engine = MigrationEngine(mock_service) metadata = {"native_filter_configuration": [{"targets": [{"datasetId": 10}]}]} with tempfile.TemporaryDirectory() as td: fp = _write_dashboard_yaml(Path(td), metadata) source_map = {10: "uuid-ds-B"} engine._patch_dashboard_metadata(fp, "target-env", source_map) with open(fp) as f: data = yaml.safe_load(f) result = json.loads(data["json_metadata"]) assert ( result["native_filter_configuration"][0]["targets"][0]["datasetId"] == 500 ) # #endregion test_patch_dashboard_metadata_replaces_dataset_ids # #region test_patch_dashboard_metadata_skips_when_no_metadata [C:2] [TYPE Function] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Ensure dashboard files without json_metadata are left unchanged by metadata patching. def test_patch_dashboard_metadata_skips_when_no_metadata(): """Verifies early return when json_metadata key is absent.""" mock_service = MockMappingService({}) engine = MigrationEngine(mock_service) with tempfile.TemporaryDirectory() as td: fp = Path(td) / "dash.yaml" with open(fp, "w") as f: yaml.dump({"title": "No metadata here"}, f) engine._patch_dashboard_metadata(fp, "target-env", {}) with open(fp) as f: data = yaml.safe_load(f) assert "json_metadata" not in data # #endregion test_patch_dashboard_metadata_skips_when_no_metadata # #region test_patch_dashboard_metadata_handles_missing_targets [C:2] [TYPE Function] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Verify patching updates mapped targets while preserving unmapped native filter IDs. def test_patch_dashboard_metadata_handles_missing_targets(): """When some source IDs have no target mapping, patches what it can and leaves the rest.""" mock_service = MockMappingService({"uuid-A": 100}) # Only uuid-A maps engine = MigrationEngine(mock_service) metadata = { "native_filter_configuration": [ {"targets": [{"datasetId": 1}, {"datasetId": 2}]} ] } with tempfile.TemporaryDirectory() as td: fp = _write_dashboard_yaml(Path(td), metadata) source_map = {1: "uuid-A", 2: "uuid-MISSING"} # uuid-MISSING won't resolve engine._patch_dashboard_metadata(fp, "target-env", source_map) with open(fp) as f: data = yaml.safe_load(f) result = json.loads(data["json_metadata"]) targets = result["native_filter_configuration"][0]["targets"] # ID 1 should be replaced to 100; ID 2 should remain 2 assert targets[0]["datasetId"] == 100 assert targets[1]["datasetId"] == 2 # --- _extract_chart_uuids_from_archive tests --- # #endregion test_patch_dashboard_metadata_handles_missing_targets # #region test_extract_chart_uuids_from_archive [C:2] [TYPE Function] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Verify chart archive scan returns complete local chart id-to-uuid mapping. def test_extract_chart_uuids_from_archive(): """Verifies that chart YAML files are parsed for id->uuid mappings.""" engine = MigrationEngine() with tempfile.TemporaryDirectory() as td: charts_dir = Path(td) / "charts" charts_dir.mkdir() chart1 = {"id": 42, "uuid": "uuid-42", "slice_name": "Chart One"} chart2 = {"id": 99, "uuid": "uuid-99", "slice_name": "Chart Two"} with open(charts_dir / "chart1.yaml", "w") as f: yaml.dump(chart1, f) with open(charts_dir / "chart2.yaml", "w") as f: yaml.dump(chart2, f) result = engine._extract_chart_uuids_from_archive(Path(td)) assert result == {42: "uuid-42", 99: "uuid-99"} # --- _transform_yaml tests --- # #endregion test_extract_chart_uuids_from_archive # #region test_transform_yaml_replaces_database_uuid [C:2] [TYPE Function] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Ensure dataset YAML database_uuid fields are replaced when source UUID mapping exists. def test_transform_yaml_replaces_database_uuid(): """Verifies that database_uuid in a dataset YAML is replaced.""" engine = MigrationEngine() with tempfile.TemporaryDirectory() as td: fp = Path(td) / "dataset.yaml" with open(fp, "w") as f: yaml.dump({"database_uuid": "source-uuid-abc", "table_name": "my_table"}, f) engine._transform_yaml(fp, {"source-uuid-abc": "target-uuid-xyz"}) with open(fp) as f: data = yaml.safe_load(f) assert data["database_uuid"] == "target-uuid-xyz" assert data["table_name"] == "my_table" # #endregion test_transform_yaml_replaces_database_uuid # #region test_transform_yaml_ignores_unmapped_uuid [C:2] [TYPE Function] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Ensure transform_yaml leaves dataset files untouched when database_uuid is not mapped. def test_transform_yaml_ignores_unmapped_uuid(): """Verifies no changes when UUID is not in the mapping.""" engine = MigrationEngine() with tempfile.TemporaryDirectory() as td: fp = Path(td) / "dataset.yaml" original = {"database_uuid": "unknown-uuid", "table_name": "test"} with open(fp, "w") as f: yaml.dump(original, f) engine._transform_yaml(fp, {"other-uuid": "replacement"}) with open(fp) as f: data = yaml.safe_load(f) assert data["database_uuid"] == "unknown-uuid" # --- [NEW] transform_zip E2E tests --- # #endregion test_transform_yaml_ignores_unmapped_uuid # #region test_transform_zip_end_to_end [C:2] [TYPE Function] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Validate full ZIP transform pipeline remaps datasets and dashboard cross-filter chart IDs. def test_transform_zip_end_to_end(): """Verifies full orchestration: extraction, transformation, patching, and re-packaging.""" mock_service = MockMappingService({"char-uuid": 101, "ds-uuid": 202}) engine = MigrationEngine(mock_service) with tempfile.TemporaryDirectory() as td: td_path = Path(td) zip_path = td_path / "source.zip" output_path = td_path / "target.zip" # Create source ZIP structure with tempfile.TemporaryDirectory() as src_dir: src_path = Path(src_dir) # 1. Dataset ds_dir = src_path / "datasets" ds_dir.mkdir() with open(ds_dir / "ds.yaml", "w") as f: yaml.dump({"database_uuid": "source-db-uuid", "table_name": "users"}, f) # 2. Chart ch_dir = src_path / "charts" ch_dir.mkdir() with open(ch_dir / "ch.yaml", "w") as f: yaml.dump({"id": 10, "uuid": "char-uuid"}, f) # 3. Dashboard db_dir = src_path / "dashboards" db_dir.mkdir() metadata = {"native_filter_configuration": [{"targets": [{"chartId": 10}]}]} with open(db_dir / "db.yaml", "w") as f: yaml.dump({"json_metadata": json.dumps(metadata)}, f) with zipfile.ZipFile(zip_path, "w") as zf: for root, _, files in os.walk(src_path): for file in files: p = Path(root) / file zf.write(p, p.relative_to(src_path)) db_mapping = {"source-db-uuid": "target-db-uuid"} # Execute transform success = engine.transform_zip( str(zip_path), str(output_path), db_mapping, target_env_id="test-target", fix_cross_filters=True, ) assert success is True assert output_path.exists() # Verify contents with tempfile.TemporaryDirectory() as out_dir: with zipfile.ZipFile(output_path, "r") as zf: zf.extractall(out_dir) out_path = Path(out_dir) # Verify dataset transformation with open(out_path / "datasets" / "ds.yaml") as f: ds_data = yaml.safe_load(f) assert ds_data["database_uuid"] == "target-db-uuid" # Verify dashboard patching with open(out_path / "dashboards" / "db.yaml") as f: db_data = yaml.safe_load(f) meta = json.loads(db_data["json_metadata"]) assert ( meta["native_filter_configuration"][0]["targets"][0]["chartId"] == 101 ) # #endregion test_transform_zip_end_to_end # #region test_transform_zip_invalid_path [C:2] [TYPE Function] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Verify transform_zip returns False when source archive path does not exist. def test_transform_zip_invalid_path(): """@PRE: Verify behavior (False) on invalid ZIP path.""" engine = MigrationEngine() success = engine.transform_zip("non_existent.zip", "output.zip", {}) assert success is False # #endregion test_transform_zip_invalid_path # #region test_transform_yaml_nonexistent_file [C:2] [TYPE Function] # @RELATION BINDS_TO -> [TestMigrationEngine] # @PURPOSE: Verify transform_yaml raises FileNotFoundError for missing YAML source files. def test_transform_yaml_nonexistent_file(): """@PRE: Verify behavior on non-existent YAML file.""" engine = MigrationEngine() with pytest.raises(FileNotFoundError): engine._transform_yaml(Path("non_existent.yaml"), {}) # #endregion test_transform_yaml_nonexistent_file # ── [NEW] Additional coverage: init, edge cases, error paths ── # #region test_init_belief_scope [C:2] [TYPE Function] # @BRIEF MigrationEngine.__init__ logs reason/reflect via belief_scope. def test_init_belief_scope(): """Engine init with mapping_service logs initialization.""" svc = MockMappingService({}) engine = MigrationEngine(svc) assert engine.mapping_service is svc # #endregion test_init_belief_scope # #region test_init_no_mapping_service [C:2] [TYPE Function] # @BRIEF MigrationEngine can be initialized without a mapping_service. def test_init_no_mapping_service(): """Engine init without arguments leaves mapping_service as None.""" engine = MigrationEngine() assert engine.mapping_service is None # #endregion test_init_no_mapping_service # #region test_transform_yaml_empty_data [C:2] [TYPE Function] # @BRIEF _transform_yaml with empty YAML (null data) returns early without error. def test_transform_yaml_empty_data(): """Empty YAML file does not raise.""" engine = MigrationEngine() with tempfile.TemporaryDirectory() as td: fp = Path(td) / "empty.yaml" fp.write_text("~\n") # null in YAML engine._transform_yaml(fp, {"src": "tgt"}) # should not raise # #endregion test_transform_yaml_empty_data # #region test_transform_yaml_no_matching_uuid [C:2] [TYPE Function] # @BRIEF _transform_yaml leaves file unchanged when uuid not in db_mapping. def test_transform_yaml_no_matching_uuid(): """Non-mapped database_uuid leaves YAML content unchanged.""" engine = MigrationEngine() with tempfile.TemporaryDirectory() as td: fp = Path(td) / "ds.yaml" with open(fp, "w") as f: yaml.dump({"database_uuid": "uuid-alpha", "table_name": "users"}, f) engine._transform_yaml(fp, {"uuid-other": "target-uuid"}) with open(fp) as f: data = yaml.safe_load(f) assert data["database_uuid"] == "uuid-alpha" # unchanged # #endregion test_transform_yaml_no_matching_uuid # #region test_transform_zip_extract_failure [C:2] [TYPE Function] # @BRIEF When the source ZIP is corrupted, transform_zip returns False. def test_transform_zip_extract_failure(): """Corrupt ZIP path returns False.""" engine = MigrationEngine() with tempfile.TemporaryDirectory() as td: bad_zip = Path(td) / "bad.zip" bad_zip.write_text("this is not a zip file") result = engine.transform_zip(str(bad_zip), str(Path(td) / "out.zip"), {}) assert result is False # #endregion test_transform_zip_extract_failure # #region test_transform_zip_strip_databases [C:2] [TYPE Function] # @BRIEF When strip_databases=True, the databases/ directory is excluded from output. def test_transform_zip_strip_databases(): """Databases directory is stripped when strip_databases=True.""" engine = MigrationEngine() with tempfile.TemporaryDirectory() as td: src = Path(td) / "src" dst = Path(td) / "out.zip" # Create archive with databases/ and datasets/ (src / "databases").mkdir(parents=True) (src / "datasets").mkdir() (src / "databases" / "db.yaml").write_text("db: test") (src / "datasets" / "ds.yaml").write_text("table: users") zip_path = src / "source.zip" with zipfile.ZipFile(zip_path, "w") as zf: for root, _, files in os.walk(src): for f in files: fp = Path(root) / f if fp.suffix == ".zip": continue zf.write(fp, fp.relative_to(src)) success = engine.transform_zip( str(zip_path), str(dst), {}, strip_databases=True, ) assert success is True # Verify databases/ is not in output with tempfile.TemporaryDirectory() as out_dir: with zipfile.ZipFile(dst, "r") as zf: names = zf.namelist() assert not any("databases" in n for n in names) assert any("datasets" in n for n in names) # #endregion test_transform_zip_strip_databases # #region test_transform_zip_cross_filters_no_mapping_service [C:2] [TYPE Function] # @BRIEF fix_cross_filters=True without mapping_service logs warning, doesn't crash. def test_transform_zip_cross_filters_no_mapping_service(): """Cross-filter fix requested but no mapping service — graceful skip.""" engine = MigrationEngine() # no mapping_service with tempfile.TemporaryDirectory() as td: src = Path(td) / "src" dst = Path(td) / "out.zip" (src / "datasets").mkdir(parents=True) (src / "datasets" / "ds.yaml").write_text("table: users") zip_path = src / "source.zip" with zipfile.ZipFile(zip_path, "w") as zf: zf.write(src / "datasets" / "ds.yaml", "datasets/ds.yaml") success = engine.transform_zip( str(zip_path), str(dst), {"old": "new"}, target_env_id="env-1", fix_cross_filters=True, ) assert success is True # #endregion test_transform_zip_cross_filters_no_mapping_service # #region test_extract_chart_uuids_malformed_yaml [C:2] [TYPE Function] # @BRIEF Malformed chart YAML files are skipped without crashing extraction. def test_extract_chart_uuids_malformed_yaml(): """Broken chart YAML files do not break the extraction loop.""" engine = MigrationEngine() with tempfile.TemporaryDirectory() as td: charts_dir = Path(td) / "charts" charts_dir.mkdir() (charts_dir / "good.yaml").write_text("id: 1\nuuid: abc-123\n") (charts_dir / "broken.yaml").write_text("{invalid: yaml: [}") # parse error (charts_dir / "no_id.yaml").write_text("uuid: def-456\n") # missing id (charts_dir / "empty.yaml").write_text("") # empty result = engine._extract_chart_uuids_from_archive(Path(td)) assert 1 in result assert result[1] == "abc-123" assert len(result) == 1 # only the valid one # #endregion test_extract_chart_uuids_malformed_yaml # #region test_patch_dashboard_missing_file [C:2] [TYPE Function] # @BRIEF _patch_dashboard_metadata with non-existent file returns early. def test_patch_dashboard_missing_file(): """Non-existent dashboard YAML is silently skipped.""" engine = MigrationEngine(MockMappingService({})) engine._patch_dashboard_metadata(Path("/nonexistent/dash.yaml"), "env-1", {}) # Should not raise # #endregion test_patch_dashboard_missing_file # #region test_patch_dashboard_empty_json_metadata [C:2] [TYPE Function] # @BRIEF When json_metadata is empty string, patch returns early. def test_patch_dashboard_empty_json_metadata(): """Empty json_metadata string is handled gracefully.""" engine = MigrationEngine(MockMappingService({})) with tempfile.TemporaryDirectory() as td: fp = Path(td) / "dash.yaml" with open(fp, "w") as f: yaml.dump({"json_metadata": ""}, f) engine._patch_dashboard_metadata(fp, "env-1", {42: "uuid-42"}) with open(fp) as f: data = yaml.safe_load(f) assert data["json_metadata"] == "" # unchanged # #endregion test_patch_dashboard_empty_json_metadata # #region test_patch_dashboard_no_target_ids [C:2] [TYPE Function] # @BRIEF When mapping_service returns no target IDs, patch returns early. def test_patch_dashboard_no_target_ids(): """No remote target IDs — metadata is left unchanged.""" empty_service = MockMappingService({}) # no mappings engine = MigrationEngine(empty_service) with tempfile.TemporaryDirectory() as td: fp = Path(td) / "dash.yaml" metadata = {"native_filter_configuration": [{"targets": [{"chartId": 42}]}]} with open(fp, "w") as f: yaml.dump({"json_metadata": json.dumps(metadata)}, f) engine._patch_dashboard_metadata(fp, "env-1", {42: "uuid-42"}) with open(fp) as f: data = yaml.safe_load(f) assert json.loads(data["json_metadata"]) == metadata # unchanged # #endregion test_patch_dashboard_no_target_ids # #region test_patch_dashboard_no_source_match [C:2] [TYPE Function] # @BRIEF When source IDs don't match any remote IDs, patch returns early. def test_patch_dashboard_no_source_match(): """Source IDs not found in remote mapping — metadata unchanged.""" service = MockMappingService({"uuid-other": 999}) # different UUID engine = MigrationEngine(service) with tempfile.TemporaryDirectory() as td: fp = Path(td) / "dash.yaml" metadata_json = json.dumps({"native_filter_configuration": [{"targets": [{"chartId": 42}]}]}) with open(fp, "w") as f: yaml.dump({"json_metadata": metadata_json}, f) engine._patch_dashboard_metadata(fp, "env-1", {42: "uuid-42"}) with open(fp) as f: data = yaml.safe_load(f) assert data["json_metadata"] == metadata_json # unchanged # #endregion test_patch_dashboard_no_source_match # #region test_patch_dashboard_exception_handling [C:2] [TYPE Function] # @BRIEF Exception in _patch_dashboard_metadata is caught and logged, not raised. def test_patch_dashboard_exception_handling(): """Corrupt json_metadata that fails to parse — exception is caught, not raised.""" engine = MigrationEngine(MockMappingService({"uuid-42": 100})) with tempfile.TemporaryDirectory() as td: fp = Path(td) / "dash.yaml" # Invalid JSON as json_metadata — will fail on json.loads() with open(fp, "w") as f: yaml.dump({"json_metadata": "{invalid json]"}, f) # Should not raise engine._patch_dashboard_metadata(fp, "env-1", {42: "uuid-42"}) # #endregion test_patch_dashboard_exception_handling # #region test_patch_dashboard_source_ids_not_in_remote [C:2] [TYPE Function] # @BRIEF When target_ids exist but none match source_map UUIDs, patch returns early at line 237. def test_patch_dashboard_source_ids_not_in_remote(): """target_ids has entries, but none match source UUIDs → source_to_target empty → skip.""" class MockServiceReturnsUnrelated(MockMappingService): """get_remote_ids_batch returns extra entries NOT in the requested UUIDs.""" def get_remote_ids_batch(self, env_id, resource_type, uuids): # Returns mappings for UUIDs NOT in the request return {"unrelated-uuid": 999} engine = MigrationEngine(MockServiceReturnsUnrelated({})) with tempfile.TemporaryDirectory() as td: fp = Path(td) / "dash.yaml" metadata = {"native_filter_configuration": [{"targets": [{"chartId": 42}]}]} with open(fp, "w") as f: yaml.dump({"json_metadata": json.dumps(metadata)}, f) engine._patch_dashboard_metadata(fp, "env-1", {42: "uuid-42"}) with open(fp) as f: data = yaml.safe_load(f) assert json.loads(data["json_metadata"]) == metadata # unchanged # #endregion test_patch_dashboard_source_ids_not_in_remote # #region test_transform_zip_without_fix_cross_filters [C:2] [TYPE Function] # @BRIEF transform_zip works without fixing cross-filters (default). def test_transform_zip_without_fix_cross_filters(): """Default fix_cross_filters=False skips dashboard patching.""" engine = MigrationEngine(MockMappingService({})) with tempfile.TemporaryDirectory() as td: src = Path(td) / "src" dst = Path(td) / "out.zip" (src / "datasets").mkdir(parents=True) (src / "datasets" / "ds.yaml").write_text(yaml.dump({"database_uuid": "old-uuid", "table_name": "t"})) zip_path = src / "source.zip" with zipfile.ZipFile(zip_path, "w") as zf: zf.write(src / "datasets" / "ds.yaml", "datasets/ds.yaml") success = engine.transform_zip(str(zip_path), str(dst), {"old-uuid": "new-uuid"}) assert success is True # Verify dataset was transformed with tempfile.TemporaryDirectory() as out_dir: with zipfile.ZipFile(dst, "r") as zf: zf.extractall(out_dir) with open(Path(out_dir) / "datasets" / "ds.yaml") as f: data = yaml.safe_load(f) assert data["database_uuid"] == "new-uuid" # #endregion test_transform_zip_without_fix_cross_filters # #endregion TestMigrationEngine