wiregui/tests/conftest.py
Stefano Bertelli 554da599ba
Some checks failed
Dev / test (push) Failing after 4m3s
Dev / docker (push) Has been skipped
fix: stop patching wiregui.db globally — broke e2e tests
Remove module-level engine/session replacement that affected all tests
including e2e. The test engine is now only used via the session fixture,
so e2e tests keep using the real DB the app writes to.
2026-03-31 22:14:34 -05:00

90 lines
3.1 KiB
Python

"""Shared test fixtures — async DB session using a test database.
Unit tests use the ``session`` fixture, which provides a per-test
savepoint-isolated session on a dedicated test engine. E2E tests do NOT
use this fixture and are therefore unaffected — they keep using the real
``wiregui.db.async_session`` that talks to the app's database.
"""
import os
from collections.abc import AsyncGenerator
import pytest_asyncio
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlmodel import SQLModel
from wiregui.config import get_settings
# All models must be imported so SQLModel.metadata knows about them
from wiregui.models import * # noqa: F401, F403
def _test_database_url() -> str:
"""Use a separate test DB locally, but in CI just use the main DB (it's ephemeral)."""
url = get_settings().database_url
if os.environ.get("CI"):
return url # CI: use the service container DB directly
base, _dbname = url.rsplit("/", 1)
return f"{base}/wiregui_test"
TEST_DATABASE_URL = _test_database_url()
def _ensure_test_db_sync():
"""Ensure test database exists. Skip in CI (uses main DB)."""
if os.environ.get("CI"):
return
import asyncio
async def _create():
base_url = get_settings().database_url.rsplit("/", 1)[0] + "/postgres"
admin_engine = create_async_engine(base_url, isolation_level="AUTOCOMMIT")
try:
async with admin_engine.connect() as conn:
result = await conn.execute(
text("SELECT 1 FROM pg_database WHERE datname = 'wiregui_test'")
)
if result.scalar() is None:
await conn.execute(text("CREATE DATABASE wiregui_test"))
finally:
await admin_engine.dispose()
asyncio.run(_create())
_ensure_test_db_sync()
# Test engine — only used by the ``session`` fixture and unit tests.
# NOT assigned to wiregui.db so e2e tests are unaffected.
_test_engine = create_async_engine(TEST_DATABASE_URL)
@pytest_asyncio.fixture(scope="session")
async def _test_tables():
"""Create all tables once per test session, drop at end."""
async with _test_engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
yield
async with _test_engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.drop_all)
await _test_engine.dispose()
@pytest_asyncio.fixture
async def session(_test_tables) -> AsyncGenerator[AsyncSession]:
"""Per-test session with transaction isolation.
The session is bound to a connection-level transaction that is always
rolled back at teardown. When tested code calls ``session.commit()``,
SQLAlchemy only releases a SAVEPOINT — the outer transaction is never
committed, so no test data persists between tests.
"""
async with _test_engine.connect() as conn:
txn = await conn.begin()
sess = AsyncSession(bind=conn, expire_on_commit=False, join_transaction_mode="create_savepoint")
yield sess
await sess.close()
await txn.rollback()