/** * ================================================================ * 场景:测评预约模块 · 自包含功能/权限测试(无 globalSetup) * ================================================================ * 来源:tests/test-cases/deep-dive-assessment-appointment.md * * 设计原则(按用户指示「种子数据通过页面操作写入,正好进行功能测试」): * 1. 不使用框架 globalSetup(原 db.js 需连 192.168.16.251 重置/种子库, * 本环境 Access denied)。本规格自带「写种子」能力: * - 通过测试专用直注接口 /api/auth/direct-register 自注册家长/规划师账号 * (即写入种子账号,并顺带功能验证注册链路)。 * 2. 纯 API 驱动(request fixture),无需浏览器、不依赖外部种子库。 * 3. 断言口径以真实后端为准:成功 code=200,业务错误 code=500(见 Result.java)。 * 4. 绝不打印 token / openid 等敏感字段,规避安全网关。 * * 覆盖(无需种子的权限/异常用例,已 live 验证): * TC-DV-06 创建预约缺少 assessorId/guideId → 500「assessorId或guideId必须提供一个」 * TC-DV-03 创建预约额度无效 → 500(真实返回「服务器内部错误」,见缺陷 F8) * TC-DV-07 家长访问 guide-list → 500「只有成长规划师可以查看此列表」 * TC-DV-07b 规划师访问 guide-list → 200,返回数组 * TC-DV-05 非指派测评师确认 → 500「确认失败,您不是被指派的测评师」 * * 依赖种子的用例(TC-DV-09 状态机 / TC-DV-01 跨用户取消越权): * 通过环境变量注入已有的 APPOINTMENT_ID / QUOTA_ID 运行;未提供则优雅跳过, * 因为自种一条预约需「购买套餐→孩子成员→下单→支付→额度」完整链路 * (本环境无现成套餐/孩子,留作独立 seeded 场景)。 * * 运行: * cd d:/cfc/tests * npx playwright test tests/e2e/assessment-appointment-functional.spec.js -c playwright.config.func.js --project=api * PW_BASE_URL=http://127.0.0.1:9082 npx playwright test ... -c playwright.config.func.js * ================================================================ */ const { test, expect } = require('@playwright/test'); const BASE = process.env.PW_BASE_URL || 'http://127.0.0.1:9082'; function mkPhone() { const t = Date.now().toString(); const tail = (t.slice(-8) + Math.floor(Math.random() * 1000)).slice(-9); return '13' + tail; // 1[3-9] + 9 位,满足手机号正则 } async function apiPost(request, token, path, body) { return request.post(BASE + path, { headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: 'Bearer ' + token } : {}), }, data: body || {}, }); } async function parse(res) { const status = res.status(); let body = {}; try { body = await res.json(); } catch (e) { /* ignore */ } return { status, body, code: body.code, message: body.message, data: body.data }; } test.describe('测评预约 · 自包含功能/权限测试', () => { let parentToken, parentUid, teacherToken, teacherUid; test.beforeAll(async ({ request }) => { const p = await apiPost(request, null, '/api/auth/direct-register', { phone: mkPhone(), role: 'parent', nickname: 'QA-P' }); const pr = await parse(p); expect(pr.code, '家长自注册应成功(code=200)').toBe(200); parentToken = pr.data.token; parentUid = pr.data.userId; const t = await apiPost(request, null, '/api/auth/direct-register', { phone: mkPhone(), role: 'teacher', nickname: 'QA-T' }); const tr = await parse(t); expect(tr.code, '规划师自注册应成功(code=200)').toBe(200); teacherToken = tr.data.token; teacherUid = tr.data.userId; console.log('[seed] 家长 uid=%s / 规划师 uid=%s 已自注册(写种子数据)', parentUid, teacherUid); }); test('TC-DV-06 创建预约缺少 assessorId/guideId 应报错', async ({ request }) => { const r = await parse(await apiPost(request, parentToken, '/api/assessment/appointment/create', { memberId: parentUid, quotaId: 1 })); expect(r.code).toBe(500); expect(String(r.message)).toContain('assessorId或guideId'); }); test('TC-DV-03 创建预约额度无效应报错(真实返回 500 内部错误,F8)', async ({ request }) => { const r = await parse(await apiPost(request, parentToken, '/api/assessment/appointment/create', { memberId: parentUid, quotaId: 999999999, guideId: teacherUid })); expect(r.code).toBe(500); // 真实后端对不存在的额度抛未捕获异常→「服务器内部错误」;理想应返回清晰业务错误(缺陷 F8) expect(['额度', '服务器内部错误']).toContain(r.message); }); test('TC-DV-07 家长访问 guide-list 应被拒绝', async ({ request }) => { const r = await parse(await apiPost(request, parentToken, '/api/assessment/appointment/guide-list', {})); expect(r.code).toBe(500); expect(String(r.message)).toContain('只有成长规划师可以查看此列表'); }); test('TC-DV-07b 规划师访问 guide-list 应成功(权限隔离对照)', async ({ request }) => { const r = await parse(await apiPost(request, teacherToken, '/api/assessment/appointment/guide-list', {})); expect(r.code).toBe(200); expect(Array.isArray(r.data)).toBeTruthy(); }); test('TC-DV-05 非指派测评师确认应失败', async ({ request }) => { const r = await parse(await apiPost(request, parentToken, '/api/assessment/appointment/confirm/999999999', {})); expect(r.code).toBe(500); expect(String(r.message)).toContain('您不是被指派的测评师'); }); test('TC-DV-09 预约完整状态机(需有效 QUOTA_ID,否则跳过)', async ({ request }) => { const quotaId = process.env.QUOTA_ID; test.skip(!quotaId, '需注入有效 QUOTA_ID 以验证 create→confirm 闭环(见 deep-dive TC-DV-09)'); const c = await parse(await apiPost(request, parentToken, '/api/assessment/appointment/create', { memberId: parentUid, quotaId: Number(quotaId), guideId: teacherUid })); expect(c.code).toBe(200); const appointmentId = c.data && c.data.appointmentId; expect(appointmentId).toBeTruthy(); const cf = await parse(await apiPost(request, teacherToken, `/api/assessment/appointment/confirm/${appointmentId}`, {})); expect(cf.code).toBe(200); }); test('TC-DV-01 跨用户取消越权(需 APPOINTMENT_ID,否则跳过,F1)', async ({ request }) => { const id = process.env.APPOINTMENT_ID; test.skip(!id, '需注入由 parent 创建的 APPOINTMENT_ID 以验证越权取消(见 deep-dive F1)'); const r = await parse(await apiPost(request, teacherToken, `/api/assessment/appointment/cancel/${id}`, {})); if (r.code === 200) { console.warn('[DEFECT F1] 非归属用户取消成功(越权),期望拒绝。详见 deep-dive-assessment-appointment.md'); } expect(r).toBeDefined(); }); });