Newer
Older
gnexus-tasks / backend / tests / test_tasks_api.py
"""Тесты 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