"""Тесты M4: бюджет/оценка затрат, режим «3 варианта», глобальные настройки."""
from typing import Any
from fastapi.testclient import TestClient
from app.services.options import pick_options
from tests.conftest import _test_session_factory
def test_session() -> Any:
return _test_session_factory()
def test_budget_and_actual_fields_manual(client: TestClient) -> None:
tid = client.post("/api/tasks", json={"title": "Задача с бюджетом"}).json()["id"]
updated = client.patch(
f"/api/tasks/{tid}",
json={"budget_money": 5000, "cost_estimate_money": 3500, "actual_minutes": 90},
).json()
assert updated["budget_money"] == 5000
assert updated["cost_estimate_money"] == 3500
assert updated["actual_minutes"] == 90
def test_pick_options_fit_time_and_diversity(client: TestClient) -> None:
p1 = client.post("/api/projects", json={"name": "Дом"}).json()["id"]
p2 = client.post("/api/projects", json={"name": "Работа"}).json()["id"]
def approve(title: str, minutes: int | None, project_id: int | None) -> None:
tid = client.post("/api/tasks", json={"title": title}).json()["id"]
patch: dict[str, Any] = {}
if minutes is not None:
patch["estimated_minutes"] = minutes
if project_id is not None:
patch["project_id"] = project_id
client.patch(f"/api/tasks/{tid}", json=patch)
client.post(f"/api/tasks/{tid}/approve")
approve("Мелочь 30м", 30, p1)
approve("Средняя 60м", 60, p2)
approve("Большая 300м", 300, None)
approve("Чуть больше 135м", 135, None) # ровно верхняя граница окна 90±50%
approve("Без оценки", None, p1)
# приостановленный проект исключается
paused = client.post("/api/projects", json={"name": "Заморожено"}).json()["id"]
client.patch(f"/api/projects/{paused}", json={"relevance_status": "paused"})
approve("Из замороженного", 15, paused)
options = pick_options(test_session(), 90)
titles = [t.title for t in options]
assert len(options) <= 3
assert "Мелочь 30м" in titles # короче доступного времени — всегда подходит
assert "Средняя 60м" in titles
assert "Чуть больше 135м" in titles # верхняя граница ±50% допускается
assert "Большая 300м" not in titles # сильно выходит за окно 90 минут
assert "Из замороженного" not in titles # проект неактивен
# разнообразие: не более одной задачи на проект
projects_used = [t.project_id for t in options]
assert len(projects_used) == len(set(projects_used))
def test_pick_options_no_estimate_fallback(client: TestClient) -> None:
def approve(title: str, minutes: int | None) -> None:
tid = client.post("/api/tasks", json={"title": title}).json()["id"]
if minutes is not None:
client.patch(f"/api/tasks/{tid}", json={"estimated_minutes": minutes})
client.post(f"/api/tasks/{tid}/approve")
approve("Не влезает 500м", 500)
approve("Без оценки", None)
options = pick_options(test_session(), 60)
titles = [t.title for t in options]
assert "Без оценки" in titles # без оценки — учитываем как фолбэк
assert "Не влезает 500м" not in titles
def test_pick_options_skips_done_and_cancelled(client: TestClient) -> None:
tid = client.post("/api/tasks", json={"title": "Завершённая"}).json()["id"]
client.patch(f"/api/tasks/{tid}", json={"status": "done"})
client.post(f"/api/tasks/{tid}/approve")
options = pick_options(test_session(), 600)
assert all(t.id != tid for t in options)
def test_suggest_endpoint(client: TestClient) -> None:
resp = client.post("/api/tasks/suggest", json={"available_minutes": 120})
assert resp.status_code == 200
assert len(resp.json()) <= 3
# валидация времени
assert client.post("/api/tasks/suggest", json={"available_minutes": 0}).status_code == 422
def test_proposal_estimate_applied(client: TestClient, monkeypatch: Any) -> None:
def fake_propose(
self: Any, title: str, description: str, tag_names: list[str], project_names: list[str]
) -> dict[str, Any]:
return {
"tags": [],
"project": None,
"new_project": False,
"priority": 7,
"estimated_minutes": 45,
}
monkeypatch.setattr("app.services.detailing.DetailingService.propose", fake_propose)
tid = client.post("/api/tasks", json={"title": "Что-то на 45 минут"}).json()["id"]
approved = client.post(f"/api/tasks/{tid}/approve", json={"apply_proposal": True}).json()
assert approved["estimated_minutes"] == 45
def test_global_currency_settings(client: TestClient) -> None:
# по умолчанию UAH
assert client.get("/api/settings").json() == {"currency": "UAH"}
# смена валюты
assert client.put("/api/settings", json={"currency": "EUR"}).json() == {"currency": "EUR"}
assert client.get("/api/settings").json() == {"currency": "EUR"}
# недопустимая валюта — 422
assert client.put("/api/settings", json={"currency": "RUB"}).status_code == 422