"""Tests for rate limiter -- token bucket algorithm.""" from __future__ import annotations import time from openjarvis.security.rate_limiter import ( RateLimitConfig, RateLimiter, TokenBucket, ) class TestTokenBucket: """Tests for the TokenBucket class.""" def test_initial_capacity(self) -> None: """New bucket allows burst_size requests.""" bucket = TokenBucket(rate=1.0, capacity=5) for _ in range(5): allowed, wait = bucket.consume() assert allowed is True assert wait == 0.0 def test_consume_reduces_tokens(self) -> None: """After consume, available decreases.""" bucket = TokenBucket(rate=1.0, capacity=10) initial = bucket.available bucket.consume(3) assert bucket.available < initial def test_refill_over_time(self) -> None: """After waiting, tokens refill.""" bucket = TokenBucket(rate=10.0, capacity=5) # Drain all tokens for _ in range(5): bucket.consume() assert bucket.available < 1.0 # Wait for refill time.sleep(0.15) assert bucket.available >= 1.0 def test_exceeds_capacity(self) -> None: """Consuming more than available returns (False, wait_time).""" bucket = TokenBucket(rate=1.0, capacity=3) # Drain all tokens for _ in range(3): bucket.consume() allowed, wait = bucket.consume() assert allowed is False assert wait > 0.0 def test_burst(self) -> None: """Can consume burst_size tokens immediately.""" capacity = 8 bucket = TokenBucket(rate=1.0, capacity=capacity) allowed, wait = bucket.consume(capacity) assert allowed is True assert wait == 0.0 class TestRateLimiter: """Tests for the RateLimiter class.""" def test_disabled(self) -> None: """When enabled=False, always allows.""" config = RateLimitConfig(enabled=False, burst_size=1, requests_per_minute=1) limiter = RateLimiter(config) for _ in range(100): allowed, wait = limiter.check("any_key") assert allowed is True assert wait == 0.0 def test_allows_within_limit(self) -> None: """Requests within RPM are allowed.""" config = RateLimitConfig(requests_per_minute=600, burst_size=10) limiter = RateLimiter(config) for _ in range(10): allowed, _ = limiter.check("agent_a") assert allowed is True def test_blocks_over_limit(self) -> None: """Rapid requests beyond burst are blocked.""" config = RateLimitConfig(requests_per_minute=60, burst_size=3) limiter = RateLimiter(config) # Exhaust burst for _ in range(3): allowed, _ = limiter.check("agent_b") assert allowed is True # Next should be blocked allowed, wait = limiter.check("agent_b") assert allowed is False assert wait > 0.0 def test_separate_keys(self) -> None: """Different keys have independent limits.""" config = RateLimitConfig(requests_per_minute=60, burst_size=2) limiter = RateLimiter(config) # Exhaust key1 limiter.check("key1") limiter.check("key1") allowed_key1, _ = limiter.check("key1") assert allowed_key1 is False # key2 should still work allowed_key2, _ = limiter.check("key2") assert allowed_key2 is True def test_reset_key(self) -> None: """Reset clears specific key.""" config = RateLimitConfig(requests_per_minute=60, burst_size=2) limiter = RateLimiter(config) limiter.check("key1") limiter.check("key1") allowed, _ = limiter.check("key1") assert allowed is False # Reset key1 limiter.reset("key1") allowed, _ = limiter.check("key1") assert allowed is True def test_reset_all(self) -> None: """Reset without key clears everything.""" config = RateLimitConfig(requests_per_minute=60, burst_size=1) limiter = RateLimiter(config) limiter.check("key1") limiter.check("key2") # Both exhausted assert limiter.check("key1")[0] is False assert limiter.check("key2")[0] is False # Reset all limiter.reset() assert limiter.check("key1")[0] is True assert limiter.check("key2")[0] is True def test_default_config(self) -> None: """Default values are reasonable.""" limiter = RateLimiter() assert limiter.config.requests_per_minute == 60 assert limiter.config.burst_size == 10 assert limiter.config.enabled is True