Initial commit: MCP Hub Community Edition v3.0.0
Community edition generated from private repo via sync pipeline. Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~587 tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
166
tests/test_rate_limiter.py
Normal file
166
tests/test_rate_limiter.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""Tests for Rate Limiter (core/rate_limiter.py)."""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from core.rate_limiter import ClientRateLimitState, RateLimitConfig, RateLimiter, TokenBucket
|
||||
|
||||
|
||||
class TestRateLimitConfig:
|
||||
"""Test rate limit configuration."""
|
||||
|
||||
def test_defaults(self):
|
||||
config = RateLimitConfig()
|
||||
assert config.per_minute == 60
|
||||
assert config.per_hour == 1000
|
||||
assert config.per_day == 10000
|
||||
|
||||
def test_custom_values(self):
|
||||
config = RateLimitConfig(per_minute=10, per_hour=100, per_day=500)
|
||||
assert config.per_minute == 10
|
||||
|
||||
def test_from_env(self, monkeypatch):
|
||||
monkeypatch.setenv("RATE_LIMIT_PER_MINUTE", "30")
|
||||
monkeypatch.setenv("RATE_LIMIT_PER_HOUR", "500")
|
||||
monkeypatch.setenv("RATE_LIMIT_PER_DAY", "5000")
|
||||
config = RateLimitConfig.from_env()
|
||||
assert config.per_minute == 30
|
||||
assert config.per_hour == 500
|
||||
assert config.per_day == 5000
|
||||
|
||||
def test_from_env_with_prefix(self, monkeypatch):
|
||||
monkeypatch.setenv("WP_RATE_LIMIT_PER_MINUTE", "20")
|
||||
config = RateLimitConfig.from_env("WP")
|
||||
assert config.per_minute == 20
|
||||
|
||||
|
||||
class TestTokenBucket:
|
||||
"""Test token bucket algorithm."""
|
||||
|
||||
def test_initial_capacity(self):
|
||||
bucket = TokenBucket(capacity=10, refill_rate=1.0)
|
||||
assert bucket.get_available_tokens() == 10
|
||||
|
||||
def test_consume_success(self):
|
||||
bucket = TokenBucket(capacity=10, refill_rate=1.0)
|
||||
assert bucket.consume(1) is True
|
||||
assert bucket.get_available_tokens() == 9
|
||||
|
||||
def test_consume_multiple(self):
|
||||
bucket = TokenBucket(capacity=10, refill_rate=1.0)
|
||||
assert bucket.consume(5) is True
|
||||
assert bucket.get_available_tokens() == 5
|
||||
|
||||
def test_consume_exceeds_capacity(self):
|
||||
bucket = TokenBucket(capacity=5, refill_rate=1.0)
|
||||
assert bucket.consume(6) is False
|
||||
# Tokens should not be consumed on failure
|
||||
assert bucket.get_available_tokens() == 5
|
||||
|
||||
def test_consume_until_empty(self):
|
||||
bucket = TokenBucket(capacity=3, refill_rate=0.0) # no refill
|
||||
assert bucket.consume(1) is True
|
||||
assert bucket.consume(1) is True
|
||||
assert bucket.consume(1) is True
|
||||
assert bucket.consume(1) is False
|
||||
|
||||
def test_get_wait_time_when_available(self):
|
||||
bucket = TokenBucket(capacity=10, refill_rate=1.0)
|
||||
assert bucket.get_wait_time(1) == 0.0
|
||||
|
||||
def test_get_wait_time_when_empty(self):
|
||||
bucket = TokenBucket(capacity=1, refill_rate=1.0)
|
||||
bucket.consume(1)
|
||||
wait = bucket.get_wait_time(1)
|
||||
assert wait > 0
|
||||
|
||||
|
||||
class TestClientRateLimitState:
|
||||
"""Test per-client rate limit state."""
|
||||
|
||||
def _make_client(self, per_minute=5, per_hour=100, per_day=1000):
|
||||
return ClientRateLimitState(
|
||||
client_id="test-client",
|
||||
minute_bucket=TokenBucket(capacity=per_minute, refill_rate=per_minute / 60.0),
|
||||
hour_bucket=TokenBucket(capacity=per_hour, refill_rate=per_hour / 3600.0),
|
||||
day_bucket=TokenBucket(capacity=per_day, refill_rate=per_day / 86400.0),
|
||||
)
|
||||
|
||||
def test_allowed_request(self):
|
||||
client = self._make_client()
|
||||
allowed, reason, retry_after = client.check_and_consume()
|
||||
assert allowed is True
|
||||
assert reason == ""
|
||||
assert retry_after == 0.0
|
||||
|
||||
def test_minute_limit_exceeded(self):
|
||||
client = self._make_client(per_minute=2)
|
||||
client.check_and_consume()
|
||||
client.check_and_consume()
|
||||
allowed, reason, retry_after = client.check_and_consume()
|
||||
assert allowed is False
|
||||
assert "per minute" in reason
|
||||
|
||||
def test_stats(self):
|
||||
client = self._make_client()
|
||||
client.check_and_consume()
|
||||
client.check_and_consume()
|
||||
stats = client.get_stats()
|
||||
assert stats["client_id"] == "test-client"
|
||||
assert stats["total_requests"] == 2
|
||||
|
||||
|
||||
class TestRateLimiter:
|
||||
"""Test the main RateLimiter class."""
|
||||
|
||||
@pytest.fixture
|
||||
def limiter(self):
|
||||
return RateLimiter()
|
||||
|
||||
def test_first_request_allowed(self, limiter):
|
||||
allowed, msg, retry = limiter.check_rate_limit("client1")
|
||||
assert allowed is True
|
||||
|
||||
def test_different_clients_independent(self, limiter):
|
||||
for _ in range(50):
|
||||
limiter.check_rate_limit("client1")
|
||||
allowed, _, _ = limiter.check_rate_limit("client2")
|
||||
assert allowed is True
|
||||
|
||||
def test_get_client_stats(self, limiter):
|
||||
limiter.check_rate_limit("client1")
|
||||
stats = limiter.get_client_stats("client1")
|
||||
assert stats is not None
|
||||
assert stats["client_id"] == "client1"
|
||||
|
||||
def test_get_client_stats_unknown(self, limiter):
|
||||
assert limiter.get_client_stats("unknown") is None
|
||||
|
||||
def test_reset_client(self, limiter):
|
||||
limiter.check_rate_limit("client1")
|
||||
assert limiter.reset_client("client1") is True
|
||||
assert limiter.get_client_stats("client1") is None
|
||||
|
||||
def test_reset_nonexistent_client(self, limiter):
|
||||
assert limiter.reset_client("nonexistent") is False
|
||||
|
||||
def test_reset_all(self, limiter):
|
||||
limiter.check_rate_limit("client1")
|
||||
limiter.check_rate_limit("client2")
|
||||
count = limiter.reset_all()
|
||||
assert count == 2
|
||||
assert limiter.get_client_stats("client1") is None
|
||||
|
||||
def test_get_all_stats(self, limiter):
|
||||
limiter.check_rate_limit("client1")
|
||||
stats = limiter.get_all_stats()
|
||||
assert "global" in stats
|
||||
assert "default_limits" in stats
|
||||
assert stats["global"]["total_requests"] >= 1
|
||||
assert stats["global"]["active_clients"] == 1
|
||||
|
||||
def test_configure_limits(self, limiter):
|
||||
limiter.configure_limits("n8n", per_minute=10, per_hour=50)
|
||||
assert limiter.plugin_configs["n8n"].per_minute == 10
|
||||
assert limiter.plugin_configs["n8n"].per_hour == 50
|
||||
Reference in New Issue
Block a user