test(infra): fail-loud DB/ffmpeg lane + client/as_user/make_user fixtures (E1 S2, TEST-4)
The DB-backed lane and the two ffmpeg tests skipped SILENTLY when the DB was unreachable or ffmpeg was absent, so `siftlode check` could print green while ~60 tests never ran. Make the compose lane authoritative: - SIFTLODE_TEST_REQUIRE_DB=1 in docker-compose.test.yml; conftest.pytest_configure turns a would-be skip (no Postgres, or no ffmpeg) into a hard UsageError. - install ffmpeg in Dockerfile.test so the thumbnail/poster tests actually run. - `-rs` in pytest.ini so skip reasons are always visible. Add the shared TestClient fixtures the risk-core suites build on: `make_user` (create + commit a login-ready User), `client` (real auth/session stack, no current_user override — for login/wallet/epoch), and `as_user` (a client pre-authenticated as a given user, for tests that aren't exercising login).
This commit is contained in:
@@ -12,14 +12,26 @@ config the videos.search_vector generated column needs). Building it via migrati
|
||||
that the migration chain applies cleanly. Each test then gets a clean database via TRUNCATE so
|
||||
tests that COMMIT (the whole point of this lane — transaction-ownership behaviour) don't leak.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
import app.models # noqa: F401 — register every model on Base.metadata
|
||||
from app import security
|
||||
from app.auth import current_user
|
||||
from app.config import settings
|
||||
from app.db import Base, SessionLocal, engine
|
||||
from app.db import Base, SessionLocal, engine, get_db
|
||||
from app.main import app
|
||||
from app.models import User
|
||||
|
||||
# The compose test lane sets this so a would-be silent skip (no DB, or no ffmpeg) becomes a hard error
|
||||
# — the gate must not print green while the DB-backed / ffmpeg tests were quietly not run.
|
||||
_REQUIRE_FULL = os.getenv("SIFTLODE_TEST_REQUIRE_DB") == "1"
|
||||
|
||||
|
||||
def _db_reachable() -> bool:
|
||||
@@ -59,6 +71,23 @@ def _truncate_all():
|
||||
conn.execute(text(f"TRUNCATE {tables} RESTART IDENTITY CASCADE"))
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Fail the whole run — loudly, once — when SIFTLODE_TEST_REQUIRE_DB=1 but the environment can't
|
||||
actually run the full suite. Without this, an unreachable DATABASE_URL or a missing ffmpeg turns
|
||||
~60 DB tests + the poster tests into silent skips and the gate still exits green."""
|
||||
if not _REQUIRE_FULL:
|
||||
return
|
||||
if not _DB_REACHABLE:
|
||||
raise pytest.UsageError(
|
||||
"SIFTLODE_TEST_REQUIRE_DB=1 but no Postgres is reachable at the test DATABASE_URL — the "
|
||||
"DB-backed lane would silently skip. Fix the DB wiring or unset the flag."
|
||||
)
|
||||
if shutil.which("ffmpeg") is None:
|
||||
raise pytest.UsageError(
|
||||
"SIFTLODE_TEST_REQUIRE_DB=1 but ffmpeg is missing — the thumbnail/poster tests would skip."
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db(_schema):
|
||||
"""A real session against the test Postgres, on a clean (empty) database. The code under test
|
||||
@@ -70,3 +99,53 @@ def db(_schema):
|
||||
finally:
|
||||
session.rollback()
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_user(db):
|
||||
"""Factory: create + commit a User in the test DB. Defaults produce a login-ready account (email
|
||||
verified, active, with a known password); override any of it via kwargs (e.g. `role="admin"`,
|
||||
`is_suspended=True`, `password=None` for a Google-only account). Returns the User."""
|
||||
seq = {"n": 0}
|
||||
|
||||
def _make(email: str | None = None, *, password: str | None = "correct horse battery", **kw):
|
||||
seq["n"] += 1
|
||||
kw.setdefault("email_verified", True)
|
||||
user = User(
|
||||
email=email or f"user{seq['n']}@example.com",
|
||||
password_hash=security.hash_password(password) if password else None,
|
||||
**kw,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
return user
|
||||
|
||||
return _make
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(db):
|
||||
"""A TestClient wired to the test DB that goes through the REAL auth/session stack — no
|
||||
`current_user` override — so login, the wallet gate and epoch revoke are exercised end to end.
|
||||
Starlette's SessionMiddleware round-trips the signed cookie, so a login in one call authenticates
|
||||
the next on this same instance. No lifespan (the scheduler / HLS sweep stay off)."""
|
||||
app.dependency_overrides[get_db] = lambda: db
|
||||
try:
|
||||
yield TestClient(app)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def as_user(db):
|
||||
"""Factory -> a TestClient already authenticated as a given User (via a `current_user` override),
|
||||
for tests that need an authed request but aren't exercising the login flow itself."""
|
||||
def _as(user: User) -> TestClient:
|
||||
app.dependency_overrides[get_db] = lambda: db
|
||||
app.dependency_overrides[current_user] = lambda: user
|
||||
return TestClient(app)
|
||||
|
||||
try:
|
||||
yield _as
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
Reference in New Issue
Block a user