# #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 -> [SupersetLookupService] # @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_init_stores_config [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_init_stores_config # #region test_resolve_env_found [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_resolve_env_found # #region test_resolve_env_not_found [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_resolve_env_not_found # #region test_lookup_env_not_found_raises [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_lookup_env_not_found_raises # #region test_lookup_success [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_lookup_success # #region test_lookup_degraded_on_exception [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_lookup_degraded_on_exception # #region test_sort_normalization [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_sort_normalization # #region test_success_total_fallback_to_items_len [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_success_total_fallback_to_items_len # #endregion Test.SupersetLookupService