From d423fc652c69e60aaa17b93e7f9de237db82cef8 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 6 Oct 2026 19:42:49 +0800 Subject: [PATCH] =?UTF-8?q?chore(build):=20=E6=96=B0=E5=A2=9E=20make=20tes?= =?UTF-8?q?t/lint/fmt=20=E5=B9=B6=E4=BF=AE=E5=A4=8D=20testdb=20=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E4=B8=B2=E7=BC=BA=E9=99=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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,并补测试锁定该行为。 --- Makefile | 30 ++++++++++++++- backend/internal/testdb/testdb.go | 14 +++++-- backend/internal/testdb/testdb_test.go | 51 ++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 backend/internal/testdb/testdb_test.go diff --git a/Makefile b/Makefile index 20f6a1e..4280f42 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: start dev backend frontend install stop docker-build docker-up docker-down docker-logs docker-ps docker-config +.PHONY: start dev backend frontend install stop fmt lint test test-db docker-build docker-up docker-down docker-logs docker-ps docker-config ifneq (,$(wildcard .env)) include .env @@ -19,6 +19,34 @@ install: cd backend && go mod tidy cd frontend && npm install +# 代码格式化(仅后端;前端由 oxlint 负责) +fmt: + @before=$$(gofmt -l backend | wc -l | tr -d ' '); \ + gofmt -w backend; \ + echo "已格式化;之前存在格式偏差的文件数: $$before" + +lint: + cd backend && go vet ./... + cd frontend && npm run lint + +# 全量测试:集成测试依赖真实 PostgreSQL,先确保容器起来再跑。 +# 测试使用临时 schema 隔离,不会污染开发库(见 backend/internal/testdb)。 +test: test-db + cd backend && go test ./... + +test-db: + @docker compose up -d postgres + @echo "等待 PostgreSQL 就绪..." + @for i in $$(seq 1 60); do \ + if docker compose exec -T postgres pg_isready -U "$${POSTGRES_USER:-affiliate}" -d "$${POSTGRES_DB:-affiliate_dash}" >/dev/null 2>&1; then \ + echo "PostgreSQL 已就绪"; \ + exit 0; \ + fi; \ + sleep 1; \ + done; \ + echo "PostgreSQL 启动超时,请检查: docker compose logs postgres" >&2; \ + exit 1 + # 停止占用默认端口的进程 stop: @-lsof -tiTCP:18080 -sTCP:LISTEN | xargs kill 2>/dev/null || true diff --git a/backend/internal/testdb/testdb.go b/backend/internal/testdb/testdb.go index 872d56e..edb39b0 100644 --- a/backend/internal/testdb/testdb.go +++ b/backend/internal/testdb/testdb.go @@ -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) == "" { diff --git a/backend/internal/testdb/testdb_test.go b/backend/internal/testdb/testdb_test.go new file mode 100644 index 0000000..4797c6f --- /dev/null +++ b/backend/internal/testdb/testdb_test.go @@ -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) + } +}