arm-core/api/tests/conftest.py

93 lines
4.5 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.

"""Общие фикстуры тестов (spec data-model; design D-7, рулинг R-16).
БД-фикстуры — только для тестов с маркером `integration` и ленивые: пока тест их не запросил, PostgreSQL не
трогается (`make check` не требует БД). Тестовая БД сессии — отдельная свежая `armcore_pytest_<hex>` на сервере из
`.env.test`: мигрируется alembic один раз на сессию и удаляется в конце; рабочая БД `armka` не используется.
Изоляция тестов — внешняя транзакция с SAVEPOINT: `commit()` внутри теста ничего не фиксирует.
"""
import uuid
from collections.abc import AsyncIterator
from pathlib import Path
import asyncpg
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, AsyncSession, create_async_engine
from tests.fixtures_helpers import fixtures_skip_reason, resolve_fixtures_dir
from tests.pg_helpers import ( # scratch_db_url — реэкспорт фикстуры, чтобы pytest её нашёл
alembic_async,
create_scratch_db,
drop_scratch_db,
scratch_db_url, # noqa: F401
)
PG_UNAVAILABLE = "PostgreSQL недоступен (make up / .env.test): {error}"
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
"""Тесты с маркером `fixtures` пропускаются с явной причиной, если каталог fixtures недоступен."""
reason = fixtures_skip_reason()
if reason is None:
return
skip = pytest.mark.skip(reason=reason)
for item in items:
if item.get_closest_marker("fixtures") is not None:
item.add_marker(skip)
@pytest.fixture(scope="session")
def fixtures_dir() -> Path:
"""Каталог реальных данных (`ARM_KA_FIXTURES` или соседний `fixtures`); без него тест пропускается."""
reason = fixtures_skip_reason()
if reason is not None:
pytest.skip(reason)
return resolve_fixtures_dir()
@pytest_asyncio.fixture(scope="session")
async def migrated_db_url() -> AsyncIterator[str]:
"""URL свежей БД `armcore_pytest_<hex>` с применённой миграцией `alembic upgrade head`; удаляется в конце сессии."""
name = f"armcore_pytest_{uuid.uuid4().hex[:12]}"
try:
url = await create_scratch_db(name)
except (OSError, asyncpg.InterfaceError) as error: # соединение не установлено: сервер не запущен, порт закрыт
pytest.fail(PG_UNAVAILABLE.format(error=f"{type(error).__name__}: {error}"), pytrace=False)
except asyncpg.PostgresError as error: # сервер ответил отказом: учётные данные, роль, права на CREATE DATABASE
pytest.fail(
f"PostgreSQL отказал при создании тестовой БД ({type(error).__name__}: {error}); "
"проверьте учётные данные и право CREATE DATABASE в .env.test",
pytrace=False,
)
try:
rendered = url.render_as_string(hide_password=False)
await alembic_async(rendered, "upgrade", "head")
yield rendered
finally:
await drop_scratch_db(name)
@pytest_asyncio.fixture(scope="session")
async def db_engine(migrated_db_url: str) -> AsyncIterator[AsyncEngine]:
"""Async-движок на мигрированную БД сессии; `search_path = dkb`."""
engine = create_async_engine(migrated_db_url, connect_args={"server_settings": {"search_path": "dkb"}})
try:
yield engine
finally:
await engine.dispose()
@pytest_asyncio.fixture
async def db_session(db_engine: AsyncEngine) -> AsyncIterator[AsyncSession]:
"""Сессия в внешней транзакции с откатом: `commit()` в тесте освобождает лишь SAVEPOINT, данные не остаются."""
connection: AsyncConnection = await db_engine.connect()
transaction = await connection.begin()
session = AsyncSession(bind=connection, join_transaction_mode="create_savepoint", expire_on_commit=False)
try:
yield session
finally:
await session.close()
await transaction.rollback()
await connection.close()