From c9e266b89a8879ca1efcaf88ee5913099466c5ba Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 8 Jul 2026 09:04:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(server+ci):=20=E4=BF=AE=20go-integration=20?= =?UTF-8?q?=E7=9C=9F=20bug=20+=20e2e=20=E5=85=8D=E7=96=AB=E4=BB=A3?= =?UTF-8?q?=E7=90=86(CI=20=E6=94=B6=E5=B0=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DinD 修复后暴露的两个 CI job,诊断: Go integration(真 test-drift bug,早前会话改动遗留): - auth/integration_test:Register/Login 补 ip + DeviceMeta 参数(sessions/ device-meta 改动后陈旧调用,构建失败)。 - usage/usage_integration_test:手写测试 schema 补 ad_bonus_minutes 列 (migration 000020 加的);重写 TestIntAdsUnlockAccumulates 断言对齐 ad-unlock 转累加式(UnlockAd→AddAdBonusMinutes,不再 stamp ad_unlocked_at)。 - devices/devices_integration_test:套餐种子 pro=5→3(migration 000019 改的)。 - devices/context.go(生产 1 行):CtxKeyUserID 别名到 codes.CtxKeyUserID——原为 独立 devices.ctxKey 类型,与 auth 注入的 codes.ctxKey 类型不同→context 取键 失配(休眠 bug,中间件目前仅测试接线)。go build 通过。 E2E(环境问题,非脚本):删 ci.yml 里多余的 apt-get(openssl/curl/python3 已在 golang:1.25 镜像内;原 apt 走 Docker Desktop 代理→本机死口,徒增脆性)。脚本 本身本机直跑通过。 验证:go test -tags integration -count=1 -p 1 ./... 全 ok;go build ./... clean。 Co-Authored-By: Claude Opus 4.8 --- .gitea/workflows/ci.yml | 4 +++- server/internal/auth/integration_test.go | 6 +++--- server/internal/devices/context.go | 12 ++++++++---- .../internal/devices/devices_integration_test.go | 2 +- server/internal/usage/usage_integration_test.go | 16 +++++++++++----- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ac43de0..db6c1bd 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -228,7 +228,9 @@ jobs: -v "$HOME/.cache/pangolin-ci/gomod:/go/pkg/mod" \ -v "$HOME/.cache/pangolin-ci/gobuild:/root/.cache/go-build" \ golang:1.25 \ - bash -c "apt-get update -qq && apt-get install -y -qq openssl curl python3 >/dev/null 2>&1 && bash scripts/e2e-smoke.sh" + bash -c "bash scripts/e2e-smoke.sh" + # 注:openssl/curl/python3 已在 golang:1.25 镜像内,无需 apt 安装 + # (原 apt-get 会走 Docker Desktop 代理→本机 clash 死口,徒增网络脆性)。 # ── Job 10: Go 集成测试 (L2:真 mysql8/redis 经 testcontainers)────────── # 跨库可移植(支柱 3)+ 按租户流量记账(usage)+ 配额(devices)+ 兑换(codes)+ diff --git a/server/internal/auth/integration_test.go b/server/internal/auth/integration_test.go index 49b623c..e3fb5bf 100644 --- a/server/internal/auth/integration_test.go +++ b/server/internal/auth/integration_test.go @@ -140,7 +140,7 @@ func TestIntegration_FullChain(t *testing.T) { } // 2. Register → trial subscription must exist for 7 days. - pair, apiErr := svc.Register(ctx, email, code, pw) + pair, apiErr := svc.Register(ctx, email, code, pw, "203.0.113.10", DeviceMeta{}) if apiErr != nil { t.Fatalf("Register: %v", apiErr) } @@ -170,12 +170,12 @@ func TestIntegration_FullChain(t *testing.T) { } // Force a fresh code regardless of rate limit. _ = rdb.Set(ctx, codeKey(email), code, 10*time.Minute).Err() - if _, e := svc.Register(ctx, email, code, pw); e == nil || e.Code != ErrCodeInvalid.Code { + if _, e := svc.Register(ctx, email, code, pw, "203.0.113.10", DeviceMeta{}); e == nil || e.Code != ErrCodeInvalid.Code { t.Fatalf("want code_invalid (anti-enumeration), got %v", e) } // 4. Login. - loginPair, _, apiErr := svc.Login(ctx, email, pw, "198.51.100.7") + loginPair, _, apiErr := svc.Login(ctx, email, pw, "198.51.100.7", DeviceMeta{}) if apiErr != nil { t.Fatalf("Login: %v", apiErr) } diff --git a/server/internal/devices/context.go b/server/internal/devices/context.go index 7754bec..8d4de6a 100644 --- a/server/internal/devices/context.go +++ b/server/internal/devices/context.go @@ -3,17 +3,21 @@ package devices import ( "context" "time" + + "github.com/wangjia/pangolin/server/internal/codes" ) // ctxKey is a private type for context keys to avoid collisions. type ctxKey string // CtxKeyUserID is the context key under which the authenticated user's -// internal int64 ID is stored by the JWT auth middleware (module #2). +// internal int64 ID is stored by the JWT auth middleware (auth.RequireAuth). // -// It mirrors the key used by the codes module so that, once the auth -// middleware lands, a single canonical key can be reconciled across modules. -const CtxKeyUserID ctxKey = "user_id" +// It is the single canonical key shared with the codes and auth modules +// (auth.UserIDFromContext reads codes.CtxKeyUserID). Aliasing it here — rather +// than declaring a distinct devices.ctxKey("user_id") — ensures the devices +// middleware/handlers resolve the same value the auth middleware injects. +const CtxKeyUserID = codes.CtxKeyUserID // ctxKeyPlan is the context key under which the resolved subscription Plan is // stored by SubscriptionMiddleware. diff --git a/server/internal/devices/devices_integration_test.go b/server/internal/devices/devices_integration_test.go index dc605ed..6e0c2ba 100644 --- a/server/internal/devices/devices_integration_test.go +++ b/server/internal/devices/devices_integration_test.go @@ -117,7 +117,7 @@ func applySchema(db *sql.DB) error { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`, `INSERT IGNORE INTO plans (code, max_devices, daily_minutes, ad_gate) - VALUES ('free', 1, 10, TRUE), ('pro', 5, NULL, FALSE), ('team', 10, NULL, FALSE)`, + VALUES ('free', 1, 10, TRUE), ('pro', 3, NULL, FALSE), ('team', 10, NULL, FALSE)`, } for _, stmt := range stmts { if _, err := db.Exec(stmt); err != nil { diff --git a/server/internal/usage/usage_integration_test.go b/server/internal/usage/usage_integration_test.go index c623c52..c08f365 100644 --- a/server/internal/usage/usage_integration_test.go +++ b/server/internal/usage/usage_integration_test.go @@ -108,6 +108,7 @@ func applySchema(db *sql.DB) error { bytes_down BIGINT UNSIGNED NOT NULL DEFAULT 0, minutes_used INT NOT NULL DEFAULT 0, ad_unlocked_at DATETIME(6) NULL, + ad_bonus_minutes INT NOT NULL DEFAULT 0, PRIMARY KEY (user_id, date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`, `CREATE TABLE IF NOT EXISTS usage_hourly ( @@ -433,11 +434,16 @@ func TestIntAdsUnlockAccumulates(t *testing.T) { t.Errorf("remaining after 2 ads=%d, want 30", rem) } - // Exactly one unlock timestamp. - var n int - db.QueryRow(`SELECT COUNT(*) FROM usage_daily WHERE user_id=? AND ad_unlocked_at IS NOT NULL`, uid).Scan(&n) - if n != 1 { - t.Errorf("expected 1 unlocked day, got %d", n) + // Additive model: the two ads accumulate into a single day row's + // ad_bonus_minutes (10 + 10 = 20). (ad_unlocked_at is legacy from the old + // per-day boolean unlock and is no longer stamped by UnlockAd.) + var rows, bonus int + db.QueryRow(`SELECT COUNT(*), COALESCE(MAX(ad_bonus_minutes),0) FROM usage_daily WHERE user_id=?`, uid).Scan(&rows, &bonus) + if rows != 1 { + t.Errorf("expected 1 usage_daily row, got %d", rows) + } + if bonus != 20 { + t.Errorf("expected ad_bonus_minutes=20, got %d", bonus) } }