Files
pangolin/server/test/e2e/smoke_test.go
T
wangjia b3cbeef7cf test(e2e): 第5刀a L4 进程级冒烟编排 + HTTP 段
真起 server 二进制(sqlite + Redis)跑 HTTP 用户旅程,验证 in-process 测试给不了的
信心(二进制起/迁移/路由/鉴权/DB 端到端):
- scripts/e2e-smoke.sh:起栈编排(Redis 三选一:已有 addr / docker / 内嵌 miniredis
  兜底)+ 随机端口 + trap 清理;调 Go driver
- server/test/e2e/smoke_test.go (tag e2e):造用户(复用 argon2 HashPassword)
  → 登录 → /v1/me(dp_uuid/plan/email) → /v1/usage 鉴权 + 无 token 401
- server/test/e2e/miniredis (tag e2e):无 docker 时 Redis 兜底 launcher
- 本地绿;go build/test ./...(无 tag)仍 25 包绿,tag e2e 文件被静默跳过

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 06:59:39 +08:00

151 lines
4.3 KiB
Go

//go:build e2e
// Package e2e 是 L4 进程级端到端冒烟的 driver(build tag e2e,不进常规 go test ./...)。
// 由 scripts/e2e-smoke.sh 起好真 server 进程后,经 E2E_* env 指向真 HTTP/gRPC 地址驱动。
//
// 5a:HTTP 段(造用户 → 登录 → /v1/me → /v1/usage)。5b 追加 gRPC 全链路。
package e2e
import (
"bytes"
"encoding/json"
"io"
"net/http"
"os"
"testing"
"time"
"github.com/wangjia/pangolin/server/internal/auth"
"github.com/wangjia/pangolin/server/internal/db"
)
const (
e2eEmail = "e2e@example.com"
e2ePass = "e2e-pass-12345"
e2eDPUUID = "e2e-dp-uuid-00000001"
)
func TestE2ESmoke(t *testing.T) {
base := os.Getenv("E2E_HTTP")
dsn := os.Getenv("E2E_DB_DSN")
if base == "" || dsn == "" {
t.Fatal("E2E_HTTP / E2E_DB_DSN 未设——请经 scripts/e2e-smoke.sh 运行")
}
seedUser(t, dsn)
// ── HTTP 用户旅程 ──────────────────────────────────────────────
token := login(t, base, e2eEmail, e2ePass)
if token == "" {
t.Fatal("登录未返回 access_token")
}
me := getJSON(t, base, "/v1/me", token)
if got := me["dp_uuid"]; got != e2eDPUUID {
t.Errorf("/v1/me dp_uuid = %v, want %s", got, e2eDPUUID)
}
if got := me["plan"]; got != "free" {
t.Errorf("/v1/me plan = %v, want free", got)
}
if got := me["email"]; got != e2eEmail {
t.Errorf("/v1/me email = %v, want %s", got, e2eEmail)
}
// /v1/usage 鉴权通过 + 可解析(初始无流量;具体 bytes 断言见 5b 注入用量后)。
assertStatus200(t, base, "/v1/usage", token)
assertStatus200(t, base, "/v1/usage/devices", token)
// 无 token → 401(鉴权真生效)。
assertUnauthorized(t, base, "/v1/me")
t.Log("✓ HTTP 段通过:登录 → /v1/me(dp_uuid/plan/email) → /v1/usage(鉴权)")
}
// seedUser 直接写 sqlite 造一个 active 用户(复用 argon2id HashPassword),
// 比走注册验证码稳。server 刚起基本空闲,一次性 INSERT 锁风险极低。
func seedUser(t *testing.T, dsn string) {
t.Helper()
conn, err := db.Open(dsn)
if err != nil {
t.Fatalf("db.Open: %v", err)
}
defer conn.Close()
hash, err := auth.HashPassword(e2ePass)
if err != nil {
t.Fatalf("HashPassword: %v", err)
}
if _, err := conn.Exec(
`INSERT INTO users (uuid, email, pw_hash, dp_uuid, status) VALUES (?, ?, ?, ?, 'active')`,
"e2e-user-uuid-0001", e2eEmail, hash, e2eDPUUID,
); err != nil {
t.Fatalf("seed user: %v", err)
}
}
func login(t *testing.T, base, email, pass string) string {
t.Helper()
body, _ := json.Marshal(map[string]string{"email": email, "password": pass})
resp, err := httpClient().Post(base+"/v1/auth/login", "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("login POST: %v", err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("login status = %d, body = %s", resp.StatusCode, raw)
}
var tp struct {
AccessToken string `json:"access_token"`
}
if err := json.Unmarshal(raw, &tp); err != nil {
t.Fatalf("login decode: %v (body=%s)", err, raw)
}
return tp.AccessToken
}
func getJSON(t *testing.T, base, path, token string) map[string]any {
t.Helper()
raw, status := doGet(t, base, path, token)
if status != http.StatusOK {
t.Fatalf("GET %s status = %d, body = %s", path, status, raw)
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatalf("GET %s decode: %v (body=%s)", path, err, raw)
}
return m
}
func assertStatus200(t *testing.T, base, path, token string) {
t.Helper()
raw, status := doGet(t, base, path, token)
if status != http.StatusOK {
t.Errorf("GET %s status = %d, want 200, body = %s", path, status, raw)
}
}
func assertUnauthorized(t *testing.T, base, path string) {
t.Helper()
_, status := doGet(t, base, path, "")
if status != http.StatusUnauthorized {
t.Errorf("GET %s (无 token) status = %d, want 401", path, status)
}
}
func doGet(t *testing.T, base, path, token string) ([]byte, int) {
t.Helper()
req, _ := http.NewRequest(http.MethodGet, base+path, nil)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := httpClient().Do(req)
if err != nil {
t.Fatalf("GET %s: %v", path, err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
return raw, resp.StatusCode
}
func httpClient() *http.Client { return &http.Client{Timeout: 10 * time.Second} }