Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,13 @@ ignore = [

[tool.ruff.lint.isort]
known-first-party = ["app"]

[tool.uv]
# FastAPI >=0.137 changed how included routers' routes are resolved, which
# breaks slowapi's SlowAPIMiddleware enforcement of `default_limits` on
# undecorated routes mounted via include_router (verified here: the global
# rate-limit canary fails on 0.140.x and passes on 0.135.x). Known upstream
# as slowapi issue #281 with fix PR #282; drop this hold once a slowapi
# release containing that fix ships. Starlette is capped alongside it since
# slowapi has never been validated against Starlette 1.x.
constraint-dependencies = ["fastapi<0.136", "starlette<1.0"]
11 changes: 11 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
async_session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)


@pytest.fixture(autouse=True)
def reset_rate_limiter():
"""Clear rate-limit buckets between tests so request counts don't leak
across tests (the limiter storage is process-global)."""
from app.rate_limit import limiter

limiter._storage.reset()
yield
limiter._storage.reset()


@pytest.fixture(autouse=True)
async def setup_database():
async with engine.begin() as conn:
Expand Down
27 changes: 27 additions & 0 deletions tests/test_rate_limiting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Global rate limiting behaviour.

The Limiter's ``default_limits`` (60/min, enforced by SlowAPIMiddleware)
must apply to every route that isn't decorated or exempt, and the
infrastructure routes must stay exempt. This is the canary for the class
of regression where a framework upgrade silently disables middleware
enforcement (see slowapi issue #281 for fastapi>=0.137).
"""

from httpx import AsyncClient


class TestGlobalRateLimit:
async def test_default_limit_enforced_on_undecorated_route(self, client: AsyncClient):
# /v1/armor has no explicit @limiter.limit, so it gets the 60/min
# default. (Matches `default_limits` in app/rate_limit.py — bump
# both together if that changes.)
for _ in range(60):
assert (await client.get("/v1/armor")).status_code == 200
assert (await client.get("/v1/armor")).status_code == 429

async def test_infrastructure_routes_are_exempt(self, client: AsyncClient):
# / and /health are @limiter.exempt — many rapid hits (well past
# the 60/min default) never 429.
for _ in range(70):
assert (await client.get("/health")).status_code == 200
assert (await client.get("/")).status_code == 200
6 changes: 6 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.