/** * 非功能 / 安全 接口冒烟(远程后端 cfc.iwintrue.com) * ================================================================ * 目的:把 tests/test-cases/*.md 中「非功能测试用例(性能/兼容/安全)」段里 * **可自动化** 的用例落地为可执行断言,提升接口层可执行覆盖率。 * * 覆盖的 TC(与文档一一对应): * - TC-G-N04 未授权访问受保护接口 → 期望 401/403 * - TC-P-N09 / TC-X-N05 IDOR 横向越权(他人家庭)→ 期望 403 * - TC-A-N05 后台接口非管理员访问 → 期望 401/403 * - TC-C-N07 双角色铁律:角色切换需登录;普通家长不可越权切换为未持有角色 * - TC-P-N10 AI 健康教练 XSS(query 注入)→ 不回显原始脚本、不 500 * - TC-A-N06 存储型 XSS:活动反馈 comment → 列表不回显原始脚本 * - TC-P-N11 / TC-X-N08 报告上传非 PDF/恶意文件 → 被拒(非 200) * * 运行:npx playwright test e2e/api/nonfunc-security.spec.js -c playwright.config.api.js --project=desktop * 说明:全部为纯 API 调用,不依赖前端 SPA,契合 CFC-ENV-001(远程无前端)限制。 * * ⚠️ 断言语义说明:本文件断言的是「安全契约」(受保护接口未授权应返回 401/403、 * 越权应 403)。若远程实际返回其它码,用例会 FAIL —— 这恰好用于暴露后端当前 * 与契约的偏差(即真实安全缺口),属预期内的审计结果,而非用例本身错误。 * * 健壮性:direct-register 是测试夹具而非被测系统,远端在 CI 部署窗口会偶发返回非 * JSON 导致注册失败;故注册逻辑内置多次重试,避免夹具抖动连累安全断言。 */ const { test, expect } = require('@playwright/test'); const { registerRole, apiPost } = require('../helpers/api'); const XSS = ''; /** 带重试的注册(规避远端部署窗口偶发非 JSON 响应);惰性缓存 parent 上下文避免重复注册 */ let _parent = null; async function getParent(request) { if (_parent && _parent.token) return _parent; let lastErr; for (let i = 0; i < 4; i++) { try { const ctx = await registerRole(request, 'parent', 'QA-SEC'); if (ctx && ctx.token) { _parent = ctx; return ctx; } } catch (e) { lastErr = e; } await new Promise((r) => setTimeout(r, 2500)); } throw lastErr || new Error('direct-register 重试后仍失败'); } test.describe('非功能/安全接口冒烟', () => { // —— 未授权访问(TC-G-N04):受保护接口未登录应被拒(401/403)—— test('[TC-G-N04] 未授权访问能量概览被拒(401/403)', async ({ request }) => { const r = await apiPost(request, '/api/energy/overview', {}, null); expect([401, 403].includes(r.code), `能量概览未授权应 401/403,实际 code=${r.code} msg=${r.message}`).toBeTruthy(); }); test('[TC-G-N04] 未授权访问家庭成员列表被拒(401/403)', async ({ request }) => { const r = await apiPost(request, '/api/family/member/list', {}, null); expect([401, 403].includes(r.code), `成员列表未授权应 401/403,实际 code=${r.code} msg=${r.message}`).toBeTruthy(); }); test('[TC-G-N04] 未授权调用 AI 健康教练被拒(401/403)', async ({ request }) => { const r = await apiPost(request, '/api/ai/health-coach/send', { query: '你好' }, null); expect([401, 403].includes(r.code), `AI 教练未授权应 401/403,实际 code=${r.code} msg=${r.message}`).toBeTruthy(); }); // —— IDOR 横向越权(TC-P-N09 / TC-X-N05):携带本人 token 越权访问他人 familyId → 403 —— test('[TC-P-N09] IDOR:越权访问他人家庭成员列表 → 403', async ({ request }) => { const ctx = await getParent(request); const r = await apiPost(request, '/api/family/member/list', { familyId: 999999999 }, ctx.token); expect(r.code, `越权访问他人家庭应 403,实际 code=${r.code} msg=${r.message}`).toBe(403); expect(String(r.message)).toContain('无权访问'); }); // —— 后台鉴权(TC-A-N05):非管理员访问后台接口被拒(401/403)—— // R170 修正:原探针用 `/api/admin-auth/login`(**登录入口,本就应匿名可访问**), // 空参返回 code=400「手机号和验证码不能为空」是正确行为 → 拿登录接口当「特权入口」 // 属用例设计错误(恒红假缺陷)。改用真实特权面 `/api/admin/families/overview` // (S-cfc-143/144 收口后匿名应 401)。 test('[TC-A-N05] 非管理员访问后台接口被拒(401/403)', async ({ request }) => { const r = await apiPost(request, '/api/admin/families/overview', {}, null); expect([401, 403].includes(r.code), `后台接口未授权应 401/403,实际 code=${r.code} msg=${r.message}`).toBeTruthy(); }); // —— 双角色铁律 / 权限提升防护(TC-C-N07)—— test('[TC-C-N07] 角色切换接口未授权被拒(401/403)', async ({ request }) => { const r = await apiPost(request, '/api/auth/switch-role', { role: 'parent' }, null); expect([401, 403].includes(r.code), `切换角色未授权应 401/403,实际 code=${r.code} msg=${r.message}`).toBeTruthy(); }); test('[TC-C-N07] 权限提升防护:家长不可切换为未持有角色', async ({ request }) => { // 普通家长仅持有 parent/child,尝试切换为 admin 必须被拒(不得签发 token) const ctx = await getParent(request); const r = await apiPost(request, '/api/auth/switch-role', { role: 'admin' }, ctx.token); expect(r.data && r.data.token, `家长不应越权切到 admin(实际 data=${JSON.stringify(r.data)})`).toBeFalsy(); }); test('[TC-C-N07] 角色切换自身合法角色正常(基线)', async ({ request }) => { const ctx = await getParent(request); const r = await apiPost(request, '/api/auth/switch-role', { role: 'parent' }, ctx.token); expect(r.code, `合法角色切换应 200,实际 code=${r.code} msg=${r.message}`).toBe(200); expect(r.data && r.data.token, '合法切换应签发 token').toBeTruthy(); }); // —— AI 健康教练 XSS(TC-P-N10):query 注入脚本不回显原始脚本且不错 500 —— test('[TC-P-N10] AI 健康教练 XSS:query 注入脚本不回显原始脚本且不错 500', async ({ request }) => { const ctx = await getParent(request); const r = await apiPost(request, '/api/ai/health-coach/send', { query: XSS, conversationId: '' }, ctx.token); expect(r.code, `AI 教练不应 500,实际 code=${r.code} msg=${r.message}`).not.toBe(500); expect(JSON.stringify(r)).not.toContain(XSS); }); // —— 存储型 XSS(TC-A-N06):核心关注点是不回显原始