diff --git a/backend/Dockerfile.test b/backend/Dockerfile.test index 6bffc2f8..03f3709b 100644 --- a/backend/Dockerfile.test +++ b/backend/Dockerfile.test @@ -8,5 +8,11 @@ # docker run --rm -v "/backend:/app" -w /app siftlode-test:local python -m pytest FROM python:3.12-slim WORKDIR /app +# ffmpeg so the thumbnail/poster tests (needs_ffmpeg) actually run in the gate instead of skipping +# silently — the runtime image already ships ffmpeg, so the test image should too (see conftest's +# SIFTLODE_TEST_REQUIRE_DB, which now also refuses to run if ffmpeg is missing). +RUN apt-get update \ + && apt-get install -y --no-install-recommends ffmpeg \ + && rm -rf /var/lib/apt/lists/* COPY requirements.txt requirements-dev.txt ./ RUN pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt diff --git a/backend/docker-compose.test.yml b/backend/docker-compose.test.yml index 7a140827..95d6ee95 100644 --- a/backend/docker-compose.test.yml +++ b/backend/docker-compose.test.yml @@ -29,6 +29,9 @@ services: condition: service_healthy environment: DATABASE_URL: postgresql+psycopg://siftlode:siftlode@testdb:5432/siftlode_test + # This lane HAS a Postgres + ffmpeg, so the DB/ffmpeg tests must actually run here: conftest turns + # a would-be silent skip into a hard failure (SIFTLODE_TEST_REQUIRE_DB). + SIFTLODE_TEST_REQUIRE_DB: "1" volumes: - ./:/app working_dir: /app diff --git a/backend/pytest.ini b/backend/pytest.ini index d052618b..6692f5b6 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -1,6 +1,8 @@ [pytest] # Run from backend/ (the image's /app). `pythonpath = .` puts the app package on sys.path so tests -# import it as `app.*`, matching runtime. The suite is pure-logic only (no DB/network) — see tests/. +# import it as `app.*`, matching runtime. Most of the suite is pure-logic; the DB-backed lane opts in +# via the `db` fixture and runs in the compose test image. `-rs` reports skip REASONS so a silently +# skipped lane is visible (the compose lane sets SIFTLODE_TEST_REQUIRE_DB=1 to make skips a hard error). testpaths = tests pythonpath = . -addopts = -q +addopts = -q -rs diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 8fa4ec87..268bd229 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -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()