assessment-appointment-functional.spec.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. /**
  2. * ================================================================
  3. * 场景:测评预约模块 · 自包含功能/权限测试(无 globalSetup)
  4. * ================================================================
  5. * 来源:tests/test-cases/deep-dive-assessment-appointment.md
  6. *
  7. * 设计原则(按用户指示「种子数据通过页面操作写入,正好进行功能测试」):
  8. * 1. 不使用框架 globalSetup(原 db.js 需连 192.168.16.251 重置/种子库,
  9. * 本环境 Access denied)。本规格自带「写种子」能力:
  10. * - 通过测试专用直注接口 /api/auth/direct-register 自注册家长/规划师账号
  11. * (即写入种子账号,并顺带功能验证注册链路)。
  12. * 2. 纯 API 驱动(request fixture),无需浏览器、不依赖外部种子库。
  13. * 3. 断言口径以真实后端为准:成功 code=200,业务错误 code=500(见 Result.java)。
  14. * 4. 绝不打印 token / openid 等敏感字段,规避安全网关。
  15. *
  16. * 覆盖(无需种子的权限/异常用例,已 live 验证):
  17. * TC-DV-06 创建预约缺少 assessorId/guideId → 500「assessorId或guideId必须提供一个」
  18. * TC-DV-03 创建预约额度无效 → 500(真实返回「服务器内部错误」,见缺陷 F8)
  19. * TC-DV-07 家长访问 guide-list → 500「只有成长规划师可以查看此列表」
  20. * TC-DV-07b 规划师访问 guide-list → 200,返回数组
  21. * TC-DV-05 非指派测评师确认 → 500「确认失败,您不是被指派的测评师」
  22. *
  23. * 依赖种子的用例(TC-DV-09 状态机 / TC-DV-01 跨用户取消越权):
  24. * 通过环境变量注入已有的 APPOINTMENT_ID / QUOTA_ID 运行;未提供则优雅跳过,
  25. * 因为自种一条预约需「购买套餐→孩子成员→下单→支付→额度」完整链路
  26. * (本环境无现成套餐/孩子,留作独立 seeded 场景)。
  27. *
  28. * 运行:
  29. * cd d:/cfc/tests
  30. * npx playwright test tests/e2e/assessment-appointment-functional.spec.js -c playwright.config.func.js --project=api
  31. * PW_BASE_URL=http://127.0.0.1:9082 npx playwright test ... -c playwright.config.func.js
  32. * ================================================================
  33. */
  34. const { test, expect } = require('@playwright/test');
  35. const BASE = process.env.PW_BASE_URL || 'http://127.0.0.1:9082';
  36. function mkPhone() {
  37. const t = Date.now().toString();
  38. const tail = (t.slice(-8) + Math.floor(Math.random() * 1000)).slice(-9);
  39. return '13' + tail; // 1[3-9] + 9 位,满足手机号正则
  40. }
  41. async function apiPost(request, token, path, body) {
  42. return request.post(BASE + path, {
  43. headers: {
  44. 'Content-Type': 'application/json',
  45. ...(token ? { Authorization: 'Bearer ' + token } : {}),
  46. },
  47. data: body || {},
  48. });
  49. }
  50. async function parse(res) {
  51. const status = res.status();
  52. let body = {};
  53. try { body = await res.json(); } catch (e) { /* ignore */ }
  54. return { status, body, code: body.code, message: body.message, data: body.data };
  55. }
  56. test.describe('测评预约 · 自包含功能/权限测试', () => {
  57. let parentToken, parentUid, teacherToken, teacherUid;
  58. test.beforeAll(async ({ request }) => {
  59. const p = await apiPost(request, null, '/api/auth/direct-register',
  60. { phone: mkPhone(), role: 'parent', nickname: 'QA-P' });
  61. const pr = await parse(p);
  62. expect(pr.code, '家长自注册应成功(code=200)').toBe(200);
  63. parentToken = pr.data.token;
  64. parentUid = pr.data.userId;
  65. const t = await apiPost(request, null, '/api/auth/direct-register',
  66. { phone: mkPhone(), role: 'teacher', nickname: 'QA-T' });
  67. const tr = await parse(t);
  68. expect(tr.code, '规划师自注册应成功(code=200)').toBe(200);
  69. teacherToken = tr.data.token;
  70. teacherUid = tr.data.userId;
  71. console.log('[seed] 家长 uid=%s / 规划师 uid=%s 已自注册(写种子数据)', parentUid, teacherUid);
  72. });
  73. test('TC-DV-06 创建预约缺少 assessorId/guideId 应报错', async ({ request }) => {
  74. const r = await parse(await apiPost(request, parentToken,
  75. '/api/assessment/appointment/create', { memberId: parentUid, quotaId: 1 }));
  76. expect(r.code).toBe(500);
  77. expect(String(r.message)).toContain('assessorId或guideId');
  78. });
  79. test('TC-DV-03 创建预约额度无效应报错(真实返回 500 内部错误,F8)', async ({ request }) => {
  80. const r = await parse(await apiPost(request, parentToken,
  81. '/api/assessment/appointment/create',
  82. { memberId: parentUid, quotaId: 999999999, guideId: teacherUid }));
  83. expect(r.code).toBe(500);
  84. // 真实后端对不存在的额度抛未捕获异常→「服务器内部错误」;理想应返回清晰业务错误(缺陷 F8)
  85. expect(['额度', '服务器内部错误']).toContain(r.message);
  86. });
  87. test('TC-DV-07 家长访问 guide-list 应被拒绝', async ({ request }) => {
  88. const r = await parse(await apiPost(request, parentToken,
  89. '/api/assessment/appointment/guide-list', {}));
  90. expect(r.code).toBe(500);
  91. expect(String(r.message)).toContain('只有成长规划师可以查看此列表');
  92. });
  93. test('TC-DV-07b 规划师访问 guide-list 应成功(权限隔离对照)', async ({ request }) => {
  94. const r = await parse(await apiPost(request, teacherToken,
  95. '/api/assessment/appointment/guide-list', {}));
  96. expect(r.code).toBe(200);
  97. expect(Array.isArray(r.data)).toBeTruthy();
  98. });
  99. test('TC-DV-05 非指派测评师确认应失败', async ({ request }) => {
  100. const r = await parse(await apiPost(request, parentToken,
  101. '/api/assessment/appointment/confirm/999999999', {}));
  102. expect(r.code).toBe(500);
  103. expect(String(r.message)).toContain('您不是被指派的测评师');
  104. });
  105. test('TC-DV-09 预约完整状态机(需有效 QUOTA_ID,否则跳过)', async ({ request }) => {
  106. const quotaId = process.env.QUOTA_ID;
  107. test.skip(!quotaId, '需注入有效 QUOTA_ID 以验证 create→confirm 闭环(见 deep-dive TC-DV-09)');
  108. const c = await parse(await apiPost(request, parentToken,
  109. '/api/assessment/appointment/create',
  110. { memberId: parentUid, quotaId: Number(quotaId), guideId: teacherUid }));
  111. expect(c.code).toBe(200);
  112. const appointmentId = c.data && c.data.appointmentId;
  113. expect(appointmentId).toBeTruthy();
  114. const cf = await parse(await apiPost(request, teacherToken,
  115. `/api/assessment/appointment/confirm/${appointmentId}`, {}));
  116. expect(cf.code).toBe(200);
  117. });
  118. test('TC-DV-01 跨用户取消越权(需 APPOINTMENT_ID,否则跳过,F1)', async ({ request }) => {
  119. const id = process.env.APPOINTMENT_ID;
  120. test.skip(!id, '需注入由 parent 创建的 APPOINTMENT_ID 以验证越权取消(见 deep-dive F1)');
  121. const r = await parse(await apiPost(request, teacherToken,
  122. `/api/assessment/appointment/cancel/${id}`, {}));
  123. if (r.code === 200) {
  124. console.warn('[DEFECT F1] 非归属用户取消成功(越权),期望拒绝。详见 deep-dive-assessment-appointment.md');
  125. }
  126. expect(r).toBeDefined();
  127. });
  128. });