"""Real 429s from the shared slowapi limiter. `tests/conftest.py` turns rate limiting OFF by default for the whole test session (see the comment there) — every ASGI test client in a pytest run would otherwise share one process-global in-memory limiter keyed by remote address, and look like a single client to it. This file is the one place that turns the limiter back on (via the `rate_limiting_enabled` fixture) to prove the feature itself still works end-to-end: hit `/api/auth/login` past its declared 10/minute budget and confirm slowapi actually returns a 429, not just that our own code assumes it would. """ from __future__ import annotations from collections.abc import AsyncIterator import aiosqlite import pytest import pytest_asyncio from httpx import ASGITransport, AsyncClient from app.dependencies import get_connection from app.main import app @pytest_asyncio.fixture async def client(connection: aiosqlite.Connection) -> AsyncIterator[AsyncClient]: """Same technique as `tests/integration/test_user_journey.py`'s `client` fixture: a real app over HTTP, with only the DB connection swapped for the test's in-memory one. """ app.dependency_overrides[get_connection] = lambda: connection async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac: yield ac app.dependency_overrides.pop(get_connection, None) @pytest.mark.usefixtures("rate_limiting_enabled") async def test_login_returns_429_once_the_per_minute_budget_is_spent( client: AsyncClient, ) -> None: payload = {"email": "nobody@example.com", "password": "wrong password entirely"} # /api/auth/login is limited to 10/minute (app/routers/auth.py, # `@limiter.limit("10/minute")`). The credentials are deliberately # wrong — slowapi counts the call before the handler body runs, and a # 401 has no side effects to clean up afterwards, unlike registering # ten real users would. for _ in range(10): resp = await client.post("/api/auth/login", json=payload) assert resp.status_code == 401, resp.text resp = await client.post("/api/auth/login", json=payload) assert resp.status_code == 429, resp.text assert "Rate limit exceeded" in resp.json()["error"]