e2e_user_flow_test.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. //go:build e2e
  2. package integration
  3. import (
  4. "bytes"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "strings"
  10. "testing"
  11. "time"
  12. )
  13. // E2E 用户流程测试
  14. // 测试完整的用户操作链路:注册 → 登录 → 创建 API Key → 调用网关 → 查询用量
  15. var (
  16. testUserEmail = "e2e-test-" + fmt.Sprintf("%d", time.Now().UnixMilli()) + "@test.local"
  17. testUserPassword = "E2eTest@12345"
  18. testUserName = "e2e-test-user"
  19. )
  20. // TestUserRegistrationAndLogin 测试用户注册和登录流程
  21. func TestUserRegistrationAndLogin(t *testing.T) {
  22. // 步骤 1: 注册新用户
  23. t.Run("注册新用户", func(t *testing.T) {
  24. payload := map[string]string{
  25. "email": testUserEmail,
  26. "password": testUserPassword,
  27. "username": testUserName,
  28. }
  29. body, _ := json.Marshal(payload)
  30. resp, err := doRequest(t, "POST", "/api/auth/register", body, "")
  31. if err != nil {
  32. t.Skipf("注册接口不可用,跳过用户流程测试: %v", err)
  33. return
  34. }
  35. defer resp.Body.Close()
  36. respBody, _ := io.ReadAll(resp.Body)
  37. // 注册可能返回 200(成功)或 400(邮箱已存在)或 403(注册已关闭)
  38. switch resp.StatusCode {
  39. case 200:
  40. t.Logf("✅ 用户注册成功: %s", testUserEmail)
  41. case 400:
  42. t.Logf("⚠️ 用户可能已存在: %s", string(respBody))
  43. case 403:
  44. t.Skipf("注册功能已关闭: %s", string(respBody))
  45. default:
  46. t.Logf("⚠️ 注册返回 HTTP %d: %s(继续尝试登录)", resp.StatusCode, string(respBody))
  47. }
  48. })
  49. // 步骤 2: 登录获取 JWT
  50. var accessToken string
  51. t.Run("用户登录获取JWT", func(t *testing.T) {
  52. payload := map[string]string{
  53. "email": testUserEmail,
  54. "password": testUserPassword,
  55. }
  56. body, _ := json.Marshal(payload)
  57. resp, err := doRequest(t, "POST", "/api/auth/login", body, "")
  58. if err != nil {
  59. t.Fatalf("登录请求失败: %v", err)
  60. }
  61. defer resp.Body.Close()
  62. respBody, _ := io.ReadAll(resp.Body)
  63. if resp.StatusCode != 200 {
  64. t.Skipf("登录失败 HTTP %d: %s(可能需要先注册用户)", resp.StatusCode, string(respBody))
  65. return
  66. }
  67. var result map[string]any
  68. if err := json.Unmarshal(respBody, &result); err != nil {
  69. t.Fatalf("解析登录响应失败: %v", err)
  70. }
  71. // 尝试从标准响应格式获取 token
  72. if token, ok := result["access_token"].(string); ok && token != "" {
  73. accessToken = token
  74. } else if data, ok := result["data"].(map[string]any); ok {
  75. if token, ok := data["access_token"].(string); ok {
  76. accessToken = token
  77. }
  78. }
  79. if accessToken == "" {
  80. t.Skipf("未获取到 access_token,响应: %s", string(respBody))
  81. return
  82. }
  83. // 验证 token 不为空且格式基本正确
  84. if len(accessToken) < 10 {
  85. t.Fatalf("access_token 格式异常: %s", accessToken)
  86. }
  87. t.Logf("✅ 登录成功,获取 JWT(长度: %d)", len(accessToken))
  88. })
  89. if accessToken == "" {
  90. t.Skip("未获取到 JWT,跳过后续测试")
  91. return
  92. }
  93. // 步骤 3: 使用 JWT 获取当前用户信息
  94. t.Run("获取当前用户信息", func(t *testing.T) {
  95. resp, err := doRequest(t, "GET", "/api/user/me", nil, accessToken)
  96. if err != nil {
  97. t.Fatalf("请求失败: %v", err)
  98. }
  99. defer resp.Body.Close()
  100. if resp.StatusCode != 200 {
  101. body, _ := io.ReadAll(resp.Body)
  102. t.Fatalf("HTTP %d: %s", resp.StatusCode, string(body))
  103. }
  104. t.Logf("✅ 成功获取用户信息")
  105. })
  106. }
  107. // TestAPIKeyLifecycle 测试 API Key 的创建和使用
  108. func TestAPIKeyLifecycle(t *testing.T) {
  109. // 先登录获取 JWT
  110. accessToken := loginTestUser(t)
  111. if accessToken == "" {
  112. t.Skip("无法登录,跳过 API Key 生命周期测试")
  113. return
  114. }
  115. var apiKey string
  116. // 步骤 1: 创建 API Key
  117. t.Run("创建API_Key", func(t *testing.T) {
  118. payload := map[string]string{
  119. "name": "e2e-test-key-" + fmt.Sprintf("%d", time.Now().UnixMilli()),
  120. }
  121. body, _ := json.Marshal(payload)
  122. resp, err := doRequest(t, "POST", "/api/keys", body, accessToken)
  123. if err != nil {
  124. t.Fatalf("创建 API Key 请求失败: %v", err)
  125. }
  126. defer resp.Body.Close()
  127. respBody, _ := io.ReadAll(resp.Body)
  128. if resp.StatusCode != 200 {
  129. t.Skipf("创建 API Key 失败 HTTP %d: %s", resp.StatusCode, string(respBody))
  130. return
  131. }
  132. var result map[string]any
  133. if err := json.Unmarshal(respBody, &result); err != nil {
  134. t.Fatalf("解析响应失败: %v", err)
  135. }
  136. // 从响应中提取 key
  137. if key, ok := result["key"].(string); ok {
  138. apiKey = key
  139. } else if data, ok := result["data"].(map[string]any); ok {
  140. if key, ok := data["key"].(string); ok {
  141. apiKey = key
  142. }
  143. }
  144. if apiKey == "" {
  145. t.Skipf("未获取到 API Key,响应: %s", string(respBody))
  146. return
  147. }
  148. // 验证 API Key 脱敏日志(只显示前 8 位)
  149. masked := apiKey
  150. if len(masked) > 8 {
  151. masked = masked[:8] + "..."
  152. }
  153. t.Logf("✅ API Key 创建成功: %s", masked)
  154. })
  155. if apiKey == "" {
  156. t.Skip("未创建 API Key,跳过后续测试")
  157. return
  158. }
  159. // 步骤 2: 使用 API Key 调用网关(需要 Claude 或 Gemini 可用)
  160. t.Run("使用API_Key调用网关", func(t *testing.T) {
  161. // 尝试调用 models 列表(最轻量的 API 调用)
  162. resp, err := doRequest(t, "GET", "/v1/models", nil, apiKey)
  163. if err != nil {
  164. t.Fatalf("网关请求失败: %v", err)
  165. }
  166. defer resp.Body.Close()
  167. respBody, _ := io.ReadAll(resp.Body)
  168. // 可能返回 200(成功)或 402(余额不足)或 403(无可用账户)
  169. switch {
  170. case resp.StatusCode == 200:
  171. t.Logf("✅ API Key 网关调用成功")
  172. case resp.StatusCode == 402:
  173. t.Logf("⚠️ 余额不足,但 API Key 认证通过")
  174. case resp.StatusCode == 403:
  175. t.Logf("⚠️ 无可用账户,但 API Key 认证通过")
  176. default:
  177. t.Logf("⚠️ 网关返回 HTTP %d: %s", resp.StatusCode, string(respBody))
  178. }
  179. })
  180. // 步骤 3: 查询用量记录
  181. t.Run("查询用量记录", func(t *testing.T) {
  182. resp, err := doRequest(t, "GET", "/api/usage/dashboard", nil, accessToken)
  183. if err != nil {
  184. t.Fatalf("用量查询请求失败: %v", err)
  185. }
  186. defer resp.Body.Close()
  187. if resp.StatusCode != 200 {
  188. body, _ := io.ReadAll(resp.Body)
  189. t.Logf("⚠️ 用量查询返回 HTTP %d: %s", resp.StatusCode, string(body))
  190. return
  191. }
  192. t.Logf("✅ 用量查询成功")
  193. })
  194. }
  195. // =============================================================================
  196. // 辅助函数
  197. // =============================================================================
  198. func doRequest(t *testing.T, method, path string, body []byte, token string) (*http.Response, error) {
  199. t.Helper()
  200. url := baseURL + path
  201. var bodyReader io.Reader
  202. if body != nil {
  203. bodyReader = bytes.NewReader(body)
  204. }
  205. req, err := http.NewRequest(method, url, bodyReader)
  206. if err != nil {
  207. return nil, fmt.Errorf("创建请求失败: %w", err)
  208. }
  209. if body != nil {
  210. req.Header.Set("Content-Type", "application/json")
  211. }
  212. if token != "" {
  213. req.Header.Set("Authorization", "Bearer "+token)
  214. }
  215. client := &http.Client{Timeout: 30 * time.Second}
  216. return client.Do(req)
  217. }
  218. func loginTestUser(t *testing.T) string {
  219. t.Helper()
  220. // 先尝试用管理员账户登录
  221. adminEmail := getEnv("ADMIN_EMAIL", "admin@sub2api.local")
  222. adminPassword := getEnv("ADMIN_PASSWORD", "")
  223. if adminPassword == "" {
  224. // 尝试用测试用户
  225. adminEmail = testUserEmail
  226. adminPassword = testUserPassword
  227. }
  228. payload := map[string]string{
  229. "email": adminEmail,
  230. "password": adminPassword,
  231. }
  232. body, _ := json.Marshal(payload)
  233. resp, err := doRequest(t, "POST", "/api/auth/login", body, "")
  234. if err != nil {
  235. return ""
  236. }
  237. defer resp.Body.Close()
  238. if resp.StatusCode != 200 {
  239. return ""
  240. }
  241. respBody, _ := io.ReadAll(resp.Body)
  242. var result map[string]any
  243. if err := json.Unmarshal(respBody, &result); err != nil {
  244. return ""
  245. }
  246. if token, ok := result["access_token"].(string); ok {
  247. return token
  248. }
  249. if data, ok := result["data"].(map[string]any); ok {
  250. if token, ok := data["access_token"].(string); ok {
  251. return token
  252. }
  253. }
  254. return ""
  255. }
  256. // redactAPIKey API Key 脱敏,只显示前 8 位
  257. func redactAPIKey(key string) string {
  258. key = strings.TrimSpace(key)
  259. if len(key) <= 8 {
  260. return "***"
  261. }
  262. return key[:8] + "..."
  263. }