package main import ( "fmt" "os" "path/filepath" "testing" "github.com/gochat/gochat/internal/database" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // setupTestMigrations creates a temporary directory with test migration files // that are compatible with SQLite (no PostgreSQL-specific features). func setupTestMigrations(t *testing.T) string { t.Helper() dir := t.TempDir() // Migration 1: Create a simple users table (SQLite-compatible) up1 := ` CREATE TABLE IF NOT EXISTS test_users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); ` down1 := `DROP TABLE IF EXISTS test_users;` require.NoError(t, os.WriteFile(filepath.Join(dir, "000001_create_test_users.up.sql"), []byte(up1), 0644)) require.NoError(t, os.WriteFile(filepath.Join(dir, "000001_create_test_users.down.sql"), []byte(down1), 0644)) // Migration 2: Add a phone column up2 := ` ALTER TABLE test_users ADD COLUMN phone TEXT; ` down2 := ` -- SQLite doesn't support DROP COLUMN before 3.35.0, so we recreate the table CREATE TABLE test_users_backup AS SELECT id, name, email, created_at, updated_at FROM test_users; DROP TABLE test_users; CREATE TABLE test_users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO test_users SELECT id, name, email, created_at, updated_at FROM test_users_backup; DROP TABLE test_users_backup; ` require.NoError(t, os.WriteFile(filepath.Join(dir, "000002_add_test_user_phone.up.sql"), []byte(up2), 0644)) require.NoError(t, os.WriteFile(filepath.Join(dir, "000002_add_test_user_phone.down.sql"), []byte(down2), 0644)) // Migration 3: Create a posts table up3 := ` CREATE TABLE IF NOT EXISTS test_posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES test_users(id), title TEXT NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); ` down3 := `DROP TABLE IF EXISTS test_posts;` require.NoError(t, os.WriteFile(filepath.Join(dir, "000003_create_test_posts.up.sql"), []byte(up3), 0644)) require.NoError(t, os.WriteFile(filepath.Join(dir, "000003_create_test_posts.down.sql"), []byte(down3), 0644)) return dir } // sqliteDBURL creates a SQLite database URL for golang-migrate. func sqliteDBURL(t *testing.T) string { t.Helper() dbPath := filepath.Join(t.TempDir(), "test.db") return fmt.Sprintf("sqlite3://%s", dbPath) } func TestRunMigrationsUp(t *testing.T) { migrationsDir := setupTestMigrations(t) dbURL := sqliteDBURL(t) err := database.RunMigrations(dbURL, migrationsDir) assert.NoError(t, err, "RunMigrations should apply all pending migrations") // Verify current version is 3 version, dirty, err := database.CurrentVersion(dbURL, migrationsDir) assert.NoError(t, err) assert.Equal(t, uint(3), version) assert.False(t, dirty) } func TestRunMigrationsNoChange(t *testing.T) { migrationsDir := setupTestMigrations(t) dbURL := sqliteDBURL(t) // First run: apply all migrations err := database.RunMigrations(dbURL, migrationsDir) require.NoError(t, err) // Second run: should return nil (ErrNoChange is handled) err = database.RunMigrations(dbURL, migrationsDir) assert.NoError(t, err, "Running migrations with no pending changes should succeed") } func TestMigrateStepsUp(t *testing.T) { migrationsDir := setupTestMigrations(t) dbURL := sqliteDBURL(t) // Apply 1 step at a time err := database.MigrateSteps(dbURL, migrationsDir, 1) require.NoError(t, err) version, dirty, err := database.CurrentVersion(dbURL, migrationsDir) require.NoError(t, err) assert.Equal(t, uint(1), version) assert.False(t, dirty) // Apply 2 more steps err = database.MigrateSteps(dbURL, migrationsDir, 2) require.NoError(t, err) version, dirty, err = database.CurrentVersion(dbURL, migrationsDir) require.NoError(t, err) assert.Equal(t, uint(3), version) assert.False(t, dirty) } func TestMigrateStepsDown(t *testing.T) { migrationsDir := setupTestMigrations(t) dbURL := sqliteDBURL(t) // First, apply all migrations err := database.RunMigrations(dbURL, migrationsDir) require.NoError(t, err) // Rollback 1 step err = database.MigrateSteps(dbURL, migrationsDir, -1) require.NoError(t, err) version, _, err := database.CurrentVersion(dbURL, migrationsDir) require.NoError(t, err) assert.Equal(t, uint(2), version) // Rollback 2 more steps err = database.MigrateSteps(dbURL, migrationsDir, -2) require.NoError(t, err) version, _, err = database.CurrentVersion(dbURL, migrationsDir) require.NoError(t, err) assert.Equal(t, uint(0), version) } func TestRollbackMigrations(t *testing.T) { migrationsDir := setupTestMigrations(t) dbURL := sqliteDBURL(t) // Apply all migrations first err := database.RunMigrations(dbURL, migrationsDir) require.NoError(t, err) // Rollback all err = database.RollbackMigrations(dbURL, migrationsDir) assert.NoError(t, err, "RollbackMigrations should succeed") version, _, err := database.CurrentVersion(dbURL, migrationsDir) require.NoError(t, err) assert.Equal(t, uint(0), version, "All migrations should be rolled back") } func TestRollbackNoChange(t *testing.T) { migrationsDir := setupTestMigrations(t) dbURL := sqliteDBURL(t) // Try to rollback when no migrations have been applied err := database.RollbackMigrations(dbURL, migrationsDir) assert.NoError(t, err, "Rollback with no applied migrations should succeed (ErrNoChange)") } func TestForceVersion(t *testing.T) { migrationsDir := setupTestMigrations(t) dbURL := sqliteDBURL(t) // Apply all migrations first err := database.RunMigrations(dbURL, migrationsDir) require.NoError(t, err) // Force version to 1 err = database.ForceVersion(dbURL, migrationsDir, 1) require.NoError(t, err) version, dirty, err := database.CurrentVersion(dbURL, migrationsDir) require.NoError(t, err) assert.Equal(t, uint(1), version) // After Force, dirty state should be cleared assert.False(t, dirty) } func TestCurrentVersion(t *testing.T) { migrationsDir := setupTestMigrations(t) dbURL := sqliteDBURL(t) // Before any migrations, version should be 0 (with ErrNoChange handled internally) version, dirty, err := database.CurrentVersion(dbURL, migrationsDir) // When no migrations have been applied, Version returns an error // This is expected — golang-migrate returns ErrNoChange for version 0 if err != nil { t.Logf("Expected: version 0 with no migrations applied may return error: %v", err) } // Apply migrations err = database.RunMigrations(dbURL, migrationsDir) require.NoError(t, err) version, dirty, err = database.CurrentVersion(dbURL, migrationsDir) require.NoError(t, err) assert.Equal(t, uint(3), version) assert.False(t, dirty) }