from types import SimpleNamespace import pytest from httpx import AsyncClient from starlette.requests import Request from porchlight.rate_limit import _client_identifier from tests.conftest import get_csrf_token @pytest.mark.asyncio async def test_password_login_rate_limited(client: AsyncClient) -> None: """After 5 failed attempts, the 6th should be rate-limited.""" token = await get_csrf_token(client) for _ in range(5): await client.post( "/login/password", data={"username": "nobody", "password": "wrong"}, headers={"X-CSRF-Token": token}, ) response = await client.post( "/login/password", data={"username": "nobody", "password": "wrong"}, headers={"X-CSRF-Token": token}, ) assert response.status_code == 429 def _make_request(headers: dict[str, str], client_host: str, trusted_proxy_count: int) -> Request: scope = { "type": "http", "headers": [(k.lower().encode(), v.encode()) for k, v in headers.items()], "client": (client_host, 12345), "app": SimpleNamespace( state=SimpleNamespace(settings=SimpleNamespace(trusted_proxy_count=trusted_proxy_count)) ), } return Request(scope) def test_client_identifier_ignores_forwarded_when_no_trusted_proxy() -> None: req = _make_request({"x-forwarded-for": "203.0.113.9"}, "127.0.0.1", trusted_proxy_count=0) # Spoofable header must be ignored when no proxy is trusted. assert _client_identifier(req) == "127.0.0.1" def test_client_identifier_uses_forwarded_with_one_trusted_proxy() -> None: req = _make_request({"x-forwarded-for": "203.0.113.9"}, "10.0.0.1", trusted_proxy_count=1) assert _client_identifier(req) == "203.0.113.9" def test_client_identifier_picks_correct_hop_with_two_trusted_proxies() -> None: req = _make_request({"x-forwarded-for": "client, proxyA, proxyB"}, "10.0.0.1", trusted_proxy_count=2) # Two trusted proxies appended their peers; the client as seen by the # outermost trusted proxy is the 2nd-from-right entry. assert _client_identifier(req) == "proxyA" def test_client_identifier_falls_back_when_forwarded_missing() -> None: req = _make_request({}, "10.0.0.1", trusted_proxy_count=1) assert _client_identifier(req) == "10.0.0.1"