safesight/control/internal/service/integration_test.go
tian 98e375dc36 chore: 合并 safesight-control 仓库为 control/ 子目录
项目合并为单仓库:设备端(根) + 管理端(control/)。
两端 git 历史完整保留(git subtree 合并)。

git-subtree-dir: control
git-subtree-mainline: 923ab10d5c
git-subtree-split: 70738edb31
2026-08-02 11:13:26 +08:00

261 lines
6.6 KiB
Go

//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 name from table cell
sourceName := ""
for _, line := range strings.Split(page, "\n") {
if idx := strings.Index(line, ">车间"); idx >= 0 {
rest := line[idx+1:]
if end := strings.Index(rest, "<"); end > 1 {
sourceName = rest[:end]
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)
}