package admission import ( "context" "encoding/base64" "errors" "testing" coreService "github.com/go-admin-team/go-admin-core/sdk/service" "gorm.io/driver/sqlite" "gorm.io/gorm" "git.ilapage.cn/ila/yovision/Sense/server/app/sense/area" "git.ilapage.cn/ila/yovision/Sense/server/app/sense/credential" deviceModels "git.ilapage.cn/ila/yovision/Sense/server/app/sense/device/models" "git.ilapage.cn/ila/yovision/Sense/server/app/sense/onvif" "git.ilapage.cn/ila/yovision/Sense/server/app/sense/rtsp" ) type fakeONVIF struct { err error profiles []onvif.Profile } func (f fakeONVIF) Profiles(context.Context, string, onvif.Credential) ([]onvif.Profile, error) { if f.err != nil { return nil, f.err } if f.profiles != nil { return f.profiles, nil } return []onvif.Profile{{Token: "main", Name: "主码流", Width: 1920, Height: 1080, Encoding: "H264", StreamURI: "rtsp://192.0.2.10/main"}, {Token: "sub", Name: "子码流", Width: 640, Height: 360, Encoding: "H264", StreamURI: "rtsp://192.0.2.10/sub"}}, nil } type fakeRTSP struct{} func (fakeRTSP) Verify(context.Context, string, rtsp.Credential) (rtsp.Result, error) { return rtsp.Result{Status: "ready", Detail: "码流可访问"}, nil } func admissionService(t *testing.T) *Service { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err = db.AutoMigrate(&deviceModels.Device{}, &credential.DeviceCredential{}, &Result{}, &Profile{}); err != nil { t.Fatal(err) } key := []byte("0123456789abcdef0123456789abcdef") t.Setenv(credential.EnvironmentKey, base64.StdEncoding.EncodeToString(key)) vault, _ := credential.NewVault(key) device := deviceModels.Device{ID: "device-1", Name: "东门摄像机", Modality: deviceModels.ModalityVideo, Version: 1, Status: "pending", AdapterStatus: "ready"} if err = db.Create(&device).Error; err != nil { t.Fatal(err) } for _, purpose := range []string{credential.PurposeONVIF, credential.PurposeRTSP} { cipher, _ := vault.Encrypt(device.ID, purpose, "synthetic-user", "synthetic-password") if err = db.Create(&credential.DeviceCredential{DeviceID: device.ID, Purpose: purpose, Ciphertext: cipher, KeyVersion: credential.Version()}).Error; err != nil { t.Fatal(err) } } return &Service{Service: coreService.Service{Orm: db}, ONVIF: fakeONVIF{}, RTSP: fakeRTSP{}} } func TestProbePersistsProfilesWithoutReturningCredentials(t *testing.T) { service := admissionService(t) result, err := service.Probe(context.Background(), ProbeRequest{DeviceID: "device-1", Address: "http://192.0.2.10/onvif", Version: 1}) if err != nil { t.Fatal(err) } if result.Status != "ready" || len(result.Profiles) != 2 || result.Profiles[0].Kind != "main" || result.Profiles[1].Kind != "sub" { t.Fatalf("result=%#v", result) } if result.Profiles[0].StreamURI == "" { t.Fatal("stream URI missing") } saved, err := service.Get("device-1") if err != nil || len(saved.Profiles) != 2 { t.Fatalf("saved=%#v err=%v", saved, err) } } func TestFailedReprobePreservesLastVerifiedProfiles(t *testing.T) { service := admissionService(t) if _, err := service.Probe(context.Background(), ProbeRequest{DeviceID: "device-1", Address: "http://192.0.2.10/onvif", Version: 1}); err != nil { t.Fatal(err) } service.ONVIF = fakeONVIF{err: onvif.ErrAuthentication} result, err := service.Probe(context.Background(), ProbeRequest{DeviceID: "device-1", Address: "http://192.0.2.10/onvif", Version: 2}) if err != nil { t.Fatal(err) } if result.Status != "authentication_failed" || len(result.Profiles) != 2 { t.Fatalf("last verified profiles lost: %#v", result) } if _, err = service.Probe(context.Background(), ProbeRequest{DeviceID: "device-1", Address: "http://192.0.2.10/onvif", Version: 2}); !errors.Is(err, ErrConflict) { t.Fatalf("stale error=%v", err) } } func TestProbeErrorsHaveActionableStates(t *testing.T) { for _, test := range []struct { err error status string }{ {onvif.ErrAuthentication, "authentication_failed"}, {onvif.ErrTargetNotAllowed, "target_not_allowed"}, {errors.New("device clock time fault"), "clock_skew"}, {context.DeadlineExceeded, "timeout"}, } { status, detail := classify(test.err) if status != test.status || detail == "" { t.Fatalf("error=%v status=%s detail=%s", test.err, status, detail) } } } func TestProfileResolutionChangeMarksAreaForRecalibration(t *testing.T) { service := admissionService(t) if _, err := service.Probe(context.Background(), ProbeRequest{DeviceID: "device-1", Address: "http://192.0.2.10/onvif", Version: 1}); err != nil { t.Fatal(err) } if err := service.Orm.AutoMigrate(&area.Definition{}, &area.Version{}); err != nil { t.Fatal(err) } definition := area.Definition{ID: "area-1", Name: "东门警戒线", Kind: area.KindDirectionLine, DeviceID: "device-1", ProfileToken: "main", ProfileWidth: 1920, ProfileHeight: 1080, ProfileEncoding: "H264", CurrentVersion: 1, CurrentVersionID: "version-1", Enabled: true, CreatedBy: 7, UpdatedBy: 7} if err := service.Orm.Create(&definition).Error; err != nil { t.Fatal(err) } service.ONVIF = fakeONVIF{profiles: []onvif.Profile{{Token: "main", Name: "主码流", Width: 1280, Height: 720, Encoding: "H264", StreamURI: "rtsp://192.0.2.10/main"}}} if _, err := service.Probe(context.Background(), ProbeRequest{DeviceID: "device-1", Address: "http://192.0.2.10/onvif", Version: 2}); err != nil { t.Fatal(err) } if err := service.Orm.First(&definition, "id = ?", "area-1").Error; err != nil { t.Fatal(err) } if !definition.NeedsRecalibration { t.Fatal("profile resolution change did not mark the bound area") } }