feat: harden migration flows and integration coverage
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user