| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- /**
- * ================================================================
- * 场景:测评预约模块 · 自包含功能/权限测试(无 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();
- });
- });
|