"""Тесты 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"]