# #region Test.Api.ReportsRoutes [C:3] [TYPE Module] [SEMANTICS test,reports,api] # @BRIEF Tests for reports.py — list reports and get report detail. # @RELATION BINDS_TO -> [ReportsRouter] # @TEST_EDGE: parse_invalid_task_type -> 400 # @TEST_EDGE: parse_invalid_status -> 400 # @TEST_EDGE: general_query_exception -> 400 # @TEST_EDGE: report_not_found -> 404 import os os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests") import sys from pathlib import Path from unittest.mock import MagicMock, patch, AsyncMock import pytest from fastapi import FastAPI from fastapi.testclient import TestClient _src = str(Path(__file__).resolve().parent.parent.parent / "src") if _src not in sys.path: sys.path.insert(0, _src) def _make_client(overrides: dict | None = None, raise_server_exceptions: bool = True) -> TestClient: from src.api.routes.reports import router from src.dependencies import get_clean_release_repository, get_task_manager, get_current_user, has_permission from src.schemas.auth import User, RoleSchema app = FastAPI() app.include_router(router) mock_user = User( id="user-1", username="admin", email="admin@x.com", auth_source="LOCAL", created_at=__import__("datetime").datetime.now(), roles=[RoleSchema(id="r1", name="Admin", description="", permissions=[])], ) defaults = { get_task_manager: lambda: MagicMock(), get_clean_release_repository: lambda: MagicMock(), get_current_user: lambda: mock_user, has_permission: lambda *a, **kw: lambda: None, } if overrides: defaults.update(overrides) for dep, mock_fn in defaults.items(): app.dependency_overrides[dep] = mock_fn return TestClient(app, raise_server_exceptions=raise_server_exceptions) # ── _parse_csv_enum_list ── class TestParseCsvEnumList: """_parse_csv_enum_list — parse comma-separated values into enum list.""" def test_none_returns_empty(self): from src.api.routes.reports import _parse_csv_enum_list from src.models.report import TaskType assert _parse_csv_enum_list(None, TaskType, "task_types") == [] def test_empty_string_returns_empty(self): from src.api.routes.reports import _parse_csv_enum_list from src.models.report import TaskType assert _parse_csv_enum_list("", TaskType, "task_types") == [] def test_valid_values_parsed(self): from src.api.routes.reports import _parse_csv_enum_list from src.models.report import TaskType result = _parse_csv_enum_list("backup, migration", TaskType, "task_types") assert TaskType.BACKUP in result assert TaskType.MIGRATION in result def test_invalid_values_raise_400(self): from fastapi import HTTPException from src.api.routes.reports import _parse_csv_enum_list from src.models.report import TaskType with pytest.raises(HTTPException) as exc: _parse_csv_enum_list("INVALID_TYPE", TaskType, "task_types") assert exc.value.status_code == 400 # ── list_reports ── class TestListReports: """GET /api/reports""" def test_list_success(self): from src.models.report import ReportCollection, ReportQuery with patch("src.api.routes.reports.ReportsService") as MockService: instance = MockService.return_value instance.list_reports.return_value = ReportCollection( items=[], total=0, page=1, page_size=20, has_next=False, applied_filters=ReportQuery(), ) client = _make_client() resp = client.get("/api/reports?page=1&page_size=20") assert resp.status_code == 200 data = resp.json() assert "items" in data assert "total" in data def test_list_with_filters(self): from src.models.report import ReportCollection, ReportQuery with patch("src.api.routes.reports.ReportsService") as MockService: instance = MockService.return_value instance.list_reports.return_value = ReportCollection( items=[], total=0, page=1, page_size=20, has_next=False, applied_filters=ReportQuery(search="test", sort_by="updated_at", sort_order="asc"), ) client = _make_client() resp = client.get("/api/reports?page=1&page_size=20&search=test&sort_by=updated_at&sort_order=asc") assert resp.status_code == 200 def test_list_invalid_task_type_400(self): client = _make_client() resp = client.get("/api/reports?task_types=INVALID") assert resp.status_code == 400 def test_list_invalid_status_400(self): client = _make_client() resp = client.get("/api/reports?statuses=INVALID") assert resp.status_code == 400 def test_list_service_exception_500(self): with patch("src.api.routes.reports.ReportsService") as MockService: instance = MockService.return_value instance.list_reports.side_effect = ValueError("Internal error") client = _make_client(raise_server_exceptions=False) resp = client.get("/api/reports?page=1&page_size=20") # Exception from service call is outside try-except -> 500 assert resp.status_code == 500 def test_list_invalid_page_size_400(self): mock_task_manager = MagicMock() mock_clean_repo = MagicMock() from src.dependencies import get_task_manager, get_clean_release_repository client = _make_client({ get_task_manager: lambda: mock_task_manager, get_clean_release_repository: lambda: mock_clean_repo, }) # page_size=0 is rejected by Query(ge=1) -> FastAPI 422 resp = client.get("/api/reports?page_size=0") assert resp.status_code == 422 # ── get_report_detail ── class TestGetReportDetail: """GET /api/reports/{report_id}""" def test_detail_success(self): from src.models.report import ReportDetailView, TaskReport, ReportStatus, TaskType dt = __import__("datetime").datetime.now() with patch("src.api.routes.reports.ReportsService") as MockService: instance = MockService.return_value instance.get_report_detail.return_value = ReportDetailView( report=TaskReport( report_id="report-1", task_id="task-1", task_type=TaskType.BACKUP, status=ReportStatus.SUCCESS, updated_at=dt, summary="Backup completed successfully", ), diagnostics=None, next_actions=[], ) client = _make_client() resp = client.get("/api/reports/report-1") assert resp.status_code == 200 data = resp.json() assert data["report"]["report_id"] == "report-1" def test_detail_not_found(self): mock_task_manager = MagicMock() mock_clean_repo = MagicMock() from src.dependencies import get_task_manager, get_clean_release_repository client = _make_client({ get_task_manager: lambda: mock_task_manager, get_clean_release_repository: lambda: mock_clean_repo, }) with patch("src.api.routes.reports.ReportsService") as MockService: instance = MockService.return_value instance.get_report_detail.return_value = None resp = client.get("/api/reports/report-missing") assert resp.status_code == 404 assert "REPORT_NOT_FOUND" in resp.text # #endregion Test.Api.ReportsRoutes