Files
raspberry-pi-mixer/tests/test_rate_limiter.py
T
shawn 9cd8292acc
Lint & Validate / lint (push) Has been cancelled
Phase 1-4: Audio stack, mixer engine, MIDI, and network API
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)
2026-05-19 20:39:17 -04:00

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