arm-core/api/tests/pg_helpers.py
Andrey Epifancev feaac9cb9a test: CREATE/DROP DATABASE через служебную БД postgres (M-9)
Команды уровня кластера в тестах (создание и удаление временных БД, роль
в тесте предвыданной схемы) подключались к рабочей БД armka. Теперь —
к служебной БД postgres того же сервера (service_url): тестам не нужна
существующая armka. Проверено прогоном с несуществующей БД в
OPENBAO__DATABASE__DATABASE_URL.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 01:55:22 +04:00

94 lines
3.9 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.

"""Помощники интеграционных тестов с PostgreSQL: временная БД внутри того же сервера, что и `OPENBAO__DATABASE`.
Фикстура `scratch_db_url` создаёт пустую БД `armcore_scratch_<hex>`, отдаёт её SQLAlchemy-URL (драйвер asyncpg) и
в конце удаляет. Рабочую БД (`armka`) тесты не трогают и от её существования не зависят: `CREATE/DROP DATABASE` и
прочие команды уровня кластера идут через служебную БД `postgres` того же сервера (`service_url`). Подключается
плагином в `conftest.py`.
"""
import asyncio
import subprocess
import sys
import uuid
from collections.abc import AsyncIterator
from pathlib import Path
import asyncpg
import pytest_asyncio
from sqlalchemy.engine import URL, make_url
from src.core.config import settings
API_DIR = Path(__file__).resolve().parents[1]
def base_url() -> URL:
"""SQLAlchemy-URL рабочей БД из настроек (`.env.test`)."""
return make_url(settings.OPENBAO.DATABASE.DATABASE_URL)
#: Служебная БД PostgreSQL, которая есть на любом сервере: к ней подключаются для команд уровня кластера.
SERVICE_DATABASE = "postgres"
def service_url() -> URL:
"""SQLAlchemy-URL служебной БД `postgres` на сервере из настроек (те же хост, порт и учётные данные)."""
return base_url().set(database=SERVICE_DATABASE)
def _asyncpg_dsn(url: URL) -> str:
return url.set(drivername="postgresql").render_as_string(hide_password=False)
def run_alembic(url: str, *args: str) -> str:
"""`alembic -x dburl=<url> <args>` из каталога api/ отдельным процессом; вернуть stdout+stderr.
`env.py` вызывает `asyncio.run(...)` — внутри event loop pytest-asyncio это невозможно. При ошибке —
`AssertionError` с выводом процесса (`ENV_FILE`/`APP_ENV` наследуются из окружения pytest-env).
"""
result = subprocess.run(
[sys.executable, "-m", "alembic", "-x", f"dburl={url}", *args],
cwd=API_DIR,
capture_output=True,
text=True,
check=False,
)
output = result.stdout + result.stderr
assert result.returncode == 0, f"alembic {' '.join(args)} завершился с кодом {result.returncode}:\n{output}"
return output
async def alembic_async(url: str, *args: str) -> str:
"""`run_alembic` в потоке, чтобы не блокировать event loop."""
return await asyncio.to_thread(run_alembic, url, *args)
async def create_scratch_db(name: str) -> URL:
"""Создать пустую БД `name` на сервере рабочей БД; вернуть SQLAlchemy-URL на неё."""
conn = await asyncpg.connect(_asyncpg_dsn(service_url()))
try:
await conn.execute(f'CREATE DATABASE "{name}"')
finally:
await conn.close()
return base_url().set(database=name)
async def drop_scratch_db(name: str) -> None:
"""Удалить БД `name` (закрывая оставшиеся подключения)."""
conn = await asyncpg.connect(_asyncpg_dsn(service_url()))
try:
await conn.execute(f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)')
finally:
await conn.close()
@pytest_asyncio.fixture
async def scratch_db_url() -> AsyncIterator[str]:
"""URL пустой временной БД (asyncpg); БД удаляется после теста."""
name = f"armcore_scratch_{uuid.uuid4().hex[:12]}"
url = await create_scratch_db(name)
try:
yield url.render_as_string(hide_password=False)
finally:
await drop_scratch_db(name)