1269 lines
45 KiB
Go
1269 lines
45 KiB
Go
package web
|
|
|
|
import (
|
|
"3588AdminBackend/internal/config"
|
|
"3588AdminBackend/internal/models"
|
|
"3588AdminBackend/internal/service"
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
func TestUI_ActionDevicesBatchAction_RedirectsToTask(t *testing.T) {
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
reg := service.NewRegistryService(cfg, nil)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "dev1", IP: "127.0.0.1", AgentPort: 9100, Online: true})
|
|
reg.UpdateDevice(&models.Device{DeviceID: "dev2", IP: "127.0.0.1", AgentPort: 9100, Online: true})
|
|
tasks := service.NewTaskService(cfg, nil, reg)
|
|
|
|
ui, err := NewUI(nil, reg, nil, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("action", "media_start")
|
|
form.Add("device_id", "dev1")
|
|
form.Add("device_id", "dev2")
|
|
form.Set("config", "cam1")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/ui/devices/batch-action", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.actionDevicesBatchAction(rr, req)
|
|
|
|
if rr.Code != http.StatusFound {
|
|
t.Fatalf("expected 302, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
loc := rr.Header().Get("Location")
|
|
if !strings.HasPrefix(loc, "/ui/tasks/") {
|
|
t.Fatalf("expected redirect to /ui/tasks/*, got %q", loc)
|
|
}
|
|
}
|
|
|
|
func TestUI_DevicePageUsesEdgeVisionConsoleShell(t *testing.T) {
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
reg := service.NewRegistryService(cfg, nil)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100, MediaPort: 9000, Online: true})
|
|
tasks := service.NewTaskService(cfg, nil, reg)
|
|
|
|
ui, err := NewUI(nil, reg, nil, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.pageDevices(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
for _, want := range []string{
|
|
"视觉识别运维台",
|
|
"配置资产",
|
|
"操作审计",
|
|
"系统",
|
|
"<h1>设备</h1>",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected rendered HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_ConsoleTypographyStaysModerate(t *testing.T) {
|
|
css, err := os.ReadFile("ui/assets/style.css")
|
|
if err != nil {
|
|
t.Fatalf("read stylesheet: %v", err)
|
|
}
|
|
text := string(css)
|
|
for _, want := range []string{
|
|
".brand-title{font-size:14px;font-weight:600",
|
|
".nav-section{padding:14px 10px 6px;font-size:11px",
|
|
".topbar h1{margin:4px 0 0;font-size:18px;font-weight:600",
|
|
".crumb,.eyebrow{font-size:11px",
|
|
".btn,button{display:inline-flex",
|
|
".stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}",
|
|
".card{background:var(--surface);border:1px solid var(--border);border-radius:8px",
|
|
} {
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("expected stylesheet to contain %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newTestUI(t *testing.T) *UI {
|
|
t.Helper()
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
reg := service.NewRegistryService(cfg, nil)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100, MediaPort: 9000, Online: true, Version: "1.0.0"})
|
|
tasks := service.NewTaskService(cfg, nil, reg)
|
|
ui, err := NewUI(nil, reg, nil, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
return ui
|
|
}
|
|
|
|
func TestUI_DeviceOverviewHidesBatchBarWithoutSelection(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.pageDevices(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
for _, forbidden := range []string{"batch-toolbar", "已选", "批量配置", "重启服务", "启动服务", "停止服务", "重载服务", "清空选择"} {
|
|
if strings.Contains(body, forbidden) {
|
|
t.Fatalf("device overview should not show batch controls without selection, found %q in:\n%s", forbidden, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_DeviceOverviewShowsBatchBarWhenDevicesSelected(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
ui.registry.UpdateDevice(&models.Device{DeviceID: "edge-02", DeviceName: "辅助节点", IP: "127.0.0.2", AgentPort: 9100, MediaPort: 9000, Online: true})
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices?selected=edge-01&selected=edge-02", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.pageDevices(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"batch-toolbar", "已选 2 台", "重启服务", "启动服务", "停止服务", "重载服务", "批量配置", "清空选择"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected batch controls HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_ActionDevicesBatchActionKeepsDevicesOnError(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
ui.registry.UpdateDevice(&models.Device{DeviceID: "edge-02", DeviceName: "辅助节点", IP: "127.0.0.2", AgentPort: 9100, MediaPort: 9000, Online: true})
|
|
|
|
form := url.Values{}
|
|
form.Set("action", "nope")
|
|
form.Add("device_id", "edge-01")
|
|
form.Add("device_id", "edge-02")
|
|
req := httptest.NewRequest(http.MethodPost, "/ui/devices/batch-action", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.actionDevicesBatchAction(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"不支持的操作: nope", "入口识别节点", "辅助节点", "已选 2 台"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected error render to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_DeviceOverviewRendersFleetOverview(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.pageDevices(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"设备", "设备总览", "多台视觉识别设备的统一运维视图", "设备总数", "在线设备", "离线设备", "失败操作", "设备列表"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected dashboard HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_DevicePageIncludesFilterAndControlEntry(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.pageDevices(rr, req)
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"device-filter", "详情", "控制"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected device page HTML to contain %q", want)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{"查看配置资产", "查看操作审计"} {
|
|
if strings.Contains(body, forbidden) {
|
|
t.Fatalf("device overview should not contain redundant link %q", forbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_DeviceOverviewShowsLiveStatusAndConfigSummary(t *testing.T) {
|
|
agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/config/status" {
|
|
t.Fatalf("unexpected agent path %s", r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"ok": true,
|
|
"config_path": "/opt/rk3588-media-server/etc/media-server.json",
|
|
"exists": true,
|
|
"sha256": "8d935c9d366637ff853c69d25e9c6644c2fbff3b8d3aa15ff99ef32847fb947c",
|
|
"metadata": {
|
|
"config_id": "local_3588_face_debug",
|
|
"config_version": "20260419.104457",
|
|
"template": "workshop_face_shoe_alarm",
|
|
"profile": "local_3588_test",
|
|
"overlays": ["face_debug", "production_quiet"]
|
|
},
|
|
"media_server": {"running": true, "pid": 1706009}
|
|
}`))
|
|
}))
|
|
defer agentServer.Close()
|
|
|
|
host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://"))
|
|
if err != nil {
|
|
t.Fatalf("parse test server address: %v", err)
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil {
|
|
t.Fatalf("parse test server port: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
agent := service.NewAgentClient(cfg)
|
|
reg := service.NewRegistryService(cfg, agent)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: host, AgentPort: port, MediaPort: 9000, Online: false, Version: "1.0.0", GitSha: "5c04681"})
|
|
tasks := service.NewTaskService(cfg, agent, reg)
|
|
ui, err := NewUI(nil, reg, agent, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices", nil)
|
|
rr := httptest.NewRecorder()
|
|
ui.pageDevices(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"在线", "运行中", "local_3588_face_debug", "20260419.104457", "face_debug, production_quiet"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected device overview HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
if strings.Contains(body, "待在详情页查看") {
|
|
t.Fatalf("device overview should show live config summary instead of placeholder text")
|
|
}
|
|
}
|
|
|
|
func TestUI_DeviceOverviewUsesCompactColumns(t *testing.T) {
|
|
agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/config/status" {
|
|
t.Fatalf("unexpected agent path %s", r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"ok": true,
|
|
"metadata": {
|
|
"config_id": "local_3588_face_debug",
|
|
"config_version": "20260419.104457",
|
|
"overlays": ["face_debug"]
|
|
},
|
|
"media_server": {"running": true}
|
|
}`))
|
|
}))
|
|
defer agentServer.Close()
|
|
|
|
host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://"))
|
|
if err != nil {
|
|
t.Fatalf("parse test server address: %v", err)
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil {
|
|
t.Fatalf("parse test server port: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
agent := service.NewAgentClient(cfg)
|
|
reg := service.NewRegistryService(cfg, agent)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "d12a4719c91641df91b76ab271280797", DeviceName: "rk3588_orangepi5plus", Hostname: "orangepi5plus", IP: host, AgentPort: port, MediaPort: 9000, Online: true, Version: "0.1.0", GitSha: "5c04681"})
|
|
tasks := service.NewTaskService(cfg, agent, reg)
|
|
ui, err := NewUI(nil, reg, agent, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices", nil)
|
|
rr := httptest.NewRecorder()
|
|
ui.pageDevices(rr, req)
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"设备", "状态", "当前配置", "操作", "orangepi5plus", "0.1.0"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected compact overview HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{"最后心跳", "版本</th>", "在线</th>", "服务</th>"} {
|
|
if strings.Contains(body, forbidden) {
|
|
t.Fatalf("device overview should not keep verbose column header %q", forbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_DeviceDetailIncludesTabs(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
routes, err := ui.Routes()
|
|
if err != nil {
|
|
t.Fatalf("Routes: %v", err)
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
|
|
routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01", nil))
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"device-tabs", "设备详情", "设备控制"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected device detail HTML to contain %q", want)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{"返回设备总览"} {
|
|
if strings.Contains(body, forbidden) {
|
|
t.Fatalf("device detail should not contain redundant nav %q", forbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_DeviceDetailIsReadOnly(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
routes, err := ui.Routes()
|
|
if err != nil {
|
|
t.Fatalf("Routes: %v", err)
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
|
|
routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01", nil))
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"设备详情", "设备状态", "当前运行配置", "最近状态摘要", "技术信息"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected read-only device detail HTML to contain %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(body, "去设备控制") {
|
|
t.Fatalf("device detail should not contain redundant cross-link to control page")
|
|
}
|
|
for _, forbidden := range []string{"只读查看页", "权威摘要位置", "当前框架版"} {
|
|
if strings.Contains(body, forbidden) {
|
|
t.Fatalf("device detail should not contain placeholder copy %q", forbidden)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{
|
|
`method="post"`,
|
|
`type="file"`,
|
|
"部署到设备",
|
|
"启动视频分析",
|
|
"重启视频分析",
|
|
"停止视频分析",
|
|
"重载识别服务",
|
|
"回滚识别配置",
|
|
"上传视频分析配置",
|
|
} {
|
|
if strings.Contains(body, forbidden) {
|
|
t.Fatalf("device detail should be read-only and not contain %q", forbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_DeviceDetailShowsRunningConfigMetadata(t *testing.T) {
|
|
agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/config/status" {
|
|
t.Fatalf("unexpected agent path %s", r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"ok": true,
|
|
"config_path": "/opt/rk3588-media-server/etc/media-server.json",
|
|
"exists": true,
|
|
"sha256": "8d935c9d366637ff853c69d25e9c6644c2fbff3b8d3aa15ff99ef32847fb947c",
|
|
"metadata": {
|
|
"config_id": "local_3588_face_debug",
|
|
"config_version": "20260419.104457",
|
|
"template": "workshop_face_shoe_alarm",
|
|
"profile": "local_3588_test",
|
|
"overlays": ["face_debug"],
|
|
"rendered_by": "tools/render_config.py",
|
|
"rendered_at": "2026-04-19T10:44:58+08:00"
|
|
},
|
|
"media_server": {"running": true, "pid": 1706009}
|
|
}`))
|
|
}))
|
|
defer agentServer.Close()
|
|
|
|
host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://"))
|
|
if err != nil {
|
|
t.Fatalf("parse test server address: %v", err)
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil {
|
|
t.Fatalf("parse test server port: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
agent := service.NewAgentClient(cfg)
|
|
reg := service.NewRegistryService(cfg, agent)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: host, AgentPort: port, MediaPort: 9000, Online: true, Version: "1.0.0"})
|
|
tasks := service.NewTaskService(cfg, agent, reg)
|
|
ui, err := NewUI(nil, reg, agent, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
routes, err := ui.Routes()
|
|
if err != nil {
|
|
t.Fatalf("Routes: %v", err)
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
|
|
routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01", nil))
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
for _, want := range []string{
|
|
"当前运行配置",
|
|
"local_3588_face_debug",
|
|
"20260419.104457",
|
|
"workshop_face_shoe_alarm",
|
|
"local_3588_test",
|
|
"face_debug",
|
|
"8d935c9d366637ff853c69d25e9c6644c2fbff3b8d3aa15ff99ef32847fb947c",
|
|
"/opt/rk3588-media-server/etc/media-server.json",
|
|
"运行中",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected device detail HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_DeviceSubpagesIncludeContextNavigation(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
dev := &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100, MediaPort: 9000, Online: true}
|
|
for _, content := range []string{"device_logs", "device_graphs", "config_ui"} {
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01", nil)
|
|
rr := httptest.NewRecorder()
|
|
ui.render(rr, req, content, PageData{Title: "节点子页面", Device: dev})
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"device-tabs", "设备详情", "设备控制"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected %s HTML to contain %q", content, want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_ConfigFriendlyIncludesWizard(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
routes, err := ui.Routes()
|
|
if err != nil {
|
|
t.Fatalf("Routes: %v", err)
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
|
|
routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01/config-friendly", nil))
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"config-wizard", "选择节点", "编辑视频通道", "预览变更", "部署结果", "data-wizard-section=\"channels\"", "下一步:编辑视频通道", "前往预览变更", "查看部署结果"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected config page HTML to contain %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_ConfigPreviewPageShowsTemplateProfileOverlayForm(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
routes, err := ui.Routes()
|
|
if err != nil {
|
|
t.Fatalf("Routes: %v", err)
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
|
|
routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01/config-preview", nil))
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
for _, want := range []string{
|
|
"配置预览",
|
|
"模板",
|
|
"Profile",
|
|
"Overlay",
|
|
"config_id",
|
|
"config_version",
|
|
"生成预览",
|
|
"workshop_face_shoe_alarm",
|
|
"local_3588_test",
|
|
"face_debug",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected config preview page to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{"返回节点详情", "完整 JSON</h2>"} {
|
|
if strings.Contains(body, forbidden) {
|
|
t.Fatalf("config preview page should not contain redundant element %q", forbidden)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{"上传为候选配置", "应用候选配置"} {
|
|
if strings.Contains(body, forbidden) {
|
|
t.Fatalf("config preview page should not show action %q before preview is generated", forbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_ConfigPreviewPageKeepsApplyActionAfterUploadResult(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.render(rr, req, "config_preview", PageData{
|
|
Title: "配置预览",
|
|
Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100},
|
|
ConfigStatus: &ConfigStatusView{
|
|
Candidate: &ConfigStatusLastGoodFile{Exists: true, Path: "/opt/rk3588-media-server/etc/media-server.json.candidate.json"},
|
|
},
|
|
ConfigPreview: &service.ConfigPreviewResult{
|
|
JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v1"}}`,
|
|
Metadata: map[string]any{
|
|
"config_id": "preview_edge-01",
|
|
"config_version": "v1",
|
|
"template": "workshop_face_shoe_alarm",
|
|
"profile": "local_3588_test",
|
|
},
|
|
Size: 123,
|
|
},
|
|
RawText: `{"ok":true}`,
|
|
ResultTitle: "应用候选配置结果",
|
|
})
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{
|
|
"应用候选配置结果",
|
|
"上传为候选配置",
|
|
"应用候选配置",
|
|
`formaction="/ui/devices/edge-01/config-candidate/apply"`,
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected config preview upload result HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
if strings.Contains(body, "<h2>执行结果</h2>") {
|
|
t.Fatalf("config preview page should not render duplicate result card, got:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestUI_ActionDeviceConfigCandidateKeepsPreviewApplyAction(t *testing.T) {
|
|
agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet && r.URL.Path == "/v1/config/status" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"ok":true,"candidate":{"exists":true,"path":"/opt/rk3588-media-server/etc/media-server.json.candidate.json"}}`))
|
|
return
|
|
}
|
|
if r.Method != http.MethodPut || r.URL.Path != "/v1/config/candidate" {
|
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer agentServer.Close()
|
|
|
|
host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://"))
|
|
if err != nil {
|
|
t.Fatalf("parse test server address: %v", err)
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil {
|
|
t.Fatalf("parse test server port: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
agent := service.NewAgentClient(cfg)
|
|
reg := service.NewRegistryService(cfg, agent)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: host, AgentPort: port, MediaPort: 9000, Online: true})
|
|
tasks := service.NewTaskService(cfg, agent, reg)
|
|
ui, err := NewUI(nil, reg, agent, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("json", `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v1","template":"workshop_face_shoe_alarm","profile":"local_3588_test"}}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/ui/devices/edge-01/config-candidate", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.SetPathValue("id", "edge-01")
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("id", "edge-01")
|
|
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.actionDeviceConfigCandidate(rr, req)
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{
|
|
"候选配置结果",
|
|
"应用候选配置",
|
|
`formaction="/ui/devices/edge-01/config-candidate/apply"`,
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected actionDeviceConfigCandidate HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_ConfigPreviewKeepsSelectedOverlayAfterPreview(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.render(rr, req, "config_preview", PageData{
|
|
Title: "配置预览",
|
|
Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100},
|
|
ConfigSources: service.ConfigPreviewSources{Templates: []service.ConfigSource{{Name: "workshop_face_shoe_alarm"}}, Profiles: []service.ConfigSource{{Name: "local_3588_test"}}, Overlays: []service.ConfigSource{{Name: "face_debug"}, {Name: "face_test_sensitive"}}},
|
|
SelectedTemplate: "workshop_face_shoe_alarm",
|
|
SelectedProfile: "local_3588_test",
|
|
SelectedOverlays: []string{"face_test_sensitive"},
|
|
SelectedConfigID: "preview_edge-01",
|
|
ConfigPreview: &service.ConfigPreviewResult{
|
|
JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v1","template":"workshop_face_shoe_alarm","profile":"local_3588_test","overlays":["face_test_sensitive"]}}`,
|
|
Metadata: map[string]any{
|
|
"config_id": "preview_edge-01",
|
|
"config_version": "v1",
|
|
"template": "workshop_face_shoe_alarm",
|
|
"profile": "local_3588_test",
|
|
"overlays": []any{"face_test_sensitive"},
|
|
},
|
|
Size: 123,
|
|
},
|
|
})
|
|
|
|
body := rr.Body.String()
|
|
if !strings.Contains(body, `value="face_test_sensitive" checked`) {
|
|
t.Fatalf("expected selected overlay to remain checked, got:\n%s", body)
|
|
}
|
|
if strings.Contains(body, `value="face_debug" checked`) {
|
|
t.Fatalf("did not expect default overlay to stay checked, got:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestUI_ConfigPreviewCollapsesJSONAfterActionResult(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.render(rr, req, "config_preview", PageData{
|
|
Title: "配置预览",
|
|
Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100},
|
|
ConfigSources: service.ConfigPreviewSources{Templates: []service.ConfigSource{{Name: "workshop_face_shoe_alarm"}}, Profiles: []service.ConfigSource{{Name: "local_3588_test"}}, Overlays: []service.ConfigSource{{Name: "face_debug"}}},
|
|
SelectedTemplate: "workshop_face_shoe_alarm",
|
|
SelectedProfile: "local_3588_test",
|
|
SelectedOverlays: []string{"face_debug"},
|
|
SelectedConfigID: "preview_edge-01",
|
|
ConfigPreview: &service.ConfigPreviewResult{
|
|
JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[]}`,
|
|
Metadata: map[string]any{"config_id": "preview_edge-01"},
|
|
Size: 64,
|
|
},
|
|
ResultTitle: "候选配置结果",
|
|
RawText: `{"ok":true}`,
|
|
})
|
|
|
|
body := rr.Body.String()
|
|
if !strings.Contains(body, "<details") {
|
|
t.Fatalf("expected json panel to use details, got:\n%s", body)
|
|
}
|
|
if strings.Contains(body, "<details open>") {
|
|
t.Fatalf("expected json panel to be collapsed after action result, got:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestUI_ConfigPreviewShowsApplySummaryAfterApplyResult(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.render(rr, req, "config_preview", PageData{
|
|
Title: "配置预览",
|
|
Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100},
|
|
SelectedTemplate: "workshop_face_shoe_alarm",
|
|
SelectedProfile: "local_3588_test",
|
|
SelectedOverlays: []string{"face_debug"},
|
|
SelectedConfigID: "preview_edge-01",
|
|
ConfigPreview: &service.ConfigPreviewResult{
|
|
JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v2"}}`,
|
|
Metadata: map[string]any{
|
|
"config_id": "preview_edge-01",
|
|
"config_version": "v2",
|
|
"overlays": []any{"face_test_sensitive", "production_quiet"},
|
|
},
|
|
Sha256: "eecdf8d422705f3affa0f892199604f037f60ea8fe578fe2a65527e1800044c5",
|
|
Size: 64,
|
|
},
|
|
ConfigStatus: &ConfigStatusView{
|
|
OK: true,
|
|
Metadata: ConfigStatusMetadata{
|
|
ConfigID: "preview_edge-01",
|
|
ConfigVersion: "v2",
|
|
Overlays: []string{"face_test_sensitive", "production_quiet"},
|
|
},
|
|
Candidate: &ConfigStatusLastGoodFile{
|
|
Exists: false,
|
|
Path: "/opt/rk3588-media-server/etc/media-server.json.candidate.json",
|
|
},
|
|
PreviousConfig: &ConfigStatusLastGoodFile{
|
|
Exists: true,
|
|
Path: "/opt/rk3588-media-server/etc/media-server.json.last_good.json",
|
|
Sha256: "07a37fabd73e98c1c131d31ddfa7b6c0e0949be854225bf2a6e990a09e60ddd3",
|
|
Metadata: ConfigStatusMetadata{
|
|
ConfigID: "local_3588_face_debug",
|
|
ConfigVersion: "20260419.120246",
|
|
Overlays: []string{"face_debug"},
|
|
},
|
|
},
|
|
MediaServer: ConfigStatusMediaServer{
|
|
Running: true,
|
|
PID: 1810489,
|
|
},
|
|
},
|
|
ResultTitle: "应用候选配置结果",
|
|
RawText: `{"ok":true}`,
|
|
})
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{
|
|
"应用结果摘要",
|
|
"当前运行",
|
|
"preview_edge-01 / v2",
|
|
"上一份配置",
|
|
"local_3588_face_debug / 20260419.120246",
|
|
"face_test_sensitive, production_quiet",
|
|
"face_debug",
|
|
"eecdf8d4",
|
|
"07a37fab",
|
|
"candidate",
|
|
"已清空",
|
|
"media-server",
|
|
"运行中",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected apply summary HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_ConfigPreviewExplainsConfigIdentityFields(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.render(rr, req, "config_preview", PageData{
|
|
Title: "配置预览",
|
|
Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100},
|
|
SelectedTemplate: "workshop_face_shoe_alarm",
|
|
SelectedProfile: "local_3588_test",
|
|
SelectedOverlays: []string{"face_debug"},
|
|
SelectedConfigID: "preview_edge-01",
|
|
ConfigPreview: &service.ConfigPreviewResult{
|
|
JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v2","overlays":["face_debug"]}}`,
|
|
Metadata: map[string]any{
|
|
"config_id": "preview_edge-01",
|
|
"config_version": "v2",
|
|
"template": "workshop_face_shoe_alarm",
|
|
"profile": "local_3588_test",
|
|
"overlays": []any{"face_debug"},
|
|
},
|
|
Sha256: "eecdf8d422705f3affa0f892199604f037f60ea8fe578fe2a65527e1800044c5",
|
|
Size: 64,
|
|
},
|
|
})
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{
|
|
"config_id</span> 是配置名,默认会带上 <span class=\"mono\">device_id",
|
|
"config_version</span> 表示本次生成版本",
|
|
"device_id",
|
|
"edge-01",
|
|
"SHA256</span> 是最终文件内容指纹",
|
|
"Overlay",
|
|
"face_debug",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected preview explanation HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_ActionDeviceConfigCandidateApplyReloadsStatusAfterApply(t *testing.T) {
|
|
statusCalls := 0
|
|
agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodGet && r.URL.Path == "/v1/config/status":
|
|
statusCalls++
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if statusCalls == 1 {
|
|
_, _ = w.Write([]byte(`{
|
|
"ok": true,
|
|
"metadata": {"config_id": "preview_edge-01", "config_version": "v2"},
|
|
"candidate": {"exists": true, "path": "/opt/rk3588-media-server/etc/media-server.json.candidate.json"},
|
|
"previous_config": {"exists": true, "path": "/opt/rk3588-media-server/etc/media-server.json.last_good.json", "metadata": {"config_id": "local_3588_face_debug", "config_version": "20260419.120246"}},
|
|
"media_server": {"running": true, "pid": 1810489}
|
|
}`))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(`{
|
|
"ok": true,
|
|
"metadata": {"config_id": "preview_edge-01", "config_version": "v2"},
|
|
"candidate": {"exists": false, "path": "/opt/rk3588-media-server/etc/media-server.json.candidate.json"},
|
|
"previous_config": {"exists": true, "path": "/opt/rk3588-media-server/etc/media-server.json.last_good.json", "metadata": {"config_id": "local_3588_face_debug", "config_version": "20260419.120246"}},
|
|
"media_server": {"running": true, "pid": 1810489}
|
|
}`))
|
|
return
|
|
case r.Method == http.MethodPost && r.URL.Path == "/v1/config/candidate/apply":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
return
|
|
default:
|
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
}))
|
|
defer agentServer.Close()
|
|
|
|
host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://"))
|
|
if err != nil {
|
|
t.Fatalf("parse test server address: %v", err)
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil {
|
|
t.Fatalf("parse test server port: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
agent := service.NewAgentClient(cfg)
|
|
reg := service.NewRegistryService(cfg, agent)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: host, AgentPort: port, MediaPort: 9000, Online: true})
|
|
tasks := service.NewTaskService(cfg, agent, reg)
|
|
ui, err := NewUI(nil, reg, agent, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("json", `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v2","template":"workshop_face_shoe_alarm","profile":"local_3588_test"}}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/ui/devices/edge-01/config-candidate/apply", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.SetPathValue("id", "edge-01")
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("id", "edge-01")
|
|
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.actionDeviceConfigCandidateApply(rr, req)
|
|
|
|
body := rr.Body.String()
|
|
if statusCalls != 2 {
|
|
t.Fatalf("expected status to be loaded twice, got %d", statusCalls)
|
|
}
|
|
for _, want := range []string{
|
|
"应用结果摘要",
|
|
"preview_edge-01 / v2",
|
|
"local_3588_face_debug / 20260419.120246",
|
|
"已清空",
|
|
"运行中",
|
|
"上一份配置",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected apply result HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
if strings.Contains(body, "仍存在") {
|
|
t.Fatalf("expected refreshed status after apply, got stale candidate state:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestUI_LoadConfigStatusPrefersPreviousConfigFields(t *testing.T) {
|
|
agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet || r.URL.Path != "/v1/config/status" {
|
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"ok": true,
|
|
"metadata": {"config_id": "preview-edge", "config_version": "v2"},
|
|
"previous_config_path": "/opt/rk3588-media-server/etc/media-server.json.last_good.json",
|
|
"previous_config": {
|
|
"exists": true,
|
|
"path": "/opt/rk3588-media-server/etc/media-server.json.last_good.json",
|
|
"sha256": "abc12345",
|
|
"metadata": {"config_id": "prev-edge", "config_version": "v1", "overlays": ["face_debug"]}
|
|
},
|
|
"media_server": {"running": true, "pid": 1234}
|
|
}`))
|
|
}))
|
|
defer agentServer.Close()
|
|
|
|
host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://"))
|
|
if err != nil {
|
|
t.Fatalf("parse test server address: %v", err)
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil {
|
|
t.Fatalf("parse test server port: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
agent := service.NewAgentClient(cfg)
|
|
reg := service.NewRegistryService(cfg, agent)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: host, AgentPort: port, MediaPort: 9000, Online: true})
|
|
tasks := service.NewTaskService(cfg, agent, reg)
|
|
ui, err := NewUI(nil, reg, agent, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
|
|
status, _, err := ui.loadConfigStatus(&models.Device{DeviceID: "edge-01", IP: host, AgentPort: port})
|
|
if err != nil {
|
|
t.Fatalf("loadConfigStatus: %v", err)
|
|
}
|
|
if status == nil || status.PreviousConfig == nil {
|
|
t.Fatalf("expected previous config to be present, got %#v", status)
|
|
}
|
|
if status.PreviousConfig.Metadata.ConfigID != "prev-edge" {
|
|
t.Fatalf("previous config metadata = %#v", status.PreviousConfig.Metadata)
|
|
}
|
|
}
|
|
|
|
func TestUI_ModelDeploymentPageRendersDeviceActions(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/models", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.pageModels(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"模型管理", "模型管理工作流", "model-summary", "summary-value", "入口识别节点", "上传模型", "查看设备模型", "人脸库"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected model deployment HTML to contain %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(body, `<div class="stats">
|
|
<div class="stat"><div class="k">目标节点`) {
|
|
t.Fatalf("model summary should not use large stat cards")
|
|
}
|
|
}
|
|
|
|
func TestUI_DiagnosticsPageRendersLogAndMetricLinks(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/diagnostics", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.pageDiagnostics(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"日志分析", "诊断工作台", "入口识别节点", "诊断日志", "运行指标", "高级调试"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected diagnostics HTML to contain %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_SidebarMatchesInformationArchitecture(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/devices", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.pageDevices(rr, req)
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{
|
|
"设备",
|
|
"配置资产",
|
|
"操作审计",
|
|
"系统",
|
|
`href="/ui/devices"`,
|
|
`href="/ui/assets"`,
|
|
`href="/ui/audit"`,
|
|
`href="/ui/system"`,
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected sidebar to contain %q", want)
|
|
}
|
|
}
|
|
for _, old := range []string{"配置管理", "识别配置", "模型管理", "高级调试", "日志分析"} {
|
|
if strings.Contains(body, old) {
|
|
t.Fatalf("sidebar should not contain old label %q", old)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_LegacyDeviceConfigRedirectsToAssets(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/device-config", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.pageDeviceConfig(rr, req)
|
|
|
|
if rr.Code != http.StatusFound {
|
|
t.Fatalf("expected redirect, got %d", rr.Code)
|
|
}
|
|
if got := rr.Header().Get("Location"); got != "/ui/assets" {
|
|
t.Fatalf("expected redirect to /ui/assets, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestUI_DeviceControlPageShowsLiveActions(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
routes, err := ui.Routes()
|
|
if err != nil {
|
|
t.Fatalf("Routes: %v", err)
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
|
|
routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01/control", nil))
|
|
body := rr.Body.String()
|
|
for _, want := range []string{
|
|
"设备控制",
|
|
"配置预览",
|
|
"配置应用",
|
|
"服务控制",
|
|
"执行结果摘要",
|
|
"次要参考信息",
|
|
`action="/ui/devices/edge-01/action"`,
|
|
`action="/ui/devices/edge-01/config-candidate/apply"`,
|
|
`href="/ui/devices/edge-01/config-preview"`,
|
|
"重载服务",
|
|
"回滚到上一份",
|
|
"打开预览器",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected device control HTML to contain %q", want)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{"框架版", "结构样机", "待接入", `disabled>`, `value="workshop_face_shoe_alarm" disabled`} {
|
|
if strings.Contains(body, forbidden) {
|
|
t.Fatalf("device control page should not contain placeholder marker %q", forbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_ActionDeviceActionCanRenderControlPage(t *testing.T) {
|
|
agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost && r.URL.Path == "/v1/media-server/reload":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
case r.Method == http.MethodGet && r.URL.Path == "/v1/config/status":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"ok": true,
|
|
"metadata": {"config_id": "preview_edge-01", "config_version": "v2"},
|
|
"previous_config": {"exists": true, "path": "/opt/rk3588-media-server/etc/media-server.json.last_good.json", "metadata": {"config_id": "local_3588_face_debug", "config_version": "20260419.120246"}},
|
|
"media_server": {"running": true, "pid": 1810489}
|
|
}`))
|
|
default:
|
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
}))
|
|
defer agentServer.Close()
|
|
|
|
host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://"))
|
|
if err != nil {
|
|
t.Fatalf("parse test server address: %v", err)
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil {
|
|
t.Fatalf("parse test server port: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
agent := service.NewAgentClient(cfg)
|
|
reg := service.NewRegistryService(cfg, agent)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: host, AgentPort: port, MediaPort: 9000, Online: true})
|
|
tasks := service.NewTaskService(cfg, agent, reg)
|
|
ui, err := NewUI(nil, reg, agent, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("action", "reload")
|
|
form.Set("return_to", "control")
|
|
req := httptest.NewRequest(http.MethodPost, "/ui/devices/edge-01/action", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("id", "edge-01")
|
|
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.actionDeviceAction(rr, req)
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"设备控制", "POST /v1/media-server/reload", "执行结果摘要"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected control page result HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_ActionDeviceConfigCandidateApplyCanRenderControlPage(t *testing.T) {
|
|
statusCalls := 0
|
|
agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodGet && r.URL.Path == "/v1/config/status":
|
|
statusCalls++
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"ok": true,
|
|
"metadata": {"config_id": "preview_edge-01", "config_version": "v2"},
|
|
"candidate": {"exists": false, "path": "/opt/rk3588-media-server/etc/media-server.json.candidate.json"},
|
|
"previous_config": {"exists": true, "path": "/opt/rk3588-media-server/etc/media-server.json.last_good.json", "metadata": {"config_id": "local_3588_face_debug", "config_version": "20260419.120246"}},
|
|
"media_server": {"running": true, "pid": 1810489}
|
|
}`))
|
|
case r.Method == http.MethodPost && r.URL.Path == "/v1/config/candidate/apply":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
default:
|
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
}))
|
|
defer agentServer.Close()
|
|
|
|
host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://"))
|
|
if err != nil {
|
|
t.Fatalf("parse test server address: %v", err)
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil {
|
|
t.Fatalf("parse test server port: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
agent := service.NewAgentClient(cfg)
|
|
reg := service.NewRegistryService(cfg, agent)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: host, AgentPort: port, MediaPort: 9000, Online: true})
|
|
tasks := service.NewTaskService(cfg, agent, reg)
|
|
ui, err := NewUI(nil, reg, agent, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("return_to", "control")
|
|
req := httptest.NewRequest(http.MethodPost, "/ui/devices/edge-01/config-candidate/apply", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("id", "edge-01")
|
|
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.actionDeviceConfigCandidateApply(rr, req)
|
|
|
|
if statusCalls < 2 {
|
|
t.Fatalf("expected status to be refreshed for control page, got %d calls", statusCalls)
|
|
}
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"设备控制", "应用候选配置结果", "preview_edge-01", "local_3588_face_debug"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected control page apply result HTML to contain %q, got:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_AssetsPageDefinesConfigAssetScope(t *testing.T) {
|
|
ui := newTestUI(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/assets", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
ui.pageAssets(rr, req)
|
|
|
|
body := rr.Body.String()
|
|
for _, want := range []string{"配置资产", "模板", "环境参数", "覆盖项", "发布记录"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected assets HTML to contain %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUI_AuditAndSystemPagesDefineNewScopes(t *testing.T) {
|
|
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
|
reg := service.NewRegistryService(cfg, nil)
|
|
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100, MediaPort: 9000, Online: true, Version: "1.0.0", GitSha: "5c04681"})
|
|
tasks := service.NewTaskService(cfg, nil, reg)
|
|
task, err := tasks.CreateTask("reload", []string{"edge-01"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateTask: %v", err)
|
|
}
|
|
task.Mu.Lock()
|
|
task.Status = models.TaskSuccess
|
|
if ds, ok := task.Devices["edge-01"]; ok && ds != nil {
|
|
ds.Status = models.TaskSuccess
|
|
}
|
|
task.Mu.Unlock()
|
|
ui, err := NewUI(nil, reg, nil, tasks, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewUI: %v", err)
|
|
}
|
|
|
|
rrAudit := httptest.NewRecorder()
|
|
ui.pageAudit(rrAudit, httptest.NewRequest(http.MethodGet, "/ui/audit", nil))
|
|
for _, want := range []string{"操作审计", "审计记录", "谁做了什么、对哪台设备做的、结果如何", "reload", "edge-01", task.ID} {
|
|
if !strings.Contains(rrAudit.Body.String(), want) {
|
|
t.Fatalf("expected audit HTML to contain %q", want)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{"框架版", "后续", `disabled`} {
|
|
if strings.Contains(rrAudit.Body.String(), forbidden) {
|
|
t.Fatalf("audit HTML should not contain placeholder marker %q", forbidden)
|
|
}
|
|
}
|
|
|
|
rrSystem := httptest.NewRecorder()
|
|
ui.pageSystem(rrSystem, httptest.NewRequest(http.MethodGet, "/ui/system", nil))
|
|
for _, want := range []string{"系统", "平台状态与访问入口", "设备发现与注册", "Agent 访问策略", "后台健康", "当前设备数", "UDP 广播发现", "/health", "/openapi.json"} {
|
|
if !strings.Contains(rrSystem.Body.String(), want) {
|
|
t.Fatalf("expected system HTML to contain %q", want)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{"框架版", "后续"} {
|
|
if strings.Contains(rrSystem.Body.String(), forbidden) {
|
|
t.Fatalf("system HTML should not contain placeholder marker %q", forbidden)
|
|
}
|
|
}
|
|
}
|