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" ) 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{}); 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 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) } }