Files
ss-tools/backend/tests/api/test_dashboard_projection.py

342 lines
14 KiB
Python

# #region Test.Api.DashboardProjection [C:3] [TYPE Module] [SEMANTICS test,dashboard,projection]
# @BRIEF Unit tests for dashboard projection/profile helpers.
# @RELATION BINDS_TO -> [DashboardProjection]
# @TEST_EDGE: owner_normalization
# @TEST_EDGE: profile_filter_binding
# @TEST_EDGE: task_matching
import sys
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
_src = str(Path(__file__).resolve().parent.parent.parent / "src")
if _src not in sys.path:
sys.path.insert(0, _src)
# ── _normalize_actor_alias_token ──
class TestNormalizeActorAliasToken:
def test_none_value(self):
from src.api.routes.dashboards._projection import _normalize_actor_alias_token
assert _normalize_actor_alias_token(None) is None
def test_empty_string(self):
from src.api.routes.dashboards._projection import _normalize_actor_alias_token
assert _normalize_actor_alias_token("") is None
def test_whitespace(self):
from src.api.routes.dashboards._projection import _normalize_actor_alias_token
assert _normalize_actor_alias_token(" ") is None
def test_normalized(self):
from src.api.routes.dashboards._projection import _normalize_actor_alias_token
assert _normalize_actor_alias_token(" Alice ") == "alice"
# ── _normalize_owner_display_token ──
class TestNormalizeOwnerDisplayToken:
def test_none(self):
from src.api.routes.dashboards._projection import _normalize_owner_display_token
assert _normalize_owner_display_token(None) is None
def test_dict_with_username(self):
from src.api.routes.dashboards._projection import _normalize_owner_display_token
assert _normalize_owner_display_token({"username": "Alice"}) == "Alice"
def test_dict_with_full_name(self):
from src.api.routes.dashboards._projection import _normalize_owner_display_token
assert _normalize_owner_display_token({"full_name": "Alice Smith"}) == "Alice Smith"
def test_dict_empty(self):
from src.api.routes.dashboards._projection import _normalize_owner_display_token
assert _normalize_owner_display_token({}) is None
def test_string(self):
from src.api.routes.dashboards._projection import _normalize_owner_display_token
assert _normalize_owner_display_token("Alice") == "Alice"
def test_int(self):
from src.api.routes.dashboards._projection import _normalize_owner_display_token
assert _normalize_owner_display_token(42) is None
# ── _normalize_dashboard_owner_values ──
class TestNormalizeDashboardOwnerValues:
def test_none(self):
from src.api.routes.dashboards._projection import _normalize_dashboard_owner_values
assert _normalize_dashboard_owner_values(None) is None
def test_list_of_dicts(self):
from src.api.routes.dashboards._projection import _normalize_dashboard_owner_values
result = _normalize_dashboard_owner_values([
{"username": "Alice"},
{"username": "Bob"},
])
assert result == ["Alice", "Bob"]
def test_single_dict(self):
from src.api.routes.dashboards._projection import _normalize_dashboard_owner_values
result = _normalize_dashboard_owner_values({"username": "Charlie"})
assert result == ["Charlie"]
def test_duplicates_removed(self):
from src.api.routes.dashboards._projection import _normalize_dashboard_owner_values
result = _normalize_dashboard_owner_values([
{"username": "Alice"},
{"username": "Alice"},
])
assert result == ["Alice"]
# ── _project_dashboard_response_items ──
class TestProjectDashboardResponseItems:
def test_owners_normalized(self):
from src.api.routes.dashboards._projection import _project_dashboard_response_items
dashboards = [
{"id": 1, "title": "Main", "owners": [{"username": "Alice"}]},
]
result = _project_dashboard_response_items(dashboards)
assert result[0]["owners"] == ["Alice"]
def test_empty_list(self):
from src.api.routes.dashboards._projection import _project_dashboard_response_items
assert _project_dashboard_response_items([]) == []
# ── _get_profile_filter_binding ──
class TestGetProfileFilterBinding:
def test_uses_get_dashboard_filter_binding(self):
from src.api.routes.dashboards._projection import _get_profile_filter_binding
profile_service = MagicMock()
profile_service.get_dashboard_filter_binding.return_value = {
"superset_username": "alice",
"superset_username_normalized": "alice",
"show_only_my_dashboards": True,
"show_only_slug_dashboards": False,
}
result = _get_profile_filter_binding(profile_service, MagicMock())
assert result["superset_username"] == "alice"
assert result["show_only_my_dashboards"] is True
def test_uses_get_my_preference_fallback(self):
from src.api.routes.dashboards._projection import _get_profile_filter_binding
profile_service = MagicMock()
# Remove the attribute so hasattr returns False, triggering fallback to get_my_preference
del profile_service.get_dashboard_filter_binding
pref = MagicMock()
pref.preference.superset_username = "bob"
pref.preference.superset_username_normalized = "bob"
pref.preference.show_only_my_dashboards = True
pref.preference.show_only_slug_dashboards = False
profile_service.get_my_preference.return_value = pref
result = _get_profile_filter_binding(profile_service, MagicMock())
assert result["superset_username"] == "bob"
def test_no_methods_returns_defaults(self):
from src.api.routes.dashboards._projection import _get_profile_filter_binding
profile_service = MagicMock(spec=[]) # no relevant methods
result = _get_profile_filter_binding(profile_service, MagicMock())
assert result["superset_username"] is None
assert result["show_only_my_dashboards"] is False
# ── _resolve_profile_actor_aliases ──
class TestResolveProfileActorAliases:
@pytest.mark.asyncio
async def test_empty_username(self):
from src.api.routes.dashboards._projection import _resolve_profile_actor_aliases
result = await _resolve_profile_actor_aliases(MagicMock(), "")
assert result == []
@pytest.mark.asyncio
async def test_lookup_success(self):
from src.api.routes.dashboards._projection import _resolve_profile_actor_aliases
mock_adapter = AsyncMock()
mock_adapter.get_users_page.return_value = {
"items": [
{"username": "alice", "display_name": "Alice Smith"},
]
}
mock_env = MagicMock()
mock_env.id = "env-1"
with patch("src.api.routes.dashboards._projection.AsyncSupersetClient") as MockClient, \
patch("src.api.routes.dashboards._projection.SupersetAccountLookupAdapter") as MockAdapter:
MockAdapter.return_value = mock_adapter
result = await _resolve_profile_actor_aliases(mock_env, "alice")
assert "alice" in result
assert "alice smith" in result
@pytest.mark.asyncio
async def test_lookup_exception(self):
from src.api.routes.dashboards._projection import _resolve_profile_actor_aliases
mock_env = MagicMock()
mock_env.id = "env-1"
with patch("src.api.routes.dashboards._projection.AsyncSupersetClient") as MockClient, \
patch("src.api.routes.dashboards._projection.SupersetAccountLookupAdapter") as MockAdapter:
MockAdapter.side_effect = Exception("Connection error")
result = await _resolve_profile_actor_aliases(mock_env, "alice")
assert result == ["alice"]
# ── _matches_dashboard_actor_aliases ──
class TestMatchesDashboardActorAliases:
def test_matches(self):
from src.api.routes.dashboards._projection import _matches_dashboard_actor_aliases
profile_service = MagicMock()
profile_service.matches_dashboard_actor.return_value = True
assert _matches_dashboard_actor_aliases(profile_service, ["alice"], [], "bob") is True
def test_no_match(self):
from src.api.routes.dashboards._projection import _matches_dashboard_actor_aliases
profile_service = MagicMock()
profile_service.matches_dashboard_actor.return_value = False
assert _matches_dashboard_actor_aliases(profile_service, ["alice"], [], None) is False
def test_first_match_short_circuits(self):
from src.api.routes.dashboards._projection import _matches_dashboard_actor_aliases
profile_service = MagicMock()
profile_service.matches_dashboard_actor.side_effect = [False, True]
assert _matches_dashboard_actor_aliases(profile_service, ["alice", "bob"], [], None) is True
assert profile_service.matches_dashboard_actor.call_count == 2
# ── _task_matches_dashboard ──
class TestTaskMatchesDashboard:
def test_llm_validation_matches(self):
from src.api.routes.dashboards._projection import _task_matches_dashboard
task = MagicMock()
task.plugin_id = "llm_dashboard_validation"
task.params = {"dashboard_id": 42, "environment_id": "env-1"}
assert _task_matches_dashboard(task, 42, "env-1") is True
def test_llm_validation_wrong_dashboard(self):
from src.api.routes.dashboards._projection import _task_matches_dashboard
task = MagicMock()
task.plugin_id = "llm_dashboard_validation"
task.params = {"dashboard_id": 99}
assert _task_matches_dashboard(task, 42, None) is False
def test_llm_validation_no_env(self):
from src.api.routes.dashboards._projection import _task_matches_dashboard
task = MagicMock()
task.plugin_id = "llm_dashboard_validation"
task.params = {"dashboard_id": 42}
assert _task_matches_dashboard(task, 42, None) is True
def test_backup_matches(self):
from src.api.routes.dashboards._projection import _task_matches_dashboard
task = MagicMock()
task.plugin_id = "superset-backup"
task.params = {"dashboard_ids": [1, 42, 3], "environment_id": "env-1"}
assert _task_matches_dashboard(task, 42, "env-1") is True
def test_backup_dashboards_key(self):
from src.api.routes.dashboards._projection import _task_matches_dashboard
task = MagicMock()
task.plugin_id = "superset-backup"
task.params = {"dashboards": [42], "env": "env-1"}
assert _task_matches_dashboard(task, 42, "env-1") is True
def test_unrelated_plugin(self):
from src.api.routes.dashboards._projection import _task_matches_dashboard
task = MagicMock()
task.plugin_id = "some-other-plugin"
assert _task_matches_dashboard(task, 42, "env-1") is False
def test_backup_wrong_dashboard(self):
from src.api.routes.dashboards._projection import _task_matches_dashboard
task = MagicMock()
task.plugin_id = "superset-backup"
task.params = {"dashboard_ids": [1, 2], "environment_id": "env-1"}
assert _task_matches_dashboard(task, 42, "env-1") is False
def test_backup_wrong_env(self):
from src.api.routes.dashboards._projection import _task_matches_dashboard
task = MagicMock()
task.plugin_id = "superset-backup"
task.params = {"dashboard_ids": [42], "environment_id": "env-1"}
assert _task_matches_dashboard(task, 42, "env-2") is False
def test_backup_no_env_id(self):
"""Backup task match with no env_id returns True (line 246)."""
from src.api.routes.dashboards._projection import _task_matches_dashboard
task = MagicMock()
task.plugin_id = "superset-backup"
task.params = {"dashboard_ids": [42]}
assert _task_matches_dashboard(task, 42, None) is True
# ── _resolve_profile_actor_aliases — edge cases ──
class TestResolveProfileActorAliasesExtended:
"""Additional _resolve_profile_actor_aliases coverage (lines 171, 176-179)."""
@pytest.mark.asyncio
async def test_lookup_with_non_dict_items(self):
"""Non-dict items in lookup results are skipped (line 171)."""
from src.api.routes.dashboards._projection import _resolve_profile_actor_aliases
mock_adapter = AsyncMock()
mock_adapter.get_users_page.return_value = {
"items": [
"not a dict",
{"username": "alice", "display_name": "Alice Smith"},
]
}
mock_env = MagicMock()
mock_env.id = "env-1"
with patch("src.api.routes.dashboards._projection.AsyncSupersetClient") as MockClient, \
patch("src.api.routes.dashboards._projection.SupersetAccountLookupAdapter") as MockAdapter:
MockAdapter.return_value = mock_adapter
result = await _resolve_profile_actor_aliases(mock_env, "alice")
assert "alice" in result
assert "alice smith" in result
@pytest.mark.asyncio
async def test_lookup_no_exact_match_first_item_fallback(self):
"""When no exact username match, first dict item becomes matched_item (lines 176-179)."""
from src.api.routes.dashboards._projection import _resolve_profile_actor_aliases
mock_adapter = AsyncMock()
mock_adapter.get_users_page.return_value = {
"items": [
{"username": "bob", "display_name": "Bob Smith"},
]
}
mock_env = MagicMock()
mock_env.id = "env-1"
with patch("src.api.routes.dashboards._projection.AsyncSupersetClient") as MockClient, \
patch("src.api.routes.dashboards._projection.SupersetAccountLookupAdapter") as MockAdapter:
MockAdapter.return_value = mock_adapter
result = await _resolve_profile_actor_aliases(mock_env, "alice")
# alice not found, bob's display_name added as alias
assert "bob smith" in result
assert "alice" in result
# #endregion Test.Api.DashboardProjection