Files
ss-tools/backend/tests/services/test_resource_service.py
busya 442596a3dc test(services): add unit tests for profile_preference, resource, validation_service
- profile_preference_service: 19 tests, 100% coverage (CRUD, validation, encryption,
  DTO conversion with mocked AuthRepository and EncryptionManager)
- resource_service: 50 tests, 100% coverage (dashboard/dataset enrichment with
  git/task status, pagination, activity summary, datetime normalization)
- validation_service: 63 tests across 3 files, 97% coverage (provider validation,
  environment validation, source resolution, run/record conversion, trigger_run,
  create/update/delete tasks, list/filter runs, get_run_detail)
2026-06-10 14:57:11 +03:00

579 lines
23 KiB
Python

# #region Test.ResourceService [C:3] [TYPE Module] [SEMANTICS test,resource,git,task,dashboard,dataset]
# # @BRIEF Tests for resource_service.py — dashboard/dataset enrichment with git status and task status.
# @RELATION BINDS_TO -> [ResourceServiceModule]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
from datetime import UTC, datetime
import pytest
def make_task(**overrides):
"""Create a mock Task object with configurable attributes."""
task = MagicMock()
defaults = {
"id": "task-1",
"plugin_id": "llm_dashboard_validation",
"status": "COMPLETED",
"params": {"dashboard_id": "42", "environment_id": "env-1"},
"result": {"status": "PASS"},
"started_at": datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC),
"finished_at": datetime(2026, 1, 1, 12, 5, 0, tzinfo=UTC),
"created_at": datetime(2026, 1, 1, 11, 0, 0, tzinfo=UTC),
}
for k, v in {**defaults, **overrides}.items():
setattr(task, k, v)
return task
class TestNormalizeTaskStatus:
"""_normalize_task_status — uppercase, strip enum prefix."""
def test_none_returns_empty(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._normalize_task_status(None) == ""
def test_plain_string(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._normalize_task_status("completed") == "COMPLETED"
def test_enum_value(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
mock_enum = MagicMock()
mock_enum.value = "RUNNING"
assert svc._normalize_task_status(mock_enum) == "RUNNING"
def test_with_enum_prefix(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._normalize_task_status("TaskStatus.RUNNING") == "RUNNING"
class TestNormalizeValidationStatus:
"""_normalize_validation_status — PASS/FAIL/WARN/UNKNOWN."""
def test_none_returns_none(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._normalize_validation_status(None) is None
def test_pass(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._normalize_validation_status("PASS") == "PASS"
def test_fail(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._normalize_validation_status("FAIL") == "FAIL"
def test_warn(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._normalize_validation_status("warn") == "WARN"
def test_unknown(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._normalize_validation_status("UNKNOWN") == "UNKNOWN"
def test_other_defaults_unknown(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._normalize_validation_status("invalid_status") == "UNKNOWN"
class TestNormalizeDatetime:
"""_normalize_datetime_for_compare — UTC-aware comparison."""
def test_naive_converted_to_utc(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
dt = datetime(2026, 1, 1, 12, 0, 0)
result = svc._normalize_datetime_for_compare(dt)
assert result.tzinfo is not None
assert result == dt.replace(tzinfo=UTC)
def test_aware_converted_to_utc(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
from datetime import timezone
dt = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
result = svc._normalize_datetime_for_compare(dt)
assert result == dt
def test_non_datetime_returns_min(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
result = svc._normalize_datetime_for_compare("not-a-date")
assert result == datetime.min.replace(tzinfo=UTC)
class TestExtractFromTask:
"""_extract_resource_name_from_task and _extract_resource_type_from_task."""
def test_name_from_params(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(params={"resource_name": "Sales Dashboard"})
assert svc._extract_resource_name_from_task(task) == "Sales Dashboard"
def test_name_fallback(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(params={})
task.id = "task-99"
assert svc._extract_resource_name_from_task(task) == "Task task-99"
def test_type_from_params(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(params={"resource_type": "dashboard"})
assert svc._extract_resource_type_from_task(task) == "dashboard"
def test_type_default(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(params={})
assert svc._extract_resource_type_from_task(task) == "unknown"
class TestGetLastTaskForResource:
"""_get_last_task_for_resource — most recent task by created_at."""
def test_no_tasks_returns_none(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._get_last_task_for_resource("dataset-42", None) is None
def test_empty_list_returns_none(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._get_last_task_for_resource("dataset-42", []) is None
def test_no_matching_tasks(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(params={"resource_id": "dataset-other"})
assert svc._get_last_task_for_resource("dataset-42", [task]) is None
def test_returns_latest_task(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
old = make_task(
id="task-old",
params={"resource_id": "dataset-42"},
created_at=datetime(2026, 1, 1, 10, 0, 0, tzinfo=UTC),
)
new = make_task(
id="task-new",
params={"resource_id": "dataset-42"},
created_at=datetime(2026, 1, 2, 10, 0, 0, tzinfo=UTC),
)
result = svc._get_last_task_for_resource("dataset-42", [old, new])
assert result is not None
assert result["task_id"] == "task-new"
def test_task_without_created_at_uses_min(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task_nodate = make_task(
id="task-nodate",
params={"resource_id": "dataset-42"},
)
del task_nodate.created_at # remove created_at
task_nodate.started_at = None
task_nodate.finished_at = None
task_dated = make_task(
id="task-dated",
params={"resource_id": "dataset-42"},
created_at=datetime(2026, 1, 1, 10, 0, 0, tzinfo=UTC),
)
result = svc._get_last_task_for_resource("dataset-42", [task_nodate, task_dated])
assert result["task_id"] == "task-dated"
class TestGetActivitySummary:
"""get_activity_summary — active count and recent tasks."""
def test_empty_tasks(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
result = svc.get_activity_summary([])
assert result["active_count"] == 0
assert result["recent_tasks"] == []
def test_active_task_count(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
running = make_task(id="t1", params={}, status="RUNNING")
waiting = make_task(id="t2", params={}, status="WAITING_INPUT")
completed = make_task(id="t3", params={}, status="COMPLETED")
result = svc.get_activity_summary([running, waiting, completed])
assert result["active_count"] == 2
def test_recent_tasks_limit(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
tasks = [
make_task(id=f"t{i}", params={}, status="COMPLETED",
created_at=datetime(2026, 1, i + 1, 0, 0, 0, tzinfo=UTC))
for i in range(10)
]
result = svc.get_activity_summary(tasks)
assert len(result["recent_tasks"]) == 5
def test_recent_tasks_sorted(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
old = make_task(
id="t-old", params={"resource_name": "Old", "resource_type": "dashboard"},
status="COMPLETED",
created_at=datetime(2026, 1, 1, 0, 0, 0, tzinfo=UTC),
)
new = make_task(
id="t-new", params={"resource_name": "New", "resource_type": "chart"},
status="FAILED",
created_at=datetime(2026, 1, 5, 0, 0, 0, tzinfo=UTC),
)
result = svc.get_activity_summary([old, new])
assert result["recent_tasks"][0]["task_id"] == "t-new"
class TestGetLastLlmTaskForDashboard:
"""_get_last_llm_task_for_dashboard — filter by plugin_id, dashboard_id, env."""
def test_no_tasks_returns_none(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
assert svc._get_last_llm_task_for_dashboard(42, "env-1", None) is None
def test_wrong_plugin_skipped(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(plugin_id="other_plugin", params={"dashboard_id": "42"})
assert svc._get_last_llm_task_for_dashboard(42, "env-1", [task]) is None
def test_wrong_dashboard_skipped(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(params={"dashboard_id": "99"})
assert svc._get_last_llm_task_for_dashboard(42, "env-1", [task]) is None
def test_wrong_env_skipped(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(params={"dashboard_id": "42", "environment_id": "env-2"})
assert svc._get_last_llm_task_for_dashboard(42, "env-1", [task]) is None
def test_matching_task_returns_summary(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(params={"dashboard_id": "42", "environment_id": "env-1"})
result = svc._get_last_llm_task_for_dashboard(42, "env-1", [task])
assert result is not None
assert result["task_id"] == "task-1"
assert result["status"] == "COMPLETED"
assert result["validation_status"] == "PASS"
def test_prefers_latest(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
old = make_task(
id="old", params={"dashboard_id": "42", "environment_id": "env-1"},
started_at=datetime(2026, 1, 1, 10, 0, 0, tzinfo=UTC),
)
new = make_task(
id="new", params={"dashboard_id": "42", "environment_id": "env-1"},
started_at=datetime(2026, 1, 2, 10, 0, 0, tzinfo=UTC),
)
result = svc._get_last_llm_task_for_dashboard(42, "env-1", [old, new])
assert result["task_id"] == "new"
def test_null_result_handled(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(result=None, params={"dashboard_id": "42", "environment_id": "env-1"})
result = svc._get_last_llm_task_for_dashboard(42, "env-1", [task])
assert result is not None
assert result["validation_status"] is None
def test_env_id_none_matches_any(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(params={"dashboard_id": "42"})
result = svc._get_last_llm_task_for_dashboard(42, None, [task])
assert result is not None
def test_env_match_with_env_key_fallback(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
task = make_task(params={"dashboard_id": "42", "env": "env-1"})
result = svc._get_last_llm_task_for_dashboard(42, "env-1", [task])
assert result is not None
class TestGetLastLlmTaskValidationStatusFallback:
"""_get_last_llm_task_for_dashboard — validation status fallback logic."""
def test_decisive_task_used_when_latest_unknown(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
unknown = make_task(
id="latest", params={"dashboard_id": "42", "environment_id": "env-1"},
result={"status": "UNKNOWN"},
started_at=datetime(2026, 1, 2, 10, 0, 0, tzinfo=UTC),
)
decisive = make_task(
id="decisive", params={"dashboard_id": "42", "environment_id": "env-1"},
result={"status": "FAIL"},
started_at=datetime(2026, 1, 1, 10, 0, 0, tzinfo=UTC),
)
result = svc._get_last_llm_task_for_dashboard(42, "env-1", [unknown, decisive])
assert result["task_id"] == "latest"
assert result["validation_status"] == "FAIL"
class TestGetGitStatusForDashboard:
"""_get_git_status_for_dashboard — git repo status enrichment."""
def test_no_repo(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
svc.git_service = MagicMock()
svc.git_service.get_repo.return_value = None
result = svc._get_git_status_for_dashboard(42)
assert result["sync_status"] == "NO_REPO"
assert result["has_repo"] is False
def test_repo_error(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
svc.git_service = MagicMock()
svc.git_service.get_repo.side_effect = Exception("Git error")
result = svc._get_git_status_for_dashboard(42)
assert result["sync_status"] == "NO_REPO"
def test_git_operations_error(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
repo = MagicMock()
repo.active_branch.name = "main"
repo.is_dirty.side_effect = Exception("Dirty check failed")
svc.git_service = MagicMock()
svc.git_service.get_repo.return_value = repo
result = svc._get_git_status_for_dashboard(42)
assert result["sync_status"] == "ERROR"
assert result["has_repo"] is True
def test_clean_repo(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
repo = MagicMock()
repo.active_branch.name = "main"
repo.is_dirty.return_value = False
svc.git_service = MagicMock()
svc.git_service.get_repo.return_value = repo
# Mock gitpython's iter_commits functionality
repo.iter_commits.return_value = []
result = svc._get_git_status_for_dashboard(42)
assert result["sync_status"] == "OK"
assert result["has_repo"] is True
assert result["branch"] == "main"
def test_dirty_repo(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
repo = MagicMock()
repo.active_branch.name = "main"
repo.is_dirty.return_value = True
svc.git_service = MagicMock()
svc.git_service.get_repo.return_value = repo
repo.iter_commits.return_value = []
result = svc._get_git_status_for_dashboard(42)
assert result["sync_status"] == "DIFF"
assert result["has_changes_for_commit"] is True
@patch("src.services.resource_service.get_superset_client")
class TestGetDashboardsWithStatus:
"""get_dashboards_with_status — enrich dashboards with git/task status."""
@pytest.fixture
def service(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
svc._get_git_status_for_dashboard = MagicMock(return_value={"sync_status": "OK"})
svc._get_last_llm_task_for_dashboard = MagicMock(return_value={"status": "PASS"})
return svc
@pytest.mark.asyncio
async def test_enriches_dashboards(self, mock_client_fn, service):
mock_client = AsyncMock()
mock_client.get_dashboards_summary.return_value = [
{"id": 42, "dashboard_title": "Sales"},
{"id": 99, "dashboard_title": "Marketing"},
]
mock_client_fn.return_value = mock_client
env = MagicMock()
env.id = "env-1"
result = await service.get_dashboards_with_status(env, tasks=[], include_git_status=True)
assert len(result) == 2
assert result[0]["git_status"] == {"sync_status": "OK"}
assert result[0]["last_task"] == {"status": "PASS"}
@pytest.mark.asyncio
async def test_include_git_status_false(self, mock_client_fn, service):
mock_client = AsyncMock()
mock_client.get_dashboards_summary.return_value = [{"id": 42}]
mock_client_fn.return_value = mock_client
env = MagicMock()
env.id = "env-1"
result = await service.get_dashboards_with_status(env, tasks=[], include_git_status=False)
assert result[0]["git_status"] is None
@patch("src.services.resource_service.get_superset_client")
class TestGetDashboardsPageWithStatus:
"""get_dashboards_page_with_status — paginated enrichment."""
@pytest.fixture
def service(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
svc._get_git_status_for_dashboard = MagicMock(return_value={"sync_status": "OK"})
svc._get_last_llm_task_for_dashboard = MagicMock(return_value={"status": "PASS"})
return svc
@pytest.mark.asyncio
async def test_paginated_result(self, mock_client_fn, service):
mock_client = AsyncMock()
mock_client.get_dashboards_summary_page.return_value = (50, [
{"id": 1, "dashboard_title": "A"},
{"id": 2, "dashboard_title": "B"},
])
mock_client_fn.return_value = mock_client
env = MagicMock()
env.id = "env-1"
result = await service.get_dashboards_page_with_status(
env, tasks=[], page=1, page_size=10
)
assert result["total"] == 50
assert len(result["dashboards"]) == 2
assert result["total_pages"] == 5
@pytest.mark.asyncio
async def test_zero_total_pages(self, mock_client_fn, service):
mock_client = AsyncMock()
mock_client.get_dashboards_summary_page.return_value = (0, [])
mock_client_fn.return_value = mock_client
env = MagicMock()
env.id = "env-1"
result = await service.get_dashboards_page_with_status(
env, tasks=[], page=1, page_size=10
)
assert result["total"] == 0
assert result["total_pages"] == 1
assert result["dashboards"] == []
@pytest.mark.asyncio
async def test_with_search(self, mock_client_fn, service):
mock_client = AsyncMock()
mock_client.get_dashboards_summary_page.return_value = (1, [{"id": 42}])
mock_client_fn.return_value = mock_client
env = MagicMock()
env.id = "env-1"
result = await service.get_dashboards_page_with_status(
env, tasks=[], page=1, page_size=20, search="Sales",
)
mock_client.get_dashboards_summary_page.assert_called_with(
page=1, page_size=20, search="Sales", require_slug=False,
)
assert len(result["dashboards"]) == 1
@pytest.mark.asyncio
async def test_include_git_status_false(self, mock_client_fn, service):
mock_client = AsyncMock()
mock_client.get_dashboards_summary_page.return_value = (1, [{"id": 1}])
mock_client_fn.return_value = mock_client
env = MagicMock()
env.id = "env-1"
result = await service.get_dashboards_page_with_status(
env, page=1, page_size=10, include_git_status=False,
)
assert result["dashboards"][0]["git_status"] is None
@patch("src.services.resource_service.get_superset_client")
class TestGetDatasetsWithStatus:
"""get_datasets_with_status — enrich datasets with task status and linked counts."""
@pytest.fixture
def service(self):
from src.services.resource_service import ResourceService
svc = ResourceService()
svc._get_last_task_for_resource = MagicMock(return_value={"status": "COMPLETED"})
return svc
@pytest.mark.asyncio
async def test_enriches_datasets(self, mock_client_fn, service):
mock_client = AsyncMock()
mock_client.get_datasets_summary.return_value = [
{"id": 1, "table_name": "users"},
{"id": 2, "table_name": "orders"},
]
mock_client.get_dataset_linked_dashboard_count = AsyncMock(return_value=3)
mock_client_fn.return_value = mock_client
env = MagicMock()
env.id = "env-1"
result = await service.get_datasets_with_status(env, tasks=[])
assert len(result) == 2
assert result[0]["last_task"] == {"status": "COMPLETED"}
assert result[0]["linked_dashboard_count"] == 3
assert result[1]["linked_dashboard_count"] == 3
@pytest.mark.asyncio
async def test_linked_count_failure_returns_zero(self, mock_client_fn, service):
mock_client = AsyncMock()
mock_client.get_datasets_summary.return_value = [{"id": 1, "table_name": "users"}]
mock_client.get_dataset_linked_dashboard_count = AsyncMock(
side_effect=TimeoutError("Superset timeout")
)
mock_client_fn.return_value = mock_client
env = MagicMock()
env.id = "env-1"
result = await service.get_datasets_with_status(env, tasks=[])
assert result[0]["linked_dashboard_count"] == 0
@pytest.mark.asyncio
async def test_no_tasks_returns_none_last_task(self, mock_client_fn, service):
mock_client = AsyncMock()
mock_client.get_datasets_summary.return_value = [{"id": 1, "table_name": "users"}]
mock_client.get_dataset_linked_dashboard_count = AsyncMock(return_value=0)
mock_client_fn.return_value = mock_client
service._get_last_task_for_resource.return_value = None
env = MagicMock()
env.id = "env-1"
result = await service.get_datasets_with_status(env, tasks=None)
assert result[0]["last_task"] is None
# #endregion Test.ResourceService