199 lines
6.8 KiB
Go
199 lines
6.8 KiB
Go
package version
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
|
|
"git.ilapage.cn/ila/yovision/Sense/server/app/sense/media"
|
|
migrationModels "git.ilapage.cn/ila/yovision/Sense/server/cmd/migrate/migration/models"
|
|
common "git.ilapage.cn/ila/yovision/Sense/server/common/models"
|
|
)
|
|
|
|
func TestMediaMigrationOnPostgres(t *testing.T) {
|
|
dsn := os.Getenv("SENSE_MEDIA_MIGRATION_TEST_DATABASE_URL")
|
|
if dsn == "" {
|
|
t.Skip("set SENSE_MEDIA_MIGRATION_TEST_DATABASE_URL to run the PostgreSQL migration test")
|
|
}
|
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const schema = "sense_media_95_test"
|
|
if err = db.Exec("DROP SCHEMA IF EXISTS " + schema + " CASCADE").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Exec("CREATE SCHEMA " + schema).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { db.Exec("DROP SCHEMA IF EXISTS " + schema + " CASCADE") })
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(1)
|
|
if err = db.Exec("SET search_path TO " + schema).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Run("migrates the legacy path constraint and preserves routes", func(t *testing.T) {
|
|
resetLegacyMediaMigration(t, db, `UNIQUE (path)`)
|
|
if err = db.Exec(`INSERT INTO sense_media_routes
|
|
(id, device_id, profile_token, path, desired, actual, readers, detail, version, updated_at)
|
|
VALUES
|
|
('route-1', 'device-1', 'profile-1', 'camera-1', 'running', 'stopped', 0, '', 1, now()),
|
|
('route-2', 'device-2', 'profile-2', 'camera-2', 'stopped', 'stopped', 0, '', 1, now())`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const migrationVersion = "2026081419000_media.go"
|
|
if err = migrateSenseMedia(db, migrationVersion); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var routes, menus, policies, applied int64
|
|
if err = db.Model(&media.Route{}).Count(&routes).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Model(&migrationModels.SysMenu{}).Where("menu_name LIKE ?", "SenseMedia%").Count(&menus).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Model(&deviceCasbinRule{}).Where("v1 LIKE ?", "/api/v1/media%").Count(&policies).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Model(&common.Migration{}).Where("version = ?", migrationVersion).Count(&applied).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if routes != 2 || menus != 3 || policies != 12 || applied != 1 {
|
|
t.Fatalf("routes=%d menus=%d policies=%d applied=%d", routes, menus, policies, applied)
|
|
}
|
|
assertMediaPathUniqueIndex(t, db)
|
|
if err = db.Exec(`UPDATE sense_media_routes SET path = 'camera-1' WHERE id = 'route-2'`).Error; err == nil {
|
|
t.Fatal("expected path uniqueness violation")
|
|
}
|
|
var runtimeState struct {
|
|
SourceReady bool
|
|
FailureCount int64
|
|
LastError string
|
|
}
|
|
if err = db.Raw(`SELECT source_ready, failure_count, last_error_code AS last_error
|
|
FROM sense_media_routes WHERE id = 'route-1'`).Scan(&runtimeState).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if runtimeState.SourceReady || runtimeState.FailureCount != 0 || runtimeState.LastError != "" {
|
|
t.Fatalf("unexpected migrated runtime state: %#v", runtimeState)
|
|
}
|
|
if err = db.Transaction(prepareLegacyMediaRouteSchema); err != nil {
|
|
t.Fatalf("repeat compatibility migration: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("does nothing when the table is absent", func(t *testing.T) {
|
|
if err = db.Exec(`DROP TABLE IF EXISTS sense_media_routes`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Transaction(prepareLegacyMediaRouteSchema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("creates a fresh media table", func(t *testing.T) {
|
|
resetEmptyMediaMigration(t, db)
|
|
if err = migrateSenseMedia(db, "2026081419000_media_fresh.go"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var routes int64
|
|
if err = db.Model(&media.Route{}).Count(&routes).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if routes != 0 {
|
|
t.Fatalf("fresh route count=%d", routes)
|
|
}
|
|
assertMediaPathUniqueIndex(t, db)
|
|
})
|
|
|
|
t.Run("rejects an unsafe composite path constraint", func(t *testing.T) {
|
|
resetLegacyMediaMigration(t, db, `UNIQUE (path, device_id)`)
|
|
if err = db.Transaction(prepareLegacyMediaRouteSchema); err == nil || !strings.Contains(err.Error(), "unsupported legacy uniqueness") {
|
|
t.Fatalf("expected unsupported uniqueness error, got %v", err)
|
|
}
|
|
var constraints int64
|
|
if err = db.Raw(`SELECT COUNT(*) FROM pg_constraint c
|
|
JOIN pg_class t ON t.oid = c.conrelid
|
|
WHERE t.relname = 'sense_media_routes' AND c.contype = 'u'`).Scan(&constraints).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if constraints != 1 {
|
|
t.Fatalf("constraint rollback count=%d", constraints)
|
|
}
|
|
})
|
|
}
|
|
|
|
func resetEmptyMediaMigration(t *testing.T, db *gorm.DB) {
|
|
t.Helper()
|
|
if err := db.Exec(`DROP TABLE IF EXISTS sense_media_routes, sys_role_menu, sys_menu, sys_role, casbin_rule, sys_migration CASCADE`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&migrationModels.SysRole{}, &migrationModels.SysMenu{}, &deviceCasbinRule{}, &common.Migration{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, role := range []string{"implementation_operator", "site_admin", "viewer"} {
|
|
if err := db.Create(&migrationModels.SysRole{RoleName: role, RoleKey: role, Status: "2"}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func resetLegacyMediaMigration(t *testing.T, db *gorm.DB, pathConstraint string) {
|
|
t.Helper()
|
|
if err := db.Exec(`DROP TABLE IF EXISTS sense_media_routes, sys_role_menu, sys_menu, sys_role, casbin_rule, sys_migration CASCADE`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createRouteTable := `CREATE TABLE sense_media_routes (
|
|
id text PRIMARY KEY,
|
|
device_id text NOT NULL,
|
|
profile_token text NOT NULL,
|
|
path text NOT NULL,
|
|
desired text NOT NULL,
|
|
actual text NOT NULL,
|
|
readers integer NOT NULL DEFAULT 0,
|
|
detail text NOT NULL DEFAULT '',
|
|
version bigint NOT NULL,
|
|
updated_at timestamptz NOT NULL,
|
|
` + pathConstraint + `
|
|
)`
|
|
if err := db.Exec(createRouteTable).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&migrationModels.SysRole{}, &migrationModels.SysMenu{}, &deviceCasbinRule{}, &common.Migration{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, role := range []string{"implementation_operator", "site_admin", "viewer"} {
|
|
if err := db.Create(&migrationModels.SysRole{RoleName: role, RoleKey: role, Status: "2"}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertMediaPathUniqueIndex(t *testing.T, db *gorm.DB) {
|
|
t.Helper()
|
|
var indexes []struct {
|
|
IndexName string
|
|
IndexDef string
|
|
}
|
|
if err := db.Raw(`SELECT indexname AS index_name, indexdef AS index_def
|
|
FROM pg_indexes
|
|
WHERE schemaname = current_schema()
|
|
AND tablename = 'sense_media_routes'
|
|
ORDER BY indexname`).Scan(&indexes).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, index := range indexes {
|
|
if strings.Contains(index.IndexDef, "UNIQUE INDEX") && strings.HasSuffix(index.IndexDef, " (path)") {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("missing unique path index: %#v", indexes)
|
|
}
|