task-create-scp10.spec.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /**
  2. * SC-P-10 创建任务+下发(task.parent.createTask / S-cfc-12)— API 级 e2e
  3. * ================================================================
  4. * 场景(tests/user-scenarios/应用场景总览.md 业务域B / 02-parent.md P-4):
  5. * 建任务(成员/积分/频率)→ 下发 → B 列表待执行;边界:重复提交不重复发分、频率/截止边界。
  6. * 关联用例:test-cases/parent.md TC-P-05(正常)/ TC-P-05-E(异常)/ TC-P-05-P(权限)。
  7. * 设计锚点(API_REFERENCE.md 4.3 / family-management-flow.md):
  8. * POST /api/tasks/create(TaskController→TaskService.createTask)、POST /api/tasks/today。
  9. *
  10. * 运行:
  11. * 本 feature 分支实现(本地/自建实例):npx playwright test api/task-create-scp10.spec.js \
  12. * -c playwright.config.api.local.js --project=desktop
  13. * 远端已部署基线:npx playwright test api/task-create-scp10.spec.js \
  14. * -c playwright.config.api.js --project=desktop
  15. * (异常流/越权注入类用例仅在本 feature 实现上通过——远端基线无参数校验/幂等/跨家庭校验)
  16. */
  17. const { test, expect } = require('@playwright/test');
  18. const { registerRole, apiPost, uniqueSuffix } = require('../helpers/api');
  19. test.describe('SC-P-10 创建任务+下发(TC-P-05/05-E/05-P)', () => {
  20. let parent; // 家庭1 家长(下发方)
  21. let childB; // 家庭1 孩子成员(执行人 B)
  22. let outsider; // 家庭2 家长(越权方)
  23. let outsiderChild; // 家庭2 孩子成员
  24. test.beforeAll(async ({ request }) => {
  25. parent = await registerRole(request, 'parent', 'QA-P10-P');
  26. const memList = await apiPost(request, '/api/family/member/list', {}, parent.token);
  27. expect(memList.code, memList.message).toBe(200);
  28. const self = (memList.data || []).find((m) => String(m.userId) === String(parent.userId));
  29. expect(self, '家长本人 member 记录').toBeTruthy();
  30. const addChild = await apiPost(request, '/api/family/member/add', {
  31. relativeMemberId: self.id,
  32. generationLevel: 'child',
  33. nickname: 'B-小王' + uniqueSuffix(),
  34. gender: 'male',
  35. birthday: '2015-06-01',
  36. }, parent.token);
  37. expect(addChild.code, '添加孩子成员: ' + addChild.message).toBe(200);
  38. childB = addChild.data.id;
  39. outsider = await registerRole(request, 'parent', 'QA-P10-X');
  40. const xList = await apiPost(request, '/api/family/member/list', {}, outsider.token);
  41. const xSelf = (xList.data || []).find((m) => String(m.userId) === String(outsider.userId));
  42. const xChild = await apiPost(request, '/api/family/member/add', {
  43. relativeMemberId: xSelf.id,
  44. generationLevel: 'child',
  45. nickname: 'Y-小明' + uniqueSuffix(),
  46. gender: 'male',
  47. }, outsider.token);
  48. expect(xChild.code, '家庭2添加孩子: ' + xChild.message).toBe(200);
  49. outsiderChild = xChild.data.id;
  50. });
  51. test.describe('正常流(TC-P-05)', () => {
  52. test('家长创建任务(标题/成员B/积分10/频率每日) → 任务出现在B列表,状态待执行', async ({ request }) => {
  53. const title = '睡前阅读' + uniqueSuffix();
  54. const created = await apiPost(request, '/api/tasks/create', {
  55. title,
  56. points: 10,
  57. memberId: childB,
  58. frequency: 'daily',
  59. needReview: 0,
  60. }, parent.token);
  61. expect(created.code, '创建任务: ' + created.message).toBe(200);
  62. const taskId = created.data;
  63. expect(taskId).toBeTruthy();
  64. const today = await apiPost(request, '/api/tasks/today', { memberId: childB }, parent.token);
  65. expect(today.code, today.message).toBe(200);
  66. const hit = (today.data || []).find((t) => String(t.id) === String(taskId));
  67. expect(hit, '任务应出现在 B 的今日列表').toBeTruthy();
  68. expect(hit.status, '下发后状态应为待执行').toBe('pending');
  69. expect(String(hit.familyMemberId)).toBe(String(childB));
  70. expect(hit.points).toBe(10);
  71. expect(hit.frequency).toBe('daily');
  72. });
  73. });
  74. test.describe('异常流(TC-P-05-E)', () => {
  75. test('频率非法(0/负数/非枚举) → 400', async ({ request }) => {
  76. for (const freq of ['0', '-1', '999']) {
  77. const r = await apiPost(request, '/api/tasks/create', {
  78. title: '频率非法' + freq, points: 5, memberId: childB, frequency: freq,
  79. }, parent.token);
  80. expect(r.code, '频率=' + freq + ' 应 400: ' + r.message).toBe(400);
  81. expect(String(r.message)).toContain('频率');
  82. }
  83. });
  84. test('负积分 → 400', async ({ request }) => {
  85. const r = await apiPost(request, '/api/tasks/create', {
  86. title: '负积分', points: -5, memberId: childB, frequency: 'daily',
  87. }, parent.token);
  88. expect(r.code, r.message).toBe(400);
  89. expect(String(r.message)).toContain('积分');
  90. });
  91. test('空标题 → 400', async ({ request }) => {
  92. const r = await apiPost(request, '/api/tasks/create', {
  93. title: '', points: 5, memberId: childB, frequency: 'daily',
  94. }, parent.token);
  95. expect(r.code, r.message).toBe(400);
  96. expect(String(r.message)).toContain('标题');
  97. });
  98. test('截止时间在过去 → 400(超期任务拒绝提交)', async ({ request }) => {
  99. const r = await apiPost(request, '/api/tasks/create', {
  100. title: '超期任务', points: 5, memberId: childB, frequency: 'daily',
  101. deadline: '2020-01-01T00:00:00',
  102. }, parent.token);
  103. expect(r.code, r.message).toBe(400);
  104. expect(String(r.message)).toContain('截止');
  105. });
  106. test('重复提交同一创建请求 → 仅生成1条任务(幂等)', async ({ request }) => {
  107. const body = {
  108. title: '幂等任务' + uniqueSuffix(), points: 7, memberId: childB, frequency: 'daily',
  109. };
  110. const r1 = await apiPost(request, '/api/tasks/create', body, parent.token);
  111. const r2 = await apiPost(request, '/api/tasks/create', body, parent.token);
  112. expect(r1.code, r1.message).toBe(200);
  113. expect(r2.code, r2.message).toBe(200);
  114. expect(String(r1.data), '重复提交应复用同一任务 id').toBe(String(r2.data));
  115. const today = await apiPost(request, '/api/tasks/today', { memberId: childB }, parent.token);
  116. const hits = (today.data || []).filter((t) => String(t.id) === String(r1.data));
  117. expect(hits.length, 'B 列表中该任务应只有 1 条').toBe(1);
  118. });
  119. test('并发双发同一创建请求 → 仅生成1条任务', async ({ request }) => {
  120. const body = {
  121. title: '并发任务' + uniqueSuffix(), points: 3, memberId: childB, frequency: 'daily',
  122. };
  123. const [a, b] = await Promise.all([
  124. apiPost(request, '/api/tasks/create', body, parent.token),
  125. apiPost(request, '/api/tasks/create', body, parent.token),
  126. ]);
  127. expect(a.code, a.message).toBe(200);
  128. expect(b.code, b.message).toBe(200);
  129. expect(String(a.data), '并发双发应复用同一任务 id').toBe(String(b.data));
  130. });
  131. });
  132. test.describe('权限流(TC-P-05-P)', () => {
  133. test('他人家庭家长为孩子 B 创建任务 → 400 拒绝(横向越权注入)', async ({ request }) => {
  134. const r = await apiPost(request, '/api/tasks/create', {
  135. title: 'X越权任务', points: 5, memberId: childB, frequency: 'daily',
  136. }, outsider.token);
  137. expect(r.code, r.message).toBe(400);
  138. expect(String(r.message)).toContain('不属于本家庭');
  139. });
  140. test('member 类型给其他家庭孩子下发 → 400 拒绝', async ({ request }) => {
  141. const r = await apiPost(request, '/api/tasks/create', {
  142. title: '跨家庭member下发', points: 5, executorType: 'member', memberIds: [outsiderChild], frequency: 'daily',
  143. }, parent.token);
  144. expect(r.code, r.message).toBe(400);
  145. expect(String(r.message)).toContain('不属于本家庭');
  146. });
  147. test('越权查看他人家庭成员列表 → 403', async ({ request }) => {
  148. const r = await apiPost(request, '/api/family/member/list', { familyId: outsider.familyId }, parent.token);
  149. expect(r.code, r.message).toBe(403);
  150. expect(String(r.message)).toContain('无权访问');
  151. });
  152. });
  153. });