Files
schedule/tests/test_api.py
Vladislav Kopnin 9c9875579c Тесты и инфраструктура
- tests/: 104 теста — lessons (все форматы ячеек), xlsx (мержи/скрытые
  столбцы/красный текст/даты), parsing (группы/недели/индексы), API (TestClient,
  TTL/modified/stale-while-error, 404/400/502)
- xlsx_builder.py: генерация xlsx в памяти для тестов без openpyxl
- docker-compose: healthz-хелскчеки (не блокируют старт), SCHEDULE_LINK
- Makefile: target test
- requirements: закреплённые версии
- .gitignore: __pycache__/, .venv/ и т.д.
2026-09-09 11:33:58 +03:00

208 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Тесты API через TestClient: маршруты, ошибки, кэш (TTL/modified/stale-while-error)."""
import time
from urllib.parse import quote
import pytest
from fastapi.testclient import TestClient
import main
from fixtures import fixture_xlsx, make_cache
from schedule import ScheduleCache, ScheduleError
from schedule import parsing
@pytest.fixture()
def cache(monkeypatch):
c, calls, fail = make_cache(monkeypatch, fixture_xlsx())
c.calls = calls
c.fail = fail
return c
@pytest.fixture()
def client(cache):
app = main.create_app(cache)
with TestClient(app) as c:
yield c
# --- обслуживание ----------------------------------------------------------------
def test_healthz(client):
assert client.get("/healthz").json() == {"ok": True}
def test_root_lists_endpoints(client):
body = client.get("/").json()
assert "/groups" in body["endpoints"]
# --- справочники ------------------------------------------------------------------
def test_groups(client):
assert client.get("/groups").json() == ["ИВТ-11БО", "ИТ-12БО"]
def test_teachers(client):
assert client.get("/teachers").json() == [
"Иванов И.И.", "Петров П.П.", "Сидорова С.С.", "Смирнов А.В.",
]
def test_rooms(client):
assert client.get("/rooms").json() == [
"ауд. 201", "ауд. 215", "ауд. 216", "ауд. 220", "ауд. 301",
]
def test_group_schedule(client):
body = client.get("/schedule/ИВТ-11БО").json()
assert body["semester_until"] == "11.01.27"
p1 = body["days"]["понедельник"]["1"]
assert p1["time"] == "9:00-10:35"
assert p1["ч"]["lessons"][0]["room"] == "ауд. 215"
assert p1["ч"]["lessons"][0]["note"] == "пара 14.09 не будет"
assert p1["з"]["lessons"][0]["room"] == "ауд. 216"
assert body["days"]["понедельник"]["2"]["ч"]["lessons"][0]["marker"] == "к.в. - 2"
def test_group_schedule_day(client):
body = client.get("/schedule/ИВТ-11БО/понедельник").json()
assert body["day"] == "понедельник"
assert "1" in body["periods"]
empty = client.get("/schedule/ИВТ-11БО/суббота").json()
assert empty["periods"] == {}
def test_group_not_found(client):
resp = client.get("/schedule/НЕТ-11БО")
assert resp.status_code == 404
def test_bad_day(client):
resp = client.get("/schedule/ИВТ-11БО/бука")
assert resp.status_code == 400
def test_today(client):
resp = client.get("/today", params={"group": "ИВТ-11БО"})
assert resp.status_code == 200
body = resp.json()
assert body["day"] in parsing.DAYS
assert body["group"] == "ИВТ-11БО"
assert client.get("/today", params={"group": "НЕТ"}).status_code == 404
def test_teacher(client):
body = client.get("/teacher/Смирнов А.В.").json()
assert body["full_name"] == "Смирнов Александр Викторович"
assert body["days"]["понедельник"]["1"]["ч"][0]["subject"] == "Математика"
assert client.get("/teacher/Нет А.Н.").status_code == 404
def test_room(client):
body = client.get("/room/" + quote("ауд. 215")).json()
assert body["info"] == "комп.класс, 12 мест"
assert body["days"]["понедельник"]["1"]["ч"][0]["subject"] == "Математика"
assert client.get("/room/" + quote("ауд. 999")).status_code == 404
def test_rooms_free(client):
free = client.get("/rooms-free", params={"day": "суббота", "period": 1}).json()
assert [r["room"] for r in free] == ["ауд. 201", "ауд. 215", "ауд. 216", "ауд. 220", "ауд. 301"]
busy_day = client.get("/rooms-free", params={"day": "понедельник", "period": 1}).json()
assert [r["room"] for r in busy_day] == ["ауд. 201", "ауд. 220"]
assert client.get("/rooms-free", params={"day": "суббота", "period": 9}).status_code == 400
assert client.get("/rooms-free", params={"day": "бука", "period": 1}).status_code == 400
def test_rooms_day(client):
body = client.get("/rooms-day", params={"day": "понедельник"}).json()
assert body["day"] == "понедельник"
rooms = {r["room"]: r for r in body["rooms"]}
assert set(rooms) == {"ауд. 201", "ауд. 215", "ауд. 216", "ауд. 220", "ауд. 301"}
room_301 = rooms["ауд. 301"]
assert room_301["periods"]["1"]["time"] == "9:00-10:35"
assert room_301["periods"]["1"]["ч"][0]["subject"] == "Физика"
assert rooms["ауд. 201"]["periods"]["2"]["з"][0]["subject"] == "Химия"
assert client.get("/rooms-day", params={"day": "бука"}).status_code == 400
def test_week(client):
body = client.get("/week").json()
assert len(body["weeks"]) == 8
assert body["weeks"][0] == {"parity": "ч", "start": "2026-08-31", "end": "2026-09-06"}
def test_status(client):
body = client.get("/status").json()
assert body["error"] is None
assert body["table_modified"] == "fixed-modified"
assert body["last_updated"]
assert body["cache_ttl_seconds"] == 3600
# --- кэш ---------------------------------------------------------------------------
def test_cache_no_repeated_download(client, cache):
client.get("/groups")
client.get("/teachers")
client.get("/rooms")
assert cache.calls["download"] == 1
def test_cache_modified_unchanged_no_download(cache):
cache._ttl = 0 # даже с истёкшим TTL
cache.get()
cache.get()
assert cache.calls["download"] == 1
assert cache.calls["modified"] >= 2
def test_cache_stale_while_error(cache):
cache._ttl = 0 # форсим повторную попытку
cache.get()
data_before, _ = cache.get()
# полный обрыв связи: и modified, и download
cache.fail["download"] = True
cache.fail["modified"] = True
try:
cache.get() # ошибка -> старые данные, без эксепсов
except ScheduleError:
pytest.fail("ожидается stale-ответ, а не ошибка")
status = cache.status()
assert status["error"] is not None
assert len(data_before) == 2
# повторная попытка в пределах retry_after не скачивает повторно
downloads = cache.calls["download"]
cache.get()
assert cache.calls["download"] == downloads
# после retry_after и исправления сети — успешное обновление
cache._retry_after = 0
cache.fail["download"] = False
cache.fail["modified"] = False
cache.get()
assert cache.status()["error"] is None
def test_cache_failure_without_data_raises(cache):
cache.fail["download"] = True
with pytest.raises(ScheduleError):
cache.get()
assert cache.status()["last_updated"] is None
def test_broken_cache_http(client=None):
from fastapi.testclient import TestClient as TC
app = main.create_app(main._BrokenCache())
with TC(app) as c:
assert c.get("/healthz").status_code == 200
assert c.get("/groups").status_code == 502
body = c.get("/status").json()
assert "SCHEDULE_LINK" in body["error"]