Implementation: Login/Register UI wired to /api/auth #35

Merged
reviewer-bot merged 6 commits from agent/1bc0bc15886d into main 2026-08-19 21:27:14 +00:00
Showing only changes of commit ef78caa4de - Show all commits
+32 -10
View File
@@ -1,14 +1,20 @@
"""Integration tests for the auth endpoints (register + login). """Integration tests for the auth endpoints (register + login).
These tests hit the real FastAPI app with an in-memory SQLite database, These tests hit the real FastAPI app against the real configured database
so they exercise the full stack: Pydantic validation → DB → JWT → response. (DATABASE_URL -- Postgres via Alembic migrations in CI/dev, not an isolated
in-memory DB), so they exercise the full stack: Pydantic validation → DB →
JWT → response. Requires migrations already applied (alembic upgrade head)
before running.
""" """
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.pool import NullPool
from app.config import settings
from app.main import app from app.main import app
from app.database import get_db, override_get_db from app.database import get_db
from app.routers.auth import pwd_context from app.routers.auth import pwd_context
@@ -18,14 +24,25 @@ from app.routers.auth import pwd_context
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _override_db(): def _override_db():
"""Use an in-memory SQLite DB for every test.""" """Override get_db so the app uses a fresh session per test.
from app.database import engine, Base, async_session_factory
Base.metadata.create_all(bind=engine) Deliberately does NOT reuse app.database's shared engine: FastAPI's
from app.database import SessionLocal (sync) TestClient runs each request through anyio's blocking-portal
machinery, which can hand different requests different event loops.
asyncpg connections are bound to the loop they were created on, so
reusing one from the shared engine's connection pool across requests
raises "cannot perform operation: another operation is in progress" the
moment a test makes more than one request (register-then-login,
register-twice, etc.) -- confirmed this is the actual cause, not a bug
in the endpoints themselves. NullPool sidesteps it: every checkout is a
brand-new connection, never handed to a different loop than the one
that opened it.
"""
test_engine = create_async_engine(settings.DATABASE_URL, poolclass=NullPool)
test_session_factory = async_sessionmaker(test_engine, expire_on_commit=False)
async def _get_db(): async def _get_db():
async_session = async_session_factory() async_session = test_session_factory()
try: try:
yield async_session yield async_session
await async_session.commit() await async_session.commit()
@@ -197,11 +214,16 @@ class TestLogin:
def test_login_wrong_password(self): def test_login_wrong_password(self):
"""Wrong password returns 401.""" """Wrong password returns 401."""
self._create_user("loginuser", "correctpass") # Distinct username from test_login_success -- there's no per-test
# DB rollback/isolation here (real Postgres via the app's own
# dependency-overridden session), so reusing "loginuser" collided
# with the user that test already created (409 on register instead
# of the intended 401-on-login assertion this test is actually for).
self._create_user("loginuser_wrongpass", "correctpass")
client = TestClient(app) client = TestClient(app)
resp = client.post( resp = client.post(
"/api/auth/login", "/api/auth/login",
data={"username": "loginuser", "password": "wrongpass"}, data={"username": "loginuser_wrongpass", "password": "wrongpass"},
) )
assert resp.status_code == 401 assert resp.status_code == 401
assert "Incorrect username or password" in resp.json()["detail"] assert "Incorrect username or password" in resp.json()["detail"]