/** * SC-G-02 微信授权登录 · API 级 e2e(auth.guest.wechatLogin / S-cfc-9) * ================================================================ * 覆盖: * - 正常流(TC-G-02):POST /api/auth/wechat-login → Result 含 token + 用户信息, * 角色默认 parent;新微信自动建 User + 家庭 + family_members 成员(自己能查到)。 * - 权限流(TC-G-02-P):微信登录是公开路径(游客无 token 可触达);受保护接口 * /api/family/member/list 无 token → 401;跨家庭 familyId → 403(越权被拒)。 * - 边界(TC-G-02-E):登录响应不泄露内部错误堆栈(Result 统一 code/message/data 信封)。 * * 目标端由 PW_BASE_URL 指定(默认 http://cfc.iwintrue.com,本地验证用 * http://127.0.0.1:9082,后端开 wechat.test-mode=true)。 * * 运行:npx playwright test wechat-login -c playwright.config.api.js --project=desktop */ const { test, expect } = require('@playwright/test'); const BASE = process.env.PW_BASE_URL || 'http://cfc.iwintrue.com'; const API_TIMEOUT = 60000; /** * 统一 POST(全项目接口一律 POST),返回 { http, code, message, data } */ async function api(request, path, body, token) { const headers = { 'Content-Type': 'application/json' }; if (token) headers.Authorization = 'Bearer ' + token; const res = await request.post(BASE + path, { data: body || {}, headers, timeout: API_TIMEOUT }); const raw = await res.text(); let json = null; try { json = raw ? JSON.parse(raw) : null; } catch (e) { json = null; } return { http: res.status(), code: json ? json.code : null, message: json ? json.message : null, data: json ? json.data : null, }; } test.describe('SC-G-02 微信授权登录(API 级)', () => { let ctx = {}; test.beforeAll(async ({ request }) => { const login = await api(request, '/api/auth/wechat-login', { code: 'scg02-' + Date.now() + '-' + String(Math.random()).slice(2, 8), nickname: 'SC-G-02游客', }); expect(login.http).toBe(200); expect(login.code).toBe(200); expect(login.data && login.data.token).toBeTruthy(); ctx = login.data; }); test('正常流:微信一键登录返回 token+用户信息,角色默认 parent', async () => { expect(ctx.role).toBe('parent'); expect(ctx.userId).toBeTruthy(); expect(ctx.openid).toBeTruthy(); expect(typeof ctx.token).toBe('string'); expect(ctx.token.length).toBeGreaterThan(20); // JWT 三段式 }); test('正常流:首次登录自动建档,家庭列表能看到自己(family_members 成员)', async ({ request }) => { const members = await api(request, '/api/family/member/list', { includeFamily: true }, ctx.token); expect(members.http).toBe(200); expect(members.code).toBe(200); expect(Array.isArray(members.data)).toBeTruthy(); const self = (members.data || []).find((m) => String(m.userId) === String(ctx.userId)); expect(self).toBeTruthy(); }); test('权限流:微信登录是公开路径,无 token 也可调用(游客可触达)', async ({ request }) => { const login = await api(request, '/api/auth/wechat-login', { code: 'scg02-anon-' + Date.now(), }); expect(login.http).toBe(200); expect(login.code).toBe(200); expect(login.data && login.data.token).toBeTruthy(); }); test('权限流:受保护接口无 token → 统一 401,不泄露内部堆栈', async ({ request }) => { const r = await api(request, '/api/family/member/list', {}); expect(r.http).toBe(401); expect(r.code).toBe(401); expect(String(r.message)).toContain('请先登录'); expect(JSON.stringify(r)).not.toContain('Exception'); expect(JSON.stringify(r)).not.toContain('at com.etotem'); }); test('权限流:登录后携带 token 可访问;跨家庭 familyId → 403 越权被拒', async ({ request }) => { const own = await api(request, '/api/family/member/list', { familyId: ctx.familyId }, ctx.token); expect(own.code).toBe(200); const cross = await api(request, '/api/family/member/list', { familyId: 999999999 }, ctx.token); expect(cross.http).toBe(403); expect(cross.code).toBe(403); expect(String(cross.message)).toContain('无权访问'); }); });