155 lines
6.4 KiB
Go
155 lines
6.4 KiB
Go
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)
|
|
}
|
|
}
|