http-client.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. /**
  2. * 集成测试 HTTP 客户端
  3. * 使用 axios 替代 Playwright 的 APIRequestContext,确保可在 Vitest 中运行
  4. *
  5. * FIX-01: Node.js 22 下 localhost 解析为 IPv6 ::1,但后端只绑 IPv4 0.0.0.0
  6. * 强制使用 127.0.0.1 (避免被环境变量 BASE_URL=/ 干扰)
  7. */
  8. import axios, { AxiosInstance, AxiosResponse } from 'axios';
  9. // 强制 127.0.0.1,不依赖环境变量(避免被 BASE_URL=/ 等干扰)
  10. const BASE_URL = 'http://127.0.0.1:3000';
  11. let httpClient: AxiosInstance | null = null;
  12. let authToken: string | null = null;
  13. /** 初始化 HTTP 客户端 */
  14. export function initHttpClient(): void {
  15. httpClient = axios.create({
  16. baseURL: BASE_URL,
  17. timeout: 30000,
  18. headers: { 'Content-Type': 'application/json' },
  19. });
  20. // 防御:如果多次 init,需要重新创建
  21. if (!httpClient) {
  22. httpClient = axios.create({
  23. baseURL: BASE_URL,
  24. timeout: 30000,
  25. headers: { 'Content-Type': 'application/json' },
  26. });
  27. }
  28. }
  29. /** 释放 HTTP 客户端 */
  30. export function disposeHttpClient(): void {
  31. httpClient = null;
  32. }
  33. /** 设置认证 token */
  34. export function setAuthToken(token: string): void {
  35. authToken = token;
  36. }
  37. /** 获取当前 token */
  38. export function getAuthToken(): string | null {
  39. return authToken;
  40. }
  41. /** 清除认证 token */
  42. export function clearAuthToken(): void {
  43. authToken = null;
  44. }
  45. /** API 响应类型 */
  46. export interface ApiResponse {
  47. code: number;
  48. message: string;
  49. data: any;
  50. }
  51. /** 解析 API 响应 */
  52. function parseResponse(response: AxiosResponse): ApiResponse {
  53. const body = response.data;
  54. if (body && typeof body === 'object' && body.code !== undefined) {
  55. return { code: body.code, message: body.message ?? '', data: body.data };
  56. }
  57. if (body && typeof body === 'object' && body.success === true) {
  58. return { code: 0, message: '', data: body.data };
  59. }
  60. return { code: response.status, message: '', data: body };
  61. }
  62. /** 确保 httpClient 已初始化 */
  63. function ensureClient(): AxiosInstance {
  64. if (!httpClient) {
  65. initHttpClient();
  66. }
  67. if (!httpClient) {
  68. throw new Error('httpClient 初始化失败');
  69. }
  70. return httpClient;
  71. }
  72. /** GET 请求 */
  73. export async function apiGet(path: string, params?: Record<string, string>): Promise<ApiResponse> {
  74. const client = ensureClient();
  75. const config: any = {};
  76. if (authToken) {
  77. config.headers = { Authorization: `Bearer ${authToken}` };
  78. }
  79. if (params) {
  80. config.params = params;
  81. }
  82. try {
  83. const response = await client.get(path, config);
  84. return parseResponse(response);
  85. } catch (error: any) {
  86. if (error.response) {
  87. return parseResponse(error.response);
  88. }
  89. return { code: -1, message: error.message || String(error), data: null };
  90. }
  91. }
  92. /** POST 请求 */
  93. export async function apiPost(path: string, data?: any): Promise<ApiResponse> {
  94. const client = ensureClient();
  95. const config: any = {};
  96. if (authToken) {
  97. config.headers = { Authorization: `Bearer ${authToken}` };
  98. }
  99. try {
  100. const response = await client.post(path, data, config);
  101. return parseResponse(response);
  102. } catch (error: any) {
  103. if (error.response) {
  104. return parseResponse(error.response);
  105. }
  106. return { code: -1, message: error.message || String(error), data: null };
  107. }
  108. }
  109. /** PUT 请求 */
  110. export async function apiPut(path: string, data?: any): Promise<ApiResponse> {
  111. const client = ensureClient();
  112. const config: any = {};
  113. if (authToken) {
  114. config.headers = { Authorization: `Bearer ${authToken}` };
  115. }
  116. try {
  117. const response = await client.put(path, data, config);
  118. return parseResponse(response);
  119. } catch (error: any) {
  120. if (error.response) {
  121. return parseResponse(error.response);
  122. }
  123. return { code: -1, message: error.message || String(error), data: null };
  124. }
  125. }
  126. /** DELETE 请求 */
  127. export async function apiDelete(path: string): Promise<ApiResponse> {
  128. const client = ensureClient();
  129. const config: any = {};
  130. if (authToken) {
  131. config.headers = { Authorization: `Bearer ${authToken}` };
  132. }
  133. try {
  134. const response = await client.delete(path, config);
  135. return parseResponse(response);
  136. } catch (error: any) {
  137. if (error.response) {
  138. return parseResponse(error.response);
  139. }
  140. return { code: -1, message: error.message || String(error), data: null };
  141. }
  142. }
  143. /** 创建测试用户(集成测试专用) */
  144. export async function createTestUser(): Promise<{ token: string; userId: string }> {
  145. initHttpClient();
  146. const phone = `13${Math.floor(Math.random() * 1_000_000_000).toString().padStart(9, '0')}`;
  147. await apiPost('/api/auth/send-code', { phone });
  148. const loginRes = await apiPost('/api/auth/login', { phone, code: '123456' });
  149. if (loginRes.code !== 0 || !loginRes.data?.token) {
  150. throw new Error('创建测试用户失败');
  151. }
  152. setAuthToken(loginRes.data.token);
  153. return { token: loginRes.data.token, userId: loginRes.data.userId || '1' };
  154. }