fix(server+ci): 修 go-integration 真 bug + e2e 免疫代理(CI 收尾)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 23s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 20s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 20s
ci-pangolin / Lint — shellcheck (pull_request) Successful in 7s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 33s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 16s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 4s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 5s
ci-pangolin / Go — build + test (pull_request) Successful in 8s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Successful in 8s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Successful in 4m33s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Successful in 19s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 23s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 20s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 20s
ci-pangolin / Lint — shellcheck (pull_request) Successful in 7s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 33s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 16s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 4s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 5s
ci-pangolin / Go — build + test (pull_request) Successful in 8s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Successful in 8s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Successful in 4m33s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Successful in 19s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)+
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user