"""test_auth.py — pure unit tests for server/auth.py (no server, no HTTP). The per-IP budget and the XFF trust rule live here; the per-username lockout's HTTP behaviour stays covered in test_api.py. """ import time from types import SimpleNamespace def _req(headers=None, host="203.0.113.7"): return SimpleNamespace(headers=dict(headers or {}), client=SimpleNamespace(host=host)) def test_ip_budget_counts_prunes_isolates(monkeypatch): from server import auth monkeypatch.setattr(auth, "IP_FAILS", 3) monkeypatch.setattr(auth, "IP_WINDOW", 60.0) auth._ip_fails.clear() assert not auth.ip_locked("203.0.113.1") for _ in range(3): auth.ip_fail("203.0.113.1") assert auth.ip_locked("203.0.113.1") assert not auth.ip_locked("203.0.113.2") # per-IP isolation auth._ip_fails["203.0.113.1"] = [time.time() - 10_000] # stale -> pruned assert not auth.ip_locked("203.0.113.1") def test_client_ip_xff_trust_rule(monkeypatch): from server import auth r = _req({"x-forwarded-for": "9.9.9.9"}) monkeypatch.delenv("ANIWEB_TRUST_XFF", raising=False) assert auth.client_ip(r) == "203.0.113.7" # header NOT trusted by default monkeypatch.setenv("ANIWEB_TRUST_XFF", "1") assert auth.client_ip(r) == "9.9.9.9" # honored only when explicitly on assert auth.client_ip(_req()) == "203.0.113.7" # no header -> socket peer def test_lock_locked_user_never_reaches_verify(monkeypatch): from server import auth monkeypatch.setattr(auth, "IP_FAILS", 1000) auth._lockout["ghost"] = {"fails": 0, "until": time.time() + 900} assert auth.lock("ghost", "x", "203.0.113.3") == "locked" def test_lock_wrong_password_is_bad_and_records_ip_failure(monkeypatch): from server import auth monkeypatch.setattr(auth, "IP_FAILS", 1000) # keep the IP gate out of the way auth._ip_fails.clear() auth._lockout.clear() # module state is shared across tests assert auth.lock("ghost", "wrong123456", "203.0.113.4") == "bad" # ghost: no user assert len(auth._ip_fails.get("203.0.113.4", [])) == 1