venthrive-api / tests /test_rate_limiter.py
GitHub Actions
Sync from GitHub · 2026-09-24 23:37 UTC
889e86b
Raw History Blame Contribute Delete
2.95 kB
"""
Unit tests for utils/rate_limiter.py's pure counting/enforcement logic.
Scope: enforce_rate_limit() itself (fixed-window counting, per-tier limits,
429 on breach). The with_rate_limit() FastAPI dependency composition and the
Firestore violation-logging side effect are thin wrappers around this and are
not separately unit-tested (per docs/qa/test-strategy.md, the FastAPI/
Firestore integration belongs at the integration layer, not here).
"""
import time
import pytest
from fastapi import HTTPException
import utils.rate_limiter as rl
@pytest.fixture(autouse=True)
def reset_limiter_state(monkeypatch):
# Each test gets a clean set of counters and a violation logger that
# never touches Firestore (no app is initialized in this test process).
monkeypatch.setattr(rl, "_counters", rl.defaultdict(rl._Window))
monkeypatch.setattr(rl, "_violation_counts", rl.defaultdict(int))
monkeypatch.setattr(rl, "_log_violation", lambda *a, **k: None)
def test_allows_requests_within_the_limit():
for _ in range(8):
rl.enforce_rate_limit("uid-1", "generation") # 8/hour is the limit — must not raise
def test_blocks_the_request_that_exceeds_the_limit():
for _ in range(8):
rl.enforce_rate_limit("uid-1", "generation")
with pytest.raises(HTTPException) as exc_info:
rl.enforce_rate_limit("uid-1", "generation")
assert exc_info.value.status_code == 429
def test_counters_are_isolated_per_uid():
for _ in range(8):
rl.enforce_rate_limit("uid-1", "generation")
# A different uid must not be affected by uid-1 exhausting its limit.
rl.enforce_rate_limit("uid-2", "generation")
def test_counters_are_isolated_per_tier():
for _ in range(8):
rl.enforce_rate_limit("uid-1", "generation")
# The same uid hitting a different tier must not be blocked by "generation".
rl.enforce_rate_limit("uid-1", "chat")
def test_a_tier_with_multiple_windows_enforces_the_tighter_one_first():
# "chat" is 10/minute and 200/day -- the per-minute window binds first.
for _ in range(10):
rl.enforce_rate_limit("uid-1", "chat")
with pytest.raises(HTTPException) as exc_info:
rl.enforce_rate_limit("uid-1", "chat")
assert exc_info.value.status_code == 429
assert "minute" in exc_info.value.detail
def test_window_resets_after_its_period_elapses(monkeypatch):
fake_now = [1_000_000.0]
monkeypatch.setattr(rl.time, "time", lambda: fake_now[0])
for _ in range(8):
rl.enforce_rate_limit("uid-1", "generation")
with pytest.raises(HTTPException):
rl.enforce_rate_limit("uid-1", "generation")
# Jump past the 1-hour window.
fake_now[0] += 3600 + 1
rl.enforce_rate_limit("uid-1", "generation") # must not raise — window reset
def test_unknown_tier_raises_a_programmer_error_not_a_silent_pass():
with pytest.raises(ValueError):
rl.enforce_rate_limit("uid-1", "not-a-real-tier")