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:
@@ -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))
|
ifneq (,$(wildcard .env))
|
||||||
include .env
|
include .env
|
||||||
@@ -19,6 +19,34 @@ install:
|
|||||||
cd backend && go mod tidy
|
cd backend && go mod tidy
|
||||||
cd frontend && npm install
|
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:
|
stop:
|
||||||
@-lsof -tiTCP:18080 -sTCP:LISTEN | xargs kill 2>/dev/null || true
|
@-lsof -tiTCP:18080 -sTCP:LISTEN | xargs kill 2>/dev/null || true
|
||||||
|
|||||||
@@ -61,12 +61,20 @@ func New(t testing.TB, _ ...interface{}) *gorm.DB {
|
|||||||
return 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 {
|
func withSearchPath(dsn, schema string) string {
|
||||||
parsed, err := url.Parse(dsn)
|
parsed, err := url.Parse(dsn)
|
||||||
if err == nil && strings.HasPrefix(parsed.Scheme, "postgres") {
|
if err == nil && strings.HasPrefix(parsed.Scheme, "postgres") {
|
||||||
query := parsed.Query()
|
separator := "&"
|
||||||
query.Set("search_path", schema)
|
if parsed.RawQuery == "" {
|
||||||
parsed.RawQuery = query.Encode()
|
separator = "?"
|
||||||
|
}
|
||||||
|
// url.URL.String() 原样输出 RawQuery,不会再做一次百分号编码。
|
||||||
|
parsed.RawQuery = parsed.RawQuery + separator + "search_path=" + url.QueryEscape(schema)
|
||||||
return parsed.String()
|
return parsed.String()
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(dsn) == "" {
|
if strings.TrimSpace(dsn) == "" {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user