Files
ss-tools/backend/tests/services/test_report_service.py
busya ce0369ae5c test(backend): add 55+ test files to push coverage to 98%
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)
2026-06-15 13:55:57 +03:00

395 lines
15 KiB
Python

# #region Test.ReportsService [C:3] [TYPE Module] [SEMANTICS test,report,service,pagination,filter]
# @BRIEF Tests for services/reports/report_service.py — ReportsService.
# @RELATION BINDS_TO -> [report_service]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# #region helper factories [C:1] [TYPE Function]
def _make_task(**overrides):
from src.core.task_manager.models import Task, TaskStatus
defaults = {
"id": "task-1",
"plugin_id": "superset-migration",
"status": TaskStatus.SUCCESS,
"params": {},
"result": {"summary": "Migration complete"},
"started_at": datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC),
"finished_at": datetime(2024, 1, 1, 12, 30, 0, tzinfo=UTC),
"logs": [],
}
defaults.update(overrides)
return Task(**defaults)
def _make_service(task_manager=None, clean_release_repo=None):
from src.services.reports.report_service import ReportsService
if task_manager is None:
task_manager = MagicMock()
task_manager.get_all_tasks.return_value = []
return ReportsService(task_manager, clean_release_repo)
# #endregion helper factories
# #region test_reports_service_init [C:2] [TYPE Function]
# @BRIEF Test ReportsService initialization.
class TestReportsServiceInit:
def test_stores_task_manager(self):
tm = MagicMock()
service = _make_service(task_manager=tm)
assert service.task_manager is tm
def test_stores_clean_release_repo(self):
crr = MagicMock()
service = _make_service(clean_release_repo=crr)
assert service.clean_release_repository is crr
def test_clean_release_repo_optional(self):
service = _make_service()
assert service.clean_release_repository is None
# #endregion test_reports_service_init
# #region test_load_normalized_reports [C:2] [TYPE Function]
# @BRIEF Test _load_normalized_reports.
class TestLoadNormalizedReports:
def test_returns_empty_list_for_no_tasks(self):
tm = MagicMock()
tm.get_all_tasks.return_value = []
service = _make_service(task_manager=tm)
reports = service._load_normalized_reports()
assert reports == []
def test_normalizes_all_tasks(self):
tm = MagicMock()
task = _make_task()
tm.get_all_tasks.return_value = [task]
service = _make_service(task_manager=tm)
reports = service._load_normalized_reports()
assert len(reports) == 1
assert reports[0].report_id == task.id
# #endregion test_load_normalized_reports
# #region test_to_utc_datetime [C:2] [TYPE Function]
# @BRIEF Test _to_utc_datetime.
class TestToUtcDatetime:
def test_none_returns_none(self):
service = _make_service()
assert service._to_utc_datetime(None) is None
def test_naive_interpreted_as_utc(self):
service = _make_service()
dt = datetime(2024, 1, 1, 12, 0, 0)
result = service._to_utc_datetime(dt)
assert result.tzinfo is not None
assert result.tzinfo == UTC
def test_aware_converted_to_utc(self):
service = _make_service()
from zoneinfo import ZoneInfo
dt = datetime(2024, 1, 1, 15, 0, 0, tzinfo=ZoneInfo("Europe/Moscow"))
result = service._to_utc_datetime(dt)
assert result.tzinfo == UTC
assert result.hour == 12 # Moscow +3 -> UTC
# #endregion test_to_utc_datetime
# #region test_datetime_sort_key [C:2] [TYPE Function]
# @BRIEF Test _datetime_sort_key.
class TestDatetimeSortKey:
def test_returns_timestamp(self):
from src.models.report import TaskReport, ReportStatus, TaskType
service = _make_service()
report = MagicMock(spec=TaskReport)
report.updated_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC)
key = service._datetime_sort_key(report)
assert key == 1704110400.0
def test_returns_zero_for_none(self):
from src.models.report import TaskReport
service = _make_service()
report = MagicMock(spec=TaskReport)
report.updated_at = None
key = service._datetime_sort_key(report)
assert key == 0.0
# #endregion test_datetime_sort_key
# #region test_matches_query [C:2] [TYPE Function]
# @BRIEF Test _matches_query.
class TestMatchesQuery:
def test_no_filters_matches_all(self):
from src.models.report import ReportQuery
service = _make_service()
report = MagicMock()
query = ReportQuery()
assert service._matches_query(report, query) is True
def test_filters_by_task_type(self):
from src.models.report import ReportQuery, TaskType
service = _make_service()
report = MagicMock()
report.task_type = TaskType.MIGRATION
query = ReportQuery(task_types=[TaskType.MIGRATION])
assert service._matches_query(report, query) is True
query = ReportQuery(task_types=[TaskType.BACKUP])
assert service._matches_query(report, query) is False
def test_filters_by_status(self):
from src.models.report import ReportQuery, ReportStatus
service = _make_service()
report = MagicMock()
report.status = ReportStatus.SUCCESS
query = ReportQuery(statuses=[ReportStatus.SUCCESS])
assert service._matches_query(report, query) is True
query = ReportQuery(statuses=[ReportStatus.FAILED])
assert service._matches_query(report, query) is False
def test_filters_by_time_range(self):
from src.models.report import ReportQuery
service = _make_service()
report = MagicMock()
report.updated_at = datetime(2024, 6, 15, 12, 0, 0, tzinfo=UTC)
# Time from before -> match
query = ReportQuery(time_from=datetime(2024, 1, 1, tzinfo=UTC))
assert service._matches_query(report, query) is True
# Time from after -> no match
query = ReportQuery(time_from=datetime(2024, 12, 1, tzinfo=UTC))
assert service._matches_query(report, query) is False
# Time to before -> no match
query = ReportQuery(time_to=datetime(2024, 1, 1, tzinfo=UTC))
assert service._matches_query(report, query) is False
def test_filters_by_search(self):
from src.models.report import ReportQuery, ReportStatus, TaskType
service = _make_service()
report = MagicMock()
report.summary = "Migration completed"
report.task_type = TaskType.MIGRATION
report.status = ReportStatus.SUCCESS
query = ReportQuery(search="migration")
assert service._matches_query(report, query) is True
query = ReportQuery(search="backup")
assert service._matches_query(report, query) is False
# #endregion test_matches_query
# #region test_sort_reports [C:2] [TYPE Function]
# @BRIEF Test _sort_reports.
class TestSortReports:
def test_sort_by_updated_at_desc_default(self):
from src.models.report import ReportQuery
service = _make_service()
reports = [
MagicMock(updated_at=datetime(2024, 1, 1, tzinfo=UTC)),
MagicMock(updated_at=datetime(2024, 6, 15, tzinfo=UTC)),
MagicMock(updated_at=datetime(2024, 3, 1, tzinfo=UTC)),
]
query = ReportQuery()
sorted_reports = service._sort_reports(reports, query)
assert sorted_reports[0].updated_at > sorted_reports[-1].updated_at
def test_sort_by_status_asc(self):
from src.models.report import ReportQuery, ReportStatus
service = _make_service()
reports = [
MagicMock(status=ReportStatus.FAILED, updated_at=datetime(2024, 1, 1, tzinfo=UTC)),
MagicMock(status=ReportStatus.SUCCESS, updated_at=datetime(2024, 6, 15, tzinfo=UTC)),
]
query = ReportQuery(sort_by="status", sort_order="asc")
sorted_reports = service._sort_reports(reports, query)
assert sorted_reports[0].status.value < sorted_reports[1].status.value
def test_sort_by_task_type_desc(self):
from src.models.report import ReportQuery, TaskType
service = _make_service()
reports = [
MagicMock(task_type=TaskType.BACKUP, updated_at=datetime(2024, 1, 1, tzinfo=UTC)),
MagicMock(task_type=TaskType.MIGRATION, updated_at=datetime(2024, 6, 15, tzinfo=UTC)),
]
query = ReportQuery(sort_by="task_type", sort_order="desc")
sorted_reports = service._sort_reports(reports, query)
assert sorted_reports[0].task_type.value > sorted_reports[1].task_type.value
# #endregion test_sort_reports
# #region test_list_reports [C:2] [TYPE Function]
# @BRIEF Test list_reports with pagination.
class TestListReports:
def test_returns_empty_collection(self):
from src.models.report import ReportQuery
tm = MagicMock()
tm.get_all_tasks.return_value = []
service = _make_service(task_manager=tm)
result = service.list_reports(ReportQuery())
assert result.total == 0
assert result.items == []
assert result.has_next is False
def test_paginates_results(self):
from src.models.report import ReportQuery
tm = MagicMock()
tasks = [_make_task(id=f"task-{i}", plugin_id="test") for i in range(5)]
tm.get_all_tasks.return_value = tasks
service = _make_service(task_manager=tm)
query = ReportQuery(page=1, page_size=2)
result = service.list_reports(query)
assert result.total == 5
assert len(result.items) == 2
assert result.has_next is True
def test_last_page_no_next(self):
from src.models.report import ReportQuery
tm = MagicMock()
tasks = [_make_task(id=f"task-{i}", plugin_id="test") for i in range(3)]
tm.get_all_tasks.return_value = tasks
service = _make_service(task_manager=tm)
query = ReportQuery(page=2, page_size=2)
result = service.list_reports(query)
assert result.total == 3
assert len(result.items) == 1
assert result.has_next is False
def test_filters_applied(self):
from src.models.report import ReportQuery, TaskType
from src.core.task_manager.models import TaskStatus
tm = MagicMock()
tasks = [
_make_task(id="task-backup", plugin_id="superset-backup"),
_make_task(id="task-migration", plugin_id="superset-migration"),
]
tm.get_all_tasks.return_value = tasks
service = _make_service(task_manager=tm)
query = ReportQuery(task_types=[TaskType.BACKUP])
result = service.list_reports(query)
assert result.total == 1
assert result.items[0].task_type == TaskType.BACKUP
def test_search_filter(self):
from src.models.report import ReportQuery
from src.core.task_manager.models import TaskStatus
tm = MagicMock()
tasks = [
_make_task(id="t1", plugin_id="superset-migration", result={"summary": "Backup completed"}),
_make_task(id="t2", plugin_id="superset-backup", result={"summary": "Migration done"}),
]
tm.get_all_tasks.return_value = tasks
service = _make_service(task_manager=tm)
query = ReportQuery(search="backup")
result = service.list_reports(query)
# Both "Backup" and "backup" summaries match
assert result.total >= 1
# #endregion test_list_reports
# #region test_get_report_detail [C:2] [TYPE Function]
# @BRIEF Test get_report_detail.
class TestGetReportDetail:
def test_returns_none_for_missing(self):
tm = MagicMock()
tm.get_all_tasks.return_value = []
service = _make_service(task_manager=tm)
result = service.get_report_detail("nonexistent")
assert result is None
def test_returns_detail_for_existing(self):
tm = MagicMock()
task = _make_task(id="task-1")
tm.get_all_tasks.return_value = [task]
service = _make_service(task_manager=tm)
result = service.get_report_detail("task-1")
assert result is not None
assert result.report.report_id == "task-1"
assert len(result.timeline) == 2 # started + updated
def test_recovery_actions_for_failed(self):
from src.core.task_manager.models import TaskStatus
tm = MagicMock()
task = _make_task(id="task-1", status=TaskStatus.FAILED, result={"error": {"message": "Failed"}})
tm.get_all_tasks.return_value = [task]
service = _make_service(task_manager=tm)
result = service.get_report_detail("task-1")
assert result is not None
assert len(result.next_actions) > 0
def test_default_next_actions_for_failed(self):
from src.core.task_manager.models import TaskStatus
tm = MagicMock()
task = _make_task(id="task-1", status=TaskStatus.FAILED, result=None)
tm.get_all_tasks.return_value = [task]
service = _make_service(task_manager=tm)
result = service.get_report_detail("task-1")
assert result is not None
assert "Review task diagnostics" in result.next_actions
def test_clean_release_enriches_detail(self):
from src.core.task_manager.models import TaskStatus
from src.models.report import TaskType
# Create a task that maps to CLEAN_RELEASE type
tm = MagicMock()
task = _make_task(id="cr-1", plugin_id="clean-release-compliance",
result={"run_id": "run-1"})
tm.get_all_tasks.return_value = [task]
crr = MagicMock()
mock_run = MagicMock()
mock_run.id = "run-1"
mock_run.candidate_id = "cand-1"
mock_run.status = "completed"
mock_run.final_status = "approved"
mock_run.requested_by = "admin"
crr.get_check_run.return_value = mock_run
crr.reports = {}
service = _make_service(task_manager=tm, clean_release_repo=crr)
result = service.get_report_detail("cr-1")
assert result is not None
assert "clean_release_run" in result.diagnostics
def test_clean_release_report_linked(self):
tm = MagicMock()
task = _make_task(id="cr-2", plugin_id="clean-release-compliance",
result={"run_id": "run-2"})
tm.get_all_tasks.return_value = [task]
crr = MagicMock()
mock_run = MagicMock()
mock_run.id = "run-2"
mock_run.candidate_id = "cand-2"
mock_run.status = "completed"
mock_run.final_status = "approved"
mock_run.requested_by = "admin"
crr.get_check_run.return_value = mock_run
mock_report = MagicMock()
mock_report.id = "report-1"
mock_report.final_status = "compliant"
crr.reports = {"rep1": mock_report}
# Make the iteration find the report by run_id
mock_report.run_id = "run-2"
service = _make_service(task_manager=tm, clean_release_repo=crr)
result = service.get_report_detail("cr-2")
assert result is not None
assert "clean_release_report" in result.diagnostics
def test_diagnostics_not_provided(self):
tm = MagicMock()
task = _make_task(id="task-1", result=None)
tm.get_all_tasks.return_value = [task]
service = _make_service(task_manager=tm)
result = service.get_report_detail("task-1")
assert result is not None
assert result.diagnostics is not None
# #endregion test_get_report_detail
# #endregion Test.ReportsService