Files

87 lines
3.1 KiB
Go
Raw Permalink Normal View History

package database
import (
"context"
"os"
"testing"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/Silo-Server/silo-server/migrations"
)
// TestMigrateDownToRestoresLegacyDisplayPrefs is the rollback rehearsal.
//
// The displayprefs move deletes rows from user_settings that the previous
// binary reads, so a binary-only rollback silently loses every Jellyfin
// client's saved view preferences. This proves the documented recovery —
// --migrate-down-to — actually restores them, and that it reaches the Go
// migrations the standalone goose CLI cannot see.
func TestMigrateDownToRestoresLegacyDisplayPrefs(t *testing.T) {
dsn := os.Getenv("SILO_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("SILO_TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer pool.Close()
if err := RunMigrations(ctx, pool, migrations.FS, "sql"); err != nil {
t.Fatalf("migrate up: %v", err)
}
var userID int
if err := pool.QueryRow(ctx, `
INSERT INTO users (username, email, password_hash, role)
VALUES ('downflag','df@example.com','x','user')
ON CONFLICT (username) DO UPDATE SET email=EXCLUDED.email RETURNING id`).Scan(&userID); err != nil {
t.Fatalf("seed user: %v", err)
}
const key = "jellycompat:displayprefs:usersettings:emby"
const blob = `{"SortBy":"SortName"}`
if _, err := pool.Exec(ctx,
`INSERT INTO user_settings (user_id,key,value) VALUES ($1,$2,$3)
ON CONFLICT (user_id,key) DO UPDATE SET value=EXCLUDED.value`, userID, key, blob); err != nil {
t.Fatalf("seed legacy row: %v", err)
}
// Apply the move by re-running it (the migration already ran before the seed).
if err := RunMigrations(ctx, pool, migrations.FS, "sql"); err != nil {
t.Fatalf("re-up: %v", err)
}
// The narrow rollback the spec recommends: revert only the
// DisplayPreferences pair, which is the destructive half. A wider target
// would also revert profile_onboarding, an older-binary migration that
// happens to sort in between and whose down drops its table.
if err := MigrateDownTo(ctx, pool, migrations.FS, "sql", 20260728132326); err != nil {
t.Fatalf("MigrateDownTo: %v", err)
}
var restored string
if err := pool.QueryRow(ctx,
`SELECT value FROM user_settings WHERE user_id=$1 AND key=$2`, userID, key).Scan(&restored); err != nil {
t.Fatalf("legacy row not restored after down: %v", err)
}
if restored != blob {
t.Errorf("restored=%q want %q", restored, blob)
}
t.Logf("down-to restored the legacy row the old binary reads: %v", restored == blob)
// And it did not take an unrelated feature's migration with it — the trap
// that makes down-to a range rather than a list.
var onboarding bool
if err := pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM information_schema.tables
WHERE table_schema='public' AND table_name='user_profile_onboarding')`).Scan(&onboarding); err != nil {
t.Fatalf("checking onboarding table: %v", err)
}
if !onboarding {
t.Error("the narrow rollback dropped user_profile_onboarding, which belongs to another release")
}
_, _ = pool.Exec(ctx, `DELETE FROM users WHERE id=$1`, userID)
}