package httpapi import ( "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "rk3588sys/agent/internal/config" "rk3588sys/agent/internal/procctl" ) type fakeProcessController struct { status procctl.Status } func (f fakeProcessController) Enabled() bool { return true } func (f fakeProcessController) Status() (procctl.Status, error) { return f.status, nil } func (f fakeProcessController) Version() (string, error) { return "", procctl.ErrNotSupported } func (f fakeProcessController) Start(string) (procctl.Status, error) { return procctl.Status{}, procctl.ErrNotSupported } func (f fakeProcessController) Stop() (procctl.Status, error) { return procctl.Status{}, procctl.ErrNotSupported } func (f fakeProcessController) Restart(string) (procctl.Status, error) { return procctl.Status{}, procctl.ErrNotSupported } func (f fakeProcessController) BinaryInfo() (procctl.BinaryUpdateResult, error) { return procctl.BinaryUpdateResult{}, procctl.ErrNotSupported } func (f fakeProcessController) UpdateBinary(io.Reader, int64, string) (procctl.BinaryUpdateResult, error) { return procctl.BinaryUpdateResult{}, procctl.ErrNotSupported } func (f fakeProcessController) RollbackBinary(string) (procctl.BinaryUpdateResult, error) { return procctl.BinaryUpdateResult{}, procctl.ErrNotSupported } func TestHandleConfigStatusReportsMetadataHashAndMediaStatus(t *testing.T) { dir := t.TempDir() cfgPath := filepath.Join(dir, "config.json") body := []byte(`{"metadata":{"config_id":"cfg-1","config_version":"v1","business_name":"A厂区视觉识别","template":"workshop_face_shoe_alarm","profile":"local_3588_test","overlays":["face_debug"],"instance_names":["cam1"],"instance_display_names":["东门入口"]},"instances":[]}`) if err := os.WriteFile(cfgPath, body, 0o644); err != nil { t.Fatalf("write config: %v", err) } if err := os.WriteFile(cfgPath+".last_good.json", []byte(`{"metadata":{"config_id":"cfg-0"}}`), 0o644); err != nil { t.Fatalf("write last good: %v", err) } s := &Server{ agentCfg: config.AgentConfig{ConfigPath: cfgPath}, proc: fakeProcessController{status: procctl.Status{ Running: true, Pid: 1234, ConfigPath: cfgPath, StartedAtMS: 1000, }}, } req := httptest.NewRequest(http.MethodGet, "/v1/config/status", nil) rr := httptest.NewRecorder() s.handleConfigStatus(rr, req) if rr.Code != http.StatusOK { t.Fatalf("status code: got %d body=%s", rr.Code, rr.Body.String()) } var got map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil { t.Fatalf("decode response: %v", err) } if got["exists"] != true { t.Fatalf("exists = %v", got["exists"]) } metadata, ok := got["metadata"].(map[string]any) if !ok { t.Fatalf("metadata missing or wrong type: %#v", got["metadata"]) } if metadata["config_id"] != "cfg-1" || metadata["config_version"] != "v1" { t.Fatalf("metadata = %#v", metadata) } if got["template"] != "workshop_face_shoe_alarm" { t.Fatalf("template = %#v", got["template"]) } if got["profile"] != "local_3588_test" { t.Fatalf("profile = %#v", got["profile"]) } if got["business_name"] != "A厂区视觉识别" { t.Fatalf("business_name = %#v", got["business_name"]) } if got["instance_display_name"] != "东门入口" { t.Fatalf("instance_display_name = %#v", got["instance_display_name"]) } overlays, ok := got["overlays"].([]any) if !ok || len(overlays) != 1 || overlays[0] != "face_debug" { t.Fatalf("overlays = %#v", got["overlays"]) } sum := sha256.Sum256(body) wantSHA := hex.EncodeToString(sum[:]) if got["sha256"] != wantSHA { t.Fatalf("sha256 = %v want %s", got["sha256"], wantSHA) } media, ok := got["media_server"].(map[string]any) if !ok || media["running"] != true || media["pid"] != float64(1234) { t.Fatalf("media_server = %#v", got["media_server"]) } previousConfig, ok := got["previous_config"].(map[string]any) if !ok || previousConfig["exists"] != true { t.Fatalf("previous_config = %#v", got["previous_config"]) } if got["previous_config_path"] != filepath.ToSlash(cfgPath+".last_good.json") { t.Fatalf("previous_config_path = %#v", got["previous_config_path"]) } } func TestHandleInfoIncludesCurrentConfigSummary(t *testing.T) { dir := t.TempDir() cfgPath := filepath.Join(dir, "config.json") body := []byte(`{"metadata":{"config_id":"cfg-1","config_version":"v1","business_name":"A厂区视觉识别","template":"workshop_face_shoe_alarm","profile":"local_3588_test","overlays":["face_debug","production_quiet"],"instance_names":["cam1"],"instance_display_names":["东门入口"]},"instances":[]}`) if err := os.WriteFile(cfgPath, body, 0o644); err != nil { t.Fatalf("write config: %v", err) } s := &Server{ agentCfg: config.AgentConfig{ ConfigPath: cfgPath, DeviceName: "rk3588_orangepi5plus", }, deviceID: "dev-1", hostname: "orangepi5plus", agentPort: 9100, mediaPort: 9000, version: "0.1.0", buildID: "20260420.001", buildType: "release", gitSHA: "abc1234", } req := httptest.NewRequest(http.MethodGet, "/v1/info", nil) rr := httptest.NewRecorder() s.handleInfo(rr, req) if rr.Code != http.StatusOK { t.Fatalf("status code: got %d body=%s", rr.Code, rr.Body.String()) } var got map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil { t.Fatalf("decode response: %v", err) } if got["config_id"] != "cfg-1" { t.Fatalf("config_id = %#v", got["config_id"]) } if got["config_version"] != "v1" { t.Fatalf("config_version = %#v", got["config_version"]) } if got["template"] != "workshop_face_shoe_alarm" { t.Fatalf("template = %#v", got["template"]) } if got["profile"] != "local_3588_test" { t.Fatalf("profile = %#v", got["profile"]) } if got["business_name"] != "A厂区视觉识别" { t.Fatalf("business_name = %#v", got["business_name"]) } if got["instance_display_name"] != "东门入口" { t.Fatalf("instance_display_name = %#v", got["instance_display_name"]) } overlays, ok := got["overlays"].([]any) if !ok || len(overlays) != 2 { t.Fatalf("overlays = %#v", got["overlays"]) } }