api-client.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. /**
  2. * 统一 API 请求封装
  3. * 提供 GET / POST / PUT / DELETE 方法,自动处理认证和错误
  4. */
  5. import { request, APIRequestContext } from '@playwright/test';
  6. const BASE_URL = process.env.BACKEND_URL || 'http://127.0.0.1:3000';
  7. let apiContext: APIRequestContext;
  8. let authToken: string | null = null;
  9. let testUserId: string | null = null;
  10. /** 初始化 API 客户端 */
  11. export async function initApiClient() {
  12. apiContext = await request.newContext({ baseURL: BASE_URL });
  13. }
  14. /** 释放 API 客户端 */
  15. export async function disposeApiClient() {
  16. if (apiContext) await apiContext.dispose();
  17. }
  18. /** 设置认证 token */
  19. export function setAuthToken(token: string, userId?: string) {
  20. authToken = token;
  21. if (userId) testUserId = userId;
  22. }
  23. /** 获取当前 token */
  24. export function getAuthToken(): string | null {
  25. return authToken;
  26. }
  27. /** 获取当前测试用户 ID */
  28. export function getTestUserId(): string {
  29. return testUserId || '1';
  30. }
  31. /** 通用请求选项 */
  32. function getOptions(method: string, data?: any): Record<string, any> {
  33. const options: Record<string, any> = {
  34. method,
  35. headers: getAuthHeaders(),
  36. };
  37. if (data) {
  38. options.data = data;
  39. }
  40. return options;
  41. }
  42. /** 提取公共请求头(Authorization + Content-Type) */
  43. function getAuthHeaders(): Record<string, string> {
  44. const headers: Record<string, string> = { 'Content-Type': 'application/json' };
  45. if (authToken) {
  46. headers['Authorization'] = `Bearer ${authToken}`;
  47. }
  48. return headers;
  49. }
  50. /** API 响应类型 */
  51. export interface ApiResponse {
  52. code: number;
  53. message: string;
  54. data: any;
  55. }
  56. /** 解析 API 响应,自动兼容多种响应格式 */
  57. function parseApiResponse(body: any, res: any): ApiResponse {
  58. // 标准格式: { code: 0, message: '...', data: ... }
  59. if (body && typeof body === 'object' && body.code !== undefined) {
  60. return { code: body.code, message: body.message ?? '', data: body.data };
  61. }
  62. // 兼容格式: { success: true, data: ... } -> code = 0
  63. if (body && typeof body === 'object' && body.success === true) {
  64. return { code: 0, message: '', data: body.data };
  65. }
  66. // 降级: 使用 HTTP 状态码
  67. return { code: res.status(), message: '', data: null };
  68. }
  69. /** 安全解析 JSON,避免非 JSON 响应崩溃 */
  70. async function safeJson(res: any): Promise<any> {
  71. const text = await res.text();
  72. if (!text || text === 'Not Found' || text === 'Method Not Allowed') {
  73. return { _raw: text };
  74. }
  75. try {
  76. return JSON.parse(text);
  77. } catch {
  78. return { _raw: text };
  79. }
  80. }
  81. /** GET 请求 */
  82. export async function apiGet(path: string, params?: Record<string, string>): Promise<ApiResponse> {
  83. let url = path;
  84. if (params) {
  85. const qs = new URLSearchParams(params).toString();
  86. url = `${path}?${qs}`;
  87. }
  88. // 必须传 headers:之前这里漏掉 headers 导致 GET 请求永远没带 Authorization,
  89. // optionalAuth 中间件 fall back 到 TEST_USER,所有需要 owner 校验的接口都 404。
  90. const res = await apiContext.get(url, { headers: getAuthHeaders() });
  91. const body = await safeJson(res);
  92. return parseApiResponse(body, res);
  93. }
  94. /** POST 请求 */
  95. export async function apiPost(path: string, data?: any): Promise<ApiResponse> {
  96. const res = await apiContext.post(path, getOptions('POST', data));
  97. const body = await safeJson(res);
  98. return parseApiResponse(body, res);
  99. }
  100. /** PUT 请求 */
  101. export async function apiPut(path: string, data?: any): Promise<ApiResponse> {
  102. const res = await apiContext.put(path, getOptions('PUT', data));
  103. const body = await safeJson(res);
  104. return parseApiResponse(body, res);
  105. }
  106. /** DELETE 请求 */
  107. export async function apiDelete(path: string): Promise<ApiResponse> {
  108. const res = await apiContext.delete(path, getOptions('DELETE'));
  109. const body = await safeJson(res);
  110. return parseApiResponse(body, res);
  111. }
  112. /** 获取原始响应(用于检查状态码等) */
  113. export async function apiRaw(method: string, path: string, data?: any) {
  114. return apiContext.fetch(path, getOptions(method, data));
  115. }