"""Тесты раздела «Расписания» (без БД — репозитории подменяются).""" from __future__ import annotations from datetime import UTC, datetime from typing import Any import pytest from fastapi.testclient import TestClient from common.repositories import schedules_repo, templates_repo TEMPLATE: dict[str, Any] = { "key": "notify.blago", "text": "Благо через 5 минут {usernames}", "description": "Благо: начало", "enabled": True, "updated_at": datetime(2026, 1, 1, 12, 0, tzinfo=UTC), "updated_by": None, } SCHEDULE: dict[str, Any] = { "id": 1, "template_key": "notify.blago", "template_description": "Благо: начало", "hour": 17, "minute": 0, "weekdays": [1, 5], "timezone": "Europe/Moscow", "enabled": True, "description": "Благо", "updated_at": datetime(2026, 1, 1, 12, 0, tzinfo=UTC), "updated_by": None, } @pytest.fixture def fake_repos(monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: state: dict[str, Any] = {} async def list_schedules() -> list[dict[str, Any]]: return [SCHEDULE] async def get_schedule(schedule_id: int) -> dict[str, Any] | None: return SCHEDULE if schedule_id == 1 else None async def schedule_history(schedule_id: int, *, limit: int = 10) -> list[dict[str, Any]]: return [] async def create_schedule(data: dict[str, Any], *, editor_id: int | None = None) -> int: state["created"] = data state["editor_id"] = editor_id return 2 async def update_schedule(schedule_id: int, data: dict[str, Any], *, editor_id: int | None = None) -> bool: state["updated"] = (schedule_id, data) return True async def delete_schedule(schedule_id: int, *, editor_id: int | None = None) -> bool: state["deleted"] = schedule_id return schedule_id == 1 async def set_enabled(schedule_id: int, enabled: bool, *, editor_id: int | None = None) -> bool: state["enabled"] = (schedule_id, enabled) return True async def list_templates() -> list[dict[str, Any]]: return [TEMPLATE] async def get_template(key: str) -> dict[str, Any] | None: return TEMPLATE if key == TEMPLATE["key"] else None monkeypatch.setattr(schedules_repo, "list_schedules", list_schedules) monkeypatch.setattr(schedules_repo, "get_schedule", get_schedule) monkeypatch.setattr(schedules_repo, "schedule_history", schedule_history) monkeypatch.setattr(schedules_repo, "create_schedule", create_schedule) monkeypatch.setattr(schedules_repo, "update_schedule", update_schedule) monkeypatch.setattr(schedules_repo, "delete_schedule", delete_schedule) monkeypatch.setattr(schedules_repo, "set_enabled", set_enabled) monkeypatch.setattr(templates_repo, "list_templates", list_templates) monkeypatch.setattr(templates_repo, "get_template", get_template) return state def test_schedules_list(fake_repos: dict[str, Any], client_factory) -> None: response = client_factory().get("/schedules") assert response.status_code == 200 assert "17:00" in response.text assert "Пн, Пт" in response.text assert "включено" in response.text def test_new_schedule_form(fake_repos: dict[str, Any], client_factory) -> None: response = client_factory().get("/schedules/new") assert response.status_code == 200 assert "Новое расписание" in response.text assert "notify.blago" in response.text def test_create_schedule(fake_repos: dict[str, Any], client_factory, csrf_token) -> None: client: TestClient = client_factory() token = csrf_token(client.get("/schedules/new").text) response = client.post( "/schedules", data={ "template_key": "notify.blago", "hour": "17", "minute": "0", "weekdays": ["1", "5"], "timezone": "Europe/Moscow", "description": "Благо", "enabled": "on", "csrf_token": token, }, ) assert response.status_code == 200 assert fake_repos["created"] == { "template_key": "notify.blago", "hour": 17, "minute": 0, "weekdays": [1, 5], "timezone": "Europe/Moscow", "enabled": True, "description": "Благо", } assert fake_repos["editor_id"] == 1 def test_create_schedule_rejects_bad_hour(fake_repos: dict[str, Any], client_factory, csrf_token) -> None: client: TestClient = client_factory() token = csrf_token(client.get("/schedules/new").text) response = client.post( "/schedules", data={ "template_key": "notify.blago", "hour": "25", "minute": "0", "timezone": "Europe/Moscow", "csrf_token": token, }, ) assert response.status_code == 400 assert "Час должен быть от 0 до 23" in response.text assert "created" not in fake_repos def test_create_schedule_rejects_unknown_template(fake_repos: dict[str, Any], client_factory, csrf_token) -> None: client: TestClient = client_factory() token = csrf_token(client.get("/schedules/new").text) response = client.post( "/schedules", data={ "template_key": "unknown.key", "hour": "17", "minute": "0", "timezone": "Europe/Moscow", "csrf_token": token, }, ) assert response.status_code == 400 assert "Шаблон не найден" in response.text def test_create_requires_csrf(fake_repos: dict[str, Any], client_factory) -> None: response = client_factory().post( "/schedules", data={"template_key": "notify.blago", "hour": "17", "minute": "0", "timezone": "Europe/Moscow"}, ) assert response.status_code == 400 def test_edit_schedule(fake_repos: dict[str, Any], client_factory) -> None: response = client_factory().get("/schedules/1") assert response.status_code == 200 assert "Расписание #1" in response.text def test_edit_missing_schedule_404(fake_repos: dict[str, Any], client_factory) -> None: assert client_factory().get("/schedules/99").status_code == 404 def test_update_schedule(fake_repos: dict[str, Any], client_factory, csrf_token) -> None: client: TestClient = client_factory() token = csrf_token(client.get("/schedules/1").text) response = client.post( "/schedules/1", data={ "template_key": "notify.blago", "hour": "18", "minute": "30", "weekdays": [], "timezone": "Europe/Moscow", "description": "", "enabled": "on", "csrf_token": token, }, ) assert response.status_code == 200 schedule_id, data = fake_repos["updated"] assert schedule_id == 1 assert data["hour"] == 18 assert data["minute"] == 30 def test_toggle_schedule(fake_repos: dict[str, Any], client_factory, csrf_token) -> None: client: TestClient = client_factory() token = csrf_token(client.get("/schedules").text) client.post("/schedules/1/toggle", data={"csrf_token": token}) assert fake_repos["enabled"] == (1, False) def test_delete_schedule(fake_repos: dict[str, Any], client_factory, csrf_token) -> None: client: TestClient = client_factory() token = csrf_token(client.get("/schedules").text) client.post("/schedules/1/delete", data={"csrf_token": token}) assert fake_repos["deleted"] == 1