Files
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

242 lines
8.9 KiB
Python

"""Tests for the REST API endpoints."""
import json
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.network.auth import APIKeyAuth, API_KEY_HEADER
from src.network.rate_limiter import RateLimiter
from src.network.rest_api import MixerStateProvider, create_router
from src.network.schemas import (
ChannelSchema,
MasterBusSchema,
MixerStateSchema,
TransportSchema,
PluginSchema,
RoutingSchema,
EQSchema,
CompressorSchema,
GateSchema,
FXSendsSchema,
AuxBusSchema,
)
class TestRESTAPI:
"""Integration tests for the REST API."""
@pytest.fixture
def state_provider(self):
"""Create a mock state provider."""
sp = MixerStateProvider()
# Mock data
channels = [
ChannelSchema(channel=i, label=f"CH{i+1}", volume_db=0.0)
for i in range(16)
]
sp.get_all_channels = lambda: channels
sp.get_channel = lambda idx: channels[idx] if 0 <= idx < len(channels) else None
sp.get_master = lambda: MasterBusSchema(
volume_db=0.0,
aux_buses=[AuxBusSchema(index=0, label="Reverb")],
)
sp.get_transport = lambda: TransportSchema(playing=False)
sp.get_plugins = lambda: [
PluginSchema(plugin_id=1, name="Ch1 Gate", role="gate", channel=0),
]
sp.get_routing = lambda: RoutingSchema()
sp.list_scenes = lambda: ["Scene A", "Scene B"]
sp.load_scene = lambda name: name in ["Scene A", "Scene B"]
sp.save_scene = lambda name: True
# Parameter handler captures
sp._param_calls = []
sp.handle_parameter = lambda pt, v, ch: sp._param_calls.append((pt, v, ch))
# Transport handler captures
sp._transport_calls = []
sp.handle_transport = lambda cmd: sp._transport_calls.append(cmd)
return sp
@pytest.fixture
def auth(self):
return APIKeyAuth(keys=["test-api-key"])
@pytest.fixture
def rate_limiter(self):
return RateLimiter(rate=1000.0, capacity=2000)
@pytest.fixture
def app(self, state_provider, auth, rate_limiter):
app = FastAPI()
router = create_router(
state=state_provider,
auth=auth,
rate_limiter=rate_limiter,
prefix="/api/v1",
)
app.include_router(router)
# Health endpoint (no auth)
@app.get("/health")
async def health():
return {"status": "ok"}
# Full state endpoint on app directly (no auth for test)
@app.get("/api/v1/state")
async def full_state():
return state_provider.get_full_state() if state_provider.get_full_state else {}
return app
@pytest.fixture
def client(self, app):
return TestClient(app)
@pytest.fixture
def auth_headers(self):
return {API_KEY_HEADER: "test-api-key"}
# ── Auth ────────────────────────────────────────────────────────────
def test_health_no_auth(self, client):
resp = client.get("/health")
assert resp.status_code == 200
def test_channels_no_auth(self, client):
resp = client.get("/api/v1/channels")
assert resp.status_code == 401
def test_channels_wrong_key(self, client):
resp = client.get("/api/v1/channels", headers={API_KEY_HEADER: "wrong"})
assert resp.status_code == 403
# ── Channels ────────────────────────────────────────────────────────
def test_list_channels(self, client, auth_headers):
resp = client.get("/api/v1/channels", headers=auth_headers)
assert resp.status_code == 200
data = resp.json()
assert len(data) == 16
assert data[0]["channel"] == 0
def test_get_channel_valid(self, client, auth_headers):
resp = client.get("/api/v1/channels/3", headers=auth_headers)
assert resp.status_code == 200
data = resp.json()
assert data["channel"] == 3
def test_get_channel_not_found(self, client, auth_headers):
resp = client.get("/api/v1/channels/99", headers=auth_headers)
assert resp.status_code == 404
def test_set_channel_parameter(self, client, auth_headers, state_provider):
body = {"param_type": "volume", "value": -3.0}
resp = client.put(
"/api/v1/channels/0/parameter",
json=body,
headers=auth_headers,
)
assert resp.status_code == 200
assert resp.json()["ok"] is True
assert len(state_provider._param_calls) == 1
assert state_provider._param_calls[0] == ("volume", -3.0, 0)
def test_set_channel_parameters_batch(self, client, auth_headers, state_provider):
body = {
"updates": [
{"param_type": "volume", "value": -6.0, "channel": 0},
{"param_type": "pan", "value": 0.5, "channel": 0},
]
}
resp = client.put(
"/api/v1/channels/0/parameters",
json=body,
headers=auth_headers,
)
assert resp.status_code == 200
assert resp.json()["ok"] is True
assert len(state_provider._param_calls) == 2
# ── Mixes / Master ───────────────────────────────────────────────────
def test_get_mixes(self, client, auth_headers):
resp = client.get("/api/v1/mixes", headers=auth_headers)
assert resp.status_code == 200
data = resp.json()
assert "volume_db" in data
assert len(data["aux_buses"]) == 1
def test_set_master_parameter(self, client, auth_headers, state_provider):
body = {"param_type": "master_volume", "value": -6.0}
resp = client.put(
"/api/v1/mixes/parameter",
json=body,
headers=auth_headers,
)
assert resp.status_code == 200
assert len(state_provider._param_calls) == 1
assert state_provider._param_calls[0] == ("master_volume", -6.0, -1)
# ── Plugins ──────────────────────────────────────────────────────────
def test_list_plugins(self, client, auth_headers):
resp = client.get("/api/v1/plugins", headers=auth_headers)
assert resp.status_code == 200
data = resp.json()
assert len(data) == 1
assert data[0]["name"] == "Ch1 Gate"
# ── Transport ────────────────────────────────────────────────────────
def test_get_transport(self, client, auth_headers):
resp = client.get("/api/v1/transport", headers=auth_headers)
assert resp.status_code == 200
data = resp.json()
assert "playing" in data
assert "tempo_bpm" in data
def test_transport_command(self, client, auth_headers, state_provider):
resp = client.put(
"/api/v1/transport/command?command=play",
headers=auth_headers,
)
assert resp.status_code == 200
assert len(state_provider._transport_calls) == 1
assert state_provider._transport_calls[0] == "play"
# ── Routing ──────────────────────────────────────────────────────────
def test_get_routing(self, client, auth_headers):
resp = client.get("/api/v1/routing", headers=auth_headers)
assert resp.status_code == 200
data = resp.json()
assert "nodes" in data
assert "edges" in data
# ── Scenes ───────────────────────────────────────────────────────────
def test_list_scenes(self, client, auth_headers):
resp = client.get("/api/v1/scenes", headers=auth_headers)
assert resp.status_code == 200
data = resp.json()
assert data == ["Scene A", "Scene B"]
def test_load_scene_valid(self, client, auth_headers):
resp = client.post("/api/v1/scenes/Scene A/load", headers=auth_headers)
assert resp.status_code == 200
assert resp.json()["ok"] is True
def test_load_scene_not_found(self, client, auth_headers):
resp = client.post("/api/v1/scenes/Nonexistent/load", headers=auth_headers)
assert resp.status_code == 404
def test_save_scene(self, client, auth_headers):
resp = client.post("/api/v1/scenes/New Scene/save", headers=auth_headers)
assert resp.status_code == 200
assert resp.json()["ok"] is True