package service import ( "crypto/rand" "encoding/json" "strings" "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/credential" "git.ilapage.cn/ila/yovision/Sense/server/app/sense/device/models" "git.ilapage.cn/ila/yovision/Sense/server/app/sense/device/service/dto" "git.ilapage.cn/ila/yovision/Sense/server/app/sense/quota" ) func testDeviceService(t *testing.T) (*Device, *gorm.DB) { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err = db.AutoMigrate(&models.Device{}, &credential.DeviceCredential{}, "a.Setting{}, "a.Change{}); err != nil { t.Fatal(err) } if err = db.Create("a.Setting{ID: quota.SettingID, Limit: 32, Source: "test", Version: 1}).Error; err != nil { t.Fatal(err) } key := make([]byte, 32) if _, err = rand.Read(key); err != nil { t.Fatal(err) } vault, err := credential.NewVault(key) if err != nil { t.Fatal(err) } service := &Device{Service: coreService.Service{Orm: db}, VaultFactory: func() (*credential.Vault, error) { return vault, nil }} return service, db } func TestCreateAndEnableUseQuotaSafetyGate(t *testing.T) { service, db := testDeviceService(t) if err := db.Model("a.Setting{}).Where("id = ?", quota.SettingID).Update("limit", 1).Error; err != nil { t.Fatal(err) } var first dto.DeviceResponse if err := service.Insert(&dto.CreateReq{Name: "第一路", Modality: "video", Capabilities: []string{"video"}}, &first); err != nil { t.Fatal(err) } var rejected dto.DeviceResponse if err := service.Insert(&dto.CreateReq{Name: "第二路", Modality: "video", Capabilities: []string{"video"}}, &rejected); err != quota.ErrExceeded { t.Fatalf("second create error=%v", err) } var disabled dto.DeviceResponse if err := service.Disable(&dto.DisableReq{ID: first.ID, Version: first.Version}, &disabled); err != nil { t.Fatal(err) } var second dto.DeviceResponse if err := service.Insert(&dto.CreateReq{Name: "第二路", Modality: "video", Capabilities: []string{"video"}}, &second); err != nil { t.Fatal(err) } if err := service.Enable(&dto.EnableReq{ID: disabled.ID, Version: disabled.Version}, &rejected); err != quota.ErrExceeded { t.Fatalf("enable over quota error=%v", err) } if err := db.Delete("a.Setting{}, quota.SettingID).Error; err != nil { t.Fatal(err) } if err := service.Insert(&dto.CreateReq{Name: "第三路", Modality: "video", Capabilities: []string{"video"}}, &rejected); err != quota.ErrUnavailable { t.Fatalf("create with unreadable quota error=%v", err) } } func TestDeviceLifecycleUsesAllowlistedFieldsAndOptimisticVersion(t *testing.T) { service, _ := testDeviceService(t) var created dto.DeviceResponse err := service.Insert(&dto.CreateReq{Name: "东门摄像机", Location: "教学楼一楼东门", Modality: "video", Capabilities: []string{"video"}, CreateBy: 7}, &created) if err != nil { t.Fatal(err) } if created.Name != "东门摄像机" || created.Location != "教学楼一楼东门" || created.Version != 1 || created.AdapterStatus != models.AdapterReady { t.Fatalf("unexpected device: %#v", created) } var updated dto.DeviceResponse err = service.Update(&dto.UpdateReq{ID: created.ID, Name: "东门主摄像机", Location: "教学楼一楼东门", Capabilities: []string{"video"}, Version: 1, UpdateBy: 8}, &updated) if err != nil || updated.Version != 2 || updated.Name != "东门主摄像机" { t.Fatalf("update failed: device=%#v err=%v", updated, err) } err = service.Update(&dto.UpdateReq{ID: created.ID, Name: "过期写入", Capabilities: []string{"video"}, Version: 1}, &updated) if err != ErrVersionConflict { t.Fatalf("stale update error=%v", err) } } func TestCredentialIsPurposeSeparatedNeverReturnedAndRequestsRetry(t *testing.T) { service, db := testDeviceService(t) var created dto.DeviceResponse if err := service.Insert(&dto.CreateReq{Name: "测试摄像机", Modality: "video", Capabilities: []string{"video"}}, &created); err != nil { t.Fatal(err) } var updated dto.DeviceResponse if err := service.UpdateCredentials(&dto.CredentialUpdateReq{ ID: created.ID, ONVIFUsername: "synthetic-onvif-user", ONVIFPassword: "synthetic-onvif-password", RTSPSameAsONVIF: false, RTSPUsername: "synthetic-rtsp-user", RTSPPassword: "synthetic-rtsp-password", Version: created.Version, }, &updated); err != nil { t.Fatal(err) } if !updated.ONVIFCredentialConfigured || !updated.RTSPCredentialConfigured || !updated.RetryPending || updated.Version != 2 { t.Fatalf("credential status not reflected: %#v", updated) } encoded, err := json.Marshal(updated) if err != nil { t.Fatal(err) } for _, secretPart := range []string{"synthetic-onvif", "synthetic-rtsp", "ciphertext", "password", "username"} { if strings.Contains(strings.ToLower(string(encoded)), secretPart) { t.Fatalf("response leaked credential material: %s", encoded) } } var stored []credential.DeviceCredential if err = db.Order("purpose").Find(&stored).Error; err != nil || len(stored) != 2 { t.Fatalf("stored credentials=%d err=%v", len(stored), err) } if string(stored[0].Ciphertext) == string(stored[1].Ciphertext) { t.Fatal("ONVIF and RTSP credentials were not purpose-separated") } } func TestUnsupportedModalityIsExplicitlyNotReadyAndPaginationExceedsDefaultQuota(t *testing.T) { service, _ := testDeviceService(t) var radar dto.DeviceResponse for index := 0; index < 20; index++ { var response dto.DeviceResponse if err := service.Insert(&dto.CreateReq{Name: "雷达" + strings.Repeat("号", index+1), Modality: "radar", Capabilities: []string{"radar"}}, &response); err != nil { t.Fatal(err) } if response.AdapterStatus != models.AdapterNotReady { t.Fatalf("adapter status=%s", response.AdapterStatus) } radar = response } var credentialResponse dto.DeviceResponse if err := service.UpdateCredentials(&dto.CredentialUpdateReq{ID: radar.ID, ONVIFUsername: "synthetic", ONVIFPassword: "synthetic", RTSPSameAsONVIF: true, Version: radar.Version}, &credentialResponse); err != ErrInvalidDevice { t.Fatalf("non-video credential update error=%v", err) } request := &dto.PageReq{} request.PageIndex, request.PageSize = 1, 20 var list []dto.DeviceResponse var count int64 if err := service.GetPage(request, &list, &count); err != nil || count != 20 || len(list) != 20 { t.Fatalf("count=%d items=%d err=%v", count, len(list), err) } }