|
@@ -0,0 +1,235 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * 【API级】家庭成员管理全流程
|
|
|
|
|
+ * ================================================================
|
|
|
|
|
+ * 对应 DOM 版:e2e/family-member-management.spec.js(远程无 SPA,DOM 版不可跑)
|
|
|
|
|
+ * 接口契约来源:FamilyMembersController(/api/family/member/*)
|
|
|
|
|
+ * add : { generationLevel, nickname, gender?, birthday?, relativeMemberId?, peerType? }
|
|
|
|
|
+ * list : { familyId? | visibleOnly? | includeFamily? } 越权 → code=403
|
|
|
|
|
+ * switch : { memberId } 切换到自己 → 「不能切换到自己」
|
|
|
|
|
+ * kick : { memberId } 仅家庭创建者可踢
|
|
|
|
|
+ * update : { memberId, nickname?, generationLevel?, ... }
|
|
|
|
|
+ * 运行:npx playwright test -c playwright.config.api.js --project=desktop
|
|
|
|
|
+ */
|
|
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
|
|
+const { registerRole, apiPost, pickMemberByUserId, uniqueSuffix } = require('../helpers/api');
|
|
|
|
|
+
|
|
|
|
|
+test.describe('【API级】家庭成员管理全流程', () => {
|
|
|
|
|
+ let ctx;
|
|
|
|
|
+
|
|
|
|
|
+ test.beforeAll(async ({ request }) => {
|
|
|
|
|
+ ctx = await registerRole(request, 'parent', 'QA-FAMILY');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ /** 创建一个独立的手动成员(非微信登录用户),返回 { id, nickname } */
|
|
|
|
|
+ async function createManualChild(request, tag) {
|
|
|
|
|
+ const nickname = 'QA_' + tag + '_' + uniqueSuffix();
|
|
|
|
|
+ const r = await apiPost(
|
|
|
|
|
+ request,
|
|
|
|
|
+ '/api/family/member/add',
|
|
|
|
|
+ { generationLevel: 'child', nickname, gender: 'male', birthday: '2015-06-15' },
|
|
|
|
|
+ ctx.token
|
|
|
|
|
+ );
|
|
|
|
|
+ expect(r.code, '添加成员应成功').toBe(200);
|
|
|
|
|
+ expect(r.data && r.data.id, '应返回成员ID').toBeTruthy();
|
|
|
|
|
+ return { id: r.data.id, nickname: r.data.nickname || nickname };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 场景1:成员列表(关系/成员数据可获取)=====
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景1] 获取成员列表 - 期望返回成员数据', async ({ request }) => {
|
|
|
|
|
+ const r = await apiPost(request, '/api/family/member/list', {}, ctx.token);
|
|
|
|
|
+ expect(r.code).toBe(200);
|
|
|
|
|
+ expect(Array.isArray(r.data)).toBeTruthy();
|
|
|
|
|
+ expect(r.data.length).toBeGreaterThan(0);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景1b] 越权访问他人家庭 - 期望 403 拒绝', async ({ request }) => {
|
|
|
|
|
+ const r = await apiPost(request, '/api/family/member/list', { familyId: 999999999 }, ctx.token);
|
|
|
|
|
+ expect(r.code).toBe(403);
|
|
|
|
|
+ expect(String(r.message)).toContain('无权访问');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 场景2:添加新家庭成员 =====
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景2] 添加新家庭成员 - 期望添加成功并返回完整信息', async ({ request }) => {
|
|
|
|
|
+ const m = await createManualChild(request, 'ADD');
|
|
|
|
|
+ const r = await apiPost(request, '/api/family/member/list', {}, ctx.token);
|
|
|
|
|
+ expect(r.code).toBe(200);
|
|
|
|
|
+ const hit = r.data.find((x) => String(x.id) === String(m.id));
|
|
|
|
|
+ expect(hit, '新增成员应出现在成员列表中').toBeTruthy();
|
|
|
|
|
+ expect(hit.nickname).toContain('QA_ADD');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景2b] 添加成员时昵称为空 - 期望返回错误提示', async ({ request }) => {
|
|
|
|
|
+ const r = await apiPost(
|
|
|
|
|
+ request,
|
|
|
|
|
+ '/api/family/member/add',
|
|
|
|
|
+ { generationLevel: 'child', nickname: '' },
|
|
|
|
|
+ ctx.token
|
|
|
|
|
+ );
|
|
|
|
|
+ expect(r.code).not.toBe(200);
|
|
|
|
|
+ expect(String(r.message)).toContain('昵称');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景2c] 添加成员时辈分等级为空 - 期望返回错误提示', async ({ request }) => {
|
|
|
|
|
+ const r = await apiPost(
|
|
|
|
|
+ request,
|
|
|
|
|
+ '/api/family/member/add',
|
|
|
|
|
+ { nickname: 'QA_NO_GEN_' + uniqueSuffix() },
|
|
|
|
|
+ ctx.token
|
|
|
|
|
+ );
|
|
|
|
|
+ expect(r.code).not.toBe(200);
|
|
|
|
|
+ expect(String(r.message)).toContain('辈分等级');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景2d] 添加成员时辈分等级非法 - 期望返回错误提示', async ({ request }) => {
|
|
|
|
|
+ const r = await apiPost(
|
|
|
|
|
+ request,
|
|
|
|
|
+ '/api/family/member/add',
|
|
|
|
|
+ { generationLevel: 'not_a_level', nickname: 'QA_BAD_GEN_' + uniqueSuffix() },
|
|
|
|
|
+ ctx.token
|
|
|
|
|
+ );
|
|
|
|
|
+ expect(r.code).not.toBe(200);
|
|
|
|
|
+ expect(String(r.message)).toContain('辈分等级');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 场景3:查看家庭成员列表 =====
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景3] 查看家庭成员列表 - 期望返回所有成员及关系信息', async ({ request }) => {
|
|
|
|
|
+ const m = await createManualChild(request, 'LIST');
|
|
|
|
|
+ const r = await apiPost(request, '/api/family/member/list', {}, ctx.token);
|
|
|
|
|
+ expect(r.code).toBe(200);
|
|
|
|
|
+ expect(r.data.length).toBeGreaterThan(1);
|
|
|
|
|
+ const hit = r.data.find((x) => String(x.id) === String(m.id));
|
|
|
|
|
+ expect(hit).toBeTruthy();
|
|
|
|
|
+ // 关系信息应随成员一并返回(FamilyMemberVO: relationship / roleLabel / relativeLabel / generation)
|
|
|
|
|
+ expect(
|
|
|
|
|
+ 'relationship' in hit || 'roleLabel' in hit || 'relativeLabel' in hit || 'generation' in hit
|
|
|
|
|
+ ).toBeTruthy();
|
|
|
|
|
+ expect(hit.relationship, '应返回关系名称').toBeTruthy();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 场景4:更新成员信息 =====
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 【已知缺陷 CFC-FM-001】远程环境下 /api/family/member/update 更新 nickname
|
|
|
|
|
+ * 返回 code=200 但昵称未变更(gender/birthday 可正常更新),属字段级不生效缺陷;
|
|
|
|
|
+ * 本地源码 FamilyMemberService.updateMember 已含 nickname 分支,
|
|
|
|
|
+ * 疑似远程部署构建落后于当前源码,需研发核对版本。
|
|
|
|
|
+ */
|
|
|
|
|
+ test('[场景4] 更新成员关系/昵称 - 期望更新成功并返回新信息', async ({ request }) => {
|
|
|
|
|
+ test.fixme(true, 'CFC-FM-001:member/update 更新 nickname 返回成功但未生效,修复后移除本标记');
|
|
|
|
|
+ const m = await createManualChild(request, 'UPD');
|
|
|
|
|
+ const newName = 'QA_UPDATED_' + uniqueSuffix();
|
|
|
|
|
+ const r = await apiPost(
|
|
|
|
|
+ request,
|
|
|
|
|
+ '/api/family/member/update',
|
|
|
|
|
+ { memberId: m.id, nickname: newName },
|
|
|
|
|
+ ctx.token
|
|
|
|
|
+ );
|
|
|
|
|
+ expect(r.code, '更新成员应成功').toBe(200);
|
|
|
|
|
+ expect(r.data).toBeTruthy();
|
|
|
|
|
+
|
|
|
|
|
+ const list = await apiPost(request, '/api/family/member/list', {}, ctx.token);
|
|
|
|
|
+ const hit = list.data.find((x) => String(x.id) === String(m.id));
|
|
|
|
|
+ expect(hit.nickname).toBe(newName);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景4c] 已知缺陷复现 - nickname 未生效但 gender/birthday 生效', async ({ request }) => {
|
|
|
|
|
+ // 特征化用例:锁定当前(缺陷)行为;研发修复后本用例会失败,提示移除 场景4 的 fixme
|
|
|
|
|
+ const m = await createManualChild(request, 'UPDC');
|
|
|
|
|
+ const newName = 'QA_NICK_' + uniqueSuffix();
|
|
|
|
|
+
|
|
|
|
|
+ const upd = await apiPost(
|
|
|
|
|
+ request,
|
|
|
|
|
+ '/api/family/member/update',
|
|
|
|
|
+ { memberId: m.id, nickname: newName, gender: 'female', birthday: '2017-05-05' },
|
|
|
|
|
+ ctx.token
|
|
|
|
|
+ );
|
|
|
|
|
+ expect(upd.code).toBe(200);
|
|
|
|
|
+ expect(upd.data.nickname, 'CFC-FM-001 当前表现为昵称未变更').toBe(m.nickname);
|
|
|
|
|
+ // 对照组:其它字段可正常更新
|
|
|
|
|
+ expect(upd.data.gender).toBe('female');
|
|
|
|
|
+ expect(String(upd.data.birthday)).toContain('2017-05-05');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 场景5:切换到成员视角 =====
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景5] 切换到成员视角 - 期望返回切换凭证', async ({ request }) => {
|
|
|
|
|
+ const m = await createManualChild(request, 'SW');
|
|
|
|
|
+ const r = await apiPost(request, '/api/family/member/switch', { memberId: m.id }, ctx.token);
|
|
|
|
|
+ expect(r.code, '切换到手动成员应成功').toBe(200);
|
|
|
|
|
+ expect(r.data).toBeTruthy();
|
|
|
|
|
+ expect(String(r.data.memberId)).toBe(String(m.id));
|
|
|
|
|
+ expect(r.data).toHaveProperty('effectiveRole');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景5b] 切换到本人视角 - 期望返回错误提示', async ({ request }) => {
|
|
|
|
|
+ const list = await apiPost(request, '/api/family/member/list', {}, ctx.token);
|
|
|
|
|
+ const self = pickMemberByUserId(list.data, ctx.userId);
|
|
|
|
|
+ expect(self, '成员列表应包含登录用户自身').toBeTruthy();
|
|
|
|
|
+
|
|
|
|
|
+ const r = await apiPost(request, '/api/family/member/switch', { memberId: self.id }, ctx.token);
|
|
|
|
|
+ expect(r.code).not.toBe(200);
|
|
|
|
|
+ expect(String(r.message)).toContain('不能切换到自己');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 场景6:移除家庭成员 =====
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景6] 移除家庭成员 - 期望成员从家庭中移除', async ({ request }) => {
|
|
|
|
|
+ const m = await createManualChild(request, 'KICK');
|
|
|
|
|
+ const before = await apiPost(request, '/api/family/member/list', {}, ctx.token);
|
|
|
|
|
+ const beforeCount = before.data.length;
|
|
|
|
|
+
|
|
|
|
|
+ const r = await apiPost(request, '/api/family/member/kick', { memberId: m.id }, ctx.token);
|
|
|
|
|
+ expect(r.code, '踢出成员应成功').toBe(200);
|
|
|
|
|
+
|
|
|
|
|
+ const after = await apiPost(request, '/api/family/member/list', {}, ctx.token);
|
|
|
|
|
+ expect(after.data.length).toBeLessThan(beforeCount);
|
|
|
|
|
+ expect(after.data.find((x) => String(x.id) === String(m.id))).toBeFalsy();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景6b] 非管理员尝试踢人 - 期望权限不足拒绝', async ({ request }) => {
|
|
|
|
|
+ const target = await createManualChild(request, 'PERM');
|
|
|
|
|
+ const other = await registerRole(request, 'parent', 'QA-OTHER');
|
|
|
|
|
+
|
|
|
|
|
+ const r = await apiPost(request, '/api/family/member/kick', { memberId: target.id }, other.token);
|
|
|
|
|
+ expect(r.code).not.toBe(200);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 场景7:完整家庭成员管理流程(端到端)=====
|
|
|
|
|
+
|
|
|
|
|
+ test('[场景7] 完整家庭成员管理流程 - 期望全流程成功', async ({ request }) => {
|
|
|
|
|
+ // 1) 添加成员
|
|
|
|
|
+ const nickname = 'QA_E2E_' + uniqueSuffix();
|
|
|
|
|
+ const add = await apiPost(
|
|
|
|
|
+ request,
|
|
|
|
|
+ '/api/family/member/add',
|
|
|
|
|
+ { generationLevel: 'child', nickname, gender: 'female', birthday: '2016-03-08' },
|
|
|
|
|
+ ctx.token
|
|
|
|
|
+ );
|
|
|
|
|
+ expect(add.code).toBe(200);
|
|
|
|
|
+ const memberId = add.data.id;
|
|
|
|
|
+
|
|
|
|
|
+ // 2) 列表可见
|
|
|
|
|
+ const list = await apiPost(request, '/api/family/member/list', {}, ctx.token);
|
|
|
|
|
+ expect(list.data.find((x) => String(x.id) === String(memberId))).toBeTruthy();
|
|
|
|
|
+
|
|
|
|
|
+ // 3) 更新成员
|
|
|
|
|
+ const updated = 'QA_E2E_UPD_' + uniqueSuffix();
|
|
|
|
|
+ const upd = await apiPost(request, '/api/family/member/update', { memberId, nickname: updated }, ctx.token);
|
|
|
|
|
+ expect(upd.code).toBe(200);
|
|
|
|
|
+
|
|
|
|
|
+ // 4) 切换视角
|
|
|
|
|
+ const sw = await apiPost(request, '/api/family/member/switch', { memberId }, ctx.token);
|
|
|
|
|
+ expect(sw.code).toBe(200);
|
|
|
|
|
+
|
|
|
|
|
+ // 5) 移除成员
|
|
|
|
|
+ const kick = await apiPost(request, '/api/family/member/kick', { memberId }, ctx.token);
|
|
|
|
|
+ expect(kick.code).toBe(200);
|
|
|
|
|
+
|
|
|
|
|
+ // 6) 终态校验:成员已不在列表
|
|
|
|
|
+ const finalList = await apiPost(request, '/api/family/member/list', {}, ctx.token);
|
|
|
|
|
+ expect(finalList.data.find((x) => String(x.id) === String(memberId))).toBeFalsy();
|
|
|
|
|
+ });
|
|
|
|
|
+});
|