"""Tests for device CRUD routes. These tests exercise the FastAPI route handlers using dependency override to inject test stores, avoiding real hardware dependencies. """ from unittest.mock import AsyncMock, MagicMock import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from wled_controller.api.routes.devices import router from wled_controller.storage.device_store import Device, DeviceStore from wled_controller.storage.output_target_store import OutputTargetStore from wled_controller.core.processing.processor_manager import ProcessorManager from wled_controller.api import dependencies as deps # --------------------------------------------------------------------------- # App + fixtures (isolated from the real main app) # --------------------------------------------------------------------------- def _make_app(): """Build a minimal FastAPI app with just the devices router + overrides.""" app = FastAPI() app.include_router(router) return app @pytest.fixture def device_store(tmp_path): return DeviceStore(tmp_path / "devices.json") @pytest.fixture def output_target_store(tmp_path): return OutputTargetStore(str(tmp_path / "output_targets.json")) @pytest.fixture def processor_manager(): """A mock ProcessorManager — avoids real hardware.""" m = MagicMock(spec=ProcessorManager) m.add_device = MagicMock() m.remove_device = AsyncMock() m.update_device_info = MagicMock() m.find_device_state = MagicMock(return_value=None) m.get_all_device_health_dicts = MagicMock(return_value=[]) return m @pytest.fixture def client(device_store, output_target_store, processor_manager): app = _make_app() # Override auth to always pass from wled_controller.api.auth import verify_api_key app.dependency_overrides[verify_api_key] = lambda: "test-user" # Override stores and manager app.dependency_overrides[deps.get_device_store] = lambda: device_store app.dependency_overrides[deps.get_output_target_store] = lambda: output_target_store app.dependency_overrides[deps.get_processor_manager] = lambda: processor_manager return TestClient(app, raise_server_exceptions=False) # --------------------------------------------------------------------------- # Helper to pre-populate a device # --------------------------------------------------------------------------- def _seed_device(store: DeviceStore, name="Test Device", led_count=100) -> Device: return store.create_device( name=name, url="http://192.168.1.100", led_count=led_count, ) # --------------------------------------------------------------------------- # LIST # --------------------------------------------------------------------------- class TestListDevices: def test_list_empty(self, client): resp = client.get("/api/v1/devices") assert resp.status_code == 200 data = resp.json() assert data["count"] == 0 assert data["devices"] == [] def test_list_with_devices(self, client, device_store): _seed_device(device_store, "Dev A") _seed_device(device_store, "Dev B") resp = client.get("/api/v1/devices") assert resp.status_code == 200 data = resp.json() assert data["count"] == 2 # --------------------------------------------------------------------------- # GET by ID # --------------------------------------------------------------------------- class TestGetDevice: def test_get_existing(self, client, device_store): d = _seed_device(device_store) resp = client.get(f"/api/v1/devices/{d.id}") assert resp.status_code == 200 data = resp.json() assert data["id"] == d.id assert data["name"] == "Test Device" def test_get_not_found(self, client): resp = client.get("/api/v1/devices/nonexistent") assert resp.status_code == 404 # --------------------------------------------------------------------------- # UPDATE # --------------------------------------------------------------------------- class TestUpdateDevice: def test_update_name(self, client, device_store): d = _seed_device(device_store) resp = client.put( f"/api/v1/devices/{d.id}", json={"name": "Renamed"}, ) assert resp.status_code == 200 assert resp.json()["name"] == "Renamed" def test_update_led_count(self, client, device_store): d = _seed_device(device_store, led_count=100) resp = client.put( f"/api/v1/devices/{d.id}", json={"led_count": 300}, ) assert resp.status_code == 200 assert resp.json()["led_count"] == 300 def test_update_not_found(self, client): resp = client.put( "/api/v1/devices/missing_id", json={"name": "X"}, ) assert resp.status_code == 404 # --------------------------------------------------------------------------- # DELETE # --------------------------------------------------------------------------- class TestDeleteDevice: def test_delete_existing(self, client, device_store): d = _seed_device(device_store) resp = client.delete(f"/api/v1/devices/{d.id}") assert resp.status_code == 204 assert device_store.count() == 0 def test_delete_not_found(self, client): resp = client.delete("/api/v1/devices/missing_id") assert resp.status_code == 404 def test_delete_referenced_by_target_returns_409( self, client, device_store, output_target_store ): d = _seed_device(device_store) output_target_store.create_target( name="Target", target_type="led", device_id=d.id, ) resp = client.delete(f"/api/v1/devices/{d.id}") assert resp.status_code == 409 assert "referenced" in resp.json()["detail"].lower() # --------------------------------------------------------------------------- # Batch states # --------------------------------------------------------------------------- class TestBatchStates: def test_batch_states(self, client): resp = client.get("/api/v1/devices/batch/states") assert resp.status_code == 200 assert "states" in resp.json()