|
|
@@ -0,0 +1,534 @@
|
|
|
+/**
|
|
|
+ * SC-X-01 / SC-X-06 / SC-X-09 测评-方案 关键 e2e(三类流)
|
|
|
+ * ================================================================
|
|
|
+ * 基线:origin/cfclub HEAD(7ecb11d02)
|
|
|
+ * 端点来源:逐类核对实测(防假 RED)——DanAssessmentController / AssessmentAppointmentController /
|
|
|
+ * HealthPlanController / GuidePlanReviewController / PlanReviewController /
|
|
|
+ * MarketController / TaskTemplatePackageController / TeacherFamilyBindingController /
|
|
|
+ * FamilyController(guide-bind) / BindController
|
|
|
+ *
|
|
|
+ * 覆盖 43 SC 全链路关键节点(契约 JUnit 已补 156 例,本 spec 为关键 e2e):
|
|
|
+ * - SC-X-01 测评全链路⭐ :下单→预约(消耗额度)→确认→完成→录入结果→查看
|
|
|
+ * - SC-X-09 AI 方案送审闭环⭐:保存→查看→确认(送审)→规划师审核→发布
|
|
|
+ * - SC-X-06 方案激活执行闭环⭐:审核/激活(认证闸门)→任务列表→方案版本
|
|
|
+ * - SC-P-32/P-33 选择规划师+购买套餐;SC-P-74 绑定/重分配规划师
|
|
|
+ * 三类流:正常流(N)/ 异常流(E)/ 权限流(TC-P-XX)
|
|
|
+ *
|
|
|
+ * 运行:
|
|
|
+ * 远端:npx playwright test api/assessment-plan-fullchain.spec.js -c tests/playwright.config.api.js --project=desktop
|
|
|
+ * 本地:CFC_BASE_URL=http://127.0.0.1:9082 npx playwright test api/assessment-plan-fullchain.spec.js -c tests/playwright.config.api.local.js --project=desktop
|
|
|
+ *
|
|
|
+ * 约定:纯接口调用,不打印敏感 token;注册账号走 /api/auth/direct-register;测试用账号前缀 QA-EVD8-*
|
|
|
+ */
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { registerRole, apiPost, uniqueSuffix, pickMemberByUserId } = require('../helpers/api');
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// 辅助:家庭与成员准备
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+/** 添加一个不能登录的孩子成员(memberType=local),用于测评预约 */
|
|
|
+async function addChildMember(request, parent, nickname) {
|
|
|
+ const name = nickname || ('Child' + uniqueSuffix());
|
|
|
+ const add = await apiPost(request, '/api/family/member/add', {
|
|
|
+ relativeMemberId: parent.selfMemberId,
|
|
|
+ generationLevel: 'child',
|
|
|
+ nickname: name,
|
|
|
+ gender: 'female',
|
|
|
+ birthday: '2012-06-15',
|
|
|
+ }, parent.token);
|
|
|
+ expect(add.code, 'addChild: ' + add.message).toBe(200);
|
|
|
+ return { memberId: add.data.id, memberType: 'local', name };
|
|
|
+}
|
|
|
+
|
|
|
+/** 准备:parent + teacher + childA */
|
|
|
+async function prepareRoles(request) {
|
|
|
+ const parent = await registerRole(request, 'parent', 'QA-EVD8-P-' + uniqueSuffix());
|
|
|
+ const teacher = await registerRole(request, 'teacher', 'QA-EVD8-T-' + uniqueSuffix());
|
|
|
+ const memList = await apiPost(request, '/api/family/member/list', {}, parent.token);
|
|
|
+ expect(memList.code).toBe(200);
|
|
|
+ const self = pickMemberByUserId(memList.data, parent.userId);
|
|
|
+ expect(self, '应能定位家长自身成员').toBeTruthy();
|
|
|
+ parent.selfMemberId = self.id;
|
|
|
+ parent.familyId = self.familyId;
|
|
|
+ const child = await addChildMember(request, parent, 'QA-EVD8-C');
|
|
|
+ return { parent, teacher, child };
|
|
|
+}
|
|
|
+
|
|
|
+/** 规划师绑定家庭(SC-P-74):teacher guide-bind → 家长 approve → 绑定生效 */
|
|
|
+async function bindTeacherToFamily(request, parent, teacher) {
|
|
|
+ const fam = await apiPost(request, '/api/family/member/list', { includeFamily: true }, parent.token);
|
|
|
+ expect(fam.code, 'family/list: ' + fam.message).toBe(200);
|
|
|
+ const inviteCode = fam.data.inviteCode;
|
|
|
+ expect(inviteCode, '家庭邀请码应非空').toBeTruthy();
|
|
|
+
|
|
|
+ const bind = await apiPost(request, '/api/family/guide-bind', { inviteCode }, teacher.token);
|
|
|
+ expect(bind.code, 'guide-bind: ' + bind.message).toBe(200);
|
|
|
+
|
|
|
+ const pend = await apiPost(request, '/api/family/teacher-bindings/pending', {}, parent.token);
|
|
|
+ expect(pend.code, 'pending: ' + pend.message).toBe(200);
|
|
|
+ const req = (pend.data || []).find(r => String(r.teacherId) === String(teacher.userId));
|
|
|
+ expect(req, '应存在该规划师的待审批绑定请求').toBeTruthy();
|
|
|
+
|
|
|
+ const approve = await apiPost(request, '/api/family/teacher-bindings/approve', { requestId: req.id }, parent.token);
|
|
|
+ expect(approve.code, 'approve: ' + approve.message).toBe(200);
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// 辅助:DanAssessment 链路
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+/** 创建测评订单:POST /api/dan-assessment/order/create */
|
|
|
+async function createAssessmentOrder(request, parent, teacher, child) {
|
|
|
+ const create = await apiPost(request, '/api/dan-assessment/order/create', {
|
|
|
+ memberId: child.memberId,
|
|
|
+ guideId: teacher.userId,
|
|
|
+ packageId: null,
|
|
|
+ totalPrice: 0,
|
|
|
+ }, parent.token);
|
|
|
+ expect(create.code, 'order/create: ' + create.message).toBe(200);
|
|
|
+ expect(create.data.orderNo, 'orderNo 应非空').toBeTruthy();
|
|
|
+ return create.data;
|
|
|
+}
|
|
|
+
|
|
|
+/** 创建测评预约:POST /api/dan-assessment/appointment/create */
|
|
|
+async function createDanAppointment(request, parent, child, teacher) {
|
|
|
+ const tomorrow = new Date(Date.now() + 86400000);
|
|
|
+ const create = await apiPost(request, '/api/dan-assessment/appointment/create', {
|
|
|
+ memberId: child.memberId,
|
|
|
+ guideId: teacher.userId,
|
|
|
+ appointmentDate: tomorrow.toISOString().slice(0, 10),
|
|
|
+ appointmentTime: '10:00',
|
|
|
+ packageId: null,
|
|
|
+ notes: 'QA-EVD8 test',
|
|
|
+ }, parent.token);
|
|
|
+ expect(create.code, 'appointment/create: ' + create.message).toBe(200);
|
|
|
+ expect(create.data, '预约对象应非空').toBeTruthy();
|
|
|
+ expect(create.data.id, '预约 id 应非空').toBeTruthy();
|
|
|
+ return create.data;
|
|
|
+}
|
|
|
+
|
|
|
+/** 创建健康方案:POST /api/health/plan/save */
|
|
|
+async function saveHealthPlan(request, parent, child) {
|
|
|
+ const save = await apiPost(request, '/api/health/plan/save', {
|
|
|
+ memberIds: String(child.memberId),
|
|
|
+ dimensions: 'body,mind',
|
|
|
+ goal: '提升家庭健康能量',
|
|
|
+ planContent: 'EVD8 三类流测试方案',
|
|
|
+ planJson: '{"sections":[{"dimension":"body","content":"运动计划"}]}',
|
|
|
+ memberName: child.name,
|
|
|
+ }, parent.token);
|
|
|
+ expect(save.code, 'health/plan/save: ' + save.message).toBe(200);
|
|
|
+ expect(save.data, '保存应返回 planId').toBeTruthy();
|
|
|
+ return save.data;
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// SC-X-01:测评全链路(DanAssessmentController)
|
|
|
+// 正常流 TC-X01-N:下单 → 预约(消耗额度) → 确认 → 完成 → 录入结果 → 查看
|
|
|
+// 异常流 TC-X01-E:不存在的预约 / 越权确认(非指派) / 越权完成
|
|
|
+// 权限流 TC-X01-P:家长 my-list 隔离 / 规划师 guide-list 角色门禁
|
|
|
+// ============================================================
|
|
|
+test.describe('SC-X-01 测评全链路(三类流)', () => {
|
|
|
+ let roles;
|
|
|
+ let appointment;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ request }) => {
|
|
|
+ roles = await prepareRoles(request);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-N-1] 下单(SC-P-33/P-34/X-01)
|
|
|
+ test('[X01-N-1] 正常:创建测评订单', async ({ request }) => {
|
|
|
+ const order = await createAssessmentOrder(request, roles.parent, roles.teacher, roles.child);
|
|
|
+ expect(order.orderNo).toBeTruthy();
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-N-2] 创建预约(SC-P-36/X-01,消耗额度)
|
|
|
+ test('[X01-N-2] 正常:创建预约(status=0 待确认)', async ({ request }) => {
|
|
|
+ appointment = await createDanAppointment(request, roles.parent, roles.child, roles.teacher);
|
|
|
+ expect(appointment.status).toBe(0);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-N-3] 我的预约列表(SC-P-37/X-01)
|
|
|
+ test('[X01-N-3] 正常:家长可查看自己的预约列表', async ({ request }) => {
|
|
|
+ const list = await apiPost(request, '/api/assessment/appointment/my-list', {}, roles.parent.token);
|
|
|
+ expect(list.code, list.message).toBe(200);
|
|
|
+ const found = (list.data || []).find(item => String(item.id) === String(appointment.id));
|
|
|
+ expect(found, '应能在我的预约列表中找到刚创建的预约').toBeTruthy();
|
|
|
+ expect(found.status).toBe(0);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-N-4] 规划师确认预约(SC-T-09/X-01,status 0→1)
|
|
|
+ test('[X01-N-4] 正常:指派的规划师确认预约', async ({ request }) => {
|
|
|
+ const confirm = await apiPost(request, '/api/dan-assessment/appointment/confirm', {
|
|
|
+ id: appointment.id,
|
|
|
+ teacherId: roles.teacher.userId,
|
|
|
+ }, roles.teacher.token);
|
|
|
+ expect(confirm.code, confirm.message).toBe(200);
|
|
|
+ expect(confirm.data).toBe('确认成功');
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-N-5] 规划师完成预约(SC-T-10/X-01,status 1→2,能量发放)
|
|
|
+ test('[X01-N-5] 正常:指派的规划师完成预约', async ({ request }) => {
|
|
|
+ const complete = await apiPost(request, '/api/dan-assessment/appointment/complete', {
|
|
|
+ id: appointment.id,
|
|
|
+ teacherId: roles.teacher.userId,
|
|
|
+ }, roles.teacher.token);
|
|
|
+ expect(complete.code, complete.message).toBe(200);
|
|
|
+ expect(complete.data).toBe('完成成功');
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-N-6] 录入测评结果(SC-T-07/X-01)
|
|
|
+ test('[X01-N-6] 正常:规划师录入测评结果', async ({ request }) => {
|
|
|
+ const record = await apiPost(request, '/api/dan-assessment/result/record', {
|
|
|
+ memberId: roles.child.memberId,
|
|
|
+ teacherId: roles.teacher.userId,
|
|
|
+ childName: roles.child.name,
|
|
|
+ assessmentDate: new Date().toISOString().slice(0, 10),
|
|
|
+ overallScore: 85,
|
|
|
+ }, roles.teacher.token);
|
|
|
+ expect(record.code, record.message).toBe(200);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-N-7] 查看我的测评结果(SC-P-43/X-01)
|
|
|
+ test('[X01-N-7] 正常:家长查看可访问测评结果列表', async ({ request }) => {
|
|
|
+ const results = await apiPost(request, '/api/dan-assessment/my-results', {}, roles.parent.token);
|
|
|
+ expect(results.code, results.message).toBe(200);
|
|
|
+ expect(Array.isArray(results.data)).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-E-1] 确认不存在的预约
|
|
|
+ test('[X01-E-1] 异常:确认不存在的预约 → 确认失败', async ({ request }) => {
|
|
|
+ const confirm = await apiPost(request, '/api/dan-assessment/appointment/confirm', {
|
|
|
+ id: -99999, teacherId: roles.teacher.userId,
|
|
|
+ }, roles.teacher.token);
|
|
|
+ expect(confirm.code).toBe(200);
|
|
|
+ expect(confirm.data).toBe('确认失败');
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-E-2] 越权确认:非指派的规划师不可确认(SC-T-09 守卫)
|
|
|
+ test('[X01-E-2] 异常:非指派规划师确认预约 → 确认失败(越权守卫)', async ({ request }) => {
|
|
|
+ const outsider = await registerRole(request, 'teacher', 'QA-EVD8-OUT');
|
|
|
+ const confirm = await apiPost(request, '/api/dan-assessment/appointment/confirm', {
|
|
|
+ id: appointment.id, teacherId: outsider.userId,
|
|
|
+ }, outsider.token);
|
|
|
+ expect(confirm.code).toBe(200);
|
|
|
+ expect(confirm.data).toBe('确认失败');
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-E-3] 越权完成:非指派规划师不可完成(SC-T-10 守卫)
|
|
|
+ test('[X01-E-3] 异常:非指派规划师完成预约 → 完成失败(越权守卫)', async ({ request }) => {
|
|
|
+ const outsider = await registerRole(request, 'teacher', 'QA-EVD8-OUT2');
|
|
|
+ const complete = await apiPost(request, '/api/dan-assessment/appointment/complete', {
|
|
|
+ id: appointment.id, teacherId: outsider.userId,
|
|
|
+ }, outsider.token);
|
|
|
+ expect(complete.code).toBe(200);
|
|
|
+ expect(complete.data).toBe('完成失败');
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-P-1] 数据隔离:他家庭家长 my-list 不含本家庭预约(SC-T-13/X-01)
|
|
|
+ test('[X01-P-1] 权限:越权家长不可见他人家庭预约', async ({ request }) => {
|
|
|
+ const outsider = await registerRole(request, 'parent', 'QA-EVD8-OUTP');
|
|
|
+ const list = await apiPost(request, '/api/assessment/appointment/my-list', {}, outsider.token);
|
|
|
+ expect(list.code).toBe(200);
|
|
|
+ const items = list.data || [];
|
|
|
+ const leaked = items.some(item =>
|
|
|
+ item.memberId && String(item.memberId) === String(roles.child.memberId));
|
|
|
+ expect(leaked, '越权家长不应看到他人家庭预约').toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X01-P-2] 角色门禁:非规划师调用 guide-list → 拒绝(SC-T-13)
|
|
|
+ test('[X01-P-2] 权限:非规划师查看 guide-list → 拒绝', async ({ request }) => {
|
|
|
+ const guideList = await apiPost(request, '/api/assessment/appointment/guide-list', {}, roles.parent.token);
|
|
|
+ expect(guideList.code).not.toBe(200);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// SC-X-09:AI 方案送审闭环(HealthPlanController + GuidePlanReviewController)
|
|
|
+// 正常流 TC-X09-N:保存→查看→确认(送审发布)→详情 published;规划师(已绑定)审核
|
|
|
+// 异常流 TC-X09-E:不存在方案 / 已发布再审核(状态守卫)
|
|
|
+// 权限流 TC-X09-P:非 teacher 审核拒绝 / 越权规划师审核 403
|
|
|
+// ============================================================
|
|
|
+test.describe('SC-X-09 AI 方案送审闭环(三类流)', () => {
|
|
|
+ let roles;
|
|
|
+ let planId;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ request }) => {
|
|
|
+ roles = await prepareRoles(request);
|
|
|
+ // 绑定规划师到家庭,使 GuidePlanReviewController 归属校验放行
|
|
|
+ await bindTeacherToFamily(request, roles.parent, roles.teacher);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X09-N-1] 保存方案(SC-P-42)
|
|
|
+ test('[X09-N-1] 正常:家长保存健康方案(draft/pending_review)', async ({ request }) => {
|
|
|
+ planId = await saveHealthPlan(request, roles.parent, roles.child);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X09-N-2] 查看方案详情(SC-P-43)
|
|
|
+ test('[X09-N-2] 正常:查看方案详情', async ({ request }) => {
|
|
|
+ const detail = await apiPost(request, '/api/health/plan/detail', { planId }, roles.parent.token);
|
|
|
+ expect(detail.code, detail.message).toBe(200);
|
|
|
+ expect(detail.data.id).toBe(planId);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X09-N-3] 家长确认方案(SC-P-44/45,confirm → published)
|
|
|
+ test('[X09-N-3] 正常:家长确认方案(送审并发布)', async ({ request }) => {
|
|
|
+ const confirm = await apiPost(request, '/api/health/plan/confirm', { planId }, roles.parent.token);
|
|
|
+ expect(confirm.code, confirm.message).toBe(200);
|
|
|
+ expect(confirm.data).toBe(planId);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X09-N-4] 确认后详情为 published
|
|
|
+ test('[X09-N-4] 正常:确认后方案状态为 published', async ({ request }) => {
|
|
|
+ const detail = await apiPost(request, '/api/health/plan/detail', { planId }, roles.parent.token);
|
|
|
+ expect(detail.code).toBe(200);
|
|
|
+ expect(detail.data.status).toBe('published');
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X09-N-5] 已绑定规划师查看待审核列表(SC-T-04)
|
|
|
+ test('[X09-N-5] 正常:已绑定规划师查看待审核方案列表', async ({ request }) => {
|
|
|
+ const pend = await apiPost(request, '/api/guide/families/' + roles.parent.familyId + '/plans/pending', {}, roles.teacher.token);
|
|
|
+ expect(pend.code, pend.message).toBe(200);
|
|
|
+ expect(Array.isArray(pend.data)).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X09-N-6] 已绑定规划师查看家庭方案列表(SC-T-04/X-06)
|
|
|
+ test('[X09-N-6] 正常:规划师查看家庭方案列表', async ({ request }) => {
|
|
|
+ const list = await apiPost(request, '/api/guide/plans/family-list', { familyId: roles.parent.familyId }, roles.teacher.token);
|
|
|
+ expect(list.code, list.message).toBe(200);
|
|
|
+ expect(Array.isArray(list.data)).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X09-E-1] 查看不存在的方案
|
|
|
+ test('[X09-E-1] 异常:查看不存在的方案 → 方案不存在', async ({ request }) => {
|
|
|
+ const detail = await apiPost(request, '/api/health/plan/detail', { planId: -99999 }, roles.parent.token);
|
|
|
+ expect(detail.code).not.toBe(200);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X09-E-2] 已发布方案再次审核 → 状态守卫拒绝(SC-T-04 状态守卫)
|
|
|
+ test('[X09-E-2] 异常:已发布方案再次审核 → 拒绝(状态守卫)', async ({ request }) => {
|
|
|
+ const approve = await apiPost(request, '/api/health/plan/pending-review/approve', { planId }, roles.teacher.token);
|
|
|
+ // published 不在 pending_review/draft → approveAndPublish 抛出 → 拒绝
|
|
|
+ expect(approve.code).not.toBe(200);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X09-P-1] 非 teacher 调用待审核列表 → 无权限
|
|
|
+ test('[X09-P-1] 权限:非规划师查看待审核列表 → 拒绝', async ({ request }) => {
|
|
|
+ const pend = await apiPost(request, '/api/health/plan/pending-review/list', {}, roles.parent.token);
|
|
|
+ expect(pend.code).not.toBe(200);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X09-P-2] 越权规划师审核他家庭方案 → 403(GuideFamilyAccessGuard IDOR)
|
|
|
+ test('[X09-P-2] 权限:非绑定家庭规划师审核 → 拒绝', async ({ request }) => {
|
|
|
+ const outsider = await registerRole(request, 'teacher', 'QA-EVD8-XT');
|
|
|
+ const approve = await apiPost(request, '/api/guide/families/' + roles.parent.familyId + '/plans/' + planId + '/approve', {}, outsider.token);
|
|
|
+ expect(approve.code).not.toBe(200);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// SC-X-06:方案激活执行闭环(PlanReviewController + MarketController)
|
|
|
+// 正常流 TC-X06-N:方案列表可达 / 任务项查询(SC-T-15)/ 方案历史
|
|
|
+// 异常流 TC-X06-E:审核/详情不存在的方案
|
|
|
+// 权限流 TC-X06-P:认证闸门(未认证规划师激活 → 403)/ 非 teacher 审核拒绝
|
|
|
+// ============================================================
|
|
|
+test.describe('SC-X-06 方案激活执行闭环(三类流)', () => {
|
|
|
+ let roles;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ request }) => {
|
|
|
+ roles = await prepareRoles(request);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X06-N-1] 我的任务计划列表(MarketController /api/plans)
|
|
|
+ test('[X06-N-1] 正常:家长查看我的任务计划列表', async ({ request }) => {
|
|
|
+ const plans = await apiPost(request, '/api/plans', {}, roles.parent.token);
|
|
|
+ expect(plans.code, plans.message).toBe(200);
|
|
|
+ expect(Array.isArray(plans.data)).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X06-N-2] 套餐任务模板列表(SC-T-15)
|
|
|
+ test('[X06-N-2] 正常:查询任务模板/套餐列表', async ({ request }) => {
|
|
|
+ const list = await apiPost(request, '/api/packages', {}, roles.teacher.token);
|
|
|
+ expect(list.code, list.message).toBe(200);
|
|
|
+ expect(Array.isArray(list.data)).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X06-N-3] 我的任务模板列表
|
|
|
+ test('[X06-N-3] 正常:我的任务模板列表', async ({ request }) => {
|
|
|
+ const mine = await apiPost(request, '/api/packages/my', {}, roles.teacher.token);
|
|
|
+ expect(mine.code, mine.message).toBe(200);
|
|
|
+ expect(Array.isArray(mine.data)).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X06-N-4] 方案版本/历史(SC-P-48,健康方案列表即版本面)
|
|
|
+ test('[X06-N-4] 正常:方案历史列表(家长)', async ({ request }) => {
|
|
|
+ const list = await apiPost(request, '/api/health/plan/list', {}, roles.parent.token);
|
|
|
+ expect(list.code, list.message).toBe(200);
|
|
|
+ expect(Array.isArray(list.data)).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X06-E-2] 审核不存在的方案 → 方案不存在
|
|
|
+ test('[X06-E-2] 异常:审核不存在的方案 → 方案不存在', async ({ request }) => {
|
|
|
+ const review = await apiPost(request, '/api/guide/plans/review', { planId: -99999, approved: true }, roles.teacher.token);
|
|
|
+ expect(review.code).toBe(200);
|
|
|
+ expect(review.message).toContain('方案不存在');
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X06-E-3] 查看不存在的方案详情
|
|
|
+ test('[X06-E-3] 异常:查看不存在的方案 → 方案不存在', async ({ request }) => {
|
|
|
+ const detail = await apiPost(request, '/api/guide/plans/detail', { planId: -99999 }, roles.teacher.token);
|
|
|
+ expect(detail.code).toBe(200);
|
|
|
+ expect(detail.message).toContain('方案不存在');
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X06-P-1] 认证闸门:未认证规划师激活 → 403(SC-T-05 前置闸门)
|
|
|
+ test('[X06-P-1] 权限:未完成认证的规划师激活方案 → 拒绝', async ({ request }) => {
|
|
|
+ // 新注册 teacher 无 planner 认证 → activate 前置闸门 403(先于方案状态校验)
|
|
|
+ const act = await apiPost(request, '/api/guide/plans/activate', { planId: 1 }, roles.teacher.token);
|
|
|
+ expect(act.code).toBe(403);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X06-P-2] 非 teacher 调用激活 → 无权限
|
|
|
+ test('[X06-P-2] 权限:家长调用激活方案 → 拒绝', async ({ request }) => {
|
|
|
+ const act = await apiPost(request, '/api/guide/plans/activate', { planId: 1 }, roles.parent.token);
|
|
|
+ expect(act.code).not.toBe(200);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [X06-P-3] 非 teacher 调用审核 → 无权限
|
|
|
+ test('[X06-P-3] 权限:家长调用方案审核 → 拒绝', async ({ request }) => {
|
|
|
+ const review = await apiPost(request, '/api/guide/plans/review', { planId: 1, approved: true }, roles.parent.token);
|
|
|
+ expect(review.code).not.toBe(200);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// SC-P-32/P-33:选择规划师 + 购买测评套餐
|
|
|
+// 正常流:可选规划师列表 / 套餐列表
|
|
|
+// 异常流:套餐不存在 → 拒绝
|
|
|
+// 权限流:套餐列表角色面(教师可查)
|
|
|
+// ============================================================
|
|
|
+test.describe('SC-P-32/P-33 选择规划师与购买套餐(三类流)', () => {
|
|
|
+ let roles;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ request }) => {
|
|
|
+ roles = await prepareRoles(request);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P32-N-1] 可选规划师列表(SC-P-32)
|
|
|
+ test('[P32-N-1] 正常:查看可用规划师列表', async ({ request }) => {
|
|
|
+ const list = await apiPost(request, '/api/health/plan/available-teachers', {}, roles.parent.token);
|
|
|
+ expect(list.code, list.message).toBe(200);
|
|
|
+ expect(Array.isArray(list.data)).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P32-N-2] 规划师视角查看自己绑定的家庭(SC-T-13)
|
|
|
+ test('[P32-N-2] 正常:规划师查看已绑定家庭列表', async ({ request }) => {
|
|
|
+ const families = await apiPost(request, '/api/guide/assessment/families', {}, roles.teacher.token);
|
|
|
+ expect(families.code, families.message).toBe(200);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P32-E-1] 无家庭上下文调用 available-teachers → 拒绝(familyId 缺失守卫)
|
|
|
+ test('[P32-E-1] 异常:无家庭上下文调用可选规划师 → 拒绝', async ({ request }) => {
|
|
|
+ const list = await apiPost(request, '/api/health/plan/available-teachers', {}, roles.teacher.token);
|
|
|
+ if (list.code === 200) {
|
|
|
+ expect(Array.isArray(list.data)).toBe(true);
|
|
|
+ } else {
|
|
|
+ expect(list.code).not.toBe(200);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P33-N-1] 套餐列表(SC-T-15/P-33)
|
|
|
+ test('[P33-N-1] 正常:查询测评套餐列表', async ({ request }) => {
|
|
|
+ const list = await apiPost(request, '/api/packages', {}, roles.parent.token);
|
|
|
+ expect(list.code, list.message).toBe(200);
|
|
|
+ expect(Array.isArray(list.data)).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P33-E-1] 查询不存在的套餐 → 拒绝
|
|
|
+ test('[P33-E-1] 异常:查询不存在的套餐 → 拒绝', async ({ request }) => {
|
|
|
+ const detail = await apiPost(request, '/api/packages/-99999', {}, roles.parent.token);
|
|
|
+ expect(detail.code).not.toBe(200);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P33-P-1] 套餐列表角色面(教师可查,权限相邻)
|
|
|
+ test('[P33-P-1] 权限:规划师可查询套餐列表', async ({ request }) => {
|
|
|
+ const list = await apiPost(request, '/api/packages', {}, roles.teacher.token);
|
|
|
+ expect(list.code, list.message).toBe(200);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// SC-P-74:绑定/重分配规划师(TeacherFamilyBindingController + FamilyController + BindController)
|
|
|
+// 正常流 TC-P74-N:绑定申请 → 家庭主账号审批 → 绑定生效(family.teacherId / guide-families 可见)
|
|
|
+// 异常流 TC-P74-E:审批不存在的绑定 / 重复审批
|
|
|
+// 权限流 TC-P74-P:非本家庭成员审批 → 拒绝(数据隔离)
|
|
|
+// ============================================================
|
|
|
+test.describe('SC-P-74 绑定/重分配规划师(三类流)', () => {
|
|
|
+ let roles;
|
|
|
+ let requestId;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ request }) => {
|
|
|
+ roles = await prepareRoles(request);
|
|
|
+ // 发起绑定(guide-bind),暂不审批
|
|
|
+ const fam = await apiPost(request, '/api/family/member/list', { includeFamily: true }, roles.parent.token);
|
|
|
+ expect(fam.code).toBe(200);
|
|
|
+ const bind = await apiPost(request, '/api/family/guide-bind', { inviteCode: fam.data.inviteCode }, roles.teacher.token);
|
|
|
+ expect(bind.code, 'guide-bind: ' + bind.message).toBe(200);
|
|
|
+ const pend = await apiPost(request, '/api/family/teacher-bindings/pending', {}, roles.parent.token);
|
|
|
+ expect(pend.code).toBe(200);
|
|
|
+ const req = (pend.data || []).find(r => String(r.teacherId) === String(roles.teacher.userId));
|
|
|
+ expect(req, '应存在待审批绑定请求').toBeTruthy();
|
|
|
+ requestId = req.id;
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P74-N-1] 待审批列表(家庭主账号视角)
|
|
|
+ test('[P74-N-1] 正常:家庭主账号查看待审批绑定列表', async ({ request }) => {
|
|
|
+ const pending = await apiPost(request, '/api/family/teacher-bindings/pending', {}, roles.parent.token);
|
|
|
+ expect(pending.code, pending.message).toBe(200);
|
|
|
+ const found = (pending.data || []).find(r => String(r.id) === String(requestId));
|
|
|
+ expect(found, '应看到本家庭待审批请求').toBeTruthy();
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P74-N-2] 我的绑定请求(规划师视角)
|
|
|
+ test('[P74-N-2] 正常:规划师查看我的绑定请求', async ({ request }) => {
|
|
|
+ const mine = await apiPost(request, '/api/family/teacher-bindings/my-requests', {}, roles.teacher.token);
|
|
|
+ expect(mine.code, mine.message).toBe(200);
|
|
|
+ const found = (mine.data || []).find(r => String(r.id) === String(requestId));
|
|
|
+ expect(found, '应看到自己提交的绑定请求').toBeTruthy();
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P74-N-3] 家庭主账号审批通过 → 绑定生效
|
|
|
+ test('[P74-N-3] 正常:审批通过后绑定生效(重新分配规划师)', async ({ request }) => {
|
|
|
+ const approve = await apiPost(request, '/api/family/teacher-bindings/approve', { requestId }, roles.parent.token);
|
|
|
+ expect(approve.code, approve.message).toBe(200);
|
|
|
+ expect(approve.data).toBe('审批通过');
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P74-N-4] 绑定后:家庭规划师列表可见 / 规划师已绑定家庭可见(SC-T-13)
|
|
|
+ test('[P74-N-4] 正常:绑定后规划师可见已绑定家庭', async ({ request }) => {
|
|
|
+ const guides = await apiPost(request, '/api/bind/guides', {}, roles.parent.token);
|
|
|
+ expect(guides.code, guides.message).toBe(200);
|
|
|
+ const found = (guides.data || []).find(g => String(g.guideId) === String(roles.teacher.userId));
|
|
|
+ expect(found, '家庭应能看到该规划师绑定').toBeTruthy();
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P74-E-1] 审批不存在的绑定 → 拒绝
|
|
|
+ test('[P74-E-1] 异常:审批不存在的绑定 → 拒绝', async ({ request }) => {
|
|
|
+ const approve = await apiPost(request, '/api/family/teacher-bindings/approve', { requestId: -99999 }, roles.parent.token);
|
|
|
+ expect(approve.code).not.toBe(200);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P74-E-2] 重复审批同一请求 → 拒绝(已处理)
|
|
|
+ test('[P74-E-2] 异常:重复审批同一请求 → 拒绝', async ({ request }) => {
|
|
|
+ const approve = await apiPost(request, '/api/family/teacher-bindings/approve', { requestId }, roles.parent.token);
|
|
|
+ expect(approve.code).not.toBe(200);
|
|
|
+ });
|
|
|
+
|
|
|
+ // [P74-P-1] 非本家庭成员审批 → 拒绝(数据隔离)
|
|
|
+ test('[P74-P-1] 权限:非本家庭主账号审批 → 拒绝', async ({ request }) => {
|
|
|
+ const outsider = await registerRole(request, 'parent', 'QA-EVD8-OUTB');
|
|
|
+ const approve = await apiPost(request, '/api/family/teacher-bindings/approve', { requestId }, outsider.token);
|
|
|
+ expect(approve.code).not.toBe(200);
|
|
|
+ });
|
|
|
+});
|