"""Regression tests for BUG-02: rate limiter must key on the real client IP relayed by the proxy (x-forwarded-for / x-real-ip), not the proxy's own IP, and default_limits must actually be enforced via SlowAPIMiddleware.""" from __future__ import annotations from types import SimpleNamespace from slowapi.middleware import SlowAPIMiddleware from app.ratelimit import client_ip_key, limiter def _req(headers: dict, peer: str | None = "10.0.0.1"): """Minimal stand-in for starlette Request: only .headers and .client are read by client_ip_key. Headers must be case-insensitive get().""" lower = {k.lower(): v for k, v in headers.items()} client = SimpleNamespace(host=peer) if peer is not None else None return SimpleNamespace( headers=SimpleNamespace(get=lambda k, d=None: lower.get(k.lower(), d)), client=client, ) def test_key_prefers_first_x_forwarded_for_hop(): # Left-most XFF entry is the original client; proxies append to the right. r = _req({"x-forwarded-for": "203.0.113.9, 10.0.0.1, 172.16.0.1"}) assert client_ip_key(r) == "203.0.113.9" def test_key_falls_back_to_x_real_ip(): r = _req({"x-real-ip": "198.51.100.7"}) assert client_ip_key(r) == "198.51.100.7" def test_key_falls_back_to_peer_when_no_headers(): r = _req({}, peer="192.0.2.50") assert client_ip_key(r) == "192.0.2.50" def test_key_handles_missing_client(): r = _req({}, peer=None) assert client_ip_key(r) == "unknown" def test_two_users_behind_same_proxy_get_distinct_keys(): # The whole point of BUG-02: same TCP peer (proxy), different end users. a = _req({"x-forwarded-for": "203.0.113.1"}, peer="10.0.0.1") b = _req({"x-forwarded-for": "203.0.113.2"}, peer="10.0.0.1") assert client_ip_key(a) != client_ip_key(b) def test_limiter_uses_custom_key_func(): assert limiter._key_func is client_ip_key def test_slowapi_middleware_registered_on_app(): # Without the middleware, default_limits never applies to undecorated # (mutation) routes. Guard against silent regression. import app.main as main classes = [m.cls for m in main.app.user_middleware] assert SlowAPIMiddleware in classes