from __future__ import annotations from datetime import UTC, datetime, timedelta from uuid import uuid4 from fastapi.testclient import TestClient from app.api.tasks import get_task_repository from app.main import app from app.storage import JsonFileTaskRepository, StoredTask def make_repo(tmp_path) -> JsonFileTaskRepository: return JsonFileTaskRepository(tmp_path / "tasks.json") def make_task( *, title: str = "Task", started: bool = False, done: bool = False, ) -> StoredTask: now = datetime.now(UTC) started_at = now - timedelta(minutes=10) if started else None done_at = now - timedelta(minutes=1) if done else None return StoredTask( id=uuid4(), title=title, created_at=now - timedelta(hours=1), started_at=started_at, done_at=done_at, ) def make_client(tmp_path) -> tuple[TestClient, JsonFileTaskRepository]: repo = make_repo(tmp_path) app.dependency_overrides[get_task_repository] = lambda: repo client = TestClient(app) return client, repo def teardown_function() -> None: app.dependency_overrides.clear() def test_ui_data_board_returns_lightweight_grouped_lists(tmp_path): client, repo = make_client(tmp_path) backlog = make_task(title="Backlog title") in_progress = make_task(title="In progress title", started=True) done = make_task(title="Done title", started=True, done=True) repo.create_task(backlog) repo.create_task(in_progress) repo.create_task(done) response = client.get("/ui_data/stats/board") assert response.status_code == 200 data = response.json() assert [item["title"] for item in data["backlog_tasks"]] == ["Backlog title"] assert [item["title"] for item in data["in_progress_tasks"]] == ["In progress title"] assert [item["title"] for item in data["done_tasks"]] == ["Done title"] assert data["backlog_tasks"][0]["display_date_label"] == "Created" assert data["in_progress_tasks"][0]["display_date_label"] == "Started" assert data["done_tasks"][0]["display_date_label"] == "Done" assert "created_at" not in data["backlog_tasks"][0] assert "started_at" not in data["in_progress_tasks"][0] assert "done_at" not in data["done_tasks"][0] assert "cycle_time" not in data["done_tasks"][0] def test_ui_data_task_details_returns_full_selected_task_data(tmp_path): client, repo = make_client(tmp_path) task = make_task(title="Detailed task", started=True, done=True) repo.create_task(task) response = client.get(f"/ui_data/tasks/{task.id}") assert response.status_code == 200 data = response.json() assert data["id"] == str(task.id) assert data["title"] == "Detailed task" assert data["status"] == "done" assert datetime.fromisoformat(data["created_at"].replace("Z", "+00:00")) == task.created_at assert datetime.fromisoformat(data["started_at"].replace("Z", "+00:00")) == task.started_at assert datetime.fromisoformat(data["done_at"].replace("Z", "+00:00")) == task.done_at assert data["cycle_time"].startswith("0h 9m") def test_ui_data_task_details_rejects_invalid_uuid(tmp_path): client, _repo = make_client(tmp_path) response = client.get("/ui_data/tasks/bad-id") assert response.status_code == 400 assert response.json() == { "error": "invalid_id", "message": "Task id must be a valid UUID", } def test_ui_data_task_details_returns_not_found_for_missing_task(tmp_path): client, _repo = make_client(tmp_path) response = client.get(f"/ui_data/tasks/{uuid4()}") assert response.status_code == 404 assert response.json() == { "error": "invalid_id", "message": "Task was not found", } def test_ui_data_routes_are_hidden_from_openapi_schema(tmp_path): client, _repo = make_client(tmp_path) response = client.get("/openapi.json") assert response.status_code == 200 paths = response.json()["paths"] assert "/ui_data/stats/board" not in paths assert "/ui_data/tasks/{task_id}" not in paths