Files
peter 33dbf70313 test(backend): pure-logic pytest harness, run in an isolated test image
The backend had zero tests. This adds 33, all pure (no DB, no network), plus the
harness to run them in the gate:

- backend/Dockerfile.test: the app's deps + pinned pytest/ruff, code bind-mounted at
  run time so it always tests the working tree. Isolated from the prod Dockerfile, so
  the published image never carries test tooling.
- requirements-dev.txt: pytest==8.4.2, ruff==0.15.21 (the version the gate already
  runs) — the backend lane is now pinned, not just host-global.
- tests: normalize_title (de-shout, hashtag strip, emoji drop, trilingual letters
  survive); storage sanitize/rel_path/download_filename and the safe_abs_path
  traversal guard (../ and absolute-path escapes rejected — verified by mutation);
  links HMAC grants (round-trip, wrong-token, tampered sig/exp, expiry) + is_expired;
  a DB-free app-assembly smoke (every router wires up, OpenAPI generates).

DB-backed router smoke (auth/feed/downloads) needs a test Postgres + migrations —
deferred. useCardPager needs hook-test infra (jsdom/renderHook) — deferred.
2026-07-21 03:29:24 +02:00

27 lines
1.3 KiB
Python

"""App-assembly smoke test — DB-free.
Uses TestClient WITHOUT its context-manager form on purpose: that skips the lifespan (which opens a
DB session + starts the scheduler), so the app object can be built and its OpenAPI schema generated
with no database. That alone proves every included router imported and wired cleanly — a broken
route registration or import fails right here.
Endpoints that actually answer (/healthz, /api/version, the auth/feed/downloads routers) all touch
the database, so exercising THEM belongs in a DB-backed integration lane (needs a test Postgres +
migrations) — deferred; see siftlode-ops/ROADMAP.md R2 S2b.
"""
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app) # no `with` → lifespan/startup does not run → no DB required
def test_app_assembles_and_serves_openapi():
schema = client.get("/openapi.json")
assert schema.status_code == 200
paths = schema.json()["paths"]
# A handful of endpoints that must always exist — a rename/removal is a real contract change,
# and their absence here would mean a router failed to register.
for path in ["/healthz", "/api/version", "/api/feed", "/api/me"]:
assert path in paths, f"missing route {path} — a router failed to wire up"