592 lines
23 KiB
Python
592 lines
23 KiB
Python
# #region Test.Api.Mappings [C:3] [TYPE Module] [SEMANTICS test,mappings,routes]
|
|
# @BRIEF Tests for mappings API routes — get, create, suggest.
|
|
# @RELATION BINDS_TO -> [Api.Mappings.MappingsApi]
|
|
|
|
import os
|
|
|
|
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
|
|
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
|
|
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
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
_src = str(Path(__file__).resolve().parent.parent.parent / "src")
|
|
if _src not in sys.path:
|
|
sys.path.insert(0, _src)
|
|
|
|
|
|
def _make_mapping(**kw):
|
|
m = MagicMock()
|
|
m.id = kw.get("id", "map-1")
|
|
m.source_env_id = kw.get("source_env_id", "env-1")
|
|
m.target_env_id = kw.get("target_env_id", "env-2")
|
|
m.source_db_uuid = kw.get("source_db_uuid", "src-uuid")
|
|
m.target_db_uuid = kw.get("target_db_uuid", "tgt-uuid")
|
|
m.source_db_name = kw.get("source_db_name", "Source DB")
|
|
m.target_db_name = kw.get("target_db_name", "Target DB")
|
|
m.engine = kw.get("engine", "postgresql")
|
|
return m
|
|
|
|
|
|
def _make_client(overrides: dict | None = None) -> TestClient:
|
|
from src.api.routes.mappings import router
|
|
from src.core.database import get_db
|
|
from src.dependencies import get_config_manager, get_current_user
|
|
from src.schemas.auth import User, RoleSchema
|
|
|
|
app = FastAPI()
|
|
app.include_router(router, prefix="/api/mappings")
|
|
|
|
mock_user = User(
|
|
id="admin-1", username="admin", email="admin@x.com",
|
|
auth_source="LOCAL",
|
|
created_at=__import__("datetime").datetime.now(),
|
|
roles=[RoleSchema(id="r1", name="Admin", description="", is_admin=True, permissions=[])],
|
|
)
|
|
|
|
app.dependency_overrides[get_db] = lambda: MagicMock()
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
app.dependency_overrides[get_config_manager] = lambda: MagicMock()
|
|
if overrides:
|
|
for dep, fn in overrides.items():
|
|
app.dependency_overrides[dep] = fn
|
|
return TestClient(app)
|
|
|
|
|
|
# ── get_mappings ──
|
|
|
|
class TestGetMappings:
|
|
"""GET /mappings"""
|
|
|
|
def test_success(self):
|
|
mock_db = MagicMock()
|
|
mock_db.query.return_value.all.return_value = [
|
|
_make_mapping(id="m1"),
|
|
_make_mapping(id="m2"),
|
|
]
|
|
|
|
from src.core.database import get_db
|
|
client = _make_client({get_db: lambda: mock_db})
|
|
resp = client.get("/api/mappings")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert len(data) == 2
|
|
assert data[0]["id"] == "m1"
|
|
|
|
def test_filter_by_source(self):
|
|
mock_db = MagicMock()
|
|
mock_filter = MagicMock()
|
|
mock_db.query.return_value.filter.return_value = mock_filter
|
|
mock_filter.filter.return_value = mock_filter
|
|
mock_filter.all.return_value = [_make_mapping(id="m1")]
|
|
|
|
from src.core.database import get_db
|
|
client = _make_client({get_db: lambda: mock_db})
|
|
resp = client.get("/api/mappings?source_env_id=env-1")
|
|
assert resp.status_code == 200
|
|
|
|
def test_filter_by_target(self):
|
|
mock_db = MagicMock()
|
|
mock_filter = MagicMock()
|
|
mock_db.query.return_value.filter.return_value = mock_filter
|
|
mock_filter.filter.return_value = mock_filter
|
|
mock_filter.all.return_value = []
|
|
|
|
from src.core.database import get_db
|
|
client = _make_client({get_db: lambda: mock_db})
|
|
resp = client.get("/api/mappings?target_env_id=env-2")
|
|
assert resp.status_code == 200
|
|
|
|
def test_empty(self):
|
|
mock_db = MagicMock()
|
|
mock_db.query.return_value.all.return_value = []
|
|
|
|
from src.core.database import get_db
|
|
client = _make_client({get_db: lambda: mock_db})
|
|
resp = client.get("/api/mappings")
|
|
assert resp.status_code == 200
|
|
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:
|
|
"""POST /mappings"""
|
|
|
|
CREATE_PAYLOAD = {
|
|
"source_env_id": "env-1",
|
|
"target_env_id": "env-2",
|
|
"source_db_uuid": "uuid-1",
|
|
"target_db_uuid": "uuid-2",
|
|
"source_db_name": "Source DB",
|
|
"target_db_name": "Target DB",
|
|
"engine": "postgresql",
|
|
}
|
|
|
|
@staticmethod
|
|
def _validated_config_and_clients():
|
|
source_env = MagicMock(id="env-1")
|
|
target_env = MagicMock(id="env-2")
|
|
config_manager = MagicMock()
|
|
config_manager.get_environment.side_effect = {
|
|
"env-1": source_env,
|
|
"env-2": target_env,
|
|
}.get
|
|
|
|
source_client = MagicMock()
|
|
source_client.get_databases_summary = AsyncMock(
|
|
return_value=[{"uuid": "uuid-1", "database_name": "Source DB"}]
|
|
)
|
|
source_client.aclose = AsyncMock()
|
|
target_client = MagicMock()
|
|
target_client.get_databases_summary = AsyncMock(
|
|
return_value=[{"uuid": "uuid-2", "database_name": "Target DB"}]
|
|
)
|
|
target_client.aclose = AsyncMock()
|
|
return config_manager, source_client, target_client
|
|
|
|
def test_create_new(self):
|
|
mock_db = MagicMock()
|
|
mock_db.query.return_value.filter.return_value.first.return_value = None
|
|
|
|
# Simulate db.refresh setting the id
|
|
def _refresh(obj):
|
|
obj.id = "map-new-1"
|
|
mock_db.refresh.side_effect = _refresh
|
|
|
|
from src.core.database import get_db
|
|
from src.dependencies import get_config_manager
|
|
|
|
config_manager, source_client, target_client = self._validated_config_and_clients()
|
|
client = _make_client({
|
|
get_db: lambda: mock_db,
|
|
get_config_manager: lambda: config_manager,
|
|
})
|
|
with patch(
|
|
"src.api.routes.mappings.AsyncSupersetClient",
|
|
side_effect=[source_client, target_client],
|
|
):
|
|
resp = client.post("/api/mappings", json=self.CREATE_PAYLOAD)
|
|
assert resp.status_code == 200
|
|
mock_db.add.assert_called_once()
|
|
mock_db.commit.assert_called_once()
|
|
|
|
def test_update_existing(self):
|
|
existing = _make_mapping()
|
|
mock_db = MagicMock()
|
|
mock_db.query.return_value.filter.return_value.first.return_value = existing
|
|
|
|
from src.core.database import get_db
|
|
from src.dependencies import get_config_manager
|
|
|
|
config_manager, source_client, target_client = self._validated_config_and_clients()
|
|
client = _make_client({
|
|
get_db: lambda: mock_db,
|
|
get_config_manager: lambda: config_manager,
|
|
})
|
|
with patch(
|
|
"src.api.routes.mappings.AsyncSupersetClient",
|
|
side_effect=[source_client, target_client],
|
|
):
|
|
resp = client.post("/api/mappings", json=self.CREATE_PAYLOAD)
|
|
assert resp.status_code == 200
|
|
mock_db.add.assert_not_called()
|
|
mock_db.commit.assert_called_once()
|
|
assert existing.target_db_uuid == "uuid-2"
|
|
assert existing.target_db_name == "Target DB"
|
|
|
|
def test_rejects_unknown_environment(self):
|
|
mock_db = MagicMock()
|
|
config_manager = MagicMock()
|
|
config_manager.get_environment.return_value = None
|
|
|
|
from src.core.database import get_db
|
|
from src.dependencies import get_config_manager
|
|
|
|
client = _make_client({
|
|
get_db: lambda: mock_db,
|
|
get_config_manager: lambda: config_manager,
|
|
})
|
|
resp = client.post("/api/mappings", json=self.CREATE_PAYLOAD)
|
|
|
|
assert resp.status_code == 400
|
|
assert resp.json()["detail"] == "Invalid source or target environment"
|
|
mock_db.add.assert_not_called()
|
|
|
|
def test_rejects_source_database_from_another_environment(self):
|
|
mock_db = MagicMock()
|
|
config_manager, source_client, target_client = self._validated_config_and_clients()
|
|
payload = {**self.CREATE_PAYLOAD, "source_db_uuid": "target-only-uuid"}
|
|
|
|
from src.core.database import get_db
|
|
from src.dependencies import get_config_manager
|
|
|
|
client = _make_client({
|
|
get_db: lambda: mock_db,
|
|
get_config_manager: lambda: config_manager,
|
|
})
|
|
with patch(
|
|
"src.api.routes.mappings.AsyncSupersetClient",
|
|
side_effect=[source_client, target_client],
|
|
):
|
|
resp = client.post("/api/mappings", json=payload)
|
|
|
|
assert resp.status_code == 400
|
|
assert "source environment" in resp.json()["detail"]
|
|
mock_db.add.assert_not_called()
|
|
|
|
def test_rejects_target_database_from_another_environment(self):
|
|
mock_db = MagicMock()
|
|
config_manager, source_client, target_client = self._validated_config_and_clients()
|
|
payload = {**self.CREATE_PAYLOAD, "target_db_uuid": "source-only-uuid"}
|
|
|
|
from src.core.database import get_db
|
|
from src.dependencies import get_config_manager
|
|
|
|
client = _make_client({
|
|
get_db: lambda: mock_db,
|
|
get_config_manager: lambda: config_manager,
|
|
})
|
|
with patch(
|
|
"src.api.routes.mappings.AsyncSupersetClient",
|
|
side_effect=[source_client, target_client],
|
|
):
|
|
resp = client.post("/api/mappings", json=payload)
|
|
|
|
assert resp.status_code == 400
|
|
assert "target environment" in resp.json()["detail"]
|
|
mock_db.add.assert_not_called()
|
|
|
|
|
|
# ── suggest_mappings_api ──
|
|
|
|
class TestSuggestMappingsApi:
|
|
"""POST /mappings/suggest"""
|
|
|
|
def test_success(self):
|
|
mock_config = MagicMock()
|
|
mock_suggestions = [{"source": "db1", "target": "db2", "score": 0.95}]
|
|
|
|
from src.dependencies import get_config_manager
|
|
with patch("src.services.mapping_service.MappingService") as MockMappingSvc:
|
|
instance = MagicMock()
|
|
instance.get_suggestions = AsyncMock(return_value=mock_suggestions)
|
|
MockMappingSvc.return_value = instance
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.post("/api/mappings/suggest", json={
|
|
"source_env_id": "env-1", "target_env_id": "env-2",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json() == mock_suggestions
|
|
|
|
def test_error(self):
|
|
mock_config = MagicMock()
|
|
from src.dependencies import get_config_manager
|
|
with patch("src.services.mapping_service.MappingService") as MockMappingSvc:
|
|
instance = MagicMock()
|
|
instance.get_suggestions = AsyncMock(side_effect=RuntimeError("suggestion failed"))
|
|
MockMappingSvc.return_value = instance
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.post("/api/mappings/suggest", json={
|
|
"source_env_id": "env-1", "target_env_id": "env-2",
|
|
})
|
|
assert resp.status_code == 500
|
|
# #region TestApplyDatasetMetadata [C:3] [TYPE Module] [SEMANTICS test,mappings,metadata,apply]
|
|
# @BRIEF Tests for apply_dataset_metadata PUT endpoint — doc application to datasets.
|
|
# @TEST_EDGE: missing_env_id -> 400 "env_id is required"
|
|
# @TEST_EDGE: env_not_found -> 404 "Environment not found"
|
|
# @TEST_EDGE: success_with_changes -> 200 status=success, dataset updated
|
|
# @TEST_EDGE: no_changes -> 200 status=no_changes, dataset not updated
|
|
# @TEST_EDGE: superset_error -> 500 propagated as HTTPException
|
|
class TestApplyDatasetMetadata:
|
|
"""PUT /mappings/datasets/{id}/metadata"""
|
|
|
|
BASE_PAYLOAD = {
|
|
"env_id": "env-1",
|
|
"description": "Dataset description",
|
|
"columns": {"col_a": "Column A verbose"},
|
|
}
|
|
|
|
MOCK_DATASET_RESPONSE = {
|
|
"result": {
|
|
"id": 42,
|
|
"table_name": "test_table",
|
|
"schema": "public",
|
|
"database": {"id": 1},
|
|
"owners": [{"id": 1}],
|
|
"columns": [
|
|
{"column_name": "col_a", "id": 1, "verbose_name": "Old Name", "type": "string"},
|
|
{"column_name": "col_b", "id": 2, "verbose_name": "Col B", "type": "integer"},
|
|
],
|
|
"metrics": [],
|
|
"extra": None,
|
|
}
|
|
}
|
|
|
|
# #region Test.Api.TestMissingEnvId [C:2] [TYPE Function]
|
|
# @BRIEF Request without env_id returns 400.
|
|
def test_missing_env_id(self):
|
|
mock_config = MagicMock()
|
|
mock_config.get_environment.return_value = None
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
|
|
resp = client.put("/api/mappings/datasets/42/metadata", json={
|
|
"description": "test",
|
|
"columns": {},
|
|
})
|
|
assert resp.status_code == 400
|
|
assert "env_id" in resp.json()["detail"]
|
|
# #endregion Test.Api.TestMissingEnvId
|
|
|
|
# #region Test.Api.TestEnvNotFound [C:2] [TYPE Function]
|
|
# @BRIEF Unknown env_id returns 404.
|
|
def test_env_not_found(self):
|
|
mock_config = MagicMock()
|
|
mock_config.get_environment.return_value = None
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
|
|
resp = client.put("/api/mappings/datasets/42/metadata", json=self.BASE_PAYLOAD)
|
|
assert resp.status_code == 404
|
|
assert "not found" in resp.json()["detail"].lower()
|
|
# #endregion Test.Api.TestEnvNotFound
|
|
|
|
# #region Test.Api.TestSuccessWithChanges [C:2] [TYPE Function]
|
|
# @BRIEF Valid request updates dataset and returns success.
|
|
def test_success_with_changes(self):
|
|
mock_config = MagicMock()
|
|
mock_config.get_environment.return_value = MagicMock()
|
|
|
|
mock_superset_client = AsyncMock()
|
|
mock_superset_client.authenticate = AsyncMock()
|
|
mock_superset_client.get_dataset.return_value = self.MOCK_DATASET_RESPONSE
|
|
mock_superset_client.update_dataset.return_value = {"result": "ok"}
|
|
|
|
from src.dependencies import get_config_manager
|
|
with patch("src.core.superset_client.SupersetClient", return_value=mock_superset_client):
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.put("/api/mappings/datasets/42/metadata", json=self.BASE_PAYLOAD)
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["status"] == "success"
|
|
assert data["dataset_id"] == 42
|
|
|
|
# Verify update_dataset was called with the description
|
|
assert mock_superset_client.update_dataset.call_count >= 1
|
|
_call_args = mock_superset_client.update_dataset.call_args
|
|
# update_dataset(dataset_id, data) -> positional args
|
|
payload = _call_args[0][1]
|
|
assert payload["description"] == "Dataset description"
|
|
# col_a verbose_name should be updated
|
|
col_a = next(c for c in payload["columns"] if c["column_name"] == "col_a")
|
|
assert col_a["verbose_name"] == "Column A verbose"
|
|
# col_b should keep its original verbose_name (not in request.columns)
|
|
col_b = next(c for c in payload["columns"] if c["column_name"] == "col_b")
|
|
assert col_b["verbose_name"] == "Col B"
|
|
# #endregion Test.Api.TestSuccessWithChanges
|
|
|
|
# #region Test.Api.TestNoChanges [C:2] [TYPE Function]
|
|
# @BRIEF When same values are sent, status returns no_changes and update_dataset is NOT called.
|
|
def test_no_changes(self):
|
|
mock_config = MagicMock()
|
|
mock_config.get_environment.return_value = MagicMock()
|
|
|
|
mock_superset_client = AsyncMock()
|
|
mock_superset_client.authenticate = AsyncMock()
|
|
mock_superset_client.get_dataset.return_value = self.MOCK_DATASET_RESPONSE
|
|
|
|
from src.dependencies import get_config_manager
|
|
with patch("src.core.superset_client.SupersetClient", return_value=mock_superset_client):
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
# Send columns that match existing verbose_name values
|
|
resp = client.put("/api/mappings/datasets/42/metadata", json={
|
|
"env_id": "env-1",
|
|
"columns": {"col_b": "Col B"}, # already matches
|
|
})
|
|
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["status"] == "no_changes"
|
|
mock_superset_client.update_dataset.assert_not_called()
|
|
# #endregion Test.Api.TestNoChanges
|
|
|
|
# #region Test.Api.TestSupersetErrorPropagated [C:2] [TYPE Function]
|
|
# @BRIEF When SupersetClient fails, endpoint returns 500 with error detail.
|
|
def test_superset_error_propagated(self):
|
|
mock_config = MagicMock()
|
|
mock_config.get_environment.return_value = MagicMock()
|
|
|
|
mock_superset_client = AsyncMock()
|
|
mock_superset_client.authenticate = AsyncMock()
|
|
mock_superset_client.get_dataset.side_effect = RuntimeError("Superset API unavailable")
|
|
|
|
from src.dependencies import get_config_manager
|
|
with patch("src.core.superset_client.SupersetClient", return_value=mock_superset_client):
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.put("/api/mappings/datasets/42/metadata", json=self.BASE_PAYLOAD)
|
|
|
|
assert resp.status_code == 500
|
|
assert "Superset API unavailable" in resp.json()["detail"]
|
|
# #endregion Test.Api.TestSupersetErrorPropagated
|
|
# #endregion TestApplyDatasetMetadata
|
|
# #endregion Test.Api.Mappings
|