From 2dffb8f708880e47f42ca27afe55847a4b68abe0 Mon Sep 17 00:00:00 2001 From: Diego Maiorano Date: Wed, 8 Jul 2026 15:57:52 +0200 Subject: [PATCH] Fix task bugs and add search filter --- app/service.py | 24 ++++++++-------- requirements.txt | 2 ++ tests/test_api.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 tests/test_api.py diff --git a/app/service.py b/app/service.py index 8022b93..8b56424 100644 --- a/app/service.py +++ b/app/service.py @@ -10,15 +10,18 @@ def list_tasks(status: str | None = None, q: str | None = None) -> list[dict[str """Return task records, optionally filtered by status and search text.""" tasks = load_tasks() filtered: list[dict[str, Any]] = [] + query = q.casefold() if q else None for task in tasks: - # Instructor note: intentional bug for the lab. - # This uses the literal string "status" instead of the query parameter value. - if status and task["status"] != "status": + if status and task["status"] != status: continue - # Instructor note: partial feature for the lab. - # The route already accepts `q`, but search is not implemented yet. + if query: + title = task["title"].casefold() + description = task["description"].casefold() + if query not in title and query not in description: + continue + filtered.append(task) return filtered @@ -53,12 +56,9 @@ def complete_task(task_id: int) -> dict[str, Any] | None: for task in tasks: if task["id"] == task_id: - updated_task = dict(task) - updated_task["status"] = "done" - updated_task["completed_at"] = datetime.now(timezone.utc).isoformat() - - # Instructor note: intentional bug for the lab. - # The updated task is returned, but the stored list is never updated or saved. - return updated_task + task["status"] = "done" + task["completed_at"] = datetime.now(timezone.utc).isoformat() + save_tasks(tasks) + return task return None diff --git a/requirements.txt b/requirements.txt index 27d3872..af11809 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ fastapi>=0.115,<1.0 uvicorn>=0.30,<1.0 pydantic>=2.7,<3.0 +httpx>=0.27,<1.0 +pytest>=8.0,<9.0 diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..aa773cc --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from pathlib import Path +import sys + +import pytest +from fastapi.testclient import TestClient + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from app import store +from app.main import app + + +@pytest.fixture(autouse=True) +def isolated_tasks_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + source = Path(__file__).resolve().parents[1] / "data" / "tasks.json" + temp_data_file = tmp_path / "tasks.json" + temp_data_file.write_text(source.read_text(encoding="utf-8"), encoding="utf-8") + monkeypatch.setattr(store, "DATA_FILE", temp_data_file) + + +@pytest.fixture +def client() -> TestClient: + return TestClient(app) + + +def test_list_tasks_filters_by_status(client: TestClient) -> None: + response = client.get("/tasks", params={"status": "open"}) + + assert response.status_code == 200 + payload = response.json() + assert [task["id"] for task in payload] == [1, 3, 4] + assert all(task["status"] == "open" for task in payload) + + +def test_list_tasks_filters_by_query_case_insensitive(client: TestClient) -> None: + title_response = client.get("/tasks", params={"q": "LAUNCH"}) + description_response = client.get("/tasks", params={"q": "oauth"}) + + assert title_response.status_code == 200 + assert [task["id"] for task in title_response.json()] == [1] + assert description_response.status_code == 200 + assert [task["id"] for task in description_response.json()] == [2] + + +def test_list_tasks_combines_status_and_query(client: TestClient) -> None: + response = client.get("/tasks", params={"status": "open", "q": "workshop"}) + + assert response.status_code == 200 + assert [task["id"] for task in response.json()] == [3] + + +def test_list_tasks_without_query_preserves_default_behavior(client: TestClient) -> None: + response = client.get("/tasks") + + assert response.status_code == 200 + assert [task["id"] for task in response.json()] == [1, 2, 3, 4] + + +def test_complete_task_persists_status(client: TestClient) -> None: + complete_response = client.post("/tasks/1/complete") + read_response = client.get("/tasks/1") + + assert complete_response.status_code == 200 + assert complete_response.json()["status"] == "done" + assert read_response.status_code == 200 + assert read_response.json()["status"] == "done" + assert read_response.json()["completed_at"] is not None