Files
schedule/tests/fixtures.py
Vladislav Kopnin 5b3c9dc00a Админка в master: секретный путь, ссылка, интервал, force, блокировка входа, история
Админка нужна на «голом» сервере, поэтому возвращаю её в основную ветку. Она
выключена, пока не заданы ADMIN_LOGIN/ADMIN_PASSWORD: маршруты /api/admin/* не
регистрируются, страницы нет — обычная установка ничего не замечает.

Панель (открывается по секретному пути ADMIN_PATH, кнопки входа на сайте нет,
вход по логину/паролю):
- состояние: группы, когда обновлялось, дата файла на Диске, изменился ли
  файл при последнем чтении (sha256), ошибки, путь файла настроек;
- новая ссылка на таблицу: проверяется скачиванием, при ошибке откат;
- интервал обновления (TTL) в минутах;
- «Обновить сейчас»: принудительно перечитывает таблицу (качает файл всегда,
  интервал — только если на Диске изменилась дата);
- история входов: последняя попытка (откуда, когда, успешна ли) и последний
  успешный вход — и в панели, и в логе службы.

Защита входа: ADMIN_MAX_ATTEMPTS неудач (по умолчанию 3), затем блокировка на
ADMIN_LOCKOUT_MINUTES минут (по умолчанию 15) — всё из .env.

Реализация:
- backend/admin.py: cookie-сессия на HMAC (ключ — пароль, 12 ч, HttpOnly,
  SameSite, Secure за https), сравнение за постоянное время, реальный IP из
  X-Forwarded-For, логирование каждой попытки;
- schedule/settings.py: ссылка, интервал и история входов в JSON; путь по
  умолчанию data/settings.json (в docker — том schedule_data), при
  недоступности файла настройки живут в памяти и панель предупреждает;
- cache.py: apply_link (с проверкой и откатом), force_reload, state();
- frontend: страница админки по ADMIN_PATH (вне static, noindex) и прокси
  /api/admin/* с телом и Set-Cookie;
- install.sh пишет переменные админки в .env, Dockerfile копирует admin.py.

Тесты: 156. Проверено локально (страница, 3 попытки -> блокировка, история) и
в собранном docker-образе (том настроек, запись истории).
2026-10-10 11:12:29 +03:00

112 lines
4.5 KiB
Python
Raw Permalink 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.

"""Общая тестовая таблица: 2 группы, 2 дня, мержи, скрытый столбец, красный текст."""
from xlsx_builder import SheetSpec, build_workbook
MAIN = "Бакалавры и магистры"
ROOMS = "аудитории"
TEACHERS = "Преподаватели"
# серии Excel
# недели ч: 08.31-09.06, 14.09-20.09, 28.09-04.10, 12.10-18.10
# недели з: 07.09-13.09, 21.09-27.09, 05.10-11.10, 19.10-25.10
W1 = (46265, 46271, 46272, 46278)
W2 = (46279, 46285, 46286, 46292)
W3 = (46293, 46299, 46300, 46306)
W4 = (46307, 46313, 46314, 46320)
def main_sheet() -> SheetSpec:
g1 = "ИВТ-11БО\nсеместр до 11.01.27"
g2 = "ИТ-12БО\nсеместр до 29.12.26"
hidden = "СКРЫ-11БО\nсеместр до 1.1.27"
math_red = [
("Математика\nСмирнов А.В.\nауд. 215\n", False),
("пара 14.09 не будет", True),
]
rows = [
# 0: заголовок
["числитель", "", "знаменатель", "", "день недели", "пара\nвремя",
g1, g2, hidden, None],
# 1: с/по
["с", "по", "с", "по", "день недели", "пара\nвремя", None, None, None, None],
# 2: понедельник, пара 1, ч
[*W1, "понедельник", "1\n9:00-\n10:35", math_red, "Физика\nИванов И.И.\nауд. 301"],
# 3: понедельник, пара 1, з
[*W2, "", "", "Математика\nСмирнов А.В.\nауд. 216", None],
# 4: понедельник, пара 2, ч
[*W3, "", "2\n10:45- 12:20", "к.в. - 2\nФизика\nИванов И.И.\nауд. 301",
"Химия\nПетров П.П.\nонлайн"],
# 5: понедельник, пара 2, з
[*W4, "", "", None, "к.в.\nХимия\nПетров П.П.\nауд. 201"],
# 6: вторник, пара 1, ч
[*W1, "вторник", "1\n9:00-\n10:35", "Биология\nСидорова С.С.\nауд. 220", None],
# 7: вторник, пара 1, з
[*W2, "", "", None, None],
]
return SheetSpec(
rows=rows,
merges=["F3:F4", "F5:F6", "F7:F8"],
hidden_cols=[(8, 8)],
)
def rooms_sheet() -> SheetSpec:
rows = [
[None, None, None, None, None, None, "215", "301", "220"],
[None, None, None, None, None, None, "комп.класс, 12 мест",
"интерактивная панель, 24 места", "20 мест"],
]
return SheetSpec(rows=rows)
def teachers_sheet() -> SheetSpec:
rows = [
["числитель", "числитель", "знаменатель", "знаменатель", "день недели", "пара\nвремя"],
[None, None, None, None, None, None,
"Смирнов\nАлександр Викторович", "Иванов\nИван Иванович"],
[*W1, "понедельник", "1\n9:00-\n10:35",
"Математика\nСмирнов А.В.\nауд. 215", "Физика\nИванов И.И.\nауд. 301"],
]
return SheetSpec(rows=rows)
def fixture_sheets() -> dict[str, SheetSpec]:
return {MAIN: main_sheet(), ROOMS: rooms_sheet(), TEACHERS: teachers_sheet()}
def fixture_xlsx() -> bytes:
return build_workbook(fixture_sheets())
def make_sheets(sheets: dict[str, SheetSpec]):
"""SheetSpec-фикстуры -> те же объекты, что парсит parse_workbook."""
from schedule.xlsx import parse_workbook
return parse_workbook(build_workbook(sheets))
def make_cache(monkeypatch, xlsx: bytes | None, modified: str = "fixed-modified", settings=None):
"""ScheduleCache с заменёнными сетевыми функциями."""
import schedule.cache as cache_mod
from schedule import ScheduleCache
calls = {"download": 0, "modified": 0}
fail = {"download": False, "modified": False}
def fake_download(url, timeout=60.0):
calls["download"] += 1
if fail["download"]:
from schedule import ScheduleError
raise ScheduleError("сетевой сбой (тест)")
return xlsx
def fake_modified(url, timeout=20.0):
calls["modified"] += 1
return None if fail["modified"] else modified
monkeypatch.setattr(cache_mod, "download_schedule", fake_download)
monkeypatch.setattr(cache_mod, "fetch_table_modified", fake_modified)
cache = ScheduleCache("test-link", ttl_seconds=3600, retry_after=60, settings=settings)
return cache, calls, fail