/** * SC-P-10 创建任务+下发(task.parent.createTask / S-cfc-12)— API 级 e2e * ================================================================ * 场景(tests/user-scenarios/应用场景总览.md 业务域B / 02-parent.md P-4): * 建任务(成员/积分/频率)→ 下发 → B 列表待执行;边界:重复提交不重复发分、频率/截止边界。 * 关联用例:test-cases/parent.md TC-P-05(正常)/ TC-P-05-E(异常)/ TC-P-05-P(权限)。 * 设计锚点(API_REFERENCE.md 4.3 / family-management-flow.md): * POST /api/tasks/create(TaskController→TaskService.createTask)、POST /api/tasks/today。 * * 运行: * 本 feature 分支实现(本地/自建实例):npx playwright test api/task-create-scp10.spec.js \ * -c playwright.config.api.local.js --project=desktop * 远端已部署基线:npx playwright test api/task-create-scp10.spec.js \ * -c playwright.config.api.js --project=desktop * (异常流/越权注入类用例仅在本 feature 实现上通过——远端基线无参数校验/幂等/跨家庭校验) */ const { test, expect } = require('@playwright/test'); const { registerRole, apiPost, uniqueSuffix } = require('../helpers/api'); test.describe('SC-P-10 创建任务+下发(TC-P-05/05-E/05-P)', () => { let parent; // 家庭1 家长(下发方) let childB; // 家庭1 孩子成员(执行人 B) let outsider; // 家庭2 家长(越权方) let outsiderChild; // 家庭2 孩子成员 test.beforeAll(async ({ request }) => { parent = await registerRole(request, 'parent', 'QA-P10-P'); const memList = await apiPost(request, '/api/family/member/list', {}, parent.token); expect(memList.code, memList.message).toBe(200); const self = (memList.data || []).find((m) => String(m.userId) === String(parent.userId)); expect(self, '家长本人 member 记录').toBeTruthy(); const addChild = await apiPost(request, '/api/family/member/add', { relativeMemberId: self.id, generationLevel: 'child', nickname: 'B-小王' + uniqueSuffix(), gender: 'male', birthday: '2015-06-01', }, parent.token); expect(addChild.code, '添加孩子成员: ' + addChild.message).toBe(200); childB = addChild.data.id; outsider = await registerRole(request, 'parent', 'QA-P10-X'); const xList = await apiPost(request, '/api/family/member/list', {}, outsider.token); const xSelf = (xList.data || []).find((m) => String(m.userId) === String(outsider.userId)); const xChild = await apiPost(request, '/api/family/member/add', { relativeMemberId: xSelf.id, generationLevel: 'child', nickname: 'Y-小明' + uniqueSuffix(), gender: 'male', }, outsider.token); expect(xChild.code, '家庭2添加孩子: ' + xChild.message).toBe(200); outsiderChild = xChild.data.id; }); test.describe('正常流(TC-P-05)', () => { test('家长创建任务(标题/成员B/积分10/频率每日) → 任务出现在B列表,状态待执行', async ({ request }) => { const title = '睡前阅读' + uniqueSuffix(); const created = await apiPost(request, '/api/tasks/create', { title, points: 10, memberId: childB, frequency: 'daily', needReview: 0, }, parent.token); expect(created.code, '创建任务: ' + created.message).toBe(200); const taskId = created.data; expect(taskId).toBeTruthy(); const today = await apiPost(request, '/api/tasks/today', { memberId: childB }, parent.token); expect(today.code, today.message).toBe(200); const hit = (today.data || []).find((t) => String(t.id) === String(taskId)); expect(hit, '任务应出现在 B 的今日列表').toBeTruthy(); expect(hit.status, '下发后状态应为待执行').toBe('pending'); expect(String(hit.familyMemberId)).toBe(String(childB)); expect(hit.points).toBe(10); expect(hit.frequency).toBe('daily'); }); }); test.describe('异常流(TC-P-05-E)', () => { test('频率非法(0/负数/非枚举) → 400', async ({ request }) => { for (const freq of ['0', '-1', '999']) { const r = await apiPost(request, '/api/tasks/create', { title: '频率非法' + freq, points: 5, memberId: childB, frequency: freq, }, parent.token); expect(r.code, '频率=' + freq + ' 应 400: ' + r.message).toBe(400); expect(String(r.message)).toContain('频率'); } }); test('负积分 → 400', async ({ request }) => { const r = await apiPost(request, '/api/tasks/create', { title: '负积分', points: -5, memberId: childB, frequency: 'daily', }, parent.token); expect(r.code, r.message).toBe(400); expect(String(r.message)).toContain('积分'); }); test('空标题 → 400', async ({ request }) => { const r = await apiPost(request, '/api/tasks/create', { title: '', points: 5, memberId: childB, frequency: 'daily', }, parent.token); expect(r.code, r.message).toBe(400); expect(String(r.message)).toContain('标题'); }); test('截止时间在过去 → 400(超期任务拒绝提交)', async ({ request }) => { const r = await apiPost(request, '/api/tasks/create', { title: '超期任务', points: 5, memberId: childB, frequency: 'daily', deadline: '2020-01-01T00:00:00', }, parent.token); expect(r.code, r.message).toBe(400); expect(String(r.message)).toContain('截止'); }); test('重复提交同一创建请求 → 仅生成1条任务(幂等)', async ({ request }) => { const body = { title: '幂等任务' + uniqueSuffix(), points: 7, memberId: childB, frequency: 'daily', }; const r1 = await apiPost(request, '/api/tasks/create', body, parent.token); const r2 = await apiPost(request, '/api/tasks/create', body, parent.token); expect(r1.code, r1.message).toBe(200); expect(r2.code, r2.message).toBe(200); expect(String(r1.data), '重复提交应复用同一任务 id').toBe(String(r2.data)); const today = await apiPost(request, '/api/tasks/today', { memberId: childB }, parent.token); const hits = (today.data || []).filter((t) => String(t.id) === String(r1.data)); expect(hits.length, 'B 列表中该任务应只有 1 条').toBe(1); }); test('并发双发同一创建请求 → 仅生成1条任务', async ({ request }) => { const body = { title: '并发任务' + uniqueSuffix(), points: 3, memberId: childB, frequency: 'daily', }; const [a, b] = await Promise.all([ apiPost(request, '/api/tasks/create', body, parent.token), apiPost(request, '/api/tasks/create', body, parent.token), ]); expect(a.code, a.message).toBe(200); expect(b.code, b.message).toBe(200); expect(String(a.data), '并发双发应复用同一任务 id').toBe(String(b.data)); }); }); test.describe('权限流(TC-P-05-P)', () => { test('他人家庭家长为孩子 B 创建任务 → 400 拒绝(横向越权注入)', async ({ request }) => { const r = await apiPost(request, '/api/tasks/create', { title: 'X越权任务', points: 5, memberId: childB, frequency: 'daily', }, outsider.token); expect(r.code, r.message).toBe(400); expect(String(r.message)).toContain('不属于本家庭'); }); test('member 类型给其他家庭孩子下发 → 400 拒绝', async ({ request }) => { const r = await apiPost(request, '/api/tasks/create', { title: '跨家庭member下发', points: 5, executorType: 'member', memberIds: [outsiderChild], frequency: 'daily', }, parent.token); expect(r.code, r.message).toBe(400); expect(String(r.message)).toContain('不属于本家庭'); }); test('越权查看他人家庭成员列表 → 403', async ({ request }) => { const r = await apiPost(request, '/api/family/member/list', { familyId: outsider.familyId }, parent.token); expect(r.code, r.message).toBe(403); expect(String(r.message)).toContain('无权访问'); }); }); });