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