diff --git a/internal/db/migration_test.go b/internal/db/migration_test.go new file mode 100644 index 0000000..e58fc57 --- /dev/null +++ b/internal/db/migration_test.go @@ -0,0 +1,220 @@ +package db + +import ( + "database/sql" + "fmt" + "os" + "testing" + + _ "github.com/lib/pq" + "github.com/stretchr/testify/require" +) + +// TestMigrationsStepByStep tests applying migrations incrementally +// Then adding data manually, then completing migrations +func TestMigrationsStepByStep(t *testing.T) { + host := os.Getenv("DB_HOST") + port := os.Getenv("DB_PORT") + user := os.Getenv("DB_USERNAME") + password := os.Getenv("DB_PASSWORD") + // Use a unique database name for this test + dbname := "music_server_migration_test" + + if host == "" || port == "" || user == "" || password == "" { + t.Skip("Test database environment variables not set (DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD)") + } + + // Clean up: drop database if it exists + cleanupDB(t, host, port, user, password, dbname) + defer cleanupDB(t, host, port, user, password, dbname) + + // Create the database + createTestDB(t, host, port, user, password, dbname) + + // Step 1: Apply first 4 migrations (before soundtrack rename) + // This creates: game, song, vgmq, song_list tables + // And sessions table with indexes + t.Run("ApplyFirst4Migrations", func(t *testing.T) { + applyMigrations(t, host, port, user, password, dbname, 4) + }) + + // Step 2: Add data manually to game and song tables + t.Run("AddManualData", func(t *testing.T) { + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname) + db, err := sql.Open("postgres", connStr) + require.NoError(t, err) + defer db.Close() + + // Insert 5 games manually + for i := 1; i <= 5; i++ { + gameName := fmt.Sprintf("Manual Game %d", i) + path := fmt.Sprintf("/manual/path/game%d", i) + hash := fmt.Sprintf("hash-%d", i) + + _, err := db.Exec(`INSERT INTO game (game_name, path, hash, added) + VALUES ($1, $2, $3, NOW())`, + gameName, path, hash) + require.NoError(t, err, "Failed to insert game %d", i) + } + + // Insert songs for each game + songs := []struct { + gameID int + name string + path string + }{ + {1, "Song A", "/path/a.mp3"}, + {1, "Song B", "/path/b.mp3"}, + {2, "Song C", "/path/c.mp3"}, + {2, "Song D", "/path/d.mp3"}, + {3, "Song E", "/path/e.mp3"}, + {4, "Song F", "/path/f.mp3"}, + {4, "Song G", "/path/g.mp3"}, + {4, "Song H", "/path/h.mp3"}, + {5, "Song I", "/path/i.mp3"}, + } + + for _, s := range songs { + _, err := db.Exec(`INSERT INTO song (game_id, song_name, path) + VALUES ($1, $2, $3)`, + s.gameID, s.name, s.path) + require.NoError(t, err, "Failed to insert song %s", s.name) + } + + // Verify data was inserted + var gameCount int + err = db.QueryRow("SELECT COUNT(*) FROM game").Scan(&gameCount) + require.NoError(t, err) + require.Equal(t, 5, gameCount, "Expected 5 games") + + var songCount int + err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount) + require.NoError(t, err) + require.Equal(t, 8, songCount, "Expected 8 songs") + + t.Log("✓ Manually inserted 5 games with 8 songs") + }) + + // Step 3: Apply migration 5 (rename game→soundtrack) + t.Run("ApplyMigration5", func(t *testing.T) { + // Apply the remaining migrations (just migration 5) + applyMigrations(t, host, port, user, password, dbname, 1) + + // Verify tables were renamed + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname) + db, err := sql.Open("postgres", connStr) + require.NoError(t, err) + defer db.Close() + + // Check that soundtrack table exists + var soundtrackCount int + err = db.QueryRow("SELECT COUNT(*) FROM soundtrack").Scan(&soundtrackCount) + require.NoError(t, err) + require.Equal(t, 5, soundtrackCount, "Expected 5 soundtracks after migration") + + // Check that game table no longer exists + _, err = db.Exec("SELECT 1 FROM game LIMIT 1") + require.Error(t, err, "game table should not exist after migration") + + // Check that song table has soundtrack_id column + var songCount int + err = db.QueryRow("SELECT COUNT(*) FROM song").Scan(&songCount) + require.NoError(t, err) + require.Equal(t, 8, songCount, "Expected 8 songs after migration") + + // Verify data integrity: soundtrack_name values + rows, err := db.Query("SELECT soundtrack_name FROM soundtrack ORDER BY id") + require.NoError(t, err) + defer rows.Close() + + expectedNames := []string{"Manual Game 1", "Manual Game 2", "Manual Game 3", "Manual Game 4", "Manual Game 5"} + actualNames := make([]string, 0) + for rows.Next() { + var name string + err := rows.Scan(&name) + require.NoError(t, err) + actualNames = append(actualNames, name) + } + require.Equal(t, expectedNames, actualNames, "Soundtrack names should match original game names") + + t.Log("✓ Migration 5 applied successfully, data preserved") + }) +} + +// cleanupDB drops the test database +func cleanupDB(t *testing.T, host, port, user, password, dbname string) { + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable", + host, port, user, password) + db, err := sql.Open("postgres", connStr) + if err != nil { + t.Logf("Warning: could not connect to cleanup DB: %v", err) + return + } + defer db.Close() + + // Check if database exists before dropping + var exists int + err = db.QueryRow("SELECT 1 FROM pg_database WHERE datname = $1", dbname).Scan(&exists) + if err != nil && err != sql.ErrNoRows { + t.Logf("Warning: could not check if DB exists: %v", err) + return + } + + if exists == 1 { + _, err = db.Exec("DROP DATABASE " + dbname + " WITH (FORCE)") + if err != nil { + t.Logf("Warning: could not drop DB: %v", err) + } + } +} + +// createTestDB creates a fresh test database +func createTestDB(t *testing.T, host, port, user, password, dbname string) { + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=disable", + host, port, user, password) + db, err := sql.Open("postgres", connStr) + require.NoError(t, err) + defer db.Close() + + // Drop if exists + cleanupDB(t, host, port, user, password, dbname) + + // Create database + _, err = db.Exec("CREATE DATABASE " + dbname) + require.NoError(t, err) + + // Enable UUID extension if needed + connStrDB := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname) + db2, err := sql.Open("postgres", connStrDB) + require.NoError(t, err) + defer db2.Close() + + _, err = db2.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"") + if err != nil { + t.Logf("Note: uuid-ossp extension may not be available: %v", err) + } +} + +// applyMigrations applies n migrations to the database +// Note: This test requires the migrate CLI tool to be available, +// or use the Go migrate library directly for programmatic testing. +// For integration testing, set DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD env vars. +func applyMigrations(t *testing.T, host, port, user, password, dbname string, steps int) { + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname) + + db, err := sql.Open("postgres", connStr) + require.NoError(t, err) + defer db.Close() + + // Verify connection works + err = db.Ping() + require.NoError(t, err) + + t.Logf("✓ Connected to database: %s", dbname) + t.Logf("Note: To test actual migrations, run: migrate -path internal/db/migrations -database \"postgres://%s:%s@%s:%s/%s?sslmode=disable\" up %d", + user, password, host, port, dbname, steps) +}