3588AdminBackend/internal/service/config_preview_test.go

427 lines
16 KiB
Go

package service
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"3588AdminBackend/internal/config"
"3588AdminBackend/internal/storage"
)
func mustImportAssetsFromMediaRepo(t *testing.T, svc *ConfigPreviewService) {
t.Helper()
if _, err := svc.ImportAssetsFromMediaRepo(); err != nil {
t.Fatalf("ImportAssetsFromMediaRepo: %v", err)
}
}
func TestConfigPreviewServiceListsSources(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "configs", "templates", "std_workshop_face_recognition_shoe_alarm.json"), `{}`)
mustWrite(t, filepath.Join(root, "configs", "profiles", "local_3588_test.json"), `{}`)
mustWrite(t, filepath.Join(root, "configs", "overlays", "face_debug.json"), `{}`)
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatalf("OpenSQLite: %v", err)
}
defer store.Close()
repo := storage.NewAssetsRepo(store.DB())
svc := NewConfigPreviewService(&config.Config{MediaRepoPath: root}, repo)
mustImportAssetsFromMediaRepo(t, svc)
sources, err := svc.ListSources()
if err != nil {
t.Fatalf("ListSources: %v", err)
}
if sources.Root != "SQLite" {
t.Fatalf("expected root %q, got %q", "SQLite", sources.Root)
}
if got := sourceNames(sources.Templates); strings.Join(got, ",") != "std_workshop_face_recognition_shoe_alarm" {
t.Fatalf("unexpected templates: %v", got)
}
if got := sourceNames(sources.Profiles); strings.Join(got, ",") != "local_3588_test" {
t.Fatalf("unexpected profiles: %v", got)
}
if got := sourceNames(sources.Overlays); strings.Join(got, ",") != "face_debug" {
t.Fatalf("unexpected overlays: %v", got)
}
}
func TestConfigPreviewServiceListSourcesAllowsEmptyConfigsDir(t *testing.T) {
root := t.TempDir()
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatalf("OpenSQLite: %v", err)
}
defer store.Close()
repo := storage.NewAssetsRepo(store.DB())
if err := repo.SaveTemplate("helmet", "helmet template", `{"name":"helmet","template":{"nodes":[],"edges":[]}}`); err != nil {
t.Fatalf("SaveTemplate: %v", err)
}
if err := repo.SaveProfile("line_a", "helmet", "Line A", "profile", `{"name":"line_a","instances":[{"name":"cam1","template":"helmet","input_bindings":{"video_input_main":{"video_source_ref":"gate_cam_01"}}}]}`); err != nil {
t.Fatalf("SaveProfile: %v", err)
}
svc := NewConfigPreviewService(&config.Config{MediaRepoPath: root}, repo)
sources, err := svc.ListSources()
if err != nil {
t.Fatalf("ListSources: %v", err)
}
if got := sourceNames(sources.Templates); len(got) != 1 || got[0] != "helmet" {
t.Fatalf("unexpected templates: %#v", got)
}
if got := sourceNames(sources.Profiles); len(got) != 1 || got[0] != "line_a" {
t.Fatalf("unexpected profiles: %#v", got)
}
}
func TestConfigPreviewServiceRejectsUnsafeNames(t *testing.T) {
root := t.TempDir()
svc := NewConfigPreviewService(&config.Config{MediaRepoPath: root})
_, err := svc.Render(ConfigPreviewRequest{
Template: "../secret",
Profile: "local_3588_test",
ConfigID: "preview",
ConfigVersion: "v1",
})
if err == nil {
t.Fatal("expected unsafe template name to be rejected")
}
}
func TestConfigPreviewServiceImportsAssetsIntoSQLite(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "configs", "templates", "std_face_recognition_stream.json"), `{"name":"std_face_recognition_stream","description":"helmet template","template":{"nodes":[],"edges":[]}}`)
mustWrite(t, filepath.Join(root, "configs", "profiles", "gate_a.json"), `{"name":"gate_a","business_name":"Gate A","description":"gate profile","instances":[{"name":"cam1","template":"helmet","scene_meta":{"display_name":"Gate A"},"input_bindings":{"video_input_main":{"video_source_ref":"gate_cam_01"}}}]}`)
mustWrite(t, filepath.Join(root, "configs", "overlays", "night_relaxed.json"), `{"name":"night_relaxed","description":"overlay","instance_overrides":{"cam1":{"override":{}}}}`)
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatalf("OpenSQLite: %v", err)
}
defer store.Close()
repo := storage.NewAssetsRepo(store.DB())
svc := NewConfigPreviewService(&config.Config{MediaRepoPath: root}, repo)
result, err := svc.ImportAssetsFromMediaRepo()
if err != nil {
t.Fatalf("ImportAssetsFromMediaRepo: %v", err)
}
if result.Templates != 1 || result.Profiles != 1 || result.Overlays != 1 {
t.Fatalf("unexpected import result: %#v", result)
}
sources, err := svc.ListSources()
if err != nil {
t.Fatalf("ListSources: %v", err)
}
if got := sourceNames(sources.Templates); len(got) != 1 || got[0] != "std_face_recognition_stream" {
t.Fatalf("unexpected templates after import: %#v", got)
}
if record, err := repo.GetTemplate("std_face_recognition_stream"); err != nil {
t.Fatalf("GetTemplate: %v", err)
} else if record == nil {
t.Fatal("expected imported standard template")
}
}
func TestConfigPreviewServiceExportsAssetJSONFromSQLite(t *testing.T) {
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatalf("OpenSQLite: %v", err)
}
defer store.Close()
repo := storage.NewAssetsRepo(store.DB())
const raw = "{\n \"name\": \"helmet\",\n \"template\": {\n \"nodes\": [],\n \"edges\": []\n }\n}\n"
if err := repo.SaveTemplate("helmet", "helmet template", raw); err != nil {
t.Fatalf("SaveTemplate: %v", err)
}
svc := NewConfigPreviewService(&config.Config{}, repo)
body, filename, err := svc.ExportAssetJSON("templates", "helmet")
if err != nil {
t.Fatalf("ExportAssetJSON: %v", err)
}
if filename != "helmet.json" {
t.Fatalf("unexpected export filename: %q", filename)
}
if string(body) != raw {
t.Fatalf("unexpected export body: %s", string(body))
}
}
func TestConfigPreviewServiceRenderProfileEditorWritesResolvedBindings(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "configs", "templates", "std_workshop_face_recognition_shoe_alarm.json"), `{
"name":"std_workshop_face_recognition_shoe_alarm",
"template":{
"nodes":[
{"id":"input_rtsp_main","type":"input_rtsp","url":"${slot:video_input_main.url}"},
{"id":"publish_stream","type":"publish","outputs":[{"proto":"hls","path":"${slot:stream_output_main.publish_hls_path}"}]}
],
"edges":[]
}
}`)
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatalf("OpenSQLite: %v", err)
}
defer store.Close()
repo := storage.NewAssetsRepo(store.DB())
if err := repo.SaveVideoSource(
"gate_cam_01",
"rtsp",
"东门入口",
"东门主入口摄像头",
`{"name":"gate_cam_01","source_type":"rtsp","config":{"url":"rtsp://10.0.0.1/live"}}`,
); err != nil {
t.Fatalf("SaveVideoSource: %v", err)
}
saveIntegrationServiceForPreviewTest(t, repo, "minio_main", "object_storage", `{"name":"minio_main","type":"object_storage","config":{"endpoint":"http://10.0.0.49:9000","bucket":"myminio","access_key":"admin","secret_key":"password"}}`)
saveIntegrationServiceForPreviewTest(t, repo, "token_main", "token_service", `{"name":"token_main","type":"token_service","config":{"get_token_url":"http://10.0.0.49:8080/api/getToken","tenant_code":"32"}}`)
saveIntegrationServiceForPreviewTest(t, repo, "alarm_main", "alarm_service", `{"name":"alarm_main","type":"alarm_service","config":{"put_message_url":"http://10.0.0.49:8080/api/putMessage","tenant_code":"32"}}`)
svc := NewConfigPreviewService(&config.Config{MediaRepoPath: root}, repo)
mustImportAssetsFromMediaRepo(t, svc)
editor := ConfigProfileEditor{
Name: "line_a",
Instances: []ConfigProfileInstanceEditor{{
Name: "cam1",
Template: "std_workshop_face_recognition_shoe_alarm",
VideoSourceRef: "gate_cam_01",
PublishHLSPath: "./web/hls/cam1/index.m3u8",
PublishRTSPPort: "8555",
PublishRTSPPath: "/live/cam1",
ChannelNo: "cam1",
ServiceBindings: map[string]ServiceBindingEditor{
"object_storage_main": {ServiceRef: "minio_main"},
"token_service_main": {ServiceRef: "token_main"},
"alarm_service_main": {ServiceRef: "alarm_main"},
},
}},
}
result, err := svc.RenderProfileEditor(editor, ConfigPreviewRequest{
Template: "std_workshop_face_recognition_shoe_alarm",
ConfigID: "preview",
ConfigVersion: "v1",
})
if err != nil {
t.Fatalf("RenderProfileEditor: %v", err)
}
var doc map[string]any
if err := json.Unmarshal([]byte(result.JSON), &doc); err != nil {
t.Fatalf("unmarshal render result: %v", err)
}
templates, _ := doc["templates"].(map[string]any)
renderedTemplate, _ := templates["std_workshop_face_recognition_shoe_alarm__cam1"].(map[string]any)
nodes, _ := renderedTemplate["nodes"].([]any)
inputNode, _ := nodes[0].(map[string]any)
if got := stringValue(inputNode["url"]); got != "rtsp://10.0.0.1/live" {
t.Fatalf("expected expanded input url, got %#v", inputNode)
}
publishNode, _ := nodes[1].(map[string]any)
outputs, _ := publishNode["outputs"].([]any)
output, _ := outputs[0].(map[string]any)
if got := stringValue(output["path"]); got != "./web/hls/cam1/index.m3u8" {
t.Fatalf("expected expanded output path, got %#v", output)
}
metadata, _ := doc["metadata"].(map[string]any)
if got := stringValue(metadata["rendered_by"]); got != "managerd" {
t.Fatalf("expected managerd renderer metadata, got %#v", metadata)
}
}
func TestConfigPreviewServiceRenderUsesSQLiteProfileAndOverlay(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "configs", "templates", "std_service_test_stream.json"), `{
"name":"std_service_test_stream",
"template":{
"nodes":[
{"id":"input_rtsp_main","type":"input_rtsp","url":"${slot:video_input_main.url}"},
{"id":"publish_stream","type":"publish","outputs":[{"proto":"hls","path":"${slot:stream_output_main.publish_hls_path}"}]}
],
"edges":[]
}
}`)
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatalf("OpenSQLite: %v", err)
}
defer store.Close()
repo := storage.NewAssetsRepo(store.DB())
if err := repo.SaveProfile("line_a", "std_service_test_stream", "Line A", "scene profile", `{
"name":"line_a",
"business_name":"Line A",
"instances":[
{
"name":"cam1",
"template":"std_service_test_stream",
"input_bindings":{"video_input_main":{"video_source_ref":"gate_cam_01"}},
"output_bindings":{"stream_output_main":{"publish_hls_path":"./web/hls/cam1/index.m3u8","publish_rtsp_port":8555,"publish_rtsp_path":"/live/cam1","channel_no":"cam1"}}
}
]
}`); err != nil {
t.Fatalf("SaveProfile: %v", err)
}
if err := repo.SaveOverlay("night_relaxed", "night overlay", `{"name":"night_relaxed","instance_overrides":{"cam1":{"params":{"debug":true}}}}`); err != nil {
t.Fatalf("SaveOverlay: %v", err)
}
if err := repo.SaveVideoSource(
"gate_cam_01",
"rtsp",
"东门入口",
"东门主入口摄像头",
`{"name":"gate_cam_01","source_type":"rtsp","config":{"url":"rtsp://10.0.0.1/live"}}`,
); err != nil {
t.Fatalf("SaveVideoSource: %v", err)
}
svc := NewConfigPreviewService(&config.Config{MediaRepoPath: root}, repo)
mustImportAssetsFromMediaRepo(t, svc)
result, err := svc.Render(ConfigPreviewRequest{
Template: "std_service_test_stream",
Profile: "line_a",
Overlays: []string{"night_relaxed"},
ConfigID: "preview",
ConfigVersion: "v1",
})
if err != nil {
t.Fatalf("Render: %v", err)
}
var doc map[string]any
if err := json.Unmarshal([]byte(result.JSON), &doc); err != nil {
t.Fatalf("unmarshal render result: %v", err)
}
metadata, _ := doc["metadata"].(map[string]any)
if got := stringValue(metadata["profile"]); got != "line_a" {
t.Fatalf("expected sqlite profile metadata, got %#v", metadata)
}
names, _ := metadata["overlays"].([]any)
if len(names) != 1 || stringValue(names[0]) != "night_relaxed" {
t.Fatalf("expected sqlite overlay metadata, got %#v", metadata["overlays"])
}
templates, _ := doc["templates"].(map[string]any)
renderedTemplate, _ := templates["std_service_test_stream__cam1"].(map[string]any)
nodes, _ := renderedTemplate["nodes"].([]any)
inputNode, _ := nodes[0].(map[string]any)
if got := stringValue(inputNode["url"]); got != "rtsp://10.0.0.1/live" {
t.Fatalf("expected expanded input url, got %#v", inputNode)
}
instances, _ := doc["instances"].([]any)
instance, _ := instances[0].(map[string]any)
params, _ := instance["params"].(map[string]any)
if got := boolValue(params["debug"], false); !got {
t.Fatalf("expected overlay params to merge into runtime instance, got %#v", instance)
}
}
func TestSaveProfileEditorRequiresAssetRepository(t *testing.T) {
svc := NewConfigPreviewService(&config.Config{})
err := svc.SaveProfileEditor(ConfigProfileEditor{
Name: "line_a",
Instances: []ConfigProfileInstanceEditor{{
Name: "cam1",
Template: "helmet",
VideoSourceRef: "gate_cam_01",
}},
})
if err == nil || !strings.Contains(err.Error(), "asset repository is not configured") {
t.Fatalf("expected asset repository error, got %v", err)
}
}
func TestConfigPreviewServiceResolveSceneBindings(t *testing.T) {
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
if err != nil {
t.Fatalf("OpenSQLite: %v", err)
}
defer store.Close()
repo := storage.NewAssetsRepo(store.DB())
if err := repo.SaveVideoSource(
"gate_cam_01",
"rtsp",
"东门入口",
"东门主入口摄像头",
`{"name":"gate_cam_01","source_type":"rtsp","config":{"url":"rtsp://10.0.0.1/live"}}`,
); err != nil {
t.Fatalf("SaveVideoSource: %v", err)
}
saveIntegrationServiceForPreviewTest(t, repo, "minio_main", "object_storage", `{"name":"minio_main","type":"object_storage","config":{"endpoint":"http://10.0.0.49:9000","bucket":"myminio","access_key":"admin","secret_key":"password"}}`)
svc := NewConfigPreviewService(&config.Config{}, repo)
resolved, err := svc.resolveSceneBindings(map[string]any{
"name": "line_a",
"instances": []any{
map[string]any{
"name": "cam1",
"template": "std_workshop_face_recognition_shoe_alarm",
"input_bindings": map[string]any{
"video_input_main": map[string]any{
"video_source_ref": "gate_cam_01",
},
},
"service_bindings": map[string]any{
"object_storage_main": map[string]any{
"service_ref": "minio_main",
},
},
},
},
})
if err != nil {
t.Fatalf("resolveSceneBindings: %v", err)
}
instances, _ := resolved["instances"].([]any)
instanceMap, _ := instances[0].(map[string]any)
inputBindings, _ := instanceMap["input_bindings"].(map[string]any)
videoInput, _ := inputBindings["video_input_main"].(map[string]any)
resolvedInput, _ := videoInput["resolved"].(map[string]any)
if got := stringValue(resolvedInput["url"]); got != "rtsp://10.0.0.1/live" {
t.Fatalf("expected resolved input url, got %#v", resolvedInput)
}
serviceBindings, _ := instanceMap["service_bindings"].(map[string]any)
objectStorage, _ := serviceBindings["object_storage_main"].(map[string]any)
resolvedService, _ := objectStorage["resolved"].(map[string]any)
if got := stringValue(resolvedService["bucket"]); got != "myminio" {
t.Fatalf("expected resolved service binding, got %#v", resolvedService)
}
}
func saveIntegrationServiceForPreviewTest(t *testing.T, repo *storage.AssetsRepo, name string, serviceType string, body string) {
t.Helper()
if err := repo.SaveIntegrationService(name, serviceType, name, true, body); err != nil {
t.Fatalf("SaveIntegrationService(%s): %v", name, err)
}
}
func sourceNames(items []ConfigSource) []string {
out := make([]string, 0, len(items))
for _, item := range items {
out = append(out, item.Name)
}
return out
}
func mustWrite(t *testing.T, path string, body string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
}
if err := os.WriteFile(path, []byte(body), 0644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}