"""Limiteur de tentatives sur `/auth/login` et `/auth/magic-link` — voir `app/auth/rate_limit.py`.""" import pytest from fastapi import HTTPException from app.auth.rate_limit import RateLimiter from app.models import Role PASSWORD = "coordination-2026" def _url(edition, suffix): return f"/editions/{edition.id}/auth/{suffix}" def test_rate_limiter_blocks_beyond_max_attempts(): limiter = RateLimiter(max_attempts=3, window_seconds=60) for _ in range(3): limiter.hit("k") with pytest.raises(HTTPException) as exc_info: limiter.hit("k") assert exc_info.value.status_code == 429 def test_rate_limiter_keys_are_independent(): limiter = RateLimiter(max_attempts=1, window_seconds=60) limiter.hit("a") # Une autre clé n'est pas affectée par les tentatives sur "a". limiter.hit("b") def test_rate_limiter_forgets_hits_outside_the_window(monkeypatch): limiter = RateLimiter(max_attempts=1, window_seconds=10) now = [1000.0] monkeypatch.setattr("app.auth.rate_limit.time.monotonic", lambda: now[0]) limiter.hit("k") now[0] += 11 # au-delà de la fenêtre de 10s limiter.hit("k") # ne lève pas : la première tentative est oubliée def test_login_is_rate_limited_after_five_attempts(raw_client, edition, make_account): make_account(Role.COORDINATOR, email="admin@e.fr", password=PASSWORD) for _ in range(5): resp = raw_client.post( _url(edition, "login"), json={"email": "admin@e.fr", "password": "faux"}, ) assert resp.status_code == 401 resp = raw_client.post( _url(edition, "login"), json={"email": "admin@e.fr", "password": "faux"}, ) assert resp.status_code == 429 assert resp.json()["detail"]["code"] == "error.too_many_requests" # Même le bon mot de passe est bloqué tant que la fenêtre n'est pas passée. resp = raw_client.post( _url(edition, "login"), json={"email": "admin@e.fr", "password": PASSWORD}, ) assert resp.status_code == 429 def test_login_rate_limit_is_scoped_per_email(raw_client, edition, make_account): make_account(Role.COORDINATOR, email="admin@e.fr", password=PASSWORD) make_account(Role.COORDINATOR, email="autre@e.fr", password=PASSWORD) for _ in range(5): raw_client.post( _url(edition, "login"), json={"email": "admin@e.fr", "password": "faux"}, ) # Un autre compte n'est pas affecté par les tentatives sur le premier. resp = raw_client.post( _url(edition, "login"), json={"email": "autre@e.fr", "password": PASSWORD}, ) assert resp.status_code == 200 def test_magic_link_is_rate_limited_after_three_requests(raw_client, edition, make_account): make_account(Role.VOLUNTEER, email="v@e.fr") for _ in range(3): resp = raw_client.post(_url(edition, "magic-link"), json={"email": "v@e.fr"}) assert resp.status_code == 202 resp = raw_client.post(_url(edition, "magic-link"), json={"email": "v@e.fr"}) assert resp.status_code == 429 assert resp.json()["detail"]["code"] == "error.too_many_requests"