From b256ecc5d2e69935645887b806025b22419a76cc Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Mon, 27 Jul 2026 21:03:14 +0800 Subject: [PATCH] test: add 9 integration tests against live safesightd API --- internal/service/integration_test.go | 265 +++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 internal/service/integration_test.go diff --git a/internal/service/integration_test.go b/internal/service/integration_test.go new file mode 100644 index 0000000..19bb10b --- /dev/null +++ b/internal/service/integration_test.go @@ -0,0 +1,265 @@ +//go:build integration +// +build integration + +package service + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "os" + "strings" + "testing" +) + +// Integration tests against a running safesightd instance. +// Set SAFESIGHT_URL env (default http://10.0.0.81:18080). +// Run with: go test -tags=integration -v -count=1 ./internal/service/ + +func safeSightURL() string { + if u := os.Getenv("SAFESIGHT_URL"); u != "" { + return strings.TrimRight(u, "/") + } + return "http://10.0.0.81:18080" +} + +func apiGet(t *testing.T, path string) (*http.Response, []byte) { + t.Helper() + resp, err := http.Get(safeSightURL() + path) + if err != nil { + t.Fatalf("GET %s: %v", path, err) + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + return resp, body +} + +func apiPost(t *testing.T, path string, payload any) (*http.Response, []byte) { + t.Helper() + var bodyReader io.Reader + if payload != nil { + b, err := json.Marshal(payload) + if err != nil { + t.Fatalf("marshal: %v", err) + } + bodyReader = bytes.NewReader(b) + } + resp, err := http.Post(safeSightURL()+path, "application/json", bodyReader) + if err != nil { + t.Fatalf("POST %s: %v", path, err) + } + respBody, _ := io.ReadAll(resp.Body) + resp.Body.Close() + return resp, respBody +} + +func apiPostForm(t *testing.T, path string, form map[string]string) (*http.Response, []byte) { + t.Helper() + data := make([]string, 0, len(form)) + for k, v := range form { + data = append(data, k+"="+v) + } + body := strings.NewReader(strings.Join(data, "&")) + resp, err := http.Post(safeSightURL()+path, "application/x-www-form-urlencoded", body) + if err != nil { + t.Fatalf("POST %s: %v", path, err) + } + respBody, _ := io.ReadAll(resp.Body) + resp.Body.Close() + return resp, respBody +} + +func getDeviceID(t *testing.T) string { + t.Helper() + resp, body := apiGet(t, "/devices") + if resp.StatusCode != 200 { + return "" + } + page := string(body) + skip := map[string]bool{"batch-config": true, "cleanup": true, "export-channels": true, "add": true, "config": true} + for _, line := range strings.Split(page, "\n") { + if idx := strings.Index(line, "href=\"/devices/"); idx >= 0 { + rest := line[idx+len("href=\"/devices/"):] + if end := strings.Index(rest, "\""); end > 10 { + id := rest[:end] + if !skip[id] { + return id + } + } + } + } + return "" +} + +// ── Health check ── + +func TestIntegration_HealthCheck(t *testing.T) { + resp, body := apiGet(t, "/dashboard") + if resp.StatusCode != 200 { + t.Fatalf("health check failed: %d — %s", resp.StatusCode, string(body)) + } + t.Logf("safesightd running, page size=%d bytes", len(body)) +} + +// ── Device listing ── + +func TestIntegration_ListDevices(t *testing.T) { + resp, _ := apiGet(t, "/devices") + if resp.StatusCode != 200 { + t.Fatalf("GET /devices: %d", resp.StatusCode) + } + t.Logf("devices page OK") +} + +// ── Template listing ── + +func TestIntegration_ListTemplates(t *testing.T) { + resp, body := apiGet(t, "/assets/templates") + if resp.StatusCode != 200 { + t.Fatalf("GET /assets/templates: %d", resp.StatusCode) + } + page := string(body) + for _, name := range []string{"std_workshop_face_recognition_shoe_alarm"} { + if !strings.Contains(page, name) { + t.Errorf("template %q not found", name) + } + } + t.Logf("templates page OK") +} + +// ── Overlay asset listing ── + +func TestIntegration_ListOverlays(t *testing.T) { + resp, _ := apiGet(t, "/assets/overlays") + if resp.StatusCode != 200 { + t.Fatalf("GET /assets/overlays: %d", resp.StatusCode) + } + t.Logf("overlays page OK") +} + +// ── Console page ── + +func TestIntegration_ConsolePage(t *testing.T) { + resp, _ := apiGet(t, "/console") + if resp.StatusCode != 200 { + t.Fatalf("GET /console: %d", resp.StatusCode) + } + t.Logf("console page OK") +} + +// ── Alarm center ── + +func TestIntegration_AlarmsPage(t *testing.T) { + resp, _ := apiGet(t, "/alarms") + if resp.StatusCode != 200 { + t.Fatalf("GET /alarms: %d", resp.StatusCode) + } + t.Logf("alarms page OK") +} + +// ── Device detail page with overlay section ── + +func TestIntegration_DeviceDetailWithOverlays(t *testing.T) { + deviceID := getDeviceID(t) + if deviceID == "" { + t.Skip("no device found") + } + + resp, body := apiGet(t, "/devices/"+deviceID) + if resp.StatusCode != 200 { + t.Fatalf("GET device detail: %d", resp.StatusCode) + } + if !strings.Contains(string(body), "调试参数覆盖") { + t.Error("missing '调试参数覆盖' section") + } + t.Logf("device %s detail page OK", deviceID) +} + +// ── Template settings page ── + +func TestIntegration_TemplateSettingsPage(t *testing.T) { + resp, body := apiGet(t, "/assets/templates") + if resp.StatusCode != 200 { + t.Fatalf("GET templates: %d", resp.StatusCode) + } + page := string(body) + + // Find user template (cloned from standard, no std_ prefix but with "workshop" or "face" pattern) + userTpl := "" + for _, candidate := range []string{"workshop_face_recognition_shoe_alarm", "face_recognition_stream"} { + if strings.Contains(page, `">`+candidate+`<`) { + userTpl = candidate + break + } + } + if userTpl == "" { + t.Skip("no user template found — deploy via wizard first") + } + + resp, body = apiGet(t, "/assets/templates/" + userTpl + "/settings") + if resp.StatusCode != 200 { + t.Fatalf("GET settings: %d", resp.StatusCode) + } + if !strings.Contains(string(body), "参数调整") { + t.Error("missing '参数调整' on settings page") + } + t.Logf("template %s settings page OK", userTpl) +} + +// ── Wizard deploy roundtrip ── + +func TestIntegration_WizardDeployRoundtrip(t *testing.T) { + deviceID := getDeviceID(t) + if deviceID == "" { + t.Skip("no device found") + } + t.Logf("device: %s", deviceID) + + // Get video sources + resp, body := apiGet(t, "/assets/video-sources") + if resp.StatusCode != 200 { + t.Fatalf("GET video-sources: %d", resp.StatusCode) + } + page := string(body) + + // Find a video source + sourceName := "" + for _, line := range strings.Split(page, "\n") { + for _, prefix := range []string{">cam", ">src_", ">gate_"} { + if idx := strings.Index(line, prefix); idx >= 0 { + rest := line[idx+1:] + if end := strings.Index(rest, "<"); end > 1 { + sourceName = rest[:end] + break + } + } + } + if sourceName != "" { + break + } + } + if sourceName == "" { + t.Skip("no video source found") + } + t.Logf("video source: %s", sourceName) + + // Deploy via wizard + req := map[string]any{ + "device_id": deviceID, + "features": []string{"face"}, + "source_names": []string{sourceName}, + } + resp, body = apiPost(t, "/wizard/apply", req) + if resp.StatusCode != 200 { + t.Fatalf("wizard apply: %d — %s", resp.StatusCode, string(body)) + } + var result AutoConfigResult + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("parse result: %v", err) + } + if result.Error != "" { + t.Fatalf("wizard error: %s", result.Error) + } + t.Logf("wizard OK: template=%s task=%s", result.TemplateName, result.TaskID) +}