diff --git a/tests/conftest.py b/tests/conftest.py index ca9e569..fa83983 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -469,6 +469,48 @@ def mock_imap_connections(monkeypatch): yield fake_client +@pytest.fixture(autouse=True) +def _reset_inmemory_rate_limiter(): + """Clear the process-local rate limiter around every test (BUG-097). + + The InMemoryRateLimiter is process-local and would otherwise accumulate + password-reset/login attempts across tests, making later tests fail with + 429 depending purely on execution order. + """ + from app.core.resilience import get_inmemory_limiter + + get_inmemory_limiter().clear_all() + yield + get_inmemory_limiter().clear_all() + + +@pytest_asyncio.fixture(autouse=True) +async def _clear_rate_limit_keys(): + """Delete Redis rate-limit keys after every test (BUG-097). + + check_rate_limit uses Redis INCR+EXPIRE keyed by client IP; all tests share + the same test-client IP, so password-reset/login counters accumulate across + tests and later tests hit 429 depending purely on execution order. + + Uses the SAME Redis connection the app uses (app settings REDIS_URL, DB 1 in + .env.test) — the session-scoped redis_client fixture points at a different + logical DB than get_redis() and would clean up nothing. + """ + import redis.asyncio as aioredis_mod + + from app.config import get_settings + + url = get_settings().redis_url + r = aioredis_mod.from_url(url, decode_responses=True) + try: + yield + keys = await r.keys("rate:*") + if keys: + await r.delete(*keys) + finally: + await r.aclose() + + @pytest_asyncio.fixture(scope="session") async def engine() -> AsyncGenerator[AsyncEngine, None]: """Async engine for the test database (session-scoped for speed)."""