package db import ( "testing" "github.com/pkg/errors" "github.com/stretchr/testify/require" "gorm.io/gorm" ) // TestRunMigrationsIdempotent 迁移账本幂等:同一组迁移跑两遍,第二遍不重复执行。 func TestRunMigrationsIdempotent(t *testing.T) { conn, clean := newTestDB(t) defer clean("schema_migrations", "users", "feature_controls") // 第一遍:全部迁移都未应用,应执行并落账本。 require.NoError(t, runMigrations(conn)) applied, err := AppliedMigrations(conn) require.NoError(t, err) require.Len(t, applied, len(migrations), "第一遍后所有版本都应已记录") // 第二遍:账本齐全,runMigrations 应立即返回且不报错。 // 若已应用版本被重复执行,迁移事务会因主键冲突报错 —— 无错跑完即证明跳过了。 require.NoError(t, runMigrations(conn)) applied2, err := AppliedMigrations(conn) require.NoError(t, err) require.Equal(t, applied, applied2, "重复执行不应改变账本") } // TestRunMigrationsStopsOnFailure 迁移失败必须阻断:失败版本不落账本、后续版本不执行。 func TestRunMigrationsStopsOnFailure(t *testing.T) { conn, clean := newTestDB(t) defer clean("schema_migrations", "users", "feature_controls") // 先建账本表(runMigrations 内部会建,但这里手动跑一次无迁移场景需要显式建表)。 require.NoError(t, conn.AutoMigrate(&SchemaMigration{})) // 用一套独立的迁移清单模拟「第二个迁移失败」: // v100 成功,v101 失败。预期:v100 落账本,v101 不落,且整体返回错误。 saved := migrations defer func() { migrations = saved }() migrations = []Migration{ {Version: 100, Name: "ok", Up: func(*gorm.DB) error { return nil }}, {Version: 101, Name: "boom", Up: func(*gorm.DB) error { return errors.New("intentional") }}, {Version: 102, Name: "never-runs", Up: func(*gorm.DB) error { t.Error("v102 不应执行,因为 v101 已失败") return nil }}, } err := runMigrations(conn) require.Error(t, err, "迁移失败必须返回错误") applied, aErr := AppliedMigrations(conn) require.NoError(t, aErr) require.True(t, applied[100], "成功的 v100 应已落账本") require.False(t, applied[101], "失败的 v101 不应落账本") require.False(t, applied[102], "被阻断的 v102 不应落账本") } // TestMigrationsOrdered 迁移清单必须按版本号严格升序且无重复。 func TestMigrationsOrdered(t *testing.T) { for i := 1; i < len(migrations); i++ { require.Less(t, migrations[i-1].Version, migrations[i].Version, "迁移清单必须按版本号严格升序,第 %d 个版本 %d 不小于第 %d 个版本 %d", i, migrations[i].Version, i-1, migrations[i-1].Version) } }