"""Тесты API M1: быстрый захват, стек, детализация, проекты, теги."""
from typing import Any
from fastapi.testclient import TestClient
def create_task(client: TestClient, title: str = "тестовая задача") -> dict[str, Any]:
resp = client.post("/api/tasks", json={"title": title})
assert resp.status_code == 200, resp.text
task_id = resp.json()["id"]
resp = client.get(f"/api/tasks/{task_id}")
assert resp.status_code == 200, resp.text
return resp.json()
def test_quick_capture_lands_in_stack(client: TestClient) -> None:
task = create_task(client, "позвонить маме")
assert task["detail_state"] == "raw"
assert task["status"] == "to_do"
assert task["title"] == "позвонить маме"
stack = client.get("/api/tasks", params={"detail_state": "raw"}).json()
assert any(t["id"] == task["id"] for t in stack)
def test_approve_moves_out_of_stack(client: TestClient) -> None:
task = create_task(client)
resp = client.post(f"/api/tasks/{task['id']}/approve")
assert resp.status_code == 200
approved = resp.json()
assert approved["detail_state"] == "approved"
assert approved["approved_at"] is not None
stack = client.get("/api/tasks", params={"detail_state": "raw"}).json()
assert all(t["id"] != task["id"] for t in stack)
def test_update_with_project_and_tags(client: TestClient) -> None:
project_id = client.post("/api/projects", json={"name": "дом"}).json()["id"]
tag_id = client.post("/api/tags", json={"name": "срочное"}).json()["id"]
task = create_task(client)
resp = client.patch(
f"/api/tasks/{task['id']}",
json={"project_id": project_id, "tag_ids": [tag_id], "priority": 3},
)
assert resp.status_code == 200, resp.text
updated = resp.json()
assert updated["project"]["id"] == project_id
assert [t["id"] for t in updated["tags"]] == [tag_id]
assert updated["priority"] == 3
def test_update_unknown_project_rejected(client: TestClient) -> None:
task = create_task(client)
resp = client.patch(f"/api/tasks/{task['id']}", json={"project_id": 99999})
assert resp.status_code == 400
def test_status_validation_and_done_at(client: TestClient) -> None:
task = create_task(client)
bad = client.patch(f"/api/tasks/{task['id']}", json={"status": "nope"})
assert bad.status_code == 422
done = client.patch(f"/api/tasks/{task['id']}", json={"status": "done"})
assert done.status_code == 200
assert done.json()["done_at"] is not None
def test_list_filter_by_priority(client: TestClient) -> None:
"""Фильтр по градациям приоритета (ТЗ 3.6): без приоритета — very_low."""
low = create_task(client, "без приоритета")
high = create_task(client, "срочная")
client.post(f"/api/tasks/{high['id']}/approve")
client.patch(f"/api/tasks/{high['id']}", json={"priority": 9})
very_low = client.get("/api/tasks", params={"priority": "very_low"}).json()
assert [t["id"] for t in very_low] == [low["id"]]
urgent = client.get("/api/tasks", params={"priority": "urgent"}).json()
assert [t["id"] for t in urgent] == [high["id"]]
bad = client.get("/api/tasks", params={"priority": "nope"})
assert bad.status_code == 422
def test_delete_task(client: TestClient) -> None:
task = create_task(client)
assert client.delete(f"/api/tasks/{task['id']}").json() == {"ok": True}
assert client.get(f"/api/tasks/{task['id']}").status_code == 404
def test_project_crud_and_duplicate_name(client: TestClient) -> None:
resp = client.post("/api/projects", json={"name": "gntodo-разработка"})
assert resp.status_code == 200
pid = resp.json()["id"]
dup = client.post("/api/projects", json={"name": "gntodo-разработка"})
assert dup.status_code == 409
resp = client.patch(f"/api/projects/{pid}", json={"relevance_status": "paused"})
assert resp.status_code == 200
assert resp.json()["relevance_status"] == "paused"
bad = client.patch(f"/api/projects/{pid}", json={"relevance_status": "nope"})
assert bad.status_code == 422
def test_project_archive_lifecycle(client: TestClient) -> None:
"""Архив (ТЗ 3.11): проект уходит в архив с задачами и возвращается."""
pid = client.post("/api/projects", json={"name": "ремонт"}).json()["id"]
task_id = create_task(client, "поклеить обои")["id"]
client.patch(f"/api/tasks/{task_id}", json={"project_id": pid})
standalone = create_task(client, "без проекта")
# в архиве пусто, в активных проект и задача видны
assert client.get("/api/projects", params={"archived": "true"}).json() == []
assert any(p["id"] == pid for p in client.get("/api/projects").json())
assert any(t["id"] == task_id for t in client.get("/api/tasks").json())
# архивируем: проект скрыт, задача с ним уходит из рабочих видов
archived = client.post(f"/api/projects/{pid}/archive")
assert archived.status_code == 200
assert archived.json()["is_archived"] is True
assert client.get("/api/projects").json() == []
archived_list = client.get("/api/projects", params={"archived": "true"}).json()
assert [p["id"] for p in archived_list] == [pid]
tasks = client.get("/api/tasks").json()
assert all(t["id"] != task_id for t in tasks)
assert any(t["id"] == standalone["id"] for t in tasks)
# но на странице проекта (фильтр по project_id) история видна
assert any(
t["id"] == task_id for t in client.get("/api/tasks", params={"project_id": pid}).json()
)
# и в выдаче задачи архивные проекты не участвуют
options = client.post("/api/tasks/suggest", json={"available_minutes": 600}).json()
assert all(t["id"] != task_id for t in options)
# восстановление возвращает всё как было
restored = client.post(f"/api/projects/{pid}/restore")
assert restored.status_code == 200
assert restored.json()["is_archived"] is False
assert any(t["id"] == task_id for t in client.get("/api/tasks").json())
def test_archive_invalid_relevance_rejected(client: TestClient) -> None:
pid = client.post("/api/projects", json={"name": "устаревший статус"}).json()["id"]
resp = client.patch(f"/api/projects/{pid}", json={"relevance_status": "archived"})
assert resp.status_code == 422
def test_tag_duplicate(client: TestClient) -> None:
assert client.post("/api/tags", json={"name": "быт"}).status_code == 200
assert client.post("/api/tags", json={"name": "быт"}).status_code == 409