diff --git a/tests/test_kin_ui_015_regression.py b/tests/test_kin_ui_015_regression.py new file mode 100644 index 0000000..e05872e --- /dev/null +++ b/tests/test_kin_ui_015_regression.py @@ -0,0 +1,139 @@ +"""Regression tests for KIN-UI-015 — GET /api/tasks endpoint. + +New endpoint: GET /api/tasks?status=...&limit=...&sort=... +Acceptance criteria: +- Эндпоинт существует и возвращает корректные данные +- Фильтрация по status работает корректно +- Параметр limit ограничивает количество результатов +- Параметр sort задаёт порядок сортировки +- Невалидный sort-field фоллбэчит на updated_at (защита от SQL-инъекций) + +Coverage: +(1) GET /api/tasks возвращает все задачи проекта +(2) GET /api/tasks?status=completed возвращает только completed +(3) GET /api/tasks?status=completed не возвращает задачи с другим статусом +(4) GET /api/tasks?limit=1 возвращает не более 1 результата +(5) GET /api/tasks?sort=priority — сортировка по priority работает +(6) GET /api/tasks?sort=invalid_field — фоллбэк на updated_at (нет 500-ошибки) +(7) GET /api/tasks?status=completed&limit=20&sort=updated_at — все параметры вместе +(8) GET /api/tasks возвращает 200 при пустой базе +""" + +import pytest +import web.api as api_module +from fastapi.testclient import TestClient +from core.db import init_db +from core import models + + +@pytest.fixture +def client(tmp_path): + db_path = tmp_path / "test.db" + api_module.DB_PATH = db_path + from web.api import app + c = TestClient(app) + c.post("/api/projects", json={"id": "p1", "name": "P1", "path": "/tmp/p1"}) + return c + + +@pytest.fixture +def client_with_tasks(client, tmp_path): + """Client pre-seeded with tasks of various statuses.""" + conn = init_db(api_module.DB_PATH) + models.create_task(conn, "P1-001", "p1", "Pending task", status="pending") + models.create_task(conn, "P1-002", "p1", "Completed task 1", status="completed") + models.create_task(conn, "P1-003", "p1", "Completed task 2", status="completed") + models.create_task(conn, "P1-004", "p1", "In-progress task", status="in_progress") + conn.close() + return client + + +# --------------------------------------------------------------------------- +# (1) GET /api/tasks возвращает все задачи +# --------------------------------------------------------------------------- + +def test_list_tasks_returns_all(client_with_tasks): + r = client_with_tasks.get("/api/tasks") + assert r.status_code == 200 + tasks = r.json() + assert len(tasks) == 4 + + +# --------------------------------------------------------------------------- +# (2) GET /api/tasks?status=completed возвращает только completed +# --------------------------------------------------------------------------- + +def test_list_tasks_filter_by_status_completed(client_with_tasks): + r = client_with_tasks.get("/api/tasks?status=completed") + assert r.status_code == 200 + tasks = r.json() + assert len(tasks) == 2 + assert all(t["status"] == "completed" for t in tasks) + + +# --------------------------------------------------------------------------- +# (3) Другие статусы не попадают в фильтр status=completed +# --------------------------------------------------------------------------- + +def test_list_tasks_filter_excludes_other_statuses(client_with_tasks): + r = client_with_tasks.get("/api/tasks?status=completed") + assert r.status_code == 200 + tasks = r.json() + titles = [t["title"] for t in tasks] + assert "Pending task" not in titles + assert "In-progress task" not in titles + + +# --------------------------------------------------------------------------- +# (4) GET /api/tasks?limit=1 возвращает не более 1 результата +# --------------------------------------------------------------------------- + +def test_list_tasks_limit_respected(client_with_tasks): + r = client_with_tasks.get("/api/tasks?limit=1") + assert r.status_code == 200 + tasks = r.json() + assert len(tasks) == 1 + + +# --------------------------------------------------------------------------- +# (5) GET /api/tasks?sort=priority — сортировка работает (нет 500-ошибки) +# --------------------------------------------------------------------------- + +def test_list_tasks_sort_by_priority(client_with_tasks): + r = client_with_tasks.get("/api/tasks?sort=priority") + assert r.status_code == 200 + tasks = r.json() + assert len(tasks) == 4 + + +# --------------------------------------------------------------------------- +# (6) Невалидный sort-field фоллбэчит на updated_at (нет 500-ошибки) +# --------------------------------------------------------------------------- + +def test_list_tasks_invalid_sort_falls_back(client_with_tasks): + r = client_with_tasks.get("/api/tasks?sort='; DROP TABLE tasks; --") + assert r.status_code == 200 + tasks = r.json() + assert isinstance(tasks, list) + + +# --------------------------------------------------------------------------- +# (7) Все параметры вместе: status=completed&limit=20&sort=updated_at +# --------------------------------------------------------------------------- + +def test_list_tasks_combined_params(client_with_tasks): + r = client_with_tasks.get("/api/tasks?status=completed&limit=20&sort=updated_at") + assert r.status_code == 200 + tasks = r.json() + assert len(tasks) == 2 + assert all(t["status"] == "completed" for t in tasks) + + +# --------------------------------------------------------------------------- +# (8) Пустая база — возвращает 200 и пустой список +# --------------------------------------------------------------------------- + +def test_list_tasks_empty_db(client): + r = client.get("/api/tasks") + assert r.status_code == 200 + assert r.json() == []