"""Тесты: дедлайны (ТЗ 3.4) и регулярные задачи (ТЗ 3.5)."""
from datetime import date, timedelta
from typing import Any
from fastapi.testclient import TestClient
from app.models import Task
from app.services.options import pick_options
from app.services.recurrence import next_date, recurrence_label, spawn_next
from tests.conftest import _test_session_factory
def test_session() -> Any:
return _test_session_factory()
# --- дедлайны ---
def test_deadlines_set_and_cleared(client: TestClient) -> None:
tid = client.post("/api/tasks", json={"title": "С дедлайном"}).json()["id"]
updated = client.patch(
f"/api/tasks/{tid}",
json={"deadline_date": "2026-10-01", "deadline_period": "week"},
).json()
assert updated["deadline_date"] == "2026-10-01"
assert updated["deadline_period"] == "week"
# неизвестный период — 422
assert (
client.patch(f"/api/tasks/{tid}", json={"deadline_period": "fortnight"}).status_code == 422
)
# очистка
cleared = client.patch(f"/api/tasks/{tid}", json={"deadline_date": None}).json()
assert cleared["deadline_date"] is None
def test_options_sorted_by_deadline_urgency(client: TestClient) -> None:
# по проекту на задачу: правило «≤1 на проект» не смешивает результаты
def approve(title: str, **patch: Any) -> None:
pid = client.post("/api/projects", json={"name": title}).json()["id"]
tid = client.post("/api/tasks", json={"title": title}).json()["id"]
client.patch(f"/api/tasks/{tid}", json={"project_id": pid, **patch})
client.post(f"/api/tasks/{tid}/approve")
tomorrow = (date.today() + timedelta(days=2)).isoformat()
far = (date.today() + timedelta(days=60)).isoformat()
approve("Близкий дедлайн", deadline_date=tomorrow)
approve("Далёкий дедлайн", deadline_date=far)
approve("Мягкий дедлайн", deadline_period="week")
approve("Без дедлайна")
options = pick_options(test_session(), 600)
titles = [t.title for t in options]
# срочность: близкий строгий → далёкий строгий → нестрогий период;
# лимит 3 — задача без дедлайна остаётся за бортом
assert "Без дедлайна" not in titles
assert titles.index("Близкий дедлайн") < titles.index("Далёкий дедлайн")
assert titles.index("Далёкий дедлайн") < titles.index("Мягкий дедлайн")
# --- правила повторения ---
def test_next_date_interval() -> None:
task = Task(
title="t",
recur_kind="interval",
recur_interval_days=7,
created_at=__import__("datetime").datetime(2026, 9, 1, 12, 0),
)
assert next_date(task, date(2026, 9, 3)) == date(2026, 9, 8)
assert next_date(task, date(2026, 9, 8)) == date(2026, 9, 15) # строго после
def test_next_date_weekdays() -> None:
task = Task(title="t", recur_kind="weekdays", recur_weekdays="1,3,5") # пн, ср, пт
# 2026-09-19 — суббота; ближайший следующий из пн/ср/пт — понедельник 21-го
assert next_date(task, date(2026, 9, 19)) == date(2026, 9, 21)
def test_next_date_monthly_clamps_to_month_end() -> None:
task = Task(title="t", recur_kind="monthly", recur_day_of_month=31)
assert next_date(task, date(2026, 2, 15)) == date(2026, 2, 28) # 2026 не високосный
assert next_date(task, date(2026, 2, 28)) == date(2026, 3, 31)
def test_recurrence_label() -> None:
assert recurrence_label(Task(title="t", recur_kind="interval", recur_interval_days=1)) == (
"ежедневно"
)
assert recurrence_label(Task(title="t", recur_kind="weekdays", recur_weekdays="1,3")) == (
"пн ср"
)
assert recurrence_label(Task(title="t", recur_kind="monthly", recur_day_of_month=1)) == (
"ежемесячно 1-го"
)
# --- спавн следующего экземпляра ---
def test_recurring_task_spawns_next_on_done(client: TestClient) -> None:
tid = client.post("/api/tasks", json={"title": "Полить цветы"}).json()["id"]
client.patch(
f"/api/tasks/{tid}",
json={
"task_type": "recurring",
"recur_kind": "interval",
"recur_interval_days": 3,
"estimated_minutes": 10,
},
)
client.post(f"/api/tasks/{tid}/approve")
client.patch(f"/api/tasks/{tid}", json={"status": "done"})
tasks = client.get("/api/tasks?status=to_do").json()
spawned = [t for t in tasks if t["title"] == "Полить цветы"]
assert len(spawned) == 1
new = spawned[0]
assert new["task_type"] == "recurring"
assert new["detail_state"] == "approved" # метаданные уже утверждены
assert new["estimated_minutes"] == 10
assert new["recur_kind"] == "interval"
def test_one_time_task_does_not_spawn(client: TestClient) -> None:
tid = client.post("/api/tasks", json={"title": "Разовая"}).json()["id"]
client.post(f"/api/tasks/{tid}/approve")
client.patch(f"/api/tasks/{tid}", json={"status": "done"})
assert client.get("/api/tasks?status=to_do").json() == []
def test_spawn_next_without_rule_is_none() -> None:
task = Task(title="t", task_type="recurring")
session = test_session()
session.add(task)
session.flush()
assert spawn_next(session, task) is None