from __future__ import annotations import asyncio import sys import tempfile import unittest from pathlib import Path import httpx sys.path.insert(0, str(Path(__file__).resolve().parents[3])) from backend.app.dependencies import get_task_service from backend.app.db.task_repository import TaskRepository from backend.app.main import app from backend.app.services.task_service import TaskService class TasksApiGoldenTest(unittest.TestCase): def setUp(self) -> None: self.temp_dir = tempfile.TemporaryDirectory() self.db_path = str(Path(self.temp_dir.name) / "tasks.db") self.repo = TaskRepository(self.db_path) self.service = TaskService(self.repo) async def _override_task_service() -> TaskService: return self.service app.dependency_overrides[get_task_service] = _override_task_service def tearDown(self) -> None: app.dependency_overrides.clear() self.temp_dir.cleanup() def _get(self, url: str) -> httpx.Response: async def _run() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: return await client.get(url) return asyncio.run(_run()) def _insert_task( self, *, task_id: str, operation: str, status: str, source: str, destination: str, created_at: str, started_at: str | None = None, finished_at: str | None = None, done_bytes: int | None = None, total_bytes: int | None = None, done_items: int | None = None, total_items: int | None = None, current_item: str | None = None, failed_item: str | None = None, error_code: str | None = None, error_message: str | None = None, ) -> None: self.repo.insert_task_for_testing( { "id": task_id, "operation": operation, "status": status, "source": source, "destination": destination, "done_bytes": done_bytes, "total_bytes": total_bytes, "done_items": done_items, "total_items": total_items, "current_item": current_item, "failed_item": failed_item, "error_code": error_code, "error_message": error_message, "created_at": created_at, "started_at": started_at, "finished_at": finished_at, } ) def test_get_tasks_empty_list(self) -> None: response = self._get("/api/tasks") self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"items": []}) def test_get_tasks_list_shape(self) -> None: self._insert_task( task_id="task-old", operation="copy", status="completed", source="storage1/a.txt", destination="storage2/a.txt", created_at="2026-03-10T10:00:00Z", finished_at="2026-03-10T10:00:05Z", ) self._insert_task( task_id="task-new", operation="move", status="running", source="storage1/b.txt", destination="storage2/b.txt", created_at="2026-03-10T10:01:00Z", ) response = self._get("/api/tasks") self.assertEqual(response.status_code, 200) self.assertEqual( response.json(), { "items": [ { "id": "task-new", "operation": "move", "status": "running", "source": "storage1/b.txt", "destination": "storage2/b.txt", "created_at": "2026-03-10T10:01:00Z", "finished_at": None, }, { "id": "task-old", "operation": "copy", "status": "completed", "source": "storage1/a.txt", "destination": "storage2/a.txt", "created_at": "2026-03-10T10:00:00Z", "finished_at": "2026-03-10T10:00:05Z", }, ] }, ) def test_get_task_detail_queued(self) -> None: self._insert_task( task_id="task-queued", operation="copy", status="queued", source="storage1/a.txt", destination="storage2/a.txt", created_at="2026-03-10T10:00:00Z", ) response = self._get("/api/tasks/task-queued") self.assertEqual(response.status_code, 200) self.assertEqual( response.json(), { "id": "task-queued", "operation": "copy", "status": "queued", "source": "storage1/a.txt", "destination": "storage2/a.txt", "done_bytes": None, "total_bytes": None, "done_items": None, "total_items": None, "current_item": None, "failed_item": None, "error_code": None, "error_message": None, "created_at": "2026-03-10T10:00:00Z", "started_at": None, "finished_at": None, }, ) def test_get_task_detail_running(self) -> None: self._insert_task( task_id="task-running", operation="move", status="running", source="storage1/a.txt", destination="storage2/a.txt", created_at="2026-03-10T10:00:00Z", started_at="2026-03-10T10:00:01Z", done_bytes=1024, total_bytes=2048, done_items=1, total_items=2, current_item="storage1/a.txt", ) response = self._get("/api/tasks/task-running") self.assertEqual(response.status_code, 200) body = response.json() self.assertEqual(body["status"], "running") self.assertEqual(body["done_bytes"], 1024) self.assertEqual(body["total_bytes"], 2048) self.assertEqual(body["current_item"], "storage1/a.txt") def test_get_task_detail_completed(self) -> None: self._insert_task( task_id="task-completed", operation="copy", status="completed", source="storage1/a.txt", destination="storage2/a.txt", created_at="2026-03-10T10:00:00Z", started_at="2026-03-10T10:00:01Z", finished_at="2026-03-10T10:00:03Z", done_bytes=2048, total_bytes=2048, ) response = self._get("/api/tasks/task-completed") self.assertEqual(response.status_code, 200) body = response.json() self.assertEqual(body["status"], "completed") self.assertEqual(body["finished_at"], "2026-03-10T10:00:03Z") self.assertEqual(body["error_code"], None) def test_get_task_detail_failed(self) -> None: self._insert_task( task_id="task-failed", operation="move", status="failed", source="storage1/a.txt", destination="storage2/a.txt", created_at="2026-03-10T10:00:00Z", started_at="2026-03-10T10:00:01Z", finished_at="2026-03-10T10:00:02Z", failed_item="storage1/a.txt", error_code="io_error", error_message="write failed", ) response = self._get("/api/tasks/task-failed") self.assertEqual(response.status_code, 200) body = response.json() self.assertEqual(body["status"], "failed") self.assertEqual(body["failed_item"], "storage1/a.txt") self.assertEqual(body["error_code"], "io_error") self.assertEqual(body["error_message"], "write failed") def test_get_task_detail_ready_archive_download(self) -> None: self._insert_task( task_id="task-download-ready", operation="download", status="ready", source="storage1/docs", destination="docs.zip", created_at="2026-03-10T10:00:00Z", started_at="2026-03-10T10:00:01Z", finished_at="2026-03-10T10:00:05Z", done_items=1, total_items=1, ) response = self._get("/api/tasks/task-download-ready") self.assertEqual(response.status_code, 200) body = response.json() self.assertEqual(body["operation"], "download") self.assertEqual(body["status"], "ready") self.assertEqual(body["destination"], "docs.zip") def test_get_task_not_found(self) -> None: response = self._get("/api/tasks/task-missing") self.assertEqual(response.status_code, 404) self.assertEqual( response.json(), { "error": { "code": "task_not_found", "message": "Task was not found", "details": {"task_id": "task-missing"}, } }, ) if __name__ == "__main__": unittest.main()