package database import ( "strings" "testing" "time" "infinite-canvas/backend/internal/model" "gorm.io/gorm" ) func legacyFolderMigrationDatabase(t *testing.T) *gorm.DB { t.Helper() db, err := Open(Config{Driver: "sqlite", DSN: "file:" + t.Name() + "?mode=memory&cache=shared"}) if err != nil { t.Fatal(err) } sqlDB, err := db.DB() if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = sqlDB.Close() }) if err := db.AutoMigrate(&schemaMigration{}); err != nil { t.Fatal(err) } for _, item := range schemaMigrations[:5] { if err := item.apply(db); err != nil { t.Fatal(err) } if err := db.Create(&schemaMigration{Version: item.version, Name: item.name, Checksum: item.checksum, AppliedAt: time.Now().UTC()}).Error; err != nil { t.Fatal(err) } } if err := migrateSchemaV7(db); err != nil { t.Fatal(err) } for _, column := range []string{"playback_status", "playback_object_key", "playback_error"} { if db.Migrator().HasColumn(&model.Resource{}, column) { if err := db.Migrator().DropColumn(&model.Resource{}, column); err != nil { t.Fatal(err) } } } if err := db.Create(&schemaMigration{Version: 6, Name: "asset_library_folders", Checksum: assetLibraryFoldersChecksum, AppliedAt: time.Date(2026, 9, 2, 0, 0, 0, 0, time.UTC)}).Error; err != nil { t.Fatal(err) } return db } func TestMigrateSchemaPreservesLegacyFolderMigration(t *testing.T) { db := legacyFolderMigrationDatabase(t) var before schemaMigration if err := db.First(&before, "version = 6").Error; err != nil { t.Fatal(err) } if err := db.Exec("INSERT INTO asset_folders (id, user_id, name) VALUES ('kept-folder', 'owner', 'Keep me')").Error; err != nil { t.Fatal(err) } for attempt := 0; attempt < 2; attempt++ { if err := MigrateSchema(db); err != nil { t.Fatal(err) } if err := RequireSchemaVersion(db); err != nil { t.Fatal(err) } } var after schemaMigration if err := db.First(&after, "version = 6").Error; err != nil { t.Fatal(err) } if before.Name != after.Name || before.Checksum != after.Checksum || !before.AppliedAt.Equal(after.AppliedAt) { t.Fatalf("historical record changed: before=%+v after=%+v", before, after) } var playback schemaMigration if err := db.First(&playback, "version = 7").Error; err != nil { t.Fatal(err) } if playback.Name != "resource_playback_variant" || playback.Checksum != resourcePlaybackChecksum { t.Fatalf("unexpected playback migration: %+v", playback) } for _, column := range []string{"playback_status", "playback_object_key", "playback_error"} { if !db.Migrator().HasColumn(&model.Resource{}, column) { t.Fatalf("missing playback column %s", column) } } var name string if err := db.Raw("SELECT name FROM asset_folders WHERE id = 'kept-folder'").Scan(&name).Error; err != nil || name != "Keep me" { t.Fatalf("folder data changed: %q %v", name, err) } } func TestMigrateSchemaRejectsUnknownLegacyLineage(t *testing.T) { for _, scenario := range []string{"checksum", "name", "version-seven"} { t.Run(scenario, func(t *testing.T) { db := legacyFolderMigrationDatabase(t) switch scenario { case "checksum": if err := db.Model(&schemaMigration{}).Where("version = 6").Update("checksum", "unknown").Error; err != nil { t.Fatal(err) } case "name": if err := db.Model(&schemaMigration{}).Where("version = 6").Update("name", "unknown").Error; err != nil { t.Fatal(err) } case "version-seven": if err := db.Create(&schemaMigration{Version: 7, Name: "asset_library_folders", Checksum: assetLibraryFoldersChecksum, AppliedAt: time.Now().UTC()}).Error; err != nil { t.Fatal(err) } } if err := MigrateSchema(db); err == nil || !strings.Contains(err.Error(), "不一致") { t.Fatalf("expected lineage rejection, got %v", err) } if db.Migrator().HasColumn(&model.Resource{}, "playback_status") { t.Fatal("rejected migration changed resource schema") } }) } }