Files
ss-tools/backend/tests/services/test_superset_lookup_service.py
busya 997329e2a5 fix(agent): resolve ModuleNotFoundError for backend, add E2E test infra
- Dockerfile.agent: fix CMD (python -m src.agent.run), use backend/requirements.txt,
  minimal COPY (only src.agent + src.core.cot_logger), add GRACE contract
- docker-compose.yml: SERVICE_TOKEN_SECRET -> SERVICE_JWT (match code)
- docker-compose.enterprise-clean.yml: same env var fix
- docker/.env.agent.example: same env var fix
- build.sh: same env var fix
- chore: semantics-testing SKILL.md, backend tests, pyproject.toml
2026-06-14 15:41:46 +03:00

166 lines
7.2 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 -> [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