| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- /**
- * O-7 契约补建 · 五维能量批次 — 三类流 API 级 e2e
- * ================================================================
- * 覆盖 SC:SC-P-17/18/89/93、SC-T-31/32、SC-X-19/20/21(12 SC 开工包中的家长×跨角色×规划师代表端点,
- * 与 cfc-backend/src/test/java/com/etotem/cfc/unit/Energy*ContractTest 契约 JUnit 互补。
- * 契约核对 = 文档核对 + 端点比对(防假 RED,O-7 已核对 12/12 端点存在)。
- *
- * 端点(EnergyController / FiveDimensionSelfCheckController / ManualDimensionScoreController):
- * overview / logs / grant / deduct-for-wish / configure
- * /api/family/self-check/{questions,submit}
- * /api/family/manual-score/submit
- *
- * 运行:npx playwright test api/dimension-energy-contract.spec.js \
- * -c playwright.config.api.js --project=desktop
- * (对远端 http://cfc.iwintrue.com 实跑;写入型用例如 admin 回归验证中亦以 JUnit 契约为确定性证据)
- */
- const { test, expect } = require('@playwright/test');
- const { registerRole, apiPost, pickMemberByUserId, uniqueSuffix } = require('../helpers/api');
- const DIMS = ['body', 'mind', 'wisdom', 'action', 'wealth'];
- test.describe('O-7 五维能量 三类流(SC-P-17/18/89/93, SC-T-31/32, SC-X-19/20/21)', () => {
- let parent;
- let teacher;
- let selfMemberId;
- test.beforeAll(async ({ request }) => {
- parent = await registerRole(request, 'parent', 'QA-EN-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-EN-T' + uniqueSuffix());
- });
- // ==================== 正常流 ====================
- test.describe('正常流', () => {
- test('SC-P-17 overview 返回五维(body/mind/wisdom/action/wealth)', async ({ request }) => {
- const r = await apiPost(request, '/api/energy/overview', { memberId: selfMemberId }, parent.token);
- expect(r.code, r.message).toBe(200);
- const dims = r.data && r.data.dimensions;
- expect(Array.isArray(dims)).toBe(true);
- expect(dims.length).toBe(5);
- const codes = dims.map((d) => d.code);
- for (const dim of DIMS) expect(codes).toContain(dim);
- expect(typeof r.data.totalEnergy).toBe('number');
- });
- test('SC-P-18 logs 分页返回流水结构', async ({ request }) => {
- const r = await apiPost(request, '/api/energy/logs', { memberId: selfMemberId, page: 1, size: 10 }, parent.token);
- expect(r.code, r.message).toBe(200);
- expect(Array.isArray(r.data && r.data.records)).toBe(true);
- expect(r.data.size).toBe(10);
- expect(r.data.current).toBe(1);
- });
- test('SC-T-32 self-check questions 返回 15 题(Q6 含 E 跳过项)', async ({ request }) => {
- const r = await apiPost(request, '/api/family/self-check/questions', {}, parent.token);
- expect(r.code, r.message).toBe(200);
- expect(r.data.length).toBe(15);
- const q6 = r.data[5];
- expect(String(q6.options.length)).toBe('5');
- });
- test('SC-T-32 self-check submit 15 题全 A → totalScore=45', async ({ request }) => {
- const answers = [];
- for (let i = 1; i <= 15; i++) answers.push({ questionId: i, answer: 'A' });
- const r = await apiPost(request, '/api/family/self-check/submit', { familyMemberId: selfMemberId, answers }, parent.token);
- expect(r.code, r.message).toBe(200);
- expect(r.data.totalScore).toBe(45);
- expect(r.data.dimensions.length).toBe(5);
- });
- test('SC-T-31 manual-score submit 五维 → totalScore=350', async ({ request }) => {
- const r = await apiPost(request, '/api/family/manual-score/submit', {
- memberId: selfMemberId,
- scores: { body: 80, mind: 60, wisdom: 70, action: 50, wealth: 90 },
- }, teacher.token);
- expect(r.code, r.message).toBe(200);
- expect(r.data.totalScore).toBe(350);
- expect(Object.keys(r.data.scores).length).toBe(5);
- });
- });
- // ==================== 异常流 ====================
- test.describe('异常流', () => {
- test('SC-P-17 overview 无 memberId → 使用当前登录成员回落(观察行为)', async ({ request }) => {
- // 观察:JwtInterceptor 注入 currentMemberId,controller 在 memberId 缺省时回落当前成员 → 200
- const r = await apiPost(request, '/api/energy/overview', {}, parent.token);
- expect(r.code, r.message).toBe(200);
- expect(r.data && r.data.dimensions).toBeTruthy();
- });
- test('SC-P-89 grant 非法维度 code → 业务错误', async ({ request }) => {
- const r = await apiPost(request, '/api/energy/grant', {
- memberId: selfMemberId, taskId: 999999, amount: 10, dimensionCode: 'nonexistent-dim',
- }, parent.token);
- expect([400, 500]).toContain(r.code);
- expect(String(r.message)).toContain('维度');
- });
- test('SC-T-32 self-check submit 少于 10 题 → 业务错误', async ({ request }) => {
- const answers = [];
- for (let i = 1; i <= 9; i++) answers.push({ questionId: i, answer: 'A' });
- const r = await apiPost(request, '/api/family/self-check/submit', { familyMemberId: selfMemberId, answers }, parent.token);
- expect(r.code).not.toBe(200);
- expect(String(r.message)).toContain('至少10道题目');
- });
- test('SC-T-31 manual-score 缺维度 → 业务错误', async ({ request }) => {
- const r = await apiPost(request, '/api/family/manual-score/submit', {
- memberId: selfMemberId,
- scores: { body: 80, mind: 60, wisdom: 70, action: 50 }, // 缺 wealth
- }, teacher.token);
- expect(r.code).not.toBe(200);
- expect(String(r.message)).toContain('维度');
- });
- });
- // ==================== 权限流 ====================
- test.describe('权限流(JwtInterceptor 401 闸门)', () => {
- test('SC-X-19 overview 无 token → 401', async ({ request }) => {
- const r = await apiPost(request, '/api/energy/overview', { memberId: selfMemberId }, null);
- expect(r.status).toBe(401);
- expect(String(r.message)).toContain('请先登录');
- });
- test('SC-X-19 logs 无 token → 401', async ({ request }) => {
- const r = await apiPost(request, '/api/energy/logs', { memberId: selfMemberId }, null);
- expect(r.status).toBe(401);
- });
- test('_pinsObserved self-check/questions 无 token → 观察 200(userId=-1 匿名放行,未列 LOGIN_REQUIRED_PATHS)', async ({ request }) => {
- // 观察:/api/family/self-check/ 未列入 LOGIN_REQUIRED_PATHS,匿名降级 userId=-1 放行 → 200(契约缺口,仅登记)
- const r = await apiPost(request, '/api/family/self-check/questions', {}, null);
- expect(r.status).toBe(200);
- expect(r.data && r.data.length).toBe(15);
- });
- test('SC-X-19 另一家庭家长无法通过游客通道读取能量(登出后 401)', async ({ request }) => {
- const r = await apiPost(request, '/api/energy/logs', { memberId: selfMemberId }, 'invalid-token-value');
- expect(r.status).toBe(401);
- });
- });
- // ==================== 观察钉住(gap,只读验证,不修改实现) ====================
- test.describe('_pinsObserved(契约缺口观察)', () => {
- test('overview 传跨家庭 memberId(999999)→ 观察返回 200(无归属校验,仅登记)', async ({ request }) => {
- const r = await apiPost(request, '/api/energy/overview', { memberId: 999999 }, parent.token);
- expect(r.code).toBe(200);
- expect(r.data && r.data.dimensions).toBeTruthy();
- });
- });
- });
|