//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} }