| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- /**
- * EVD-7 报告闭环批次(23 SC)— 三类流 API 级 e2e
- * ================================================================
- * 覆盖 SC:SC-A-02/A-16、SC-C-06/C-07、SC-P-24/P-25/P-39/P-67/P-84/P-91、
- * SC-S-09、SC-T-03/T-11/T-12/T-21/T-23、SC-X-02/X-10/X-13。
- * 与 cfc-backend/src/test/java/com/etotem/cfc/unit/*Report*ContractTest 契约 JUnit 互补:
- * - JUnit:Service/Controller 分支级契约(纯 Mockito,确定性);
- * - 本 spec:真实 HTTP + 真实 DB,验证「登录 → 家庭上下文 → 端点 → Result 外壳」主链路。
- * 不在本 spec 覆盖:SC-B-04/B-11(LangGraph 报告解读/追问,见
- * cfc-langgraph/tests_contract/test_growth_report_contract.py)、SC-P-82(MCP JSON-RPC
- * 需 Mcp-Api-Key,见 GlobalSearchScP82ContractTest)。
- *
- * 运行(默认远端;可用 CFC_API_BASE 指向本地 9082):
- * npx playwright test api/report-closedloop-contract.spec.js \
- * -c playwright.config.api.js --project=desktop
- * CFC_API_BASE=http://127.0.0.1:9082 npx playwright test api/report-closedloop-contract.spec.js \
- * -c playwright.config.api.js --project=desktop
- *
- * 安全约定:用例只自注册临时账号、只读写自己的数据;不打印 token / 手机号。
- */
- const { test, expect } = require('@playwright/test');
- const { registerRole, apiPost, pickMemberByUserId, uniqueSuffix } = require('../helpers/api');
- test.describe('EVD-7 报告闭环 三类流(23 SC)', () => {
- let parent;
- let teacher;
- let selfMemberId;
- let teacherMemberId;
- test.beforeAll(async ({ request }) => {
- parent = await registerRole(request, 'parent', 'QA-EVD7-P' + uniqueSuffix());
- const list = await apiPost(request, '/api/family/member/list', {}, parent.token);
- expect(list.code, list.message).toBe(200);
- const self = pickMemberByUserId(list.data, parent.userId);
- expect(self, '成员列表应包含登录用户自身').toBeTruthy();
- selfMemberId = self.id;
- teacher = await registerRole(request, 'teacher', 'QA-EVD7-T' + uniqueSuffix());
- const teacherList = await apiPost(request, '/api/family/member/list', {}, teacher.token);
- expect(teacherList.code, teacherList.message).toBe(200);
- const teacherSelf = pickMemberByUserId(teacherList.data, teacher.userId);
- expect(teacherSelf, 'teacher 成员列表应包含自身').toBeTruthy();
- teacherMemberId = teacherSelf.id;
- });
- // ==================== 正常流 ====================
- test.describe('正常流', () => {
- test('SC-C-06/P-24 报告列表按成员返回数组(只读)', async ({ request }) => {
- const r = await apiPost(request, '/api/health/report/list', { memberId: selfMemberId }, parent.token);
- expect(r.code, r.message).toBe(200);
- expect(Array.isArray(r.data)).toBe(true);
- });
- test('SC-P-24/X-02 家庭报告列表返回数组', async ({ request }) => {
- const r = await apiPost(request, '/api/health/reports/family', {}, parent.token);
- expect(r.code, r.message).toBe(200);
- expect(Array.isArray(r.data)).toBe(true);
- });
- test('SC-P-25/X-10 报告趋势返回 scores/dates 结构', async ({ request }) => {
- const r = await apiPost(request, '/api/health/nutrition/trend', { memberId: selfMemberId }, parent.token);
- expect(r.code, r.message).toBe(200);
- expect(r.data && Array.isArray(r.data.scores)).toBe(true);
- expect(r.data && Array.isArray(r.data.dates)).toBe(true);
- });
- test('SC-P-84 授权→查询→撤销闭环(成员级报告权限)', async ({ request }) => {
- const grant = await apiPost(request, '/api/health/report/member-permission/grant',
- { targetMemberId: selfMemberId, granteeUserId: parent.userId }, parent.token);
- expect(grant.code, grant.message).toBe(200);
- expect(grant.data).toBe(true);
- const byMember = await apiPost(request,
- '/api/health/report/member-permission/members/' + selfMemberId, {}, parent.token);
- expect(byMember.code, byMember.message).toBe(200);
- expect(byMember.data.map(String)).toContain(String(parent.userId));
- const all = await apiPost(request, '/api/health/report/member-permission/list', {}, parent.token);
- expect(all.code, all.message).toBe(200);
- expect(Array.isArray(all.data)).toBe(true);
- // 家庭管理员 visible-members 返回 null(全部可见语义);普通成员返回集合
- const visible = await apiPost(request,
- '/api/health/report/member-permission/visible-members', {}, parent.token);
- expect(visible.code, visible.message).toBe(200);
- expect(visible.data === null || Array.isArray(visible.data)).toBe(true);
- const revoke = await apiPost(request, '/api/health/report/member-permission/revoke',
- { targetMemberId: selfMemberId, granteeUserId: parent.userId }, parent.token);
- expect(revoke.code, revoke.message).toBe(200);
- expect(revoke.data).toBe(true);
- });
- test('SC-C-07/P-39/T-12 DAN 报告列表返回数组', async ({ request }) => {
- const r = await apiPost(request, '/api/dan-report/list', { memberId: selfMemberId }, parent.token);
- expect(r.code, r.message).toBe(200);
- expect(Array.isArray(r.data)).toBe(true);
- });
- test('SC-S-09/A-02 报告解析类型↔商品链接列表(服务商接入真实契约)', async ({ request }) => {
- const r = await apiPost(request, '/api/report-parser/types/links', {}, parent.token);
- expect(r.code, r.message).toBe(200);
- expect(Array.isArray(r.data)).toBe(true);
- });
- test('SC-X-13 报告转交请求列表返回 incoming/outgoing', async ({ request }) => {
- const r = await apiPost(request, '/api/health/report/transfer/list', {}, parent.token);
- expect(r.code, r.message).toBe(200);
- expect(Array.isArray(r.data && r.data.incoming)).toBe(true);
- expect(Array.isArray(r.data && r.data.outgoing)).toBe(true);
- });
- test('SC-T-03/T-23 代传确认(teacher 角色通过权限闸门后进入参数校验)', async ({ request }) => {
- // teacher 属于代传上传者角色:通过 isProxyUploader 后,缺 draftId 返回业务错误而非「无权」
- const r = await apiPost(request, '/api/health/report/proxy-confirm', {}, teacher.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('draftId');
- });
- });
- // ==================== 异常流 ====================
- test.describe('异常流', () => {
- test('SC-P-25 趋势缺 memberId → memberId不能为空', async ({ request }) => {
- const r = await apiPost(request, '/api/health/nutrition/trend', {}, parent.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('memberId不能为空');
- });
- test('SC-P-24 指标对比缺 reportId → reportId不能为空', async ({ request }) => {
- const r = await apiPost(request, '/api/health/report/indicators', {}, parent.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('reportId不能为空');
- });
- test('SC-P-91 指标对比 reportId 不存在 → 报告不存在', async ({ request }) => {
- const r = await apiPost(request, '/api/health/report/indicators', { reportId: 999999999 }, parent.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('报告不存在');
- });
- test('SC-A-02/A-16 解析检测缺 filePath → filePath 不能为空', async ({ request }) => {
- const r = await apiPost(request, '/api/report-parser/detect', {}, parent.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('filePath');
- });
- test('SC-P-84 授权缺参数 → targetMemberId/granteeUserId 不能为空', async ({ request }) => {
- const r = await apiPost(request, '/api/health/report/member-permission/grant', {}, parent.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('不能为空');
- });
- test('SC-T-21/X-10 新成员无两次测评 → 对比返回业务错误', async ({ request }) => {
- const r = await apiPost(request, '/api/assessment/compare/by-child', { memberId: selfMemberId }, parent.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('两次');
- });
- test('SC-C-07/T-12 DAN 对比缺 uploadId → 缺少 uploadIdA 与 uploadIdB', async ({ request }) => {
- const r = await apiPost(request, '/api/dan-report/compare', {}, parent.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('uploadIdA');
- });
- test('SC-X-13 转交审批缺 requestId → 请求ID不能为空', async ({ request }) => {
- const r = await apiPost(request, '/api/health/report/transfer/approve', {}, parent.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('申请ID不能为空');
- });
- test('SC-T-03/X-02 确认不存在的草稿 → 草稿不存在或已归档', async ({ request }) => {
- const r = await apiPost(request, '/api/health/report/audit/confirm', { draftId: 999999999 }, parent.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('草稿不存在');
- });
- });
- // ==================== 权限流 ====================
- test.describe('权限流', () => {
- test('SC-C-06/X-02 报告列表无 token → 401', async ({ request }) => {
- const r = await apiPost(request, '/api/health/report/list', { memberId: selfMemberId }, null);
- expect(r.status).toBe(401);
- });
- test('SC-X-13 转交列表无 token → 401', async ({ request }) => {
- const r = await apiPost(request, '/api/health/report/transfer/list', {}, null);
- expect(r.status).toBe(401);
- });
- test('SC-P-67/T-03 代传确认无 token → 401(LOGIN_REQUIRED_PATHS 闸门)', async ({ request }) => {
- const r = await apiPost(request, '/api/health/report/proxy-confirm', { draftId: 1 }, null);
- expect(r.status).toBe(401);
- });
- test('SC-C-07/T-12 DAN 列表非法 token → 401', async ({ request }) => {
- const r = await apiPost(request, '/api/dan-report/list', { memberId: selfMemberId }, 'invalid-token-value');
- expect(r.status).toBe(401);
- });
- test('SC-S-09 解析类型链接非法 token → 401', async ({ request }) => {
- const r = await apiPost(request, '/api/report-parser/types/links', {}, 'invalid-token-value');
- expect(r.status).toBe(401);
- });
- test('SC-P-67/T-23 家长非代传角色调用代传确认 → 无权', async ({ request }) => {
- const r = await apiPost(request, '/api/health/report/proxy-confirm', { draftId: 1 }, parent.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('无权');
- });
- test('SC-T-03/X-02 非草稿所有者确认他人草稿 → 无权确认他人创建的草稿', async ({ request }) => {
- const pending = await apiPost(request, '/api/health/report/audit/pending', { page: 1, size: 20 }, parent.token);
- expect(pending.code, pending.message).toBe(200);
- const othersDraft = (pending.data || []).find((d) => String(d.userId) !== String(parent.userId));
- if (!othersDraft) {
- test.skip(true, '当前测试库无他人待审草稿,跳过越权确认用例');
- return;
- }
- const r = await apiPost(request, '/api/health/report/audit/confirm',
- { draftId: othersDraft.id }, parent.token);
- expect(r.code, r.message).not.toBe(200);
- expect(String(r.message)).toContain('无权确认他人创建的草稿');
- });
- test('SC-P-84/C-06 跨家庭成员报告查询 → 空列表(成员级权限隔离,不泄露)', async ({ request }) => {
- // parent 传 teacher 家庭的成员 ID:非本人、非本家庭管理员、未授权 → 返回空列表而非他人报告
- const r = await apiPost(request, '/api/health/report/list', { memberId: teacherMemberId }, parent.token);
- expect(r.code, r.message).toBe(200);
- expect(Array.isArray(r.data)).toBe(true);
- expect(r.data.length).toBe(0);
- const dan = await apiPost(request, '/api/dan-report/list', { memberId: teacherMemberId }, parent.token);
- expect(dan.code, dan.message).toBe(200);
- expect(Array.isArray(dan.data)).toBe(true);
- expect(dan.data.length).toBe(0);
- });
- });
- });
|