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,241 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user