"""The sliding-window limiter behind /auth/register, password-login and password-reset. Pure logic, no DB. `reset()` in particular is only ever called by a test fixture (conftest clears the module-level limiters between tests), so without a test here a refactor could turn it into a no-op and nothing would notice until an unrelated test started failing with a 429. """ from app import ratelimit from app.ratelimit import RateLimiter def test_allow_trips_at_the_cap_and_stays_tripped(): lim = RateLimiter(max_events=2, window_seconds=300) assert lim.allow("ip") is True assert lim.allow("ip") is True assert lim.allow("ip") is False assert lim.allow("ip") is False def test_hammering_while_blocked_does_not_push_the_window_forward(monkeypatch): # A blocked attempt must not be recorded, otherwise someone who keeps retrying holds their own # window open forever — an indefinite lockout for anyone sharing that IP. Only observable across # the window boundary, so drive the clock: two attempts fill the cap, ten blocked retries land # just before it expires, and the caller must still be let back in on the far side. clock = {"t": 1000.0} monkeypatch.setattr(ratelimit.time, "monotonic", lambda: clock["t"]) lim = RateLimiter(max_events=2, window_seconds=60) assert lim.allow("ip") is True assert lim.allow("ip") is True for _ in range(10): clock["t"] += 5 # still inside the window; every one of these is refused assert lim.allow("ip") is False clock["t"] = 1000.0 + 61 # past the window of the two attempts that actually counted assert lim.allow("ip") is True def test_keys_are_independent(): lim = RateLimiter(max_events=1, window_seconds=300) assert lim.allow("a") is True assert lim.allow("a") is False assert lim.allow("b") is True def test_reset_clears_every_key(): lim = RateLimiter(max_events=1, window_seconds=300) lim.allow("a") lim.allow("b") assert lim.allow("a") is False lim.reset() assert lim.allow("a") is True assert lim.allow("b") is True def test_exhausted_reports_a_full_window_without_counting_an_attempt(): # The asking half of a limit that counts only some outcomes (a share link's unlock counts only # wrong passwords toward its ceiling): asking must never use up the window it asks about. lim = RateLimiter(max_events=2, window_seconds=300) for _ in range(5): assert lim.exhausted("link") is False assert lim.allow("link") is True assert lim.exhausted("link") is False assert lim.allow("link") is True assert lim.exhausted("link") is True def test_expired_attempts_fall_out_of_the_window(): # A zero-length window means every previous attempt is already outside it. lim = RateLimiter(max_events=1, window_seconds=0) assert lim.allow("ip") is True assert lim.allow("ip") is True