wechat-login.spec.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. /**
  2. * SC-G-02 微信授权登录 · API 级 e2e(auth.guest.wechatLogin / S-cfc-9)
  3. * ================================================================
  4. * 覆盖:
  5. * - 正常流(TC-G-02):POST /api/auth/wechat-login → Result 含 token + 用户信息,
  6. * 角色默认 parent;新微信自动建 User + 家庭 + family_members 成员(自己能查到)。
  7. * - 权限流(TC-G-02-P):微信登录是公开路径(游客无 token 可触达);受保护接口
  8. * /api/family/member/list 无 token → 401;跨家庭 familyId → 403(越权被拒)。
  9. * - 边界(TC-G-02-E):登录响应不泄露内部错误堆栈(Result 统一 code/message/data 信封)。
  10. *
  11. * 目标端由 PW_BASE_URL 指定(默认 http://cfc.iwintrue.com,本地验证用
  12. * http://127.0.0.1:9082,后端开 wechat.test-mode=true)。
  13. *
  14. * 运行:npx playwright test wechat-login -c playwright.config.api.js --project=desktop
  15. */
  16. const { test, expect } = require('@playwright/test');
  17. const BASE = process.env.PW_BASE_URL || 'http://cfc.iwintrue.com';
  18. const API_TIMEOUT = 60000;
  19. /**
  20. * 统一 POST(全项目接口一律 POST),返回 { http, code, message, data }
  21. */
  22. async function api(request, path, body, token) {
  23. const headers = { 'Content-Type': 'application/json' };
  24. if (token) headers.Authorization = 'Bearer ' + token;
  25. const res = await request.post(BASE + path, { data: body || {}, headers, timeout: API_TIMEOUT });
  26. const raw = await res.text();
  27. let json = null;
  28. try {
  29. json = raw ? JSON.parse(raw) : null;
  30. } catch (e) {
  31. json = null;
  32. }
  33. return {
  34. http: res.status(),
  35. code: json ? json.code : null,
  36. message: json ? json.message : null,
  37. data: json ? json.data : null,
  38. };
  39. }
  40. test.describe('SC-G-02 微信授权登录(API 级)', () => {
  41. let ctx = {};
  42. test.beforeAll(async ({ request }) => {
  43. const login = await api(request, '/api/auth/wechat-login', {
  44. code: 'scg02-' + Date.now() + '-' + String(Math.random()).slice(2, 8),
  45. nickname: 'SC-G-02游客',
  46. });
  47. expect(login.http).toBe(200);
  48. expect(login.code).toBe(200);
  49. expect(login.data && login.data.token).toBeTruthy();
  50. ctx = login.data;
  51. });
  52. test('正常流:微信一键登录返回 token+用户信息,角色默认 parent', async () => {
  53. expect(ctx.role).toBe('parent');
  54. expect(ctx.userId).toBeTruthy();
  55. expect(ctx.openid).toBeTruthy();
  56. expect(typeof ctx.token).toBe('string');
  57. expect(ctx.token.length).toBeGreaterThan(20); // JWT 三段式
  58. });
  59. test('正常流:首次登录自动建档,家庭列表能看到自己(family_members 成员)', async ({ request }) => {
  60. const members = await api(request, '/api/family/member/list', { includeFamily: true }, ctx.token);
  61. expect(members.http).toBe(200);
  62. expect(members.code).toBe(200);
  63. expect(Array.isArray(members.data)).toBeTruthy();
  64. const self = (members.data || []).find((m) => String(m.userId) === String(ctx.userId));
  65. expect(self).toBeTruthy();
  66. });
  67. test('权限流:微信登录是公开路径,无 token 也可调用(游客可触达)', async ({ request }) => {
  68. const login = await api(request, '/api/auth/wechat-login', {
  69. code: 'scg02-anon-' + Date.now(),
  70. });
  71. expect(login.http).toBe(200);
  72. expect(login.code).toBe(200);
  73. expect(login.data && login.data.token).toBeTruthy();
  74. });
  75. test('权限流:受保护接口无 token → 统一 401,不泄露内部堆栈', async ({ request }) => {
  76. const r = await api(request, '/api/family/member/list', {});
  77. expect(r.http).toBe(401);
  78. expect(r.code).toBe(401);
  79. expect(String(r.message)).toContain('请先登录');
  80. expect(JSON.stringify(r)).not.toContain('Exception');
  81. expect(JSON.stringify(r)).not.toContain('at com.etotem');
  82. });
  83. test('权限流:登录后携带 token 可访问;跨家庭 familyId → 403 越权被拒', async ({ request }) => {
  84. const own = await api(request, '/api/family/member/list', { familyId: ctx.familyId }, ctx.token);
  85. expect(own.code).toBe(200);
  86. const cross = await api(request, '/api/family/member/list', { familyId: 999999999 }, ctx.token);
  87. expect(cross.http).toBe(403);
  88. expect(cross.code).toBe(403);
  89. expect(String(cross.message)).toContain('无权访问');
  90. });
  91. });