- 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
166 lines
7.2 KiB
Python
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
|