platform-auth.test.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import { test } from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { createPlatformAuthClient, PlatformSession, AUTH_STORAGE_KEY } from '../src/services/platform-auth.js';
  4. const config = { parseUrl: 'https://platform.example/parse', appId: 'platform-app', companyId: 'company-one', companyName: '测试企业' };
  5. const profile = { objectId: 'user-one', username: 'member', nickname: '测试成员', company: { __type: 'Pointer', className: 'Company', objectId: 'company-one' }, hash: 'private-field', ACL: {}, sessionToken: 'fixture-session-only' };
  6. const publicUser = { id: 'user-one', username: 'member', displayName: '测试成员', companyId: 'company-one', companyName: '测试企业' };
  7. const reply = (data, status = 200) => ({ ok: status >= 200 && status < 300, status, json: async () => data });
  8. const memory = () => {
  9. const map = new Map();
  10. return { map, getItem: key => map.get(key) || null, setItem: (key, value) => map.set(key, value), removeItem: key => map.delete(key) };
  11. };
  12. const deferred = () => { let resolve, reject;const promise = new Promise((a, b) => { resolve = a;reject = b; });return { promise, resolve, reject }; };
  13. test('平台登录用POST密码,随后核会话和企业;公开状态仅保留必要身份字段', async () => {
  14. const calls = [];
  15. const client = createPlatformAuthClient({ ...config, fetchImpl: async (url, options) => {
  16. calls.push({ url, options });
  17. return reply(url.endsWith('/login') ? { ...profile, company: null } : url.endsWith('/logout') ? {} : profile);
  18. } });
  19. const result = await client.login(' member ', 'fixture-password-only');
  20. assert.deepEqual(result.user, publicUser);
  21. assert.equal(result.token, profile.sessionToken);
  22. assert.equal(calls[0].options.method, 'POST');
  23. assert.deepEqual(JSON.parse(calls[0].options.body), { username: 'member', password: 'fixture-password-only' });
  24. assert.equal(calls[1].options.headers['X-Parse-Session-Token'], profile.sessionToken);
  25. assert.equal(calls[1].url, config.parseUrl + '/users/me');
  26. await client.logout(result.token);
  27. assert.equal(calls[2].options.method, 'POST');
  28. assert.ok(calls.every(c => !c.url.includes('fixture-') && c.options.credentials === 'omit' && c.options.redirect === 'error'));
  29. assert.ok(calls.every(c => !Object.keys(c.options.headers).some(k => /master|service/i.test(k))));
  30. });
  31. test('账号密码错误不重试,不把平台原始错误内容交给界面', async () => {
  32. let calls = 0;
  33. const client = createPlatformAuthClient({ ...config, fetchImpl: async () => { calls++;return reply({ code: 101, error: 'sensitive internal diagnostic' }, 404); } });
  34. await assert.rejects(client.login('member', 'fixture-wrong-only'), error => error.code === 'INVALID_CREDENTIALS' && !error.message.includes('sensitive'));
  35. assert.equal(calls, 1);
  36. });
  37. test('登录返回企业正确也必须以users/me为准,跨企业拒绝并关闭新会话', async () => {
  38. const calls = [];
  39. const client = createPlatformAuthClient({ ...config, fetchImpl: async (url) => {
  40. calls.push(url);
  41. return reply(url.endsWith('/login') ? profile : url.endsWith('/logout') ? {} : { ...profile, company: { ...profile.company, objectId: 'another-company' } });
  42. } });
  43. await assert.rejects(client.login('member', 'fixture-password-only'), { code: 'COMPANY_FORBIDDEN' });
  44. assert.deepEqual(calls.map(url => url.slice(config.parseUrl.length)), ['/login', '/users/me', '/logout']);
  45. });
  46. test('网络、限流、非JSON响应提示可恢复;不会泄露原始响应', async () => {
  47. for (const [fetchImpl, code] of [
  48. [async () => { throw Error('raw connection secret'); }, 'AUTH_UNAVAILABLE'],
  49. [async () => reply({ error: 'private limit details' }, 429), 'AUTH_RATE_LIMITED'],
  50. [async () => ({ ok: false, status: 502, json: async () => { throw Error('raw proxy body'); } }), 'AUTH_UNAVAILABLE']
  51. ]) {
  52. const client = createPlatformAuthClient({ ...config, fetchImpl });
  53. await assert.rejects(client.login('member', 'fixture-password-only'), error => error.code === code && !/private|raw|secret/.test(error.message));
  54. }
  55. });
  56. test('刷新从服务端重新核验,不信任浏览器伪造的用户或姓名', async () => {
  57. const storage = memory();
  58. storage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ token: 'fixture-session-only', user: { displayName: '伪造管理员' } }));
  59. let calls = 0;
  60. const session = new PlatformSession({ storage, client: { current: async token => { calls++;assert.equal(token, 'fixture-session-only');return publicUser; } } });
  61. assert.equal(session.state.user, null);
  62. await session.restore();
  63. assert.equal(calls, 1);
  64. assert.deepEqual(session.state.user, publicUser);
  65. assert.equal(session.state.status, 'authenticated');
  66. });
  67. test('登录只持久化会话,不持久化密码/平台档案;退出先清本机再关闭服务端会话', async () => {
  68. const storage = memory(), logout = deferred();
  69. const session = new PlatformSession({ storage, client: { login: async () => ({ token: 'fixture-session-only', user: publicUser }), logout: () => logout.promise } });
  70. await session.login('member', 'fixture-password-only');
  71. assert.deepEqual(JSON.parse(storage.getItem(AUTH_STORAGE_KEY)), { token: 'fixture-session-only' });
  72. assert.ok(!JSON.stringify(session.state).includes('fixture-'));
  73. const pending = session.logout();
  74. assert.equal(session.state.user, null);
  75. assert.equal(storage.getItem(AUTH_STORAGE_KEY), null);
  76. logout.resolve({});
  77. assert.deepEqual(await pending, { remoteClosed: true });
  78. });
  79. test('失效会话清除身份,临时网络故障保留可重试凭据但不冒称已登录', async () => {
  80. for (const code of ['AUTH_REQUIRED', 'COMPANY_FORBIDDEN', 'AUTH_UNAVAILABLE']) {
  81. const storage = memory();storage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ token: 'fixture-session-only' }));
  82. let online = false;
  83. const session = new PlatformSession({ storage, client: { current: async () => {
  84. if (online) return publicUser;
  85. throw Object.assign(Error('会话检查失败'), { code });
  86. } } });
  87. await session.restore();
  88. assert.equal(session.state.user, null);
  89. assert.equal(session.state.status, code === 'AUTH_UNAVAILABLE' ? 'unavailable' : 'guest');
  90. assert.equal(!!storage.getItem(AUTH_STORAGE_KEY), code === 'AUTH_UNAVAILABLE');
  91. if (code === 'AUTH_UNAVAILABLE') { online = true;await session.restore();assert.deepEqual(session.state.user, publicUser); }
  92. }
  93. });
  94. test('浏览器禁用存储仍可登录,公开状态不暴露会话', async () => {
  95. const storage = { getItem() { throw Error('denied'); }, setItem() { throw Error('denied'); }, removeItem() { throw Error('denied'); } };
  96. const session = new PlatformSession({ storage, client: { login: async () => ({ token: 'fixture-session-only', user: publicUser }), logout: async () => ({}) } });
  97. await session.restore();
  98. await session.login('member', 'fixture-password-only');
  99. assert.equal(session.state.status, 'authenticated');
  100. assert.equal(session.state.retained, false);
  101. assert.ok(!JSON.stringify(session.state).includes('fixture-session-only'));
  102. await session.logout();
  103. assert.equal(session.state.user, null);
  104. });
  105. test('退出后才返回的登录请求不恢复身份,并撤销刚创建的会话', async () => {
  106. const response = deferred(), closed = [];
  107. const session = new PlatformSession({ storage: memory(), client: { login: () => response.promise, logout: async token => closed.push(token) } });
  108. const pending = session.login('member', 'fixture-password-only');
  109. await session.logout();
  110. response.resolve({ token: 'fixture-late-session', user: publicUser });
  111. assert.equal(await pending, null);
  112. assert.equal(session.state.status, 'guest');
  113. assert.deepEqual(closed, ['fixture-late-session']);
  114. });
  115. test('并发登录仅保留最后一次明确提交的账号;迟到响应不覆盖', async () => {
  116. const first = deferred(), second = deferred(), closed = [], storage = memory();
  117. let count = 0;
  118. const session = new PlatformSession({ storage, client: { login: () => (++count === 1 ? first : second).promise, logout: async token => closed.push(token) } });
  119. const a = session.login('member', 'fixture-one'), b = session.login('member-two', 'fixture-two');
  120. second.resolve({ token: 'fixture-second-session', user: { ...publicUser, id: 'user-two', username: 'member-two' } });
  121. await b;
  122. first.resolve({ token: 'fixture-first-session', user: publicUser });
  123. await a;
  124. assert.equal(session.state.user.id, 'user-two');
  125. assert.equal(JSON.parse(storage.getItem(AUTH_STORAGE_KEY)).token, 'fixture-second-session');
  126. assert.deepEqual(closed, ['fixture-first-session']);
  127. });
  128. test('退出中断刷新核验;恢复请求迟到也不能再次变为登录', async () => {
  129. const storage = memory(), response = deferred();storage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ token: 'fixture-session-only' }));
  130. const session = new PlatformSession({ storage, client: { current: () => response.promise, logout: async () => ({}) } });
  131. const pending = session.restore();
  132. await session.logout();response.resolve(publicUser);await pending;
  133. assert.equal(session.state.user, null);
  134. assert.equal(storage.getItem(AUTH_STORAGE_KEY), null);
  135. });