dimension-energy-contract.spec.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. /**
  2. * O-7 契约补建 · 五维能量批次 — 三类流 API 级 e2e
  3. * ================================================================
  4. * 覆盖 SC:SC-P-17/18/89/93、SC-T-31/32、SC-X-19/20/21(12 SC 开工包中的家长×跨角色×规划师代表端点,
  5. * 与 cfc-backend/src/test/java/com/etotem/cfc/unit/Energy*ContractTest 契约 JUnit 互补。
  6. * 契约核对 = 文档核对 + 端点比对(防假 RED,O-7 已核对 12/12 端点存在)。
  7. *
  8. * 端点(EnergyController / FiveDimensionSelfCheckController / ManualDimensionScoreController):
  9. * overview / logs / grant / deduct-for-wish / configure
  10. * /api/family/self-check/{questions,submit}
  11. * /api/family/manual-score/submit
  12. *
  13. * 运行:npx playwright test api/dimension-energy-contract.spec.js \
  14. * -c playwright.config.api.js --project=desktop
  15. * (对远端 http://cfc.iwintrue.com 实跑;写入型用例如 admin 回归验证中亦以 JUnit 契约为确定性证据)
  16. */
  17. const { test, expect } = require('@playwright/test');
  18. const { registerRole, apiPost, pickMemberByUserId, uniqueSuffix } = require('../helpers/api');
  19. const DIMS = ['body', 'mind', 'wisdom', 'action', 'wealth'];
  20. test.describe('O-7 五维能量 三类流(SC-P-17/18/89/93, SC-T-31/32, SC-X-19/20/21)', () => {
  21. let parent;
  22. let teacher;
  23. let selfMemberId;
  24. test.beforeAll(async ({ request }) => {
  25. parent = await registerRole(request, 'parent', 'QA-EN-P' + uniqueSuffix());
  26. const list = await apiPost(request, '/api/family/member/list', {}, parent.token);
  27. expect(list.code, list.message).toBe(200);
  28. const self = pickMemberByUserId(list.data, parent.userId);
  29. expect(self, '成员列表应包含登录用户自身').toBeTruthy();
  30. selfMemberId = self.id;
  31. teacher = await registerRole(request, 'teacher', 'QA-EN-T' + uniqueSuffix());
  32. });
  33. // ==================== 正常流 ====================
  34. test.describe('正常流', () => {
  35. test('SC-P-17 overview 返回五维(body/mind/wisdom/action/wealth)', async ({ request }) => {
  36. const r = await apiPost(request, '/api/energy/overview', { memberId: selfMemberId }, parent.token);
  37. expect(r.code, r.message).toBe(200);
  38. const dims = r.data && r.data.dimensions;
  39. expect(Array.isArray(dims)).toBe(true);
  40. expect(dims.length).toBe(5);
  41. const codes = dims.map((d) => d.code);
  42. for (const dim of DIMS) expect(codes).toContain(dim);
  43. expect(typeof r.data.totalEnergy).toBe('number');
  44. });
  45. test('SC-P-18 logs 分页返回流水结构', async ({ request }) => {
  46. const r = await apiPost(request, '/api/energy/logs', { memberId: selfMemberId, page: 1, size: 10 }, parent.token);
  47. expect(r.code, r.message).toBe(200);
  48. expect(Array.isArray(r.data && r.data.records)).toBe(true);
  49. expect(r.data.size).toBe(10);
  50. expect(r.data.current).toBe(1);
  51. });
  52. test('SC-T-32 self-check questions 返回 15 题(Q6 含 E 跳过项)', async ({ request }) => {
  53. const r = await apiPost(request, '/api/family/self-check/questions', {}, parent.token);
  54. expect(r.code, r.message).toBe(200);
  55. expect(r.data.length).toBe(15);
  56. const q6 = r.data[5];
  57. expect(String(q6.options.length)).toBe('5');
  58. });
  59. test('SC-T-32 self-check submit 15 题全 A → totalScore=45', async ({ request }) => {
  60. const answers = [];
  61. for (let i = 1; i <= 15; i++) answers.push({ questionId: i, answer: 'A' });
  62. const r = await apiPost(request, '/api/family/self-check/submit', { familyMemberId: selfMemberId, answers }, parent.token);
  63. expect(r.code, r.message).toBe(200);
  64. expect(r.data.totalScore).toBe(45);
  65. expect(r.data.dimensions.length).toBe(5);
  66. });
  67. test('SC-T-31 manual-score submit 五维 → totalScore=350', async ({ request }) => {
  68. const r = await apiPost(request, '/api/family/manual-score/submit', {
  69. memberId: selfMemberId,
  70. scores: { body: 80, mind: 60, wisdom: 70, action: 50, wealth: 90 },
  71. }, teacher.token);
  72. expect(r.code, r.message).toBe(200);
  73. expect(r.data.totalScore).toBe(350);
  74. expect(Object.keys(r.data.scores).length).toBe(5);
  75. });
  76. });
  77. // ==================== 异常流 ====================
  78. test.describe('异常流', () => {
  79. test('SC-P-17 overview 无 memberId → 使用当前登录成员回落(观察行为)', async ({ request }) => {
  80. // 观察:JwtInterceptor 注入 currentMemberId,controller 在 memberId 缺省时回落当前成员 → 200
  81. const r = await apiPost(request, '/api/energy/overview', {}, parent.token);
  82. expect(r.code, r.message).toBe(200);
  83. expect(r.data && r.data.dimensions).toBeTruthy();
  84. });
  85. test('SC-P-89 grant 非法维度 code → 业务错误', async ({ request }) => {
  86. const r = await apiPost(request, '/api/energy/grant', {
  87. memberId: selfMemberId, taskId: 999999, amount: 10, dimensionCode: 'nonexistent-dim',
  88. }, parent.token);
  89. expect([400, 500]).toContain(r.code);
  90. expect(String(r.message)).toContain('维度');
  91. });
  92. test('SC-T-32 self-check submit 少于 10 题 → 业务错误', async ({ request }) => {
  93. const answers = [];
  94. for (let i = 1; i <= 9; i++) answers.push({ questionId: i, answer: 'A' });
  95. const r = await apiPost(request, '/api/family/self-check/submit', { familyMemberId: selfMemberId, answers }, parent.token);
  96. expect(r.code).not.toBe(200);
  97. expect(String(r.message)).toContain('至少10道题目');
  98. });
  99. test('SC-T-31 manual-score 缺维度 → 业务错误', async ({ request }) => {
  100. const r = await apiPost(request, '/api/family/manual-score/submit', {
  101. memberId: selfMemberId,
  102. scores: { body: 80, mind: 60, wisdom: 70, action: 50 }, // 缺 wealth
  103. }, teacher.token);
  104. expect(r.code).not.toBe(200);
  105. expect(String(r.message)).toContain('维度');
  106. });
  107. });
  108. // ==================== 权限流 ====================
  109. test.describe('权限流(JwtInterceptor 401 闸门)', () => {
  110. test('SC-X-19 overview 无 token → 401', async ({ request }) => {
  111. const r = await apiPost(request, '/api/energy/overview', { memberId: selfMemberId }, null);
  112. expect(r.status).toBe(401);
  113. expect(String(r.message)).toContain('请先登录');
  114. });
  115. test('SC-X-19 logs 无 token → 401', async ({ request }) => {
  116. const r = await apiPost(request, '/api/energy/logs', { memberId: selfMemberId }, null);
  117. expect(r.status).toBe(401);
  118. });
  119. test('_pinsObserved self-check/questions 无 token → 观察 200(userId=-1 匿名放行,未列 LOGIN_REQUIRED_PATHS)', async ({ request }) => {
  120. // 观察:/api/family/self-check/ 未列入 LOGIN_REQUIRED_PATHS,匿名降级 userId=-1 放行 → 200(契约缺口,仅登记)
  121. const r = await apiPost(request, '/api/family/self-check/questions', {}, null);
  122. expect(r.status).toBe(200);
  123. expect(r.data && r.data.length).toBe(15);
  124. });
  125. test('SC-X-19 另一家庭家长无法通过游客通道读取能量(登出后 401)', async ({ request }) => {
  126. const r = await apiPost(request, '/api/energy/logs', { memberId: selfMemberId }, 'invalid-token-value');
  127. expect(r.status).toBe(401);
  128. });
  129. });
  130. // ==================== 观察钉住(gap,只读验证,不修改实现) ====================
  131. test.describe('_pinsObserved(契约缺口观察)', () => {
  132. test('overview 传跨家庭 memberId(999999)→ 观察返回 200(无归属校验,仅登记)', async ({ request }) => {
  133. const r = await apiPost(request, '/api/energy/overview', { memberId: 999999 }, parent.token);
  134. expect(r.code).toBe(200);
  135. expect(r.data && r.data.dimensions).toBeTruthy();
  136. });
  137. });
  138. });