Целый раздел ТЗ, от которого не было ни одной функции. - Учётные записи: завести, сменить роль и службу, заблокировать, сбросить пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль и проверка «это твой разбор». - Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия классификатора, число живых занятий. Отдельной строкой — секрет сессии: значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть её должен администратор, а не проверяющий на защите. - Журнал действий с фильтром, только на чтение. - Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный, а если его нет — из контейнера базы; если нет ни того ни другого, администратор видит, чего не хватает, а кнопка не молчит. Границы роли выражены отсутствием маршрутов, а не проверками внутри них: в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста проверяют это перебором маршрутов роутера — так запрет нельзя ослабить, случайно добавив обработчик. Найдено по ходу: тесты с базой падали в компании и проходили поодиночке. Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура, дающая движок на тест. 193 теста зелёных (12 новых).
49 lines
1.8 KiB
Python
49 lines
1.8 KiB
Python
"""Подключение к Postgres. Асинхронный движок, одна фабрика сессий на процесс."""
|
||
|
||
from collections.abc import AsyncIterator
|
||
|
||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||
from sqlalchemy.orm import DeclarativeBase
|
||
|
||
from app.config import get_settings
|
||
|
||
|
||
class Base(DeclarativeBase):
|
||
"""Общая база моделей. Alembic сравнивает миграции с `Base.metadata`."""
|
||
|
||
|
||
_engine = None
|
||
_sessionmaker: async_sessionmaker[AsyncSession] | None = None
|
||
|
||
|
||
def get_engine():
|
||
global _engine
|
||
if _engine is None:
|
||
_engine = create_async_engine(get_settings().database_url, pool_pre_ping=True)
|
||
return _engine
|
||
|
||
|
||
def get_sessionmaker() -> async_sessionmaker[AsyncSession]:
|
||
global _sessionmaker
|
||
if _sessionmaker is None:
|
||
_sessionmaker = async_sessionmaker(get_engine(), expire_on_commit=False)
|
||
return _sessionmaker
|
||
|
||
|
||
def reset() -> None:
|
||
"""Забыть движок и фабрику.
|
||
|
||
Нужно тестам: каждый `TestClient` поднимает свой событийный цикл, а пул
|
||
соединений привязан к тому, в котором создан. Переиспользование даёт
|
||
«Future attached to a different loop» — причём не сразу, а на втором
|
||
обращении к базе, и выглядит это как случайно падающий тест.
|
||
"""
|
||
global _engine, _sessionmaker
|
||
_engine = None
|
||
_sessionmaker = None
|
||
|
||
|
||
async def get_session() -> AsyncIterator[AsyncSession]:
|
||
"""Зависимость FastAPI: сессия на запрос, коммит явный."""
|
||
async with get_sessionmaker()() as session:
|
||
yield session
|