Files
ss-tools/backend/tests/services/test_superset_lookup_service.py
root 632b730fff chore: migrate GRACE-Poly anchors to hierarchical dotted naming
Systematic rename of all semantic anchors (#region, [DEF], @RELATION)
across 1400+ files — backend Python, frontend Svelte/TS, specs, docs:
- Flat anchors become Namespace.Module.Entity
- @RELATION references updated to match new anchor paths
- Zero business logic changes
2026-07-22 11:48:15 +03:00

166 lines
7.6 KiB
Python

# #region Test.SupersetLookupService [C:3] [TYPE Module] [SEMANTICS test,superset,lookup]
# @BRIEF Verify SupersetLookupService — environment resolution, success lookup, degradation fallback, sort normalization.
# @RELATION BINDS_TO -> [Services.SupersetLookupServiceClass]
# @TEST_EDGE: environment_not_found -> raises EnvironmentNotFoundError
# @TEST_EDGE: external_fail -> returns degraded response with warning
# @TEST_EDGE: invalid_sort_column -> falls back to "username"
# @TEST_EDGE: invalid_sort_order -> falls back to "desc"
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.schemas.profile import SupersetAccountLookupRequest
class _Env:
def __init__(self, env_id):
self.id = env_id
def _make_request(**overrides):
defaults = dict(
environment_id="env-1", search=None, page_index=0,
page_size=20, sort_column="username", sort_order="desc",
)
defaults.update(overrides)
req = MagicMock(spec=SupersetAccountLookupRequest)
for k, v in defaults.items():
setattr(req, k, v)
return req
def _wire_client(mock_client_cls):
client = MagicMock()
client.client = MagicMock()
mock_client_cls.return_value = client
class TestSupersetLookupService:
"""SupersetLookupService — environment-scoped Superset account lookup."""
@pytest.fixture
def config_manager(self):
cm = MagicMock()
cm.get_environments.return_value = [_Env("env-1"), _Env("env-2")]
return cm
@pytest.fixture
def service(self, config_manager):
from src.services.superset_lookup_service import SupersetLookupService
return SupersetLookupService(config_manager)
# #region Test.SupersetLookupService.TestInitStoresConfig [C:2] [TYPE Function]
# @BRIEF Constructor stores config_manager reference.
def test_init_stores_config(self, config_manager):
from src.services.superset_lookup_service import SupersetLookupService
svc = SupersetLookupService(config_manager)
assert svc.config_manager is config_manager
# #endregion Test.SupersetLookupService.TestInitStoresConfig
# #region Test.SupersetLookupService.TestResolveEnvFound [C:2] [TYPE Function]
# @BRIEF Returns environment when id matches.
def test_resolve_env_found(self, service):
env = service._resolve_environment("env-1")
assert env is not None and env.id == "env-1"
# #endregion Test.SupersetLookupService.TestResolveEnvFound
# #region Test.SupersetLookupService.TestResolveEnvNotFound [C:2] [TYPE Function]
# @BRIEF Returns None for unknown environment id.
def test_resolve_env_not_found(self, service):
assert service._resolve_environment("non-existent") is None
# #endregion Test.SupersetLookupService.TestResolveEnvNotFound
# #region Test.SupersetLookupService.TestLookupEnvNotFoundRaises [C:2] [TYPE Function]
# @BRIEF Raises EnvironmentNotFoundError when environment_id is unknown.
@pytest.mark.asyncio
async def test_lookup_env_not_found_raises(self, service):
from src.services.superset_lookup_service import EnvironmentNotFoundError
with pytest.raises(EnvironmentNotFoundError, match="non-existent"):
await service.lookup_superset_accounts(MagicMock(), _make_request(environment_id="non-existent"))
# #endregion Test.SupersetLookupService.TestLookupEnvNotFoundRaises
# #region Test.SupersetLookupService.TestLookupSuccess [C:2] [TYPE Function]
# @BRIEF Happy path: adapter returns items → success response with candidates.
@pytest.mark.asyncio
async def test_lookup_success(self, service):
adapter = AsyncMock()
adapter.get_users_page.return_value = {
"items": [{"environment_id": "env-1", "username": "admin", "display_name": "Admin", "email": "a@x.com"}],
"total": 1,
}
with patch("src.services.superset_lookup_service.AsyncSupersetClient") as mc, \
patch("src.services.superset_lookup_service.SupersetAccountLookupAdapter", return_value=adapter):
_wire_client(mc)
resp = await service.lookup_superset_accounts(MagicMock(id="u1"), _make_request())
assert resp.status == "success"
assert resp.environment_id == "env-1"
assert resp.total == 1
assert len(resp.items) == 1
assert resp.items[0].username == "admin"
assert resp.warning is None
# #endregion Test.SupersetLookupService.TestLookupSuccess
# #region Test.SupersetLookupService.TestLookupDegradedOnException [C:2] [TYPE Function]
# @BRIEF External failure → degraded response with warning, empty items.
@pytest.mark.asyncio
async def test_lookup_degraded_on_exception(self, service):
with patch("src.services.superset_lookup_service.AsyncSupersetClient", side_effect=ConnectionError("down")):
resp = await service.lookup_superset_accounts(MagicMock(id="u1"), _make_request())
assert resp.status == "degraded"
assert resp.total == 0
assert resp.items == []
assert "Cannot load" in resp.warning
# #endregion Test.SupersetLookupService.TestLookupDegradedOnException
# #region Test.SupersetLookupService.TestSortNormalization [C:2] [TYPE Function]
# @BRIEF Invalid sort_column → "username"; invalid sort_order → "desc"; None → defaults.
@pytest.mark.asyncio
async def test_sort_normalization(self, service):
adapter = AsyncMock()
adapter.get_users_page.return_value = {"items": [], "total": 0}
cases = [
({"sort_column": "invalid_col", "sort_order": "desc"}, "username", "desc"),
({"sort_column": "username", "sort_order": "invalid"}, "username", "desc"),
({"sort_column": None, "sort_order": None}, "username", "desc"),
]
for overrides, expected_col, expected_order in cases:
adapter.reset_mock()
with patch("src.services.superset_lookup_service.AsyncSupersetClient") as mc, \
patch("src.services.superset_lookup_service.SupersetAccountLookupAdapter", return_value=adapter):
_wire_client(mc)
await service.lookup_superset_accounts(MagicMock(id="u1"), _make_request(**overrides))
kw = adapter.get_users_page.call_args.kwargs
assert kw["sort_column"] == expected_col, f"Failed for overrides={overrides}"
assert kw["sort_order"] == expected_order, f"Failed for overrides={overrides}"
# #endregion Test.SupersetLookupService.TestSortNormalization
# #region Test.SupersetLookupService.TestSuccessTotalFallbackToItemsLen [C:2] [TYPE Function]
# @BRIEF When adapter omits 'total', response total falls back to len(items).
@pytest.mark.asyncio
async def test_success_total_fallback_to_items_len(self, service):
adapter = AsyncMock()
adapter.get_users_page.return_value = {
"items": [
{"environment_id": "env-1", "username": "a"},
{"environment_id": "env-1", "username": "b"},
],
}
with patch("src.services.superset_lookup_service.AsyncSupersetClient") as mc, \
patch("src.services.superset_lookup_service.SupersetAccountLookupAdapter", return_value=adapter):
_wire_client(mc)
resp = await service.lookup_superset_accounts(MagicMock(id="u1"), _make_request())
assert resp.status == "success"
assert resp.total == 2
# #endregion Test.SupersetLookupService.TestSuccessTotalFallbackToItemsLen
# #endregion Test.SupersetLookupService