Files
siftlode/backend/tests/test_ratelimit.py
T
peter cb1f4f00b0 docs(auth): correct the last round of over-claiming comments and notes
Round 5's correctness/security pass came back clean; what was left was text that
said more than the code does.

- The register-path notify comment (and its test) claimed the admin "is still
  told". The call fires, but on a no-SMTP instance _send drops it — which is the
  open half of the problem, not something this branch fixed. Both now say so.
- The release notes tell admins what to actually do: an account whose address was
  whitelisted in advance has no pending request to approve, so the whitelist box
  is what activates it. That instruction only existed in the roadmap and in
  chores, which the dialog does not render.
- "That one line is the whole production change" now accounts for the test-only
  RateLimiter.reset() that also ships.
- The rate-limiter test claimed to pin that blocked attempts are not counted, on
  an assertion that held either way. Replaced with one that drives the clock
  across the window boundary and fails if hammering extends it.
2026-09-01 22:02:43 +02:00

66 lines
2.3 KiB
Python

"""The sliding-window limiter behind /auth/register, password-login, password-reset and demo.
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_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