c0310ed573
- Add pytest test suite: test_auth.py, test_admin.py, test_pass.py, test_proxy.py, test_rate_limit.py, test_client_contract.py - Fix SQLite 'database is locked' errors: moved log_audit() calls outside with get_db() blocks in register, login, logout, refresh, activate_pass - Enable WAL mode and busy_timeout in get_db() for concurrent access - Fix /admin/me: removed non-existent 'email' column from query - Fix /admin/users list: disambiguated activated_at column in JOIN query - Fix /auth/refresh: now returns refresh_token + expires_in + username/uuid/role to match AuthManager.AuthSession expectations; revokes old refresh token - Fix conftest.py: unique usernames per test to avoid conflicts - All 47 tests passing
71 lines
2.3 KiB
Python
71 lines
2.3 KiB
Python
"""Tests for rate limiting (TTLCache-based)."""
|
|
import pytest
|
|
from auth import check_rate_limit, record_login_attempt, MAX_LOGIN_ATTEMPTS, LOGIN_BLOCK_MINUTES
|
|
|
|
|
|
class TestRateLimit:
|
|
"""Test rate limiting functions."""
|
|
|
|
def test_no_attempts_allowed(self):
|
|
"""Fresh IP should be allowed."""
|
|
allowed, wait = check_rate_limit("fresh-ip")
|
|
assert allowed is True
|
|
assert wait is None
|
|
|
|
def test_single_attempt_allowed(self):
|
|
"""One failed attempt should still be allowed."""
|
|
ip = "single-attempt-ip"
|
|
record_login_attempt(ip, False)
|
|
allowed, wait = check_rate_limit(ip)
|
|
assert allowed is True
|
|
|
|
def test_max_attempts_blocks(self):
|
|
"""MAX_LOGIN_ATTEMPTS failed attempts should block."""
|
|
ip = "blocked-ip"
|
|
for _ in range(MAX_LOGIN_ATTEMPTS):
|
|
record_login_attempt(ip, False)
|
|
|
|
allowed, wait = check_rate_limit(ip)
|
|
assert allowed is False
|
|
assert wait is not None
|
|
assert wait > 0
|
|
# Wait should be approximately LOGIN_BLOCK_MINUTES * 60
|
|
assert wait <= LOGIN_BLOCK_MINUTES * 60
|
|
|
|
def test_success_resets_attempts(self):
|
|
"""Successful login should reset rate limit."""
|
|
ip = "reset-ip"
|
|
for _ in range(MAX_LOGIN_ATTEMPTS - 1):
|
|
record_login_attempt(ip, False)
|
|
|
|
# One success should reset
|
|
record_login_attempt(ip, True)
|
|
|
|
allowed, wait = check_rate_limit(ip)
|
|
assert allowed is True
|
|
assert wait is None
|
|
|
|
def test_success_then_fail_starts_fresh(self):
|
|
"""After success reset, failing again should start from 1."""
|
|
ip = "fresh-start-ip"
|
|
record_login_attempt(ip, False)
|
|
record_login_attempt(ip, True)
|
|
record_login_attempt(ip, False)
|
|
|
|
allowed, wait = check_rate_limit(ip)
|
|
assert allowed is True # Only 1 attempt after reset
|
|
|
|
def test_separate_ips_independent(self):
|
|
"""Rate limit should be per-IP."""
|
|
ip1 = "ip-one"
|
|
ip2 = "ip-two"
|
|
|
|
for _ in range(MAX_LOGIN_ATTEMPTS):
|
|
record_login_attempt(ip1, False)
|
|
|
|
allowed1, _ = check_rate_limit(ip1)
|
|
allowed2, _ = check_rate_limit(ip2)
|
|
|
|
assert allowed1 is False
|
|
assert allowed2 is True
|