Full-stack self-hosted band rehearsal platform: Backend (FastAPI + SQLAlchemy 2.0 async): - Auth with JWT (register, login, /me, settings) - Band management with Nextcloud folder integration - Song management with audio version tracking - Nextcloud scan to auto-import audio files - Band membership with link-based invite system - Song comments - Audio analysis worker (BPM, key, loudness, waveform) - Nextcloud activity watcher for auto-import - WebSocket support for real-time annotation updates - Alembic migrations (0001–0003) - Repository pattern, Ruff + mypy configured Frontend (React 18 + Vite + TypeScript strict): - Login/register page with post-login redirect - Home page with band list and creation form - Band page with member panel, invite link, song list, NC scan - Song page with waveform player, annotations, comment thread - Settings page for per-user Nextcloud credentials - Invite acceptance page (/invite/:token) - ESLint v9 flat config + TypeScript strict mode Infrastructure: - Docker Compose: PostgreSQL, Redis, API, worker, watcher, nginx - nginx reverse proxy for static files + /api/ proxy - make check runs all linters before docker compose build Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
99 lines
3.1 KiB
Python
99 lines
3.1 KiB
Python
"""Integration test fixtures using testcontainers for a real Postgres."""
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from httpx import ASGITransport, AsyncClient
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
|
|
from rehearsalhub.db.models import Base
|
|
from rehearsalhub.main import create_app
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def pg_container():
|
|
"""Start a real Postgres container for the test session."""
|
|
from testcontainers.postgres import PostgresContainer
|
|
|
|
with PostgresContainer("postgres:16-alpine") as pg:
|
|
yield pg
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def pg_url(pg_container) -> str:
|
|
url = pg_container.get_connection_url()
|
|
return url.replace("postgresql+psycopg2://", "postgresql+asyncpg://").replace(
|
|
"postgresql://", "postgresql+asyncpg://"
|
|
)
|
|
|
|
|
|
@pytest_asyncio.fixture(scope="session")
|
|
async def pg_engine(pg_url):
|
|
engine = create_async_engine(pg_url, echo=False)
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
yield engine
|
|
await engine.dispose()
|
|
|
|
|
|
@pytest_asyncio.fixture(scope="function")
|
|
async def db_session(pg_engine) -> AsyncSession:
|
|
"""Each test gets its own connection with a rolled-back transaction."""
|
|
async with pg_engine.connect() as conn:
|
|
trans = await conn.begin()
|
|
session = AsyncSession(bind=conn, expire_on_commit=False)
|
|
yield session
|
|
await session.close()
|
|
await trans.rollback()
|
|
|
|
|
|
@pytest_asyncio.fixture(scope="function")
|
|
async def client(db_session):
|
|
"""httpx AsyncClient with DB session dependency overridden."""
|
|
from rehearsalhub.db.engine import get_session
|
|
|
|
app = create_app()
|
|
app.dependency_overrides[get_session] = lambda: db_session
|
|
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
|
yield c
|
|
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
@pytest_asyncio.fixture(scope="function")
|
|
async def auth_headers(client, db_session):
|
|
"""Register + login a test user, return Authorization headers."""
|
|
from tests.factories import create_member
|
|
|
|
member = await create_member(db_session, email="auth@test.com")
|
|
await db_session.commit()
|
|
|
|
resp = await client.post(
|
|
"/api/v1/auth/login", json={"email": "auth@test.com", "password": "testpassword123"}
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
token = resp.json()["access_token"]
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
@pytest_asyncio.fixture(scope="function")
|
|
async def current_member(db_session):
|
|
from tests.factories import create_member
|
|
|
|
member = await create_member(db_session, email=f"member_{__import__('uuid').uuid4().hex[:6]}@test.com")
|
|
await db_session.commit()
|
|
return member
|
|
|
|
|
|
@pytest_asyncio.fixture(scope="function")
|
|
async def auth_headers_for(client, db_session):
|
|
"""Factory fixture: given a member, return auth headers for them."""
|
|
|
|
async def _make(member):
|
|
from rehearsalhub.services.auth import create_access_token
|
|
|
|
token = create_access_token(str(member.id), member.email)
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
return _make
|