"""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