Files
ttstd-infinite-canvas/backend/internal/database/profile_migration_test.go
2026-10-13 05:31:30 +08:00

142 lines
5.4 KiB
Go

package database
import (
"bytes"
"testing"
"yingce/backend/internal/model"
)
func TestProfileMigrationUpgradesLegacyIdentityRows(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: ":memory:"})
if err != nil {
t.Fatal(err)
}
for _, sql := range []string{
`CREATE TABLE users (id TEXT PRIMARY KEY, username TEXT, email TEXT, password_hash TEXT, status TEXT)`,
`INSERT INTO users VALUES ('legacy','legacy','legacy@example.com','preserved-hash','active')`,
`CREATE TABLE auth_sessions (id TEXT PRIMARY KEY,user_id TEXT,token_hash TEXT,expires_at DATETIME)`,
`INSERT INTO auth_sessions VALUES ('legacy-session','legacy','preserved-token','2099-01-01')`,
`CREATE TABLE o_auth_states (id TEXT PRIMARY KEY,provider TEXT,state_hash TEXT)`,
`INSERT INTO o_auth_states VALUES ('legacy-state','linuxdo','preserved-state')`,
} {
if err := db.Exec(sql).Error; err != nil {
t.Fatal(err)
}
}
var migration *migration
for i := range schemaMigrations {
if schemaMigrations[i].version == 51 {
migration = &schemaMigrations[i]
}
}
if migration == nil {
t.Fatal("profile migration absent")
}
for i := 0; i < 2; i++ {
if err := db.Transaction(migration.apply); err != nil {
t.Fatal(err)
}
}
for _, field := range []string{"AvatarMode", "AvatarVersion"} {
if !db.Migrator().HasColumn(&model.User{}, field) {
t.Fatalf("missing %s", field)
}
}
if !db.Migrator().HasColumn(&model.AuthSession{}, "ReauthenticatedAt") || !db.Migrator().HasColumn(&model.OAuthState{}, "ReauthUserID") || !db.Migrator().HasColumn(&model.OAuthState{}, "ReauthSessionID") || !db.Migrator().HasColumn(&model.AuthVerification{}, "SessionID") || !db.Migrator().HasColumn(&model.AuthVerification{}, "CredentialHash") || !db.Migrator().HasTable(&model.UserAvatar{}) {
t.Fatal("profile schema incomplete")
}
var user model.User
var session model.AuthSession
var state model.OAuthState
if err := db.First(&user, "id = ?", "legacy").Error; err != nil {
t.Fatal(err)
}
if err := db.First(&session, "id = ?", "legacy-session").Error; err != nil {
t.Fatal(err)
}
if err := db.First(&state, "id = ?", "legacy-state").Error; err != nil {
t.Fatal(err)
}
if user.PasswordHash != "preserved-hash" || user.Email != "legacy@example.com" || session.TokenHash != "preserved-token" || session.ReauthenticatedAt != nil || state.StateHash != "preserved-state" || state.ReauthUserID != "" || state.ReauthSessionID != "" {
t.Fatal("legacy credentials or verification changed")
}
for _, m := range Models() {
if _, ok := m.(*model.UserAvatar); ok {
return
}
}
t.Fatal("avatar missing from Models")
}
func TestAvatarPresetMigrationUpgradesV51WithoutChangingUsersOrBlobs(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: ":memory:"})
if err != nil {
t.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sqlDB.Close() })
if err := MigrateSchema(db); err != nil {
t.Fatal(err)
}
// Reconstruct the already applied v51 shape and migration ledger. The new
// migration must add one column without replaying or changing v51 metadata.
if err := db.Migrator().DropColumn(&model.User{}, "AvatarPreset"); err != nil {
t.Fatal(err)
}
if err := db.Delete(&schemaMigration{}, "version = ?", 52).Error; err != nil {
t.Fatal(err)
}
var v51Before schemaMigration
if err := db.First(&v51Before, "version = ?", 51).Error; err != nil {
t.Fatal(err)
}
if v51Before.Name != "personal_profile_security" || v51Before.Checksum != "sha256:personal-profile-security-v51-20261012" {
t.Fatal("existing v51 identity changed")
}
user := model.User{ID: "preset-migration-user", Username: "preset-user", Email: "preset@example.com", DisplayName: "Creator", PasswordHash: "preserved-hash", Status: model.UserStatusActive, AvatarMode: "custom", AvatarVersion: "preserved-avatar-version"}
if err := db.Omit("AvatarPreset").Create(&user).Error; err != nil {
t.Fatal(err)
}
payload := []byte("preserved-avatar-payload")
if err := db.Create(&model.UserAvatar{UserID: user.ID, Data: payload}).Error; err != nil {
t.Fatal(err)
}
for i := 0; i < 2; i++ {
if err := MigrateSchema(db); err != nil {
t.Fatal(err)
}
}
if !db.Migrator().HasColumn(&model.User{}, "AvatarPreset") {
t.Fatal("missing avatar preset column")
}
var migrated model.User
var avatar model.UserAvatar
var v51After schemaMigration
var v52 schemaMigration
if err := db.First(&migrated, "id = ?", user.ID).Error; err != nil {
t.Fatal(err)
}
if err := db.First(&avatar, "user_id = ?", user.ID).Error; err != nil {
t.Fatal(err)
}
if err := db.First(&v51After, "version = ?", 51).Error; err != nil {
t.Fatal(err)
}
if err := db.First(&v52, "version = ?", 52).Error; err != nil {
t.Fatal(err)
}
if migrated.PasswordHash != user.PasswordHash || migrated.Email != user.Email || migrated.DisplayName != user.DisplayName || migrated.AvatarMode != "custom" || migrated.AvatarVersion != user.AvatarVersion || migrated.AvatarPreset != "" || !bytes.Equal(avatar.Data, payload) {
t.Fatal("v52 migration changed existing profile or custom avatar")
}
if v51After != v51Before || v52.Name != "user_avatar_presets" || v52.Checksum == v51Before.Checksum {
t.Fatal("migration identity was rewritten or new checksum was reused")
}
status, err := ReadSchemaStatus(db)
if err != nil || !status.Ready || status.Current != CurrentSchemaVersion {
t.Fatalf("current schema status: %#v, %v", status, err)
}
}