package migrations_test import ( "database/sql" "path/filepath" "strings" "testing" "testing/fstest" _ "modernc.org/sqlite" "github.com/dariuszjaniak/warp/server/internal/storage/migrations" ) var dialect = migrations.Dialect{ Name: "sqlite", CreateTable: `CREATE TABLE IF NOT EXISTS schema_migrations ( version INTEGER PRIMARY KEY, name TEXT NOT NULL, checksum TEXT NOT NULL, applied_at TIMESTAMP NOT NULL)`, SelectApplied: `SELECT version, name, checksum, applied_at FROM schema_migrations ORDER BY version`, InsertApplied: `INSERT INTO schema_migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)`, } func openDB(t *testing.T) *sql.DB { t.Helper() path := filepath.ToSlash(filepath.Join(t.TempDir(), "test.db")) db, err := sql.Open("sqlite", "file:"+path) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = db.Close() }) return db } func fsWith(files map[string]string) fstest.MapFS { out := fstest.MapFS{} for name, body := range files { out["migrations/"+name] = &fstest.MapFile{Data: []byte(body)} } return out } func TestLoadOrdersByVersionAndParsesNames(t *testing.T) { ms, err := migrations.Load(fsWith(map[string]string{ "0002_devices.sql": "CREATE TABLE devices (id TEXT);", "0001_server_settings.sql": "CREATE TABLE server_settings (k TEXT);", "notes.txt": "ignored", }), "migrations") if err != nil { t.Fatalf("Load: %v", err) } if len(ms) != 2 { t.Fatalf("loaded %d migrations, want 2", len(ms)) } if ms[0].Version != 1 || ms[0].Name != "server_settings" { t.Errorf("first migration = %d/%s", ms[0].Version, ms[0].Name) } if ms[1].Version != 2 || ms[1].Name != "devices" { t.Errorf("second migration = %d/%s", ms[1].Version, ms[1].Name) } if ms[0].Checksum == "" || ms[0].Checksum == ms[1].Checksum { t.Error("each migration needs its own checksum") } } func TestLoadRejectsBadFilenames(t *testing.T) { for _, name := range []string{"nope.sql", "abc_x.sql", "0000_zero.sql"} { if _, err := migrations.Load(fsWith(map[string]string{name: "SELECT 1;"}), "migrations"); err == nil { t.Errorf("Load(%s) = nil error, want a complaint", name) } } } func TestRunIsIdempotent(t *testing.T) { db := openDB(t) fsys := fsWith(map[string]string{ "0001_server_settings.sql": "CREATE TABLE server_settings (k TEXT PRIMARY KEY);", }) ms, err := migrations.Load(fsys, "migrations") if err != nil { t.Fatal(err) } n, err := migrations.Run(t.Context(), db, dialect, ms, nil) if err != nil { t.Fatalf("Run: %v", err) } if n != 1 { t.Errorf("applied %d migrations, want 1", n) } n, err = migrations.Run(t.Context(), db, dialect, ms, nil) if err != nil { t.Fatalf("second Run: %v", err) } if n != 0 { t.Errorf("second Run applied %d migrations, want 0", n) } } func TestRunDetectsEditedMigration(t *testing.T) { db := openDB(t) before, err := migrations.Load(fsWith(map[string]string{ "0001_server_settings.sql": "CREATE TABLE server_settings (k TEXT PRIMARY KEY);", }), "migrations") if err != nil { t.Fatal(err) } if _, err := migrations.Run(t.Context(), db, dialect, before, nil); err != nil { t.Fatal(err) } after, err := migrations.Load(fsWith(map[string]string{ "0001_server_settings.sql": "CREATE TABLE server_settings (k TEXT PRIMARY KEY, v TEXT);", }), "migrations") if err != nil { t.Fatal(err) } _, err = migrations.Run(t.Context(), db, dialect, after, nil) if err == nil { t.Fatal("expected an error for an edited migration") } if !strings.Contains(err.Error(), "has changed since it was applied") { t.Errorf("unhelpful error: %v", err) } } func TestRunRollsBackFailedMigration(t *testing.T) { db := openDB(t) ms, err := migrations.Load(fsWith(map[string]string{ "0001_ok.sql": "CREATE TABLE a (k TEXT);", "0002_broken.sql": "CREATE TABLE b (k TEXT); THIS IS NOT SQL;", }), "migrations") if err != nil { t.Fatal(err) } n, err := migrations.Run(t.Context(), db, dialect, ms, nil) if err == nil { t.Fatal("expected the broken migration to fail") } if n != 1 { t.Errorf("applied %d migrations before failing, want 1", n) } // The failed migration must leave nothing behind. var count int if err := db.QueryRowContext(t.Context(), `SELECT count(*) FROM sqlite_master WHERE type = 'table' AND name = 'b'`).Scan(&count); err != nil { t.Fatal(err) } if count != 0 { t.Error("table from the failed migration was not rolled back") } pending, err := migrations.Pending(t.Context(), db, dialect, ms) if err != nil { t.Fatal(err) } if len(pending) != 1 || pending[0].Version != 2 { t.Errorf("pending = %v, want just migration 2", pending) } } func TestCheckNotAhead(t *testing.T) { applied := []migrations.Applied{{Version: 7, Name: "future"}} ms := []migrations.Migration{{Version: 1, Name: "server_settings"}} if err := migrations.CheckNotAhead(applied, ms); err == nil { t.Fatal("expected a dirty-schema error") } if err := migrations.CheckNotAhead(nil, ms); err != nil { t.Errorf("fresh database must be fine, got %v", err) } }