129 lines
4.5 KiB
Go
129 lines
4.5 KiB
Go
package media
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"git.ilapage.cn/ila/yovision/Sense/server/app/sense/credential"
|
|
)
|
|
|
|
type fakeController struct {
|
|
healthErr, applyErr, statusErr error
|
|
applies int
|
|
status PathStatus
|
|
}
|
|
|
|
func (f *fakeController) Health(context.Context) error { return f.healthErr }
|
|
func (f *fakeController) Apply(context.Context, Source) error { f.applies++; return f.applyErr }
|
|
func (f *fakeController) Delete(context.Context, string) error { return nil }
|
|
func (f *fakeController) Status(context.Context, string) (PathStatus, error) {
|
|
return f.status, f.statusErr
|
|
}
|
|
|
|
func mediaTestService(t *testing.T, controller Controller) (*Service, *gorm.DB) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.AutoMigrate(&Route{}, &admissionProfile{}, &credential.DeviceCredential{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key := make([]byte, 32)
|
|
if _, err = rand.Read(key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(credential.EnvironmentKey, base64.StdEncoding.EncodeToString(key))
|
|
vault, _ := credential.NewVault(key)
|
|
ciphertext, err := vault.Encrypt("device-1", credential.PurposeRTSP, "synthetic-user", "synthetic-password")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Create(&credential.DeviceCredential{DeviceID: "device-1", Purpose: credential.PurposeRTSP, Ciphertext: ciphertext, KeyVersion: credential.Version()}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Create(&admissionProfile{DeviceID: "device-1", Token: "main", StreamURI: "rtsp://192.0.2.1/live", VerificationStatus: "ready"}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := NewService(db, controller, nil, RuntimeConfig{})
|
|
service.now = func() time.Time { return time.Date(2026, 8, 14, 0, 0, 0, 0, time.UTC) }
|
|
return service, db
|
|
}
|
|
|
|
func TestEnsureAndReconcileAreIdempotent(t *testing.T) {
|
|
controller := &fakeController{status: PathStatus{Exists: true, Ready: true, Readers: 2}}
|
|
service, db := mediaTestService(t, controller)
|
|
if err := service.EnsureDevice(context.Background(), "device-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.EnsureDevice(context.Background(), "device-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var count int64
|
|
if err := db.Model(&Route{}).Count(&count).Error; err != nil || count != 1 {
|
|
t.Fatalf("count=%d err=%v", count, err)
|
|
}
|
|
item, err := service.Reconcile(context.Background(), "device-1:main")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if item.Actual != "ready" || item.Readers != 2 || controller.applies != 1 {
|
|
t.Fatalf("item=%#v applies=%d", item, controller.applies)
|
|
}
|
|
if err = service.ReconcileDue(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
items, err := service.List(context.Background())
|
|
if err != nil || controller.applies != 1 || items[0].Version != item.Version {
|
|
t.Fatalf("steady route was rewritten: items=%#v applies=%d err=%v", items, controller.applies, err)
|
|
}
|
|
}
|
|
|
|
func TestColdStartDoesNotReactivateStoppedRoute(t *testing.T) {
|
|
controller := &fakeController{status: PathStatus{Exists: true}}
|
|
service, _ := mediaTestService(t, controller)
|
|
if err := service.EnsureDevice(context.Background(), "device-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := service.StopRoute(context.Background(), "device-1:main"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.EnsureAllVerified(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
items, err := service.List(context.Background())
|
|
if err != nil || len(items) != 1 || items[0].Desired != DesiredStopped {
|
|
t.Fatalf("stopped route was reactivated: %#v err=%v", items, err)
|
|
}
|
|
}
|
|
|
|
func TestFailurePersistsBackoffWithoutChangingProfile(t *testing.T) {
|
|
controller := &fakeController{applyErr: errors.New("synthetic apply failure")}
|
|
service, db := mediaTestService(t, controller)
|
|
if err := service.EnsureDevice(context.Background(), "device-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
item, err := service.Reconcile(context.Background(), "device-1:main")
|
|
if err == nil || item.Actual != "apply_failed" || item.FailureCount != 1 || item.NextRetryAt == nil {
|
|
t.Fatalf("item=%#v err=%v", item, err)
|
|
}
|
|
var profile admissionProfile
|
|
if err = db.First(&profile, "device_id = ? AND token = ?", "device-1", "main").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if profile.VerificationStatus != "ready" {
|
|
t.Fatalf("profile changed: %#v", profile)
|
|
}
|
|
if value := os.Getenv(credential.EnvironmentKey); value == "" {
|
|
t.Fatal("test key unexpectedly missing")
|
|
}
|
|
}
|