# #region Test.RateLimiter [C:3] [TYPE Module] [SEMANTICS test,rate_limit,auth,throttle] # @BRIEF Tests for core/rate_limiter.py — in-memory rate limiter, ban/window logic. # @RELATION BINDS_TO -> [RateLimiterModule] from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from unittest.mock import patch, MagicMock import pytest class TestRateLimiter: """RateLimiter — in-memory sliding-window rate limiter.""" @pytest.fixture def limiter(self): from src.core.rate_limiter import RateLimiter return RateLimiter() def test_is_banned_initial_false(self, limiter): assert limiter.is_banned("192.168.1.1") is False def test_record_attempt_under_limit(self, limiter): with patch("src.core.rate_limiter.time") as mock_time: mock_time.monotonic.return_value = 1000.0 for i in range(5): # 5 attempts, well under MAX_ATTEMPTS=10 limiter.record_attempt("192.168.1.1") assert limiter.is_banned("192.168.1.1") is False def test_record_attempt_exceeds_limit(self, limiter): from src.core.rate_limiter import MAX_ATTEMPTS with patch("src.core.rate_limiter.time") as mock_time: mock_time.monotonic.return_value = 1000.0 for i in range(MAX_ATTEMPTS + 1): limiter.record_attempt("192.168.1.1") assert limiter.is_banned("192.168.1.1") is True def test_ban_expires(self, limiter): from src.core.rate_limiter import MAX_ATTEMPTS, BAN_DURATION with patch("src.core.rate_limiter.time") as mock_time: mock_time.monotonic.return_value = 1000.0 for i in range(MAX_ATTEMPTS + 1): limiter.record_attempt("192.168.1.1") assert limiter.is_banned("192.168.1.1") is True # Advance time past ban duration mock_time.monotonic.return_value = 1000.0 + BAN_DURATION + 1 assert limiter.is_banned("192.168.1.1") is False def test_old_attempts_pruned(self, limiter): from src.core.rate_limiter import MAX_ATTEMPTS with patch("src.core.rate_limiter.time") as mock_time: mock_time.monotonic.return_value = 1000.0 for i in range(MAX_ATTEMPTS): limiter.record_attempt("192.168.1.1") # Advance time past window but not to ban expiry mock_time.monotonic.return_value = 1500.0 # 500s later, past 300s window # Old attempts should be pruned. One new attempt should not trigger ban. limiter.record_attempt("192.168.1.1") assert limiter.is_banned("192.168.1.1") is False def test_record_success_clears_attempts(self, limiter): with patch("src.core.rate_limiter.time") as mock_time: mock_time.monotonic.return_value = 1000.0 for i in range(5): limiter.record_attempt("192.168.1.1") limiter.record_success("192.168.1.1") assert limiter.is_banned("192.168.1.1") is False def test_different_ips_independent(self, limiter): with patch("src.core.rate_limiter.time") as mock_time: mock_time.monotonic.return_value = 1000.0 for i in range(15): # exceeds limit for ip1 limiter.record_attempt("192.168.1.1") assert limiter.is_banned("192.168.1.1") is True # Different IP should not be affected assert limiter.is_banned("10.0.0.1") is False class TestRateLimiterSingleton: """rate_limiter singleton is pre-created.""" def test_singleton_exists(self): from src.core.rate_limiter import rate_limiter assert rate_limiter is not None assert hasattr(rate_limiter, "is_banned") assert hasattr(rate_limiter, "record_attempt") assert hasattr(rate_limiter, "record_success") # #endregion Test.RateLimiter