Subagents delivered tests across all uncovered backend modules: Schemas (100%): agent, auth, health, profile, settings, validation Services (98-100%): auth, profile, health, llm, mapping, resource, security, git, superset_lookup, sql_table_extractor, rbac API routes (new): auth, admin, health, environments, plugins, dashboards (helpers, projection, actions, listing), git (config, deps, env, helpers) Clean Release (100%): DTO, facade, policy_engine, stages, repos, preparation, source_isolation, compliance Git services: base, remote_providers Agent module: app, run, middleware, langgraph_setup Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching Reports: normalizer, report_service, type_profiles Notifications: service, providers Also: - .gitignore: add .coverage, *.cover, coverage-* dirs - src/schemas/auth.py: fix AD group DN regex (comma in CN=...) - Remove co-located src/services/__tests__/ (caused pytest module collision)
343 lines
14 KiB
Python
343 lines
14 KiB
Python
# #region Test.Schemas.Profile [C:2] [TYPE Module] [SEMANTICS test,schema,profile]
|
|
# @BRIEF Tests for schemas/profile.py — ProfilePermissionState, ProfileSecuritySummary,
|
|
# ProfilePreference, ProfilePreferenceUpdateRequest, ProfilePreferenceResponse,
|
|
# SupersetAccountLookupRequest, SupersetAccountCandidate, SupersetAccountLookupResponse.
|
|
# @RELATION BINDS_TO -> [ProfileSchemas]
|
|
# @TEST_EDGE: missing_field -> optional fields default correctly
|
|
# @TEST_EDGE: invalid_type -> field constraints (ge/le) reject bad values
|
|
# @TEST_EDGE: serialization -> round-trip via model_dump/model_validate
|
|
|
|
from datetime import datetime, timezone
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from src.schemas.profile import (
|
|
ProfilePermissionState,
|
|
ProfilePreference,
|
|
ProfilePreferenceResponse,
|
|
ProfilePreferenceUpdateRequest,
|
|
ProfileSecuritySummary,
|
|
SupersetAccountCandidate,
|
|
SupersetAccountLookupRequest,
|
|
SupersetAccountLookupResponse,
|
|
)
|
|
|
|
|
|
class TestProfilePermissionState:
|
|
"""ProfilePermissionState — key and allowed flag."""
|
|
|
|
def test_minimal(self):
|
|
p = ProfilePermissionState(key="dashboard", allowed=True)
|
|
assert p.key == "dashboard"
|
|
assert p.allowed is True
|
|
|
|
def test_not_allowed(self):
|
|
p = ProfilePermissionState(key="dataset", allowed=False)
|
|
assert p.allowed is False
|
|
|
|
def test_serialize_roundtrip(self):
|
|
p = ProfilePermissionState(key="dashboard:write", allowed=False)
|
|
data = p.model_dump()
|
|
restored = ProfilePermissionState.model_validate(data)
|
|
assert restored.key == "dashboard:write"
|
|
assert restored.allowed is False
|
|
|
|
|
|
class TestProfileSecuritySummary:
|
|
"""ProfileSecuritySummary — security snapshot defaults."""
|
|
|
|
def test_defaults(self):
|
|
s = ProfileSecuritySummary()
|
|
assert s.read_only is True
|
|
assert s.auth_source is None
|
|
assert s.current_role is None
|
|
assert s.role_source is None
|
|
assert s.roles == []
|
|
assert s.permissions == []
|
|
|
|
def test_with_values(self):
|
|
perms = [ProfilePermissionState(key="dashboard", allowed=True)]
|
|
s = ProfileSecuritySummary(
|
|
read_only=False, auth_source="ADFS", current_role="Admin",
|
|
role_source="ADFS", roles=["Admin"], permissions=perms,
|
|
)
|
|
assert s.read_only is False
|
|
assert s.current_role == "Admin"
|
|
assert len(s.permissions) == 1
|
|
|
|
def test_serialize_roundtrip(self):
|
|
s = ProfileSecuritySummary(auth_source="LOCAL", roles=["Viewer"])
|
|
data = s.model_dump()
|
|
restored = ProfileSecuritySummary.model_validate(data)
|
|
assert restored.auth_source == "LOCAL"
|
|
assert "Viewer" in restored.roles
|
|
|
|
|
|
class TestProfilePreference:
|
|
"""ProfilePreference — full preference model with defaults."""
|
|
|
|
def test_minimal(self):
|
|
ts = datetime.now(timezone.utc)
|
|
p = ProfilePreference(user_id="u-1", created_at=ts, updated_at=ts)
|
|
assert p.user_id == "u-1"
|
|
assert p.superset_username is None
|
|
assert p.superset_username_normalized is None
|
|
assert p.show_only_my_dashboards is False
|
|
assert p.show_only_slug_dashboards is True
|
|
assert p.start_page == "dashboards"
|
|
assert p.auto_open_task_drawer is True
|
|
assert p.dashboards_table_density == "comfortable"
|
|
assert p.notify_on_fail is True
|
|
assert p.has_git_personal_access_token is False
|
|
|
|
def test_with_all_fields(self):
|
|
ts = datetime.now(timezone.utc)
|
|
p = ProfilePreference(
|
|
user_id="u-1",
|
|
superset_username="jdoe",
|
|
superset_username_normalized="jdoe",
|
|
show_only_my_dashboards=True,
|
|
show_only_slug_dashboards=False,
|
|
git_username="jdoe",
|
|
git_email="j@d.com",
|
|
has_git_personal_access_token=True,
|
|
git_personal_access_token_masked="jd***oe",
|
|
start_page="datasets",
|
|
auto_open_task_drawer=False,
|
|
dashboards_table_density="compact",
|
|
telegram_id="@jdoe",
|
|
email_address="j@d.com",
|
|
notify_on_fail=False,
|
|
created_at=ts,
|
|
updated_at=ts,
|
|
)
|
|
assert p.show_only_my_dashboards is True
|
|
assert p.git_username == "jdoe"
|
|
assert p.start_page == "datasets"
|
|
assert p.dashboards_table_density == "compact"
|
|
assert p.notify_on_fail is False
|
|
|
|
def test_serialize_roundtrip(self):
|
|
ts = datetime.now(timezone.utc)
|
|
p = ProfilePreference(user_id="u-1", created_at=ts, updated_at=ts)
|
|
data = p.model_dump()
|
|
restored = ProfilePreference.model_validate(data)
|
|
assert restored.user_id == "u-1"
|
|
assert restored.start_page == "dashboards"
|
|
|
|
def test_from_attributes(self):
|
|
class FakeORM:
|
|
user_id = "u-1"
|
|
superset_username = "admin"
|
|
superset_username_normalized = "admin"
|
|
show_only_my_dashboards = False
|
|
show_only_slug_dashboards = True
|
|
git_username = None
|
|
git_email = None
|
|
has_git_personal_access_token = False
|
|
git_personal_access_token_masked = None
|
|
start_page = "dashboards"
|
|
auto_open_task_drawer = True
|
|
dashboards_table_density = "comfortable"
|
|
telegram_id = None
|
|
email_address = None
|
|
notify_on_fail = True
|
|
created_at = datetime.now(timezone.utc)
|
|
updated_at = datetime.now(timezone.utc)
|
|
p = ProfilePreference.model_validate(FakeORM())
|
|
assert p.user_id == "u-1"
|
|
|
|
|
|
class TestProfilePreferenceUpdateRequest:
|
|
"""ProfilePreferenceUpdateRequest — all fields optional."""
|
|
|
|
def test_defaults(self):
|
|
req = ProfilePreferenceUpdateRequest()
|
|
assert req.superset_username is None
|
|
assert req.show_only_my_dashboards is None
|
|
assert req.show_only_slug_dashboards is None
|
|
assert req.git_username is None
|
|
assert req.git_email is None
|
|
assert req.git_personal_access_token is None
|
|
assert req.start_page is None
|
|
assert req.auto_open_task_drawer is None
|
|
assert req.dashboards_table_density is None
|
|
assert req.telegram_id is None
|
|
assert req.email_address is None
|
|
assert req.notify_on_fail is None
|
|
|
|
def test_partial_update(self):
|
|
req = ProfilePreferenceUpdateRequest(superset_username="jdoe", show_only_my_dashboards=True)
|
|
assert req.superset_username == "jdoe"
|
|
assert req.show_only_my_dashboards is True
|
|
|
|
def test_start_page_literals(self):
|
|
for page in ["dashboards", "datasets", "reports", "reports-logs"]:
|
|
req = ProfilePreferenceUpdateRequest(start_page=page)
|
|
assert req.start_page == page
|
|
|
|
def test_invalid_start_page_raises(self):
|
|
with pytest.raises(ValidationError):
|
|
ProfilePreferenceUpdateRequest(start_page="invalid")
|
|
|
|
def test_density_literals(self):
|
|
for density in ["compact", "comfortable", "free"]:
|
|
req = ProfilePreferenceUpdateRequest(dashboards_table_density=density)
|
|
assert req.dashboards_table_density == density
|
|
|
|
def test_invalid_density_raises(self):
|
|
with pytest.raises(ValidationError):
|
|
ProfilePreferenceUpdateRequest(dashboards_table_density="wide")
|
|
|
|
def test_serialize_roundtrip(self):
|
|
req = ProfilePreferenceUpdateRequest(superset_username="admin", email_address="a@b.com")
|
|
data = req.model_dump()
|
|
restored = ProfilePreferenceUpdateRequest.model_validate(data)
|
|
assert restored.superset_username == "admin"
|
|
assert restored.email_address == "a@b.com"
|
|
|
|
|
|
class TestProfilePreferenceResponse:
|
|
"""ProfilePreferenceResponse — response envelope with preference + security."""
|
|
|
|
def test_defaults(self):
|
|
ts = datetime.now(timezone.utc)
|
|
pref = ProfilePreference(user_id="u-1", created_at=ts, updated_at=ts)
|
|
resp = ProfilePreferenceResponse(preference=pref)
|
|
assert resp.status == "success"
|
|
assert resp.message is None
|
|
assert resp.validation_errors == []
|
|
assert resp.security.read_only is True
|
|
|
|
def test_with_errors(self):
|
|
ts = datetime.now(timezone.utc)
|
|
pref = ProfilePreference(user_id="u-1", created_at=ts, updated_at=ts)
|
|
resp = ProfilePreferenceResponse(
|
|
preference=pref, status="error", message="Failed",
|
|
validation_errors=["username required"],
|
|
)
|
|
assert resp.status == "error"
|
|
assert resp.validation_errors == ["username required"]
|
|
|
|
def test_serialize_roundtrip(self):
|
|
ts = datetime.now(timezone.utc)
|
|
pref = ProfilePreference(user_id="u-1", created_at=ts, updated_at=ts)
|
|
resp = ProfilePreferenceResponse(preference=pref)
|
|
data = resp.model_dump()
|
|
restored = ProfilePreferenceResponse.model_validate(data)
|
|
assert restored.status == "success"
|
|
assert restored.preference.user_id == "u-1"
|
|
|
|
|
|
class TestSupersetAccountLookupRequest:
|
|
"""SupersetAccountLookupRequest — pagination with constraints."""
|
|
|
|
def test_defaults(self):
|
|
req = SupersetAccountLookupRequest(environment_id="env-1")
|
|
assert req.environment_id == "env-1"
|
|
assert req.search is None
|
|
assert req.page_index == 0
|
|
assert req.page_size == 20
|
|
assert req.sort_column == "username"
|
|
assert req.sort_order == "desc"
|
|
|
|
def test_page_size_constraints(self):
|
|
req = SupersetAccountLookupRequest(environment_id="env-1", page_size=100)
|
|
assert req.page_size == 100
|
|
with pytest.raises(ValidationError):
|
|
SupersetAccountLookupRequest(environment_id="env-1", page_size=0)
|
|
with pytest.raises(ValidationError):
|
|
SupersetAccountLookupRequest(environment_id="env-1", page_size=101)
|
|
|
|
def test_page_index_ge_zero(self):
|
|
with pytest.raises(ValidationError):
|
|
SupersetAccountLookupRequest(environment_id="env-1", page_index=-1)
|
|
|
|
def test_custom_values(self):
|
|
req = SupersetAccountLookupRequest(
|
|
environment_id="env-1", search="admin", page_index=1, page_size=50,
|
|
sort_column="email", sort_order="asc",
|
|
)
|
|
assert req.search == "admin"
|
|
assert req.sort_column == "email"
|
|
assert req.sort_order == "asc"
|
|
|
|
def test_serialize_roundtrip(self):
|
|
req = SupersetAccountLookupRequest(environment_id="env-1", search="test")
|
|
data = req.model_dump()
|
|
restored = SupersetAccountLookupRequest.model_validate(data)
|
|
assert restored.search == "test"
|
|
assert restored.environment_id == "env-1"
|
|
|
|
|
|
class TestSupersetAccountCandidate:
|
|
"""SupersetAccountCandidate — projected Superset user."""
|
|
|
|
def test_minimal(self):
|
|
c = SupersetAccountCandidate(environment_id="env-1", username="admin")
|
|
assert c.environment_id == "env-1"
|
|
assert c.username == "admin"
|
|
assert c.display_name is None
|
|
assert c.email is None
|
|
assert c.is_active is None
|
|
|
|
def test_with_all_fields(self):
|
|
c = SupersetAccountCandidate(
|
|
environment_id="env-1", username="jdoe",
|
|
display_name="John Doe", email="j@d.com", is_active=True,
|
|
)
|
|
assert c.display_name == "John Doe"
|
|
assert c.is_active is True
|
|
|
|
def test_serialize_roundtrip(self):
|
|
c = SupersetAccountCandidate(environment_id="env-1", username="admin")
|
|
data = c.model_dump()
|
|
restored = SupersetAccountCandidate.model_validate(data)
|
|
assert restored.username == "admin"
|
|
|
|
|
|
class TestSupersetAccountLookupResponse:
|
|
"""SupersetAccountLookupResponse — paginated response envelope."""
|
|
|
|
def test_minimal(self):
|
|
resp = SupersetAccountLookupResponse(status="success", environment_id="env-1", page_index=0, page_size=20, total=0)
|
|
assert resp.items == []
|
|
assert resp.warning is None
|
|
|
|
def test_with_items(self):
|
|
items = [SupersetAccountCandidate(environment_id="env-1", username="admin")]
|
|
resp = SupersetAccountLookupResponse(
|
|
status="success", environment_id="env-1",
|
|
page_index=0, page_size=20, total=1, items=items,
|
|
)
|
|
assert len(resp.items) == 1
|
|
|
|
def test_degraded_status(self):
|
|
resp = SupersetAccountLookupResponse(
|
|
status="degraded", environment_id="env-1",
|
|
page_index=0, page_size=20, total=0,
|
|
warning="Cannot load accounts",
|
|
)
|
|
assert resp.status == "degraded"
|
|
assert resp.warning == "Cannot load accounts"
|
|
|
|
def test_constraints(self):
|
|
with pytest.raises(ValidationError):
|
|
SupersetAccountLookupResponse(status="success", environment_id="env-1", page_index=0, page_size=0, total=0)
|
|
with pytest.raises(ValidationError):
|
|
SupersetAccountLookupResponse(status="success", environment_id="env-1", page_index=0, page_size=101, total=0)
|
|
with pytest.raises(ValidationError):
|
|
SupersetAccountLookupResponse(status="success", environment_id="env-1", page_index=-1, page_size=20, total=0)
|
|
with pytest.raises(ValidationError):
|
|
SupersetAccountLookupResponse(status="success", environment_id="env-1", page_index=0, page_size=20, total=-1)
|
|
with pytest.raises(ValidationError):
|
|
SupersetAccountLookupResponse(status="invalid", environment_id="env-1", page_index=0, page_size=20, total=0)
|
|
|
|
def test_serialize_roundtrip(self):
|
|
resp = SupersetAccountLookupResponse(status="success", environment_id="env-1", page_index=0, page_size=20, total=5)
|
|
data = resp.model_dump()
|
|
restored = SupersetAccountLookupResponse.model_validate(data)
|
|
assert restored.total == 5
|
|
assert restored.status == "success"
|
|
# #endregion Test.Schemas.Profile
|