package integration import ( "context" "fmt" "os" "path/filepath" "testing" "time" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" testcontainers_postgres "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" postgresDriver "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) // TestMigration_UpAndDown 测试迁移脚本的向上和向下迁移 func TestMigration_UpAndDown(t *testing.T) { ctx := context.Background() // 启动 PostgreSQL 容器 postgresContainer, err := testcontainers_postgres.RunContainer(ctx, testcontainers.WithImage("postgres:14-alpine"), testcontainers_postgres.WithDatabase("testdb"), testcontainers_postgres.WithUsername("postgres"), testcontainers_postgres.WithPassword("password"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). WithStartupTimeout(30*time.Second), ), ) require.NoError(t, err, "启动 PostgreSQL 容器失败") defer func() { if err := postgresContainer.Terminate(ctx); err != nil { t.Logf("终止容器失败: %v", err) } }() // 获取连接字符串 connStr, err := postgresContainer.ConnectionString(ctx, "sslmode=disable") require.NoError(t, err, "获取数据库连接字符串失败") // 应用数据库迁移 migrationsPath := getMigrationsPath(t) m, err := migrate.New( fmt.Sprintf("file://%s", migrationsPath), connStr, ) require.NoError(t, err, "创建迁移实例失败") defer func() { _, _ = m.Close() }() t.Run("向上迁移", func(t *testing.T) { err := m.Up() require.NoError(t, err, "执行向上迁移失败") // 验证表已创建 db, err := gorm.Open(postgresDriver.Open(connStr), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err, "连接数据库失败") // 检查 RBAC 表存在 tables := []string{ "tb_account", "tb_role", "tb_permission", "tb_account_role", "tb_role_permission", } for _, table := range tables { var exists bool err := db.Raw("SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = ?)", table).Scan(&exists).Error assert.NoError(t, err) assert.True(t, exists, "表 %s 应该存在", table) } // 检查索引 var indexCount int64 err = db.Raw(` SELECT COUNT(*) FROM pg_indexes WHERE tablename = 'tb_account' AND indexname LIKE 'idx_account_%' `).Scan(&indexCount).Error assert.NoError(t, err) assert.Greater(t, indexCount, int64(0), "tb_account 表应该有索引") sqlDB, _ := db.DB() if sqlDB != nil { _ = sqlDB.Close() } }) t.Run("向下迁移", func(t *testing.T) { err := m.Down() require.NoError(t, err, "执行向下迁移失败") // 验证表已删除 db, err := gorm.Open(postgresDriver.Open(connStr), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err, "连接数据库失败") // 检查 RBAC 表已删除 tables := []string{ "tb_account", "tb_role", "tb_permission", "tb_account_role", "tb_role_permission", } for _, table := range tables { var exists bool err := db.Raw("SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = ?)", table).Scan(&exists).Error assert.NoError(t, err) assert.False(t, exists, "表 %s 应该已删除", table) } sqlDB, _ := db.DB() if sqlDB != nil { _ = sqlDB.Close() } }) } // TestMigration_NoForeignKeys 验证迁移脚本不包含外键约束 func TestMigration_NoForeignKeys(t *testing.T) { // 获取迁移目录 migrationsPath := getMigrationsPath(t) // 读取所有迁移文件 files, err := filepath.Glob(filepath.Join(migrationsPath, "*.up.sql")) require.NoError(t, err) forbiddenKeywords := []string{ "FOREIGN KEY", "REFERENCES", "ON DELETE CASCADE", "ON UPDATE CASCADE", } for _, file := range files { content, err := os.ReadFile(file) require.NoError(t, err) for _, keyword := range forbiddenKeywords { assert.NotContains(t, string(content), keyword, "迁移文件 %s 不应包含外键约束关键字: %s", filepath.Base(file), keyword) } } } // TestMigration_SoftDeleteSupport 验证表支持软删除 func TestMigration_SoftDeleteSupport(t *testing.T) { ctx := context.Background() // 启动 PostgreSQL 容器 postgresContainer, err := testcontainers_postgres.RunContainer(ctx, testcontainers.WithImage("postgres:14-alpine"), testcontainers_postgres.WithDatabase("testdb"), testcontainers_postgres.WithUsername("postgres"), testcontainers_postgres.WithPassword("password"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). WithStartupTimeout(30*time.Second), ), ) require.NoError(t, err, "启动 PostgreSQL 容器失败") defer func() { if err := postgresContainer.Terminate(ctx); err != nil { t.Logf("终止容器失败: %v", err) } }() // 获取连接字符串 connStr, err := postgresContainer.ConnectionString(ctx, "sslmode=disable") require.NoError(t, err, "获取数据库连接字符串失败") // 应用迁移 migrationsPath := getMigrationsPath(t) m, err := migrate.New( fmt.Sprintf("file://%s", migrationsPath), connStr, ) require.NoError(t, err, "创建迁移实例失败") defer func() { _, _ = m.Close() }() err = m.Up() require.NoError(t, err, "执行向上迁移失败") // 连接数据库验证 db, err := gorm.Open(postgresDriver.Open(connStr), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err, "连接数据库失败") defer func() { sqlDB, _ := db.DB() if sqlDB != nil { _ = sqlDB.Close() } }() // 检查每个表都有 deleted_at 列和索引 tables := []string{ "tb_account", "tb_role", "tb_permission", "tb_account_role", "tb_role_permission", } for _, table := range tables { // 检查 deleted_at 列存在 var columnExists bool err := db.Raw(` SELECT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = ? AND column_name = 'deleted_at' ) `, table).Scan(&columnExists).Error assert.NoError(t, err) assert.True(t, columnExists, "表 %s 应该有 deleted_at 列", table) } }