api.js 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. /**
  2. * API 级 e2e 共享助手
  3. * ================================================================
  4. * 背景:远程环境 http://cfc.iwintrue.com 仅部署后端 API(Spring Boot),
  5. * 未托管前端 SPA(访问任意路径均返回 Spring Boot 白标 404),
  6. * 因此基于 DOM 的 e2e(page.goto + 选择器等待)在远程不可运行。
  7. * 本助手为「API 级 e2e」提供统一入口:
  8. * - registerRole:测试专用直注接口 /api/auth/direct-register 自注册,
  9. * 不依赖种子数据库、不依赖前端表单登录、不打印任何敏感信息;
  10. * - apiPost:统一 POST 封装(项目约定接口一律使用 POST)。
  11. *
  12. * 安全约定:本文件及调用方一律不打印 token / 手机号等敏感信息。
  13. */
  14. const { expect } = require('@playwright/test');
  15. /** 生成合法手机号(严格 11 位,匹配后端 ^1[3-9]\d{9}$ 校验,否则返回「手机号格式不正确」) */
  16. function randPhone() {
  17. return '13' + String(Math.floor(Math.random() * 900000000) + 100000000);
  18. }
  19. /**
  20. * 自注册账号并返回会话上下文
  21. * @param {import('@playwright/test').APIRequestContext} request
  22. * @param {string} role 角色:parent / teacher / admin ...
  23. * @returns {Promise<{token:string,userId:*,familyId:*,role:string,roles:*}>}
  24. */
  25. async function registerRole(request, role, nickname) {
  26. const res = await request.post('/api/auth/direct-register', {
  27. data: { phone: randPhone(), role, nickname: nickname || 'QA-' + role }, timeout: API_TIMEOUT_MS,
  28. });
  29. const body = await res.json().catch(() => ({}));
  30. expect(body.code, 'direct-register 应当成功(code=200)').toBe(200);
  31. const d = body.data || {};
  32. expect(typeof d.token, '响应应包含 token').toBe('string');
  33. return {
  34. token: d.token,
  35. userId: d.userId,
  36. familyId: d.familyId,
  37. role: d.role,
  38. roles: d.roles,
  39. };
  40. }
  41. /**
  42. * 统一 POST 调用,返回 { status, code, message, data }
  43. * 若响应非 JSON(如未捕获异常导致的 Spring 500),回退以 HTTP 状态码作为 code,
  44. * 避免断言拿到 undefined 造成误判。
  45. *
  46. * 超时说明:远程 /api/family/member/add 冷启动首调实测约 16s(后续约 1.5s),
  47. * 超过 Playwright 默认 actionTimeout(15s) 会误报超时,故显式放宽到 60s。
  48. */
  49. const API_TIMEOUT_MS = 120000; // 冷启动容忍:远端 member/add 冷启动实测 16s+,降级窗口可能超过 60s
  50. async function apiPost(request, path, body, token) {
  51. const headers = {};
  52. if (token) headers.Authorization = 'Bearer ' + token;
  53. const res = await request.post(path, { data: body || {}, headers, timeout: API_TIMEOUT_MS });
  54. const raw = await res.text();
  55. let json = null;
  56. try {
  57. json = raw ? JSON.parse(raw) : null;
  58. } catch (e) {
  59. json = null;
  60. }
  61. if (!json || typeof json !== 'object') {
  62. return {
  63. status: res.status(),
  64. code: res.status(),
  65. message: 'NON_JSON_RESPONSE',
  66. data: null,
  67. };
  68. }
  69. return {
  70. status: res.status(),
  71. code: json.code,
  72. message: json.message,
  73. data: json.data,
  74. };
  75. }
  76. /** 从成员列表中找出对应登录用户的成员记录(用于「切换到自己」等用例) */
  77. function pickMemberByUserId(members, userId) {
  78. if (!Array.isArray(members)) return null;
  79. return members.find((m) => String(m.userId) === String(userId)) || null;
  80. }
  81. /** 统一时间戳后缀,避免同名字段唯一约束冲突 */
  82. function uniqueSuffix() {
  83. return String(Date.now()).slice(-8);
  84. }
  85. module.exports = { randPhone, registerRole, apiPost, pickMemberByUserId, uniqueSuffix };