package testutil import ( "bytes" "database/sql" "fmt" "os" "path/filepath" "strings" "testing" "time" "github.com/go-sql-driver/mysql" "github.com/wmms/wmms/pkg/database" ) // NewIsolationDB 在显式配置的 MySQL 服务器(TEST_DB_DSN 指向 company_isolation_test*)上 // 创建一个一次性的库,载入 scripts/init.sql 的空库结构,并把 database.DB 指向它;测试结束 // 自动删库并恢复。与 repository 侧的同名测试集共用同一台服务器与同一套空库结构, // 让 Service 层的事务场景能直接跑真实 MySQL。未配置 TEST_DB_DSN 时跳过。 func NewIsolationDB(t *testing.T) *sql.DB { t.Helper() dsn := os.Getenv("TEST_DB_DSN") if dsn == "" { t.Skip("real MySQL integration requires TEST_DB_DSN") } cfg, err := mysql.ParseDSN(dsn) if err != nil { t.Fatal(err) } if !strings.HasPrefix(cfg.DBName, "company_isolation_test") { t.Fatal("TEST_DB_DSN must name a company_isolation_test schema") } cfg.DBName = "" cfg.MultiStatements = true cfg.ParseTime = true admin, err := sql.Open("mysql", cfg.FormatDSN()) if err != nil { t.Fatal(err) } name := fmt.Sprintf("company_isolation_test_%d", time.Now().UnixNano()) if _, err = admin.Exec("CREATE DATABASE `" + name + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); err != nil { admin.Close() t.Fatal(err) } cfg.DBName = name db, err := sql.Open("mysql", cfg.FormatDSN()) if err != nil { t.Fatal(err) } original := database.DB database.DB = db t.Cleanup(func() { database.DB = original; db.Close(); admin.Exec("DROP DATABASE `" + name + "`"); admin.Close() }) content, err := os.ReadFile(filepath.Join("..", "..", "scripts", "init.sql")) if err != nil { t.Fatal(err) } // init.sql 可能带 UTF-8 BOM,按字节剥掉,避免第一行 CREATE TABLE 解析失败。 content = bytes.TrimPrefix(content, []byte{0xEF, 0xBB, 0xBF}) var lines []string for _, line := range strings.Split(string(content), "\n") { if strings.HasPrefix(line, "CREATE DATABASE") || strings.HasPrefix(line, "USE ") { continue } lines = append(lines, line) } if _, err = db.Exec(strings.Join(lines, "\n")); err != nil { t.Fatalf("initialize scripts/init.sql: %v", err) } return db }