test: a handler that flushes a write and never commits it fails its test
Helpers flush and handlers commit — but every test request shared the test's session, so a follow-up GET or an assertion read the flushed write and the suite stayed green over a handler whose commit was missing (production's get_db closes the session: the write is rolled back behind a 200). The db fixture now hands out `db.info["request"]`: the same session, recording each request that ends normally with a flushed, uncommitted write, and failing the test on it at teardown. All 28 get_db overrides use it (a convention test keeps it so); the full suite passes under it, and dropping the commit from add_item turns a playlist test red.
This commit is contained in:
@@ -21,7 +21,8 @@ from alembic import command
|
||||
from alembic.config import Config
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy import create_engine, event, text
|
||||
from starlette.requests import HTTPConnection
|
||||
|
||||
import app.models # noqa: F401 — register every model on Base.metadata
|
||||
import app.state as app_state
|
||||
@@ -100,17 +101,57 @@ def pytest_configure(config):
|
||||
)
|
||||
|
||||
|
||||
def _request_dep(db, unfinished: list[str]):
|
||||
"""`get_db` for a test request: the test's own session, plus the check a shared session hides.
|
||||
|
||||
A route helper flushes and its HANDLER commits (CLAUDE.md). Production's `get_db` closes the
|
||||
session after the request, which rolls back anything flushed but not committed — yet here every
|
||||
request shares the test's session, so a follow-up GET or an assertion reads the flushed write
|
||||
and the suite stays green over a handler that forgot to commit (deep review, debt burn). So a
|
||||
request that ends normally with a flushed, uncommitted write is recorded, and the test fails on
|
||||
it at teardown. The test's own staged rows are flushed before the request starts — they are the
|
||||
test's business, not the handler's. A request that raises is exempt: its writes are meant to
|
||||
roll back."""
|
||||
|
||||
def dep(conn: HTTPConnection):
|
||||
db.flush()
|
||||
state = {"dirty": False}
|
||||
|
||||
def flushed(session, ctx):
|
||||
state["dirty"] = True
|
||||
|
||||
def ended(session):
|
||||
state["dirty"] = False
|
||||
|
||||
hooks = (("after_flush", flushed), ("after_commit", ended), ("after_rollback", ended))
|
||||
for name, fn in hooks:
|
||||
event.listen(db, name, fn)
|
||||
try:
|
||||
yield db
|
||||
if state["dirty"]:
|
||||
unfinished.append(f"{conn.scope.get('method', 'WS')} {conn.url.path}")
|
||||
finally:
|
||||
for name, fn in hooks:
|
||||
event.remove(db, name, fn)
|
||||
|
||||
return dep
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db(_schema):
|
||||
"""A real session against the test Postgres, on a clean (empty) database. The code under test
|
||||
may commit freely."""
|
||||
may commit freely. `db.info["request"]` is the `get_db` override for a request against it — the
|
||||
one every test client uses — and a handler that left a write uncommitted fails the test here."""
|
||||
_truncate_all()
|
||||
session = SessionLocal()
|
||||
unfinished: list[str] = []
|
||||
session.info["request"] = _request_dep(session, unfinished)
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.rollback()
|
||||
session.close()
|
||||
assert unfinished == [], "a handler flushed a write and never committed it"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -168,7 +209,7 @@ def client(db, override_dep, _configured):
|
||||
`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)."""
|
||||
override_dep(get_db, lambda: db)
|
||||
override_dep(get_db, db.info["request"])
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@@ -187,7 +228,7 @@ def as_user(db, override_dep, _configured):
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
return registry[uid]
|
||||
|
||||
override_dep(get_db, lambda: db)
|
||||
override_dep(get_db, db.info["request"])
|
||||
override_dep(current_user, _resolver)
|
||||
|
||||
def _as(user: User) -> TestClient:
|
||||
|
||||
Reference in New Issue
Block a user