gabriel / musehub public
conftest.py python
131 lines 4.1 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """Pytest configuration and fixtures."""
2 from __future__ import annotations
3
4 import logging
5 from collections.abc import AsyncGenerator, Generator
6 import pytest
7 import pytest_asyncio
8 from httpx import AsyncClient, ASGITransport
9 from sqlalchemy.ext.asyncio import (
10 AsyncSession,
11 async_sessionmaker,
12 create_async_engine,
13 )
14 from sqlalchemy.pool import StaticPool
15
16 from musehub.db import database
17 from musehub.db.database import Base, get_db
18 from musehub.db.models import User
19 from musehub.main import app
20
21
22 def pytest_configure(config: pytest.Config) -> None:
23
24 """Ensure asyncio_mode is auto so async fixtures work (e.g. in Docker when pyproject not in cwd)."""
25 if hasattr(config.option, "asyncio_mode") and config.option.asyncio_mode is None:
26 config.option.asyncio_mode = "auto"
27 logging.getLogger("httpcore").setLevel(logging.CRITICAL)
28
29
30 @pytest.fixture
31 def anyio_backend() -> str:
32 return "asyncio"
33
34
35 @pytest.fixture(autouse=True)
36 def _disable_storpheus_hard_gate() -> Generator[None, None, None]:
37 """Tests don't have Storpheus running — disable the pre-flight hard gate."""
38 from musehub.config import settings
39 original = settings.storpheus_required
40 settings.storpheus_required = False
41 yield
42 settings.storpheus_required = original
43
44
45 @pytest.fixture(autouse=True)
46 def _reset_variation_store() -> Generator[None, None, None]:
47 """Reset the singleton VariationStore between tests to prevent cross-test pollution."""
48 yield
49 from musehub.variation.storage.variation_store import reset_variation_store
50 reset_variation_store()
51
52
53 @pytest_asyncio.fixture
54 async def db_session() -> AsyncGenerator[AsyncSession, None]:
55 """Create an in-memory test database session."""
56 engine = create_async_engine(
57 "sqlite+aiosqlite:///:memory:",
58 connect_args={"check_same_thread": False},
59 poolclass=StaticPool,
60 )
61 async with engine.begin() as conn:
62 await conn.run_sync(Base.metadata.create_all)
63
64 async_session_factory = async_sessionmaker(
65 bind=engine,
66 class_=AsyncSession,
67 expire_on_commit=False,
68 )
69 old_engine = database._engine
70 old_factory = database._async_session_factory
71 database._engine = engine
72 database._async_session_factory = async_session_factory
73 try:
74 async with async_session_factory() as session:
75 async def override_get_db() -> AsyncGenerator[AsyncSession, None]:
76 yield session
77 app.dependency_overrides[get_db] = override_get_db
78 yield session
79 app.dependency_overrides.clear()
80 finally:
81 database._engine = old_engine
82 database._async_session_factory = old_factory
83 async with engine.begin() as conn:
84 await conn.run_sync(Base.metadata.drop_all)
85 await engine.dispose()
86
87
88 @pytest_asyncio.fixture
89 async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
90
91 """Create an async test client. Depends on db_session so auth revocation check uses test DB."""
92 transport = ASGITransport(app=app)
93 async with AsyncClient(transport=transport, base_url="http://test") as ac:
94 yield ac
95
96
97 # -----------------------------------------------------------------------------
98 # Auth fixtures for API contract and integration tests
99 # -----------------------------------------------------------------------------
100
101 @pytest_asyncio.fixture
102 async def test_user(db_session: AsyncSession) -> User:
103
104 """Create a test user with budget (for authenticated route tests)."""
105 user = User(
106 id="550e8400-e29b-41d4-a716-446655440000",
107 budget_cents=500,
108 budget_limit_cents=500,
109 )
110 db_session.add(user)
111 await db_session.commit()
112 await db_session.refresh(user)
113 return user
114
115
116 @pytest.fixture
117 def auth_token(test_user: User) -> str:
118
119 """JWT for test_user (1 hour)."""
120 from musehub.auth.tokens import create_access_token
121 return create_access_token(user_id=test_user.id, expires_hours=1)
122
123
124 @pytest.fixture
125 def auth_headers(auth_token: str) -> dict[str, str]:
126
127 """Headers with Bearer token and JSON content type."""
128 return {
129 "Authorization": f"Bearer {auth_token}",
130 "Content-Type": "application/json",
131 }