Phase 1-4: Audio stack, mixer engine, MIDI, and network API
Lint & Validate / lint (push) Has been cancelled
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)
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user