Initial commit: RehearsalHub POC
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>
This commit is contained in:
98
api/tests/integration/conftest.py
Normal file
98
api/tests/integration/conftest.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user