- make test 自动拉起并等待 PostgreSQL 就绪;测试用临时 schema 隔离,不污染开发库 - 新增 make test-db / lint / fmt - 修复 testdb.withSearchPath:原实现用 url.Values.Encode() 重编码整个查询串, 把 TimeZone=Asia/Shanghai 变成 Asia%2FShanghai,pgx 原样传给服务端后报 "unknown time zone Asia%2FShanghai",导致数据库起来后集成测试依然全挂。 改为只追加 search_path,并补测试锁定该行为。
85 lines
2.4 KiB
Go
85 lines
2.4 KiB
Go
package testdb
|
||
|
||
import (
|
||
"fmt"
|
||
"net/url"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"affiliate_dash/internal/database"
|
||
|
||
"gorm.io/driver/postgres"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/logger"
|
||
)
|
||
|
||
// New 使用真实 PostgreSQL 创建测试隔离 schema,避免 SQLite 与生产 SQL 行为不一致。
|
||
func New(t testing.TB, _ ...interface{}) *gorm.DB {
|
||
t.Helper()
|
||
dsn := os.Getenv("TEST_DATABASE_URL")
|
||
if dsn == "" {
|
||
dsn = os.Getenv("DATABASE_URL")
|
||
}
|
||
if dsn == "" {
|
||
dsn = "postgres://affiliate:affiliate_dev_password@127.0.0.1:15432/affiliate_dash?sslmode=disable"
|
||
}
|
||
|
||
cfg := &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}
|
||
adminDB, err := gorm.Open(postgres.Open(dsn), cfg)
|
||
if err != nil {
|
||
t.Fatalf("连接 PostgreSQL 测试库失败: %v", err)
|
||
}
|
||
adminSQL, err := adminDB.DB()
|
||
if err != nil {
|
||
t.Fatalf("获取 PostgreSQL 连接失败: %v", err)
|
||
}
|
||
|
||
schema := fmt.Sprintf("test_%d", time.Now().UnixNano())
|
||
if err := adminDB.Exec(`CREATE SCHEMA "` + schema + `"`).Error; err != nil {
|
||
t.Fatalf("创建测试 schema 失败: %v", err)
|
||
}
|
||
t.Cleanup(func() {
|
||
_ = adminDB.Exec(`DROP SCHEMA IF EXISTS "` + schema + `" CASCADE`).Error
|
||
_ = adminSQL.Close()
|
||
})
|
||
|
||
db, err := gorm.Open(postgres.Open(withSearchPath(dsn, schema)), cfg)
|
||
if err != nil {
|
||
t.Fatalf("连接测试 schema 失败: %v", err)
|
||
}
|
||
sqlDB, err := db.DB()
|
||
if err != nil {
|
||
t.Fatalf("获取测试 schema 连接失败: %v", err)
|
||
}
|
||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||
|
||
if err := database.Migrate(db); err != nil {
|
||
t.Fatalf("迁移测试 schema 失败: %v", err)
|
||
}
|
||
return db
|
||
}
|
||
|
||
// withSearchPath 给 DSN 追加 search_path,让每个测试用例落在自己的临时 schema 里。
|
||
//
|
||
// 注意:这里刻意只「追加」而不用 url.Values.Encode() 重新编码整个查询串。
|
||
// 重新编码会把 TimeZone=Asia/Shanghai 变成 Asia%2FShanghai,pgx 会把这个值原样
|
||
// 传给服务端并报错 "unknown time zone Asia%2FShanghai",导致所有集成测试连不上库。
|
||
func withSearchPath(dsn, schema string) string {
|
||
parsed, err := url.Parse(dsn)
|
||
if err == nil && strings.HasPrefix(parsed.Scheme, "postgres") {
|
||
separator := "&"
|
||
if parsed.RawQuery == "" {
|
||
separator = "?"
|
||
}
|
||
// url.URL.String() 原样输出 RawQuery,不会再做一次百分号编码。
|
||
parsed.RawQuery = parsed.RawQuery + separator + "search_path=" + url.QueryEscape(schema)
|
||
return parsed.String()
|
||
}
|
||
if strings.TrimSpace(dsn) == "" {
|
||
return dsn
|
||
}
|
||
return dsn + " search_path=" + schema
|
||
}
|