427 lines
16 KiB
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)
|
|
}
|
|
}
|