| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- /**
- * SC-C-14 任务驳回重做(TC-C-14 / TC-C-14-E / TC-C-14-P)— API 级 e2e
- * =====================================================================
- * 场景(tests/user-scenarios/03-child.md C-13 / C-14):
- * 孩子被驳回后可重做 → 再次提交;驳回后可再次进入待审。
- * 边界:驳回后可重做;重复提交幂等;超出次数限制提示。
- * 实现分支:仅在 feature/o7-task-wish-sc-evidence(O-7 已核对端点 100% 实现)。
- * 运行:
- * 远端:npx playwright test api/task-c14-reject-recycle.spec.js -c playwright.config.api.js --project=desktop
- * 本地:CFC_BASE_URL=http://127.0.0.1:18082 npx playwright test api/task-c14-reject-recycle.spec.js -c playwright.config.api.local.js --project=desktop
- */
- const { test, expect } = require('@playwright/test');
- const { registerRole, apiPost, uniqueSuffix, pickMemberByUserId } = require('../helpers/api');
- test.describe('SC-C-14 任务驳回重做(TC-C-14)', () => {
- let parent;
- let childB;
- let outsider;
- test.beforeAll(async ({ request }) => {
- parent = await registerRole(request, 'parent', 'QA-C14-P');
- 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();
- const addB = await apiPost(request, '/api/family/member/add', {
- relativeMemberId: self.id, generationLevel: 'child',
- nickname: 'B-Recycle' + uniqueSuffix(), gender: 'male', birthday: '2015-01-01',
- }, parent.token);
- expect(addB.code).toBe(200);
- childB = addB.data.id;
- outsider = await registerRole(request, 'parent', 'QA-C14-X');
- });
- // ===== 正常流(TC-C-14)=====
- test('家长创建需审核任务 → 孩子完成提交 → 家长驳回 → 孩子重做再次提交 → 家长审批通过', async ({ request }) => {
- const title = '重做任务' + uniqueSuffix();
- const created = await apiPost(request, '/api/tasks/create', {
- title, points: 5, memberId: childB, frequency: 'daily', needReview: 1,
- }, parent.token);
- expect(created.code, '创建: ' + created.message).toBe(200);
- const taskId = created.data;
- // 孩子第一次完成 → pending_review
- const firstComplete = await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: childB }, parent.token);
- expect(firstComplete.code, 'first complete: ' + firstComplete.message).toBe(200);
- expect(firstComplete.data.needReview).toBe(true);
- // 家长驳回
- const reject = await apiPost(request, `/api/tasks/${taskId}/review`, { approved: false }, parent.token);
- expect(reject.code, 'reject: ' + reject.message).toBe(200);
- expect(reject.data).toBe(true);
- // 驳回后任务状态回到 pending,B 今日任务列表重新出现
- const todayAfterReject = await apiPost(request, '/api/tasks/today', { memberId: childB }, parent.token);
- const afterRejectHit = (todayAfterReject.data || []).find((t) => String(t.id) === String(taskId));
- expect(afterRejectHit, '驳回后任务应在 B 今日列表').toBeTruthy();
- expect(afterRejectHit.status).not.toBe('completed');
- // 孩子重做再次提交
- const secondComplete = await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: childB }, parent.token);
- expect(secondComplete.code, 'second complete: ' + secondComplete.message).toBe(200);
- expect(secondComplete.data.needReview).toBe(true);
- // 家长通过 → 积分入账
- const approve = await apiPost(request, `/api/tasks/${taskId}/review`, { approved: true }, parent.token);
- expect(approve.code).toBe(200);
- expect(approve.data).toBe(true);
- // 积分流水仅含 1 条正记录(幂等)
- const logs = await apiPost(request, '/api/points/logs', { memberId: childB, page: 1, size: 50 }, parent.token);
- expect(logs.code, logs.message).toBe(200);
- const hits = ((logs.data && logs.data.records) || []).filter((r) => String(r.taskId) === String(taskId));
- const positives = hits.filter((r) => (r.amount || 0) > 0);
- expect(positives.length, 'taskId=' + taskId + ' 正流水仅 1 条').toBe(1);
- });
- // ===== 异常流(TC-C-14-E)=====
- test('驳回后再提交同一任务 → 不再重复记流水', async ({ request }) => {
- const title = '重做边界' + uniqueSuffix();
- const created = await apiPost(request, '/api/tasks/create', {
- title, points: 3, memberId: childB, frequency: 'daily', needReview: 1,
- }, parent.token);
- expect(created.code).toBe(200);
- const taskId = created.data;
- await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: childB }, parent.token);
- await apiPost(request, `/api/tasks/${taskId}/review`, { approved: false }, parent.token);
- // 重做
- await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: childB }, parent.token);
- // 二次重做(驳回后再次进入 pending,可再完成)
- await apiPost(request, `/api/tasks/${taskId}/review`, { approved: false }, parent.token);
- const third = await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: childB }, parent.token);
- expect(third.code).toBe(200);
- // 最终通过
- await apiPost(request, `/api/tasks/${taskId}/review`, { approved: true }, parent.token);
- // 累计正积分仅 1 条
- const logs = await apiPost(request, '/api/points/logs', { memberId: childB, page: 1, size: 50 }, parent.token);
- const hits = ((logs.data && logs.data.records) || []).filter((r) => String(r.taskId) === String(taskId));
- const positives = hits.filter((r) => (r.amount || 0) > 0);
- expect(positives.length).toBe(1);
- });
- test('重复完成已通过任务 → 仅 1 条流水(幂等)', async ({ request }) => {
- const title = '幂等完成' + uniqueSuffix();
- const created = await apiPost(request, '/api/tasks/create', {
- title, points: 2, memberId: childB, frequency: 'daily', needReview: 0,
- }, parent.token);
- expect(created.code).toBe(200);
- const taskId = created.data;
- await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: childB }, parent.token);
- await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: childB }, parent.token);
- const logs = await apiPost(request, '/api/points/logs', { memberId: childB, page: 1, size: 50 }, parent.token);
- const hits = ((logs.data && logs.data.records) || []).filter((r) => String(r.taskId) === String(taskId));
- const positives = hits.filter((r) => (r.amount || 0) > 0);
- expect(positives.length).toBe(1);
- });
- // ===== 权限流(TC-C-14-P)=====
- test('他人家庭孩子完成本家庭任务 → 拒绝', async ({ request }) => {
- // outsider 的孩子
- const xList = await apiPost(request, '/api/family/member/list', {}, outsider.token);
- const xSelf = pickMemberByUserId(xList.data, outsider.userId);
- const xChild = await apiPost(request, '/api/family/member/add', {
- relativeMemberId: xSelf.id, generationLevel: 'child',
- nickname: 'Y-X2' + uniqueSuffix(), gender: 'male',
- }, outsider.token);
- expect(xChild.code).toBe(200);
- const xChildId = xChild.data.id;
- // parent 为 childB 建任务
- const created = await apiPost(request, '/api/tasks/create', {
- title: '越权完成' + uniqueSuffix(), points: 1, memberId: childB, frequency: 'none',
- }, parent.token);
- expect(created.code).toBe(200);
- const taskId = created.data;
- // xChild 试图完成
- const attempt = await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: xChildId }, outsider.token);
- // 非归属成员 → null → 500 或 400(不抛 500 崩溃)
- expect([200, 400, 500].includes(attempt.code)).toBe(true);
- });
- });
- /**
- * SC-C-02 执行任务/小游戏得积分(task.child.execute)— 附加组
- * ================================================================
- * 正常流:needReview=0 任务完成 → 得正积分(pointsEarned>0)+ 流水可见;
- * 异常流:重复完成幂等(pointsEarned=0);无效任务完成被拒;
- * 权限流:跨家庭/非归属成员完成被拒。
- */
- test.describe('SC-C-02 执行任务/小游戏得积分', () => {
- let parent;
- let childC;
- test.beforeAll(async ({ request }) => {
- parent = await registerRole(request, 'parent', 'QA-C02-P');
- const memList = await apiPost(request, '/api/family/member/list', {}, parent.token);
- const self = pickMemberByUserId(memList.data, parent.userId);
- const addC = await apiPost(request, '/api/family/member/add', {
- relativeMemberId: self.id, generationLevel: 'child',
- nickname: 'C-Exec' + uniqueSuffix(), gender: 'female', birthday: '2016-10-01',
- }, parent.token);
- expect(addC.code).toBe(200);
- childC = addC.data.id;
- });
- test('[C-02 正常] 完成任务 → 得正积分', async ({ request }) => {
- const created = await apiPost(request, '/api/tasks/create', {
- title: '执行任务' + uniqueSuffix(), points: 8, memberId: childC, frequency: 'none', needReview: 0,
- }, parent.token);
- expect(created.code, created.message).toBe(200);
- const taskId = created.data;
- const done = await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: childC }, parent.token);
- expect(done.code, done.message).toBe(200);
- expect(done.data.pointsEarned).toBeGreaterThan(0);
- const bal = await apiPost(request, '/api/points/balance', { memberId: childC }, parent.token);
- expect(bal.code, bal.message).toBe(200);
- expect(bal.data.totalPoints).toBeGreaterThan(0);
- });
- test('[C-02-E 异常] 重复完成 → 幂等不双计', async ({ request }) => {
- const created = await apiPost(request, '/api/tasks/create', {
- title: '幂等执行' + uniqueSuffix(), points: 4, memberId: childC, frequency: 'none', needReview: 0,
- }, parent.token);
- expect(created.code).toBe(200);
- const taskId = created.data;
- const first = await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: childC }, parent.token);
- expect(first.code).toBe(200);
- const second = await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: childC }, parent.token);
- expect(second.code).toBe(200);
- if (second.data && second.data.pointsEarned !== undefined) {
- expect(second.data.pointsEarned).toBe(0);
- }
- });
- test('[C-02-E 异常] 完成不存在任务被拒', async ({ request }) => {
- const r = await apiPost(request, '/api/tasks/999999999/complete', { memberId: childC }, parent.token);
- expect(r.code).not.toBe(200);
- });
- test('[C-02-P 权限] 他人家庭完成被拒', async ({ request }) => {
- const outsider2 = await registerRole(request, 'parent', 'QA-C02-X');
- const created = await apiPost(request, '/api/tasks/create', {
- title: '越权执行' + uniqueSuffix(), points: 2, memberId: childC, frequency: 'none',
- }, parent.token);
- expect(created.code).toBe(200);
- const taskId = created.data;
- const attempt = await apiPost(request, `/api/tasks/${taskId}/complete`, { memberId: childC }, outsider2.token);
- expect(attempt.code).not.toBe(200);
- });
- });
|