chore(build): 新增 make test/lint/fmt 并修复 testdb 连接串缺陷

- 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,并补测试锁定该行为。
This commit is contained in:
yml2213
2026-10-06 19:42:49 +08:00
parent d821fe13df
commit d423fc652c
3 changed files with 91 additions and 4 deletions
+11 -3
View File
@@ -61,12 +61,20 @@ func New(t testing.TB, _ ...interface{}) *gorm.DB {
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") {
query := parsed.Query()
query.Set("search_path", schema)
parsed.RawQuery = query.Encode()
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) == "" {
+51
View File
@@ -0,0 +1,51 @@
package testdb
import (
"net/url"
"strings"
"testing"
)
// TestWithSearchPathKeepsOriginalQuery 防止回归:曾经用 url.Values.Encode() 重编码整个查询串,
// 把 TimeZone=Asia/Shanghai 变成 Asia%2FShanghai,导致所有集成测试报
// "unknown time zone Asia%2FShanghai" 而连不上数据库。
func TestWithSearchPathKeepsOriginalQuery(t *testing.T) {
const dsn = "postgres://affiliate:pw@127.0.0.1:15432/affiliate_dash?sslmode=disable&TimeZone=Asia/Shanghai"
got := withSearchPath(dsn, "test_123")
if strings.Contains(got, "%2F") {
t.Fatalf("search_path 追加不应改写已有查询参数,实际得到 %q", got)
}
if !strings.Contains(got, "TimeZone=Asia/Shanghai") {
t.Fatalf("原查询参数必须原样保留,实际得到 %q", got)
}
parsed, err := url.Parse(got)
if err != nil {
t.Fatalf("结果不是合法 URL: %v", err)
}
if gotSchema := parsed.Query().Get("search_path"); gotSchema != "test_123" {
t.Fatalf("search_path 期望 test_123,实际 %q", gotSchema)
}
if gotSSL := parsed.Query().Get("sslmode"); gotSSL != "disable" {
t.Fatalf("sslmode 期望 disable,实际 %q", gotSSL)
}
}
func TestWithSearchPathWithoutQueryString(t *testing.T) {
got := withSearchPath("postgres://affiliate:pw@127.0.0.1:15432/affiliate_dash", "test_9")
if !strings.Contains(got, "?search_path=test_9") {
t.Fatalf("无查询串时应以 ? 开头追加,实际得到 %q", got)
}
}
// TestWithSearchPathKeywordValueForm keyword/value 形式(非 URL)走空格拼接分支。
func TestWithSearchPathKeywordValueForm(t *testing.T) {
got := withSearchPath("host=127.0.0.1 user=affiliate", "test_7")
if got != "host=127.0.0.1 user=affiliate search_path=test_7" {
t.Fatalf("keyword/value 形式拼接结果不符,实际得到 %q", got)
}
}