feat: вход, роли и аудит действий (lct-23)
Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались
ссылкой с номером занятия, и пускало знание адреса.
- Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная
cookie; роль на сокетах читается из той же cookie в момент рукопожатия,
отдельного протокола авторизации в канале нет.
- Разграничение: control — преподавателю, observe — преподавателю и админу,
call и station — обучающемуся и преподавателю. Отказ приходит событием
error с кодом forbidden.
- Обучающийся не видит чужого: история подменяет фильтр на его собственный
идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ
к чужим результатам, а не только к чужим экранам.
- Администратору закрыта правка оценок — ТЗ запрещает это прямо.
- make users заводит по записи на роль и печатает случайные пароли один раз:
зашитый в репозиторий admin/admin пережил бы сдачу.
- Экран входа и проверка роли на каждом маршруте фронта.
Наши инструменты не сломались: make lesson и тесты входят через dev-token
за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной
функции не должно быть видно вовсе. У тестов появился conftest.py.
Role уехала в домен и в generated.ts через EventCatalog.principal: иначе
фронт переписывал бы список ролей руками.
181 тест зелёный (14 новых), make typecheck чистый.
2026-09-20 09:01:05 +03:00
|
|
|
|
"""Общая настройка тестов.
|
|
|
|
|
|
|
|
|
|
|
|
Голосовой контур выключен: он грузит модели около десяти секунд, а проверяется
|
|
|
|
|
|
отдельно в `test_voice_pipeline.py`. Вход включён как на стенде, но тесты
|
|
|
|
|
|
логинятся через `dev-token` — так же, как `make lesson` (tasks/lct-23-auth-roles.md).
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
|
|
os.environ.setdefault("DEV_AUTH_BYPASS", "true")
|
|
|
|
|
|
|
|
|
|
|
|
import pytest # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
from app.config import get_settings # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True, scope="session")
|
|
|
|
|
|
def _dev_auth():
|
|
|
|
|
|
"""Флаг обхода читается один раз при создании настроек."""
|
|
|
|
|
|
get_settings.cache_clear()
|
|
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
assert settings.dev_auth_bypass, "тесты должны ходить через dev-token"
|
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
|
|
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
|
def _fresh_engine():
|
|
|
|
|
|
"""Движок базы — на тест, а не на процесс.
|
|
|
|
|
|
|
|
|
|
|
|
Пул соединений привязан к событийному циклу, в котором создан, а каждый
|
|
|
|
|
|
`TestClient` поднимает свой. Общий движок даёт «Future attached to
|
|
|
|
|
|
a different loop» на втором обращении к базе — тест падает не там, где
|
|
|
|
|
|
ошибка, и только в компании других тестов.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from app.db import base
|
|
|
|
|
|
|
|
|
|
|
|
base.reset()
|
|
|
|
|
|
yield
|
|
|
|
|
|
base.reset()
|
|
|
|
|
|
|
|
|
|
|
|
|
feat: вход, роли и аудит действий (lct-23)
Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались
ссылкой с номером занятия, и пускало знание адреса.
- Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная
cookie; роль на сокетах читается из той же cookie в момент рукопожатия,
отдельного протокола авторизации в канале нет.
- Разграничение: control — преподавателю, observe — преподавателю и админу,
call и station — обучающемуся и преподавателю. Отказ приходит событием
error с кодом forbidden.
- Обучающийся не видит чужого: история подменяет фильтр на его собственный
идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ
к чужим результатам, а не только к чужим экранам.
- Администратору закрыта правка оценок — ТЗ запрещает это прямо.
- make users заводит по записи на роль и печатает случайные пароли один раз:
зашитый в репозиторий admin/admin пережил бы сдачу.
- Экран входа и проверка роли на каждом маршруте фронта.
Наши инструменты не сломались: make lesson и тесты входят через dev-token
за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной
функции не должно быть видно вовсе. У тестов появился conftest.py.
Role уехала в домен и в generated.ts через EventCatalog.principal: иначе
фронт переписывал бы список ролей руками.
181 тест зелёный (14 новых), make typecheck чистый.
2026-09-20 09:01:05 +03:00
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def as_instructor(client):
|
|
|
|
|
|
"""Клиент, вошедший преподавателем. Cookie живёт в самом TestClient."""
|
|
|
|
|
|
response = client.post("/api/auth/dev-token")
|
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
return client
|