9cd8292acc
Lint & Validate / lint (push) Has been cancelled
P2-R1: ALSA + JACK2 low-latency config (scripts, quirks, tuning) P2-R2: Carla integration (build scripts, 8ch rack config, NAM LV2 support) P2-R3: Plugin manager, categories, blacklist, NAM model support P3-R1: Mixer DSP engine (channel strip, routing matrix, bus mgr, automation) P4-R1: MIDI engine (learn mode, clock sync, HID discovery, mapping store) P4-R2: Network API (OSC server, FastAPI REST, WebSocket, auth, rate limiter) P5-R1: Touchscreen UI evaluation + main entry point docs: Audio stack, Carla integration, MIDI support, UI evaluation tests: Full test suite (292 passing)
143 lines
4.4 KiB
Python
143 lines
4.4 KiB
Python
"""Tests for token bucket rate limiter."""
|
|
|
|
import asyncio
|
|
import time
|
|
import pytest
|
|
|
|
from fastapi import FastAPI, Depends, Request
|
|
from fastapi.testclient import TestClient
|
|
|
|
from src.network.rate_limiter import RateLimiter, TokenBucket, rate_limit
|
|
from src.network.auth import API_KEY_HEADER
|
|
|
|
|
|
class TestTokenBucket:
|
|
"""Tests for TokenBucket."""
|
|
|
|
def test_initial_tokens(self):
|
|
bucket = TokenBucket(rate=10.0, capacity=20)
|
|
assert bucket.available == 20.0
|
|
|
|
def test_consume_reduces_tokens(self):
|
|
bucket = TokenBucket(rate=10.0, capacity=20)
|
|
assert bucket.consume(1)
|
|
# available may refill a tiny amount based on timing
|
|
assert bucket.available == pytest.approx(19.0, abs=0.1)
|
|
|
|
def test_consume_multiple(self):
|
|
bucket = TokenBucket(rate=10.0, capacity=20)
|
|
assert bucket.consume(5)
|
|
assert bucket.available == pytest.approx(15.0, abs=0.1)
|
|
|
|
def test_cannot_consume_more_than_available(self):
|
|
bucket = TokenBucket(rate=10.0, capacity=5)
|
|
# Consume all
|
|
for _ in range(5):
|
|
assert bucket.consume(1)
|
|
# No more
|
|
assert not bucket.consume(1)
|
|
|
|
def test_refill_over_time(self):
|
|
bucket = TokenBucket(rate=100.0, capacity=100)
|
|
# Drain all tokens
|
|
for _ in range(100):
|
|
assert bucket.consume(1)
|
|
|
|
assert bucket.available < 1.0
|
|
|
|
# Wait for refill
|
|
time.sleep(0.05) # 50ms → ~5 tokens at 100/s
|
|
assert bucket.available >= 4.0 # allow slight timing variance
|
|
|
|
def test_capacity_cap(self):
|
|
bucket = TokenBucket(rate=1000.0, capacity=10)
|
|
time.sleep(0.1) # 100ms → 100 tokens, but capped at 10
|
|
assert bucket.available <= 10.0 + 0.01 # small epsilon
|
|
|
|
def test_zero_rate(self):
|
|
bucket = TokenBucket(rate=0.0, capacity=10)
|
|
for _ in range(10):
|
|
assert bucket.consume(1)
|
|
# No refill
|
|
assert not bucket.consume(1)
|
|
|
|
|
|
class TestRateLimiter:
|
|
"""Tests for RateLimiter."""
|
|
|
|
@pytest.fixture
|
|
def limiter(self):
|
|
return RateLimiter(rate=100.0, capacity=200, eviction_timeout=0.5)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_rest_allows_first_request(self, limiter):
|
|
"""First request from an IP should be allowed."""
|
|
# We can't easily test this without a FastAPI request,
|
|
# but we can test the bucket directly.
|
|
# The check_rest method needs a Request object, so we test
|
|
# via FastAPI integration below.
|
|
pass
|
|
|
|
|
|
class TestRateLimitIntegration:
|
|
"""Integration tests for rate limiting with FastAPI."""
|
|
|
|
@pytest.fixture
|
|
def app(self):
|
|
limiter = RateLimiter(rate=50.0, capacity=100)
|
|
app = FastAPI()
|
|
|
|
@app.get("/limited")
|
|
async def limited(_: None = Depends(rate_limit(limiter))):
|
|
return {"ok": True}
|
|
|
|
@app.get("/unlimited")
|
|
async def unlimited():
|
|
return {"ok": True}
|
|
|
|
return app
|
|
|
|
@pytest.fixture
|
|
def client(self, app):
|
|
return TestClient(app)
|
|
|
|
def test_unlimited_endpoint(self, client):
|
|
resp = client.get("/unlimited")
|
|
assert resp.status_code == 200
|
|
|
|
def test_limited_endpoint_allows_requests(self, client):
|
|
"""Rate-limited endpoint should allow normal traffic."""
|
|
for _ in range(10):
|
|
resp = client.get("/limited")
|
|
assert resp.status_code == 200
|
|
|
|
def test_rate_limit_headers_on_client(self, client):
|
|
"""Basic sanity — requests should work."""
|
|
resp = client.get("/limited")
|
|
assert resp.status_code == 200
|
|
assert resp.json() == {"ok": True}
|
|
|
|
def test_multiple_requests_within_limit(self, client):
|
|
"""Multiple requests within the rate limit should all succeed."""
|
|
for i in range(50):
|
|
resp = client.get("/limited")
|
|
assert resp.status_code == 200, f"Request {i} failed"
|
|
|
|
|
|
class TestRateLimiterStats:
|
|
"""Tests for rate limiter statistics."""
|
|
|
|
def test_initial_stats(self):
|
|
limiter = RateLimiter(rate=50.0, capacity=100)
|
|
assert limiter.active_connections == 0
|
|
stats = limiter.stats
|
|
assert stats["active_clients"] == 0
|
|
assert stats["rest_rate"] == 50.0
|
|
assert stats["rest_capacity"] == 100
|
|
|
|
def test_ws_rates(self):
|
|
limiter = RateLimiter(ws_rate=500.0, ws_capacity=1000)
|
|
stats = limiter.stats
|
|
assert stats["ws_rate"] == 500.0
|
|
assert stats["ws_capacity"] == 1000
|