208 lines
7.4 KiB
Go
208 lines
7.4 KiB
Go
package version
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
|
|
deviceModels "git.ilapage.cn/ila/yovision/Sense/server/app/sense/device/models"
|
|
migrationModels "git.ilapage.cn/ila/yovision/Sense/server/cmd/migrate/migration/models"
|
|
common "git.ilapage.cn/ila/yovision/Sense/server/common/models"
|
|
)
|
|
|
|
func TestCanonicalLegacyCapabilities(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input sql.NullString
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{name: "null", input: sql.NullString{}, want: "[]"},
|
|
{name: "blank", input: sql.NullString{String: " ", Valid: true}, want: "[]"},
|
|
{name: "single", input: sql.NullString{String: " video ", Valid: true}, want: `["video"]`},
|
|
{name: "array", input: sql.NullString{String: `["radar", "contact"]`, Valid: true}, want: `["radar","contact"]`},
|
|
{name: "empty array", input: sql.NullString{String: `[]`, Valid: true}, want: `[]`},
|
|
{name: "unknown", input: sql.NullString{String: "unknown", Valid: true}, wantErr: true},
|
|
{name: "object", input: sql.NullString{String: `{"video":true}`, Valid: true}, wantErr: true},
|
|
{name: "unknown array item", input: sql.NullString{String: `["video","unknown"]`, Valid: true}, wantErr: true},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := canonicalLegacyCapabilities(test.input)
|
|
if test.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("expected error, got %q", got)
|
|
}
|
|
return
|
|
}
|
|
if err != nil || got != test.want {
|
|
t.Fatalf("got %q, %v; want %q", got, err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeviceCapabilitiesMigrationOnPostgres(t *testing.T) {
|
|
dsn := os.Getenv("SENSE_DEVICE_MIGRATION_TEST_DATABASE_URL")
|
|
if dsn == "" {
|
|
t.Skip("set SENSE_DEVICE_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_device_92_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("converts legacy values and remains idempotent", func(t *testing.T) {
|
|
resetLegacyDeviceTable(t, db)
|
|
for _, row := range [][2]string{{"single", "video"}, {"blank", ""}, {"array", `["radar","contact"]`}} {
|
|
if err = db.Exec(`INSERT INTO sense_devices (id, capabilities) VALUES (?, ?)`, row[0], row[1]).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err = db.Transaction(prepareLegacyDeviceCapabilities); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.AutoMigrate(&deviceModels.Device{}); err != nil {
|
|
t.Fatalf("AutoMigrate after compatibility conversion: %v", err)
|
|
}
|
|
assertCapabilitiesColumn(t, db, "jsonb", "'[]'::jsonb")
|
|
var values []struct{ ID, Capabilities string }
|
|
if err = db.Raw(`SELECT id, capabilities::text AS capabilities FROM sense_devices ORDER BY id`).Scan(&values).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := map[string]string{}
|
|
for _, value := range values {
|
|
got[value.ID] = strings.ReplaceAll(value.Capabilities, " ", "")
|
|
}
|
|
if got["single"] != `["video"]` || got["blank"] != `[]` || got["array"] != `["radar","contact"]` {
|
|
t.Fatalf("unexpected converted values: %#v", got)
|
|
}
|
|
if err = db.Transaction(prepareLegacyDeviceCapabilities); err != nil {
|
|
t.Fatalf("repeat migration: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("rejects unknown values without partial conversion", func(t *testing.T) {
|
|
resetLegacyDeviceTable(t, db)
|
|
if err = db.Exec(`INSERT INTO sense_devices (id, capabilities) VALUES ('valid', 'video'), ('invalid', 'unknown')`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Transaction(prepareLegacyDeviceCapabilities); err == nil {
|
|
t.Fatal("expected unsupported legacy data error")
|
|
}
|
|
assertCapabilitiesColumn(t, db, "text", "''::text")
|
|
var value string
|
|
if err = db.Raw(`SELECT capabilities FROM sense_devices WHERE id = 'valid'`).Scan(&value).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if value != "video" {
|
|
t.Fatalf("transaction left partial data: %q", value)
|
|
}
|
|
})
|
|
|
|
t.Run("does nothing when the table is absent", func(t *testing.T) {
|
|
if err = db.Exec(`DROP TABLE sense_devices`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.Transaction(prepareLegacyDeviceCapabilities); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("completes the device migration and records its version", func(t *testing.T) {
|
|
resetLegacyDeviceTable(t, db)
|
|
if err = db.Exec(`INSERT INTO sense_devices (id, capabilities) VALUES ('legacy', 'video')`).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)
|
|
}
|
|
}
|
|
const migrationVersion = "2026081414000_device.go"
|
|
if err = migrateSenseDeviceLedger(db, migrationVersion); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertCapabilitiesColumn(t, db, "jsonb", "'[]'::jsonb")
|
|
var migratedValue string
|
|
if err = db.Raw(`SELECT capabilities::text FROM sense_devices WHERE id = 'legacy'`).Scan(&migratedValue).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.ReplaceAll(migratedValue, " ", "") != `["video"]` {
|
|
t.Fatalf("unexpected full-migration value: %q", migratedValue)
|
|
}
|
|
var applied int64
|
|
if err = db.Model(&common.Migration{}).Where("version = ?", migrationVersion).Count(&applied).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if applied != 1 {
|
|
t.Fatalf("migration record count=%d", applied)
|
|
}
|
|
})
|
|
}
|
|
|
|
func resetLegacyDeviceTable(t *testing.T, db *gorm.DB) {
|
|
t.Helper()
|
|
if err := db.Exec(`DROP TABLE IF EXISTS sense_devices`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`CREATE TABLE sense_devices (
|
|
id text PRIMARY KEY,
|
|
name varchar(128) NOT NULL DEFAULT '',
|
|
location varchar(255) NOT NULL DEFAULT '',
|
|
modality varchar(32) NOT NULL DEFAULT 'video',
|
|
capabilities text NOT NULL DEFAULT '',
|
|
status varchar(32) NOT NULL DEFAULT 'active',
|
|
adapter_status varchar(32) NOT NULL DEFAULT 'ready',
|
|
rtsp_credential_same_as_onvif boolean NOT NULL DEFAULT true,
|
|
credential_updated_at timestamptz,
|
|
retry_requested_at timestamptz,
|
|
version bigint NOT NULL DEFAULT 1,
|
|
create_by bigint NOT NULL DEFAULT 0,
|
|
update_by bigint NOT NULL DEFAULT 0,
|
|
created_at timestamptz NOT NULL DEFAULT current_timestamp,
|
|
updated_at timestamptz NOT NULL DEFAULT current_timestamp,
|
|
deleted_at timestamptz
|
|
)`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func assertCapabilitiesColumn(t *testing.T, db *gorm.DB, wantType, wantDefault string) {
|
|
t.Helper()
|
|
var column struct{ DataType, ColumnDefault string }
|
|
if err := db.Raw(`SELECT data_type, column_default
|
|
FROM information_schema.columns
|
|
WHERE table_schema = current_schema()
|
|
AND table_name = 'sense_devices'
|
|
AND column_name = 'capabilities'`).Scan(&column).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if column.DataType != wantType || column.ColumnDefault != wantDefault {
|
|
t.Fatalf("type/default=%q/%q; want %q/%q", column.DataType, column.ColumnDefault, wantType, wantDefault)
|
|
}
|
|
}
|