- app.py split from 1966-line test_app.py into 7 files (68 tests, 92% coverage): - test_app_lifespan: lifespan, ensure_initial_admin_user - test_app_handlers: exception handlers, HSTS - test_app_middleware: log_requests, middleware chain - test_app_ws_auth: _authenticate_websocket - test_app_ws_endpoint: WS main loop, 5 endpoint handlers - test_app_ws_events: task/maintenance/dataset/translate WS streams - test_app_spa: SPA serving, TestClient integration - migration_engine: extended coverage with init, edge cases, error paths
630 lines
25 KiB
Python
630 lines
25 KiB
Python
# #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
|