family-member-api.spec.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. /**
  2. * 【API级】家庭成员管理全流程
  3. * ================================================================
  4. * 对应 DOM 版:e2e/family-member-management.spec.js(远程无 SPA,DOM 版不可跑)
  5. * 接口契约来源:FamilyMembersController(/api/family/member/*)
  6. * add : { generationLevel, nickname, gender?, birthday?, relativeMemberId?, peerType? }
  7. * list : { familyId? | visibleOnly? | includeFamily? } 越权 → code=403
  8. * switch : { memberId } 切换到自己 → 「不能切换到自己」
  9. * kick : { memberId } 仅家庭创建者可踢
  10. * update : { memberId, nickname?, generationLevel?, ... }
  11. * 运行:npx playwright test -c playwright.config.api.js --project=desktop
  12. */
  13. const { test, expect } = require('@playwright/test');
  14. const { registerRole, apiPost, pickMemberByUserId, uniqueSuffix } = require('../helpers/api');
  15. test.describe('【API级】家庭成员管理全流程', () => {
  16. let ctx;
  17. test.beforeAll(async ({ request }) => {
  18. ctx = await registerRole(request, 'parent', 'QA-FAMILY');
  19. });
  20. /** 创建一个独立的手动成员(非微信登录用户),返回 { id, nickname } */
  21. async function createManualChild(request, tag) {
  22. const nickname = 'QA_' + tag + '_' + uniqueSuffix();
  23. const r = await apiPost(
  24. request,
  25. '/api/family/member/add',
  26. { generationLevel: 'child', nickname, gender: 'male', birthday: '2015-06-15' },
  27. ctx.token
  28. );
  29. expect(r.code, '添加成员应成功').toBe(200);
  30. expect(r.data && r.data.id, '应返回成员ID').toBeTruthy();
  31. return { id: r.data.id, nickname: r.data.nickname || nickname };
  32. }
  33. // ===== 场景1:成员列表(关系/成员数据可获取)=====
  34. test('[场景1] 获取成员列表 - 期望返回成员数据', async ({ request }) => {
  35. const r = await apiPost(request, '/api/family/member/list', {}, ctx.token);
  36. expect(r.code).toBe(200);
  37. expect(Array.isArray(r.data)).toBeTruthy();
  38. expect(r.data.length).toBeGreaterThan(0);
  39. });
  40. test('[场景1b] 越权访问他人家庭 - 期望 403 拒绝', async ({ request }) => {
  41. const r = await apiPost(request, '/api/family/member/list', { familyId: 999999999 }, ctx.token);
  42. expect(r.code).toBe(403);
  43. expect(String(r.message)).toContain('无权访问');
  44. });
  45. // ===== 场景2:添加新家庭成员 =====
  46. test('[场景2] 添加新家庭成员 - 期望添加成功并返回完整信息', async ({ request }) => {
  47. const m = await createManualChild(request, 'ADD');
  48. const r = await apiPost(request, '/api/family/member/list', {}, ctx.token);
  49. expect(r.code).toBe(200);
  50. const hit = r.data.find((x) => String(x.id) === String(m.id));
  51. expect(hit, '新增成员应出现在成员列表中').toBeTruthy();
  52. expect(hit.nickname).toContain('QA_ADD');
  53. });
  54. test('[场景2b] 添加成员时昵称为空 - 期望返回错误提示', async ({ request }) => {
  55. const r = await apiPost(
  56. request,
  57. '/api/family/member/add',
  58. { generationLevel: 'child', nickname: '' },
  59. ctx.token
  60. );
  61. expect(r.code).not.toBe(200);
  62. expect(String(r.message)).toContain('昵称');
  63. });
  64. test('[场景2c] 添加成员时辈分等级为空 - 期望返回错误提示', async ({ request }) => {
  65. const r = await apiPost(
  66. request,
  67. '/api/family/member/add',
  68. { nickname: 'QA_NO_GEN_' + uniqueSuffix() },
  69. ctx.token
  70. );
  71. expect(r.code).not.toBe(200);
  72. expect(String(r.message)).toContain('辈分等级');
  73. });
  74. test('[场景2d] 添加成员时辈分等级非法 - 期望返回错误提示', async ({ request }) => {
  75. const r = await apiPost(
  76. request,
  77. '/api/family/member/add',
  78. { generationLevel: 'not_a_level', nickname: 'QA_BAD_GEN_' + uniqueSuffix() },
  79. ctx.token
  80. );
  81. expect(r.code).not.toBe(200);
  82. expect(String(r.message)).toContain('辈分等级');
  83. });
  84. // ===== 场景3:查看家庭成员列表 =====
  85. test('[场景3] 查看家庭成员列表 - 期望返回所有成员及关系信息', async ({ request }) => {
  86. const m = await createManualChild(request, 'LIST');
  87. const r = await apiPost(request, '/api/family/member/list', {}, ctx.token);
  88. expect(r.code).toBe(200);
  89. expect(r.data.length).toBeGreaterThan(1);
  90. const hit = r.data.find((x) => String(x.id) === String(m.id));
  91. expect(hit).toBeTruthy();
  92. // 关系信息应随成员一并返回(FamilyMemberVO: relationship / roleLabel / relativeLabel / generation)
  93. expect(
  94. 'relationship' in hit || 'roleLabel' in hit || 'relativeLabel' in hit || 'generation' in hit
  95. ).toBeTruthy();
  96. expect(hit.relationship, '应返回关系名称').toBeTruthy();
  97. });
  98. // ===== 场景4:更新成员信息 =====
  99. /**
  100. * 【已知缺陷 CFC-FM-001】远程环境下 /api/family/member/update 更新 nickname
  101. * 返回 code=200 但昵称未变更(gender/birthday 可正常更新),属字段级不生效缺陷;
  102. * 本地源码 FamilyMemberService.updateMember 已含 nickname 分支,
  103. * 疑似远程部署构建落后于当前源码,需研发核对版本。
  104. */
  105. test('[场景4] 更新成员关系/昵称 - 期望更新成功并返回新信息', async ({ request }) => {
  106. test.fixme(true, 'CFC-FM-001:member/update 更新 nickname 返回成功但未生效,修复后移除本标记');
  107. const m = await createManualChild(request, 'UPD');
  108. const newName = 'QA_UPDATED_' + uniqueSuffix();
  109. const r = await apiPost(
  110. request,
  111. '/api/family/member/update',
  112. { memberId: m.id, nickname: newName },
  113. ctx.token
  114. );
  115. expect(r.code, '更新成员应成功').toBe(200);
  116. expect(r.data).toBeTruthy();
  117. const list = await apiPost(request, '/api/family/member/list', {}, ctx.token);
  118. const hit = list.data.find((x) => String(x.id) === String(m.id));
  119. expect(hit.nickname).toBe(newName);
  120. });
  121. test('[场景4c] 已知缺陷复现 - nickname 未生效但 gender/birthday 生效', async ({ request }) => {
  122. // 特征化用例:锁定当前(缺陷)行为;研发修复后本用例会失败,提示移除 场景4 的 fixme
  123. const m = await createManualChild(request, 'UPDC');
  124. const newName = 'QA_NICK_' + uniqueSuffix();
  125. const upd = await apiPost(
  126. request,
  127. '/api/family/member/update',
  128. { memberId: m.id, nickname: newName, gender: 'female', birthday: '2017-05-05' },
  129. ctx.token
  130. );
  131. expect(upd.code).toBe(200);
  132. expect(upd.data.nickname, 'CFC-FM-001 当前表现为昵称未变更').toBe(m.nickname);
  133. // 对照组:其它字段可正常更新
  134. expect(upd.data.gender).toBe('female');
  135. expect(String(upd.data.birthday)).toContain('2017-05-05');
  136. });
  137. // ===== 场景5:切换到成员视角 =====
  138. test('[场景5] 切换到成员视角 - 期望返回切换凭证', async ({ request }) => {
  139. const m = await createManualChild(request, 'SW');
  140. const r = await apiPost(request, '/api/family/member/switch', { memberId: m.id }, ctx.token);
  141. expect(r.code, '切换到手动成员应成功').toBe(200);
  142. expect(r.data).toBeTruthy();
  143. expect(String(r.data.memberId)).toBe(String(m.id));
  144. expect(r.data).toHaveProperty('effectiveRole');
  145. });
  146. test('[场景5b] 切换到本人视角 - 期望返回错误提示', async ({ request }) => {
  147. const list = await apiPost(request, '/api/family/member/list', {}, ctx.token);
  148. const self = pickMemberByUserId(list.data, ctx.userId);
  149. expect(self, '成员列表应包含登录用户自身').toBeTruthy();
  150. const r = await apiPost(request, '/api/family/member/switch', { memberId: self.id }, ctx.token);
  151. expect(r.code).not.toBe(200);
  152. expect(String(r.message)).toContain('不能切换到自己');
  153. });
  154. // ===== 场景6:移除家庭成员 =====
  155. test('[场景6] 移除家庭成员 - 期望成员从家庭中移除', async ({ request }) => {
  156. const m = await createManualChild(request, 'KICK');
  157. const before = await apiPost(request, '/api/family/member/list', {}, ctx.token);
  158. const beforeCount = before.data.length;
  159. const r = await apiPost(request, '/api/family/member/kick', { memberId: m.id }, ctx.token);
  160. expect(r.code, '踢出成员应成功').toBe(200);
  161. const after = await apiPost(request, '/api/family/member/list', {}, ctx.token);
  162. expect(after.data.length).toBeLessThan(beforeCount);
  163. expect(after.data.find((x) => String(x.id) === String(m.id))).toBeFalsy();
  164. });
  165. test('[场景6b] 非管理员尝试踢人 - 期望权限不足拒绝', async ({ request }) => {
  166. const target = await createManualChild(request, 'PERM');
  167. const other = await registerRole(request, 'parent', 'QA-OTHER');
  168. const r = await apiPost(request, '/api/family/member/kick', { memberId: target.id }, other.token);
  169. expect(r.code).not.toBe(200);
  170. });
  171. // ===== 场景7:完整家庭成员管理流程(端到端)=====
  172. test('[场景7] 完整家庭成员管理流程 - 期望全流程成功', async ({ request }) => {
  173. // 1) 添加成员
  174. const nickname = 'QA_E2E_' + uniqueSuffix();
  175. const add = await apiPost(
  176. request,
  177. '/api/family/member/add',
  178. { generationLevel: 'child', nickname, gender: 'female', birthday: '2016-03-08' },
  179. ctx.token
  180. );
  181. expect(add.code).toBe(200);
  182. const memberId = add.data.id;
  183. // 2) 列表可见
  184. const list = await apiPost(request, '/api/family/member/list', {}, ctx.token);
  185. expect(list.data.find((x) => String(x.id) === String(memberId))).toBeTruthy();
  186. // 3) 更新成员
  187. const updated = 'QA_E2E_UPD_' + uniqueSuffix();
  188. const upd = await apiPost(request, '/api/family/member/update', { memberId, nickname: updated }, ctx.token);
  189. expect(upd.code).toBe(200);
  190. // 4) 切换视角
  191. const sw = await apiPost(request, '/api/family/member/switch', { memberId }, ctx.token);
  192. expect(sw.code).toBe(200);
  193. // 5) 移除成员
  194. const kick = await apiPost(request, '/api/family/member/kick', { memberId }, ctx.token);
  195. expect(kick.code).toBe(200);
  196. // 6) 终态校验:成员已不在列表
  197. const finalList = await apiPost(request, '/api/family/member/list', {}, ctx.token);
  198. expect(finalList.data.find((x) => String(x.id) === String(memberId))).toBeFalsy();
  199. });
  200. });