- 10 translate plugin test files (100% coverage on 12 modules) - assistant/handler tools: 85+ tests covering dispatch, registry, resolvers, routes, llm_planner, 13 tool handlers - clean release: artifact_catalog_loader, mappers, approval, publication tests - API routes: translate_helpers, validation_service extensions, datasets to 100% - notifications: providers/service tests - services: profile_preference_service - docs/orthogonal-test-report.md — full speckit.tests audit - Fixes: 3 git_base async mock failures, 4 assistant handler permission-check patches - .gitignore: coverage artifacts
597 lines
24 KiB
Python
597 lines
24 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
|
|
|
|
|
|
# ── Autouse fixture: prevent GitService from touching real filesystem ──
|
|
# When run in the full suite, a prior test writes global config to the DB
|
|
# (AppConfigRecord with storage.root_path), causing ResourceService.__init__
|
|
# → GitService.__init__ → _resolve_base_path → _ensure_base_path_exists
|
|
# → mkdir /app/storage/repositories → PermissionError.
|
|
# This fixture patches GitService so every test gets an isolated Mock.
|
|
# See ADR-0013 and tests/api/ contamination issue for context.
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _patch_git_service(monkeypatch: pytest.MonkeyPatch):
|
|
"""Replace GitService with MagicMock to prevent filesystem access."""
|
|
import src.services.resource_service as rs_mod
|
|
original = rs_mod.GitService
|
|
rs_mod.GitService = MagicMock # type: ignore[misc]
|
|
yield
|
|
rs_mod.GitService = original
|
|
|
|
|
|
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
|