"""test_auth.py — server/auth.py unit tests (no server, no network). Bucket logic in isolation: per-username lockout, per-source-IP budget, sliding-window expiry, and the spoof-gating of X-Forwarded-For. The module reads its env knobs at import; inside the pytest process they stay at defaults (5 / 20 / 900s) — the tests read auth.FAILS_USER/IP_FAILS/ WINDOW rather than hardcoding, so they hold either way. """ import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from server import auth # noqa: E402 class _Peer: host = "203.0.113.9" class _Req: def __init__(self, headers=None): self.headers = headers or {} self.client = _Peer() def test_username_lockout_and_sliding_expiry(): auth.reset() t = 1_000_000.0 for _ in range(auth.FAILS_USER): auth.record("locked-guy", "198.51.100.7", now=t) assert auth.locked("locked-guy", "198.51.100.7", now=t) # window is sliding: once every fail ages out, the name is free again assert not auth.locked("locked-guy", "198.51.100.7", now=t + auth.WINDOW + 1) # a locked username does not gate other usernames from another ip assert not auth.locked("fresh-guy", "198.51.100.8", now=t) # a correct password clears only that username's bucket auth.clear("locked-guy") assert not auth.locked("locked-guy", "198.51.100.9", now=t) auth.reset() def test_ip_budget_survives_username_rotation(): auth.reset() t = 2_000_000.0 for i in range(auth.IP_FAILS): # one fresh username per fail... auth.record(f"sweep-{i}", "198.51.100.9", now=t) assert auth.locked("brand-new-user", "198.51.100.9", now=t) # ...still blocked assert not auth.locked("brand-new-user", "198.51.100.10", now=t) # other ip fine assert not auth.locked("brand-new-user", "198.51.100.9", now=t + auth.WINDOW + 1) auth.reset() def test_client_ip_trust_gating(): # XFF is only consulted when the trust flag is on (it is off here — the # flag is for the prod proxy path, not the test process) r = _Req({"x-forwarded-for": "5.6.7.8, 10.0.0.1"}) assert auth.TRUST_XFF is False assert auth.client_ip(r) == "203.0.113.9" # spoofed header ignored assert auth.client_ip(_Req()) == "203.0.113.9"