package api_test import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "visiona-local/server/internal/api" "visiona-local/server/internal/api/handlers" "visiona-local/server/internal/api/ws" "visiona-local/server/internal/camera" "visiona-local/server/internal/device" "visiona-local/server/internal/inference" "visiona-local/server/internal/model" "visiona-local/server/pkg/logger" ) // apiResponse is the standard JSON envelope for all API responses. type apiResponse struct { Success bool `json:"success"` Data json.RawMessage `json:"data,omitempty"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error,omitempty"` } // setupTestServer creates a fully wired Gin router with mock devices. func setupTestServer(t *testing.T, mockCount int) *httptest.Server { t.Helper() registry := device.NewRegistry() deviceMgr := device.NewManager(registry, true, mockCount, "") // Drain event bus to prevent blocking go func() { for range deviceMgr.Events() { } }() modelRepo := model.NewRepository("") modelStore := model.NewModelStore(t.TempDir()) cameraMgr := camera.NewManager(true) inferenceSvc := inference.NewService(deviceMgr) wsHub := ws.NewHub() logBroadcaster := logger.NewBroadcaster(100, nil) sysHandler := handlers.NewSystemHandler("test", "now", nil) router := api.NewRouter( modelRepo, modelStore, deviceMgr, cameraMgr, inferenceSvc, wsHub, nil, logBroadcaster, sysHandler, ) return httptest.NewServer(router) } func getJSON(t *testing.T, ts *httptest.Server, path string) apiResponse { t.Helper() resp, err := http.Get(ts.URL + path) if err != nil { t.Fatalf("GET %s: %v", path, err) } defer resp.Body.Close() var r apiResponse if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { t.Fatalf("GET %s: decode error: %v", path, err) } return r } func postJSON(t *testing.T, ts *httptest.Server, path string, body string) apiResponse { t.Helper() resp, err := http.Post(ts.URL+path, "application/json", strings.NewReader(body)) if err != nil { t.Fatalf("POST %s: %v", path, err) } defer resp.Body.Close() var r apiResponse if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { t.Fatalf("POST %s: decode error: %v", path, err) } return r } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- func TestHealthCheck(t *testing.T) { ts := setupTestServer(t, 0) defer ts.Close() resp, err := http.Get(ts.URL + "/api/system/health") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != 200 { t.Fatalf("health check status = %d, want 200", resp.StatusCode) } var data map[string]string json.NewDecoder(resp.Body).Decode(&data) if data["status"] != "ok" { t.Fatalf("health check status = %q, want 'ok'", data["status"]) } } func TestDeviceWorkflow_MockMode(t *testing.T) { ts := setupTestServer(t, 2) defer ts.Close() // 1. List devices — should have 2 mock devices t.Run("list devices", func(t *testing.T) { r := getJSON(t, ts, "/api/devices") if !r.Success { t.Fatal("list devices should succeed") } var data struct { Devices []json.RawMessage `json:"devices"` } json.Unmarshal(r.Data, &data) if len(data.Devices) != 2 { t.Fatalf("expected 2 devices, got %d", len(data.Devices)) } }) // 2. Get single device t.Run("get device", func(t *testing.T) { r := getJSON(t, ts, "/api/devices/mock-device-1") if !r.Success { t.Fatal("get device should succeed") } }) // 3. Get non-existing device → 404 t.Run("get device not found", func(t *testing.T) { r := getJSON(t, ts, "/api/devices/nonexistent") if r.Success { t.Fatal("should fail for non-existing device") } if r.Error == nil || r.Error.Code != "DEVICE_NOT_FOUND" { t.Fatalf("expected DEVICE_NOT_FOUND error, got %+v", r.Error) } }) // 4. Connect device t.Run("connect device", func(t *testing.T) { r := postJSON(t, ts, "/api/devices/mock-device-1/connect", "") if !r.Success { t.Fatalf("connect should succeed: %+v", r.Error) } // Verify status changed to connected r2 := getJSON(t, ts, "/api/devices/mock-device-1") var info struct { Status string `json:"status"` } json.Unmarshal(r2.Data, &info) if info.Status != "connected" { t.Fatalf("expected status 'connected', got '%s'", info.Status) } }) // 5. Start inference t.Run("start inference", func(t *testing.T) { r := postJSON(t, ts, "/api/devices/mock-device-1/inference/start", "") if !r.Success { t.Fatalf("start inference should succeed: %+v", r.Error) } }) // 6. Stop inference t.Run("stop inference", func(t *testing.T) { r := postJSON(t, ts, "/api/devices/mock-device-1/inference/stop", "") if !r.Success { t.Fatalf("stop inference should succeed: %+v", r.Error) } }) // 7. Disconnect device t.Run("disconnect device", func(t *testing.T) { r := postJSON(t, ts, "/api/devices/mock-device-1/disconnect", "") if !r.Success { t.Fatalf("disconnect should succeed: %+v", r.Error) } }) } func TestDeviceScan_MockMode(t *testing.T) { ts := setupTestServer(t, 1) defer ts.Close() // In mock mode, scan just returns existing mock devices r := postJSON(t, ts, "/api/devices/scan", "") if !r.Success { t.Fatalf("scan should succeed: %+v", r.Error) } var data struct { Devices []json.RawMessage `json:"devices"` } json.Unmarshal(r.Data, &data) if len(data.Devices) != 1 { t.Fatalf("expected 1 device after scan, got %d", len(data.Devices)) } } func TestModelList(t *testing.T) { ts := setupTestServer(t, 0) defer ts.Close() r := getJSON(t, ts, "/api/models") if !r.Success { t.Fatal("list models should succeed") } } func TestConnectNonExistentDevice(t *testing.T) { ts := setupTestServer(t, 1) defer ts.Close() r := postJSON(t, ts, "/api/devices/nonexistent/connect", "") if r.Success { t.Fatal("connect nonexistent device should fail") } } func TestMultiDeviceIsolation(t *testing.T) { ts := setupTestServer(t, 3) defer ts.Close() // Connect device 1 only r := postJSON(t, ts, "/api/devices/mock-device-1/connect", "") if !r.Success { t.Fatalf("connect device 1 should succeed: %+v", r.Error) } // Device 2 should still be detected (not connected) r2 := getJSON(t, ts, "/api/devices/mock-device-2") var info struct { Status string `json:"status"` } json.Unmarshal(r2.Data, &info) if info.Status != "detected" { t.Fatalf("device 2 should be 'detected', got '%s'", info.Status) } }