feat: harden migration flows and integration coverage

This commit is contained in:
2026-07-21 18:59:26 +03:00
parent d1d2a0f92e
commit 456d531a13
77 changed files with 7257 additions and 1481 deletions

View File

@@ -10,6 +10,7 @@ os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests")
os.environ.setdefault("DEV_MODE", "true")
import sys
from types import SimpleNamespace
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
@@ -115,6 +116,153 @@ class TestGetMappings:
assert resp.json() == []
# #region Test.Api.MappingAnalysis [C:4] [TYPE Module] [SEMANTICS test,mappings,analysis,coverage]
# @ingroup Test.Api.Mappings
# @BRIEF Verify all-pair coverage classification, partial failure isolation, and API-key scope propagation.
# @RELATION VERIFIES -> [Mapping.Analysis.Build]
# @RELATION VERIFIES -> [Mapping.AnalysisEndpoint]
# @TEST_INVARIANT Saved, suggested, and unmapped counts partition current source databases.
# @TEST_EDGE One catalog failure marks only affected pairs as error.
class TestMappingAnalysis:
# #region Test.Api.MappingAnalysis.Classification [C:3] [TYPE Function] [SEMANTICS test,mappings,analysis]
# @ingroup Test.Api.Mappings
# @BRIEF Saved mappings are excluded before fuzzy suggestions and unmapped counts are calculated.
@pytest.mark.asyncio
async def test_classifies_saved_suggested_and_unmapped(self):
from src.services.mapping_analysis import build_mapping_coverage
environments = [
SimpleNamespace(id="env-1", name="Development"),
SimpleNamespace(id="env-2", name="Production"),
]
config_manager = MagicMock()
config_manager.get_environments.return_value = environments
saved = _make_mapping(
source_env_id="env-1",
target_env_id="env-2",
source_db_uuid="saved-source",
)
mock_db = MagicMock()
mock_db.query.return_value.all.return_value = [saved]
clients = []
for catalog in (
[
{"uuid": "saved-source", "database_name": "Saved"},
{"uuid": "suggested-source", "database_name": "Sales"},
{"uuid": "unmapped-source", "database_name": "Telemetry"},
],
[{"uuid": "sales-target", "database_name": "Sales"}],
):
client = MagicMock()
client.get_databases_summary = AsyncMock(return_value=catalog)
client.aclose = AsyncMock()
clients.append(client)
with patch("src.services.mapping_analysis.SupersetClient", side_effect=clients):
result = await build_mapping_coverage(config_manager, mock_db)
forward = next(
pair
for pair in result["pairs"]
if pair["source_env_id"] == "env-1" and pair["target_env_id"] == "env-2"
)
assert forward["saved_count"] == 1
assert forward["unsaved_suggestion_count"] == 1
assert forward["unmapped_count"] == 1
assert forward["status"] == "attention"
assert all(client.aclose.await_count == 1 for client in clients)
# #endregion Test.Api.MappingAnalysis.Classification
# #region Test.Api.MappingAnalysis.PartialFailure [C:3] [TYPE Function] [SEMANTICS test,mappings,analysis,failure]
# @ingroup Test.Api.Mappings
# @BRIEF A failed catalog does not prevent healthy environment pairs from being analysed.
@pytest.mark.asyncio
async def test_isolates_catalog_failure(self):
from src.services.mapping_analysis import build_mapping_coverage
environments = [
SimpleNamespace(id="env-1", name="Development"),
SimpleNamespace(id="env-2", name="Preproduction"),
SimpleNamespace(id="env-3", name="Production"),
]
config_manager = MagicMock()
config_manager.get_environments.return_value = environments
mock_db = MagicMock()
mock_db.query.return_value.all.return_value = []
clients = []
for result in (
[{"uuid": "one", "database_name": "One"}],
RuntimeError("Superset unavailable"),
[{"uuid": "three", "database_name": "Three"}],
):
client = MagicMock()
client.get_databases_summary = AsyncMock(
side_effect=result if isinstance(result, Exception) else None,
return_value=None if isinstance(result, Exception) else result,
)
client.aclose = AsyncMock()
clients.append(client)
with patch("src.services.mapping_analysis.SupersetClient", side_effect=clients):
response = await build_mapping_coverage(config_manager, mock_db)
healthy = next(
pair
for pair in response["pairs"]
if pair["source_env_id"] == "env-1" and pair["target_env_id"] == "env-3"
)
failed = next(
pair
for pair in response["pairs"]
if pair["source_env_id"] == "env-1" and pair["target_env_id"] == "env-2"
)
assert healthy["status"] != "error"
assert failed["status"] == "error"
assert failed["errors"] == {"env-2": "Superset unavailable"}
# #endregion Test.Api.MappingAnalysis.PartialFailure
# #region Test.Api.MappingAnalysis.ApiKeyScope [C:3] [TYPE Function] [SEMANTICS test,mappings,analysis,auth]
# @ingroup Test.Api.Mappings
# @BRIEF Environment-scoped API keys pass their scope to the analysis service.
def test_passes_api_key_environment_scope(self):
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = MagicMock(
environment_id="env-1"
)
expected = {
"environments": [],
"pairs": [],
"totals": {
"saved": 0,
"unsaved_suggestions": 0,
"unmapped": 0,
"stale": 0,
},
}
from src.core.database import get_db
client = _make_client({get_db: lambda: mock_db})
with (
patch("src.api.routes.mappings.hash_api_key", return_value="hash"),
patch(
"src.api.routes.mappings.build_mapping_coverage",
new=AsyncMock(return_value=expected),
) as build,
):
response = client.get(
"/api/mappings/analysis",
headers={"X-API-Key": "secret"},
)
assert response.status_code == 200
assert response.json() == expected
assert build.await_args.kwargs["environment_scope"] == "env-1"
# #endregion Test.Api.MappingAnalysis.ApiKeyScope
# #endregion Test.Api.MappingAnalysis
# ── create_mapping ──
class TestCreateMapping: