import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createPlatformAuthClient, PlatformSession, AUTH_STORAGE_KEY } from '../src/services/platform-auth.js'; const config = { parseUrl: 'https://platform.example/parse', appId: 'platform-app', companyId: 'company-one', companyName: '测试企业' }; const profile = { objectId: 'user-one', username: 'member', nickname: '测试成员', company: { __type: 'Pointer', className: 'Company', objectId: 'company-one' }, hash: 'private-field', ACL: {}, sessionToken: 'fixture-session-only' }; const publicUser = { id: 'user-one', username: 'member', displayName: '测试成员', companyId: 'company-one', companyName: '测试企业' }; const reply = (data, status = 200) => ({ ok: status >= 200 && status < 300, status, json: async () => data }); const memory = () => { const map = new Map(); return { map, getItem: key => map.get(key) || null, setItem: (key, value) => map.set(key, value), removeItem: key => map.delete(key) }; }; const deferred = () => { let resolve, reject;const promise = new Promise((a, b) => { resolve = a;reject = b; });return { promise, resolve, reject }; }; test('平台登录用POST密码,随后核会话和企业;公开状态仅保留必要身份字段', async () => { const calls = []; const client = createPlatformAuthClient({ ...config, fetchImpl: async (url, options) => { calls.push({ url, options }); return reply(url.endsWith('/login') ? { ...profile, company: null } : url.endsWith('/logout') ? {} : profile); } }); const result = await client.login(' member ', 'fixture-password-only'); assert.deepEqual(result.user, publicUser); assert.equal(result.token, profile.sessionToken); assert.equal(calls[0].options.method, 'POST'); assert.deepEqual(JSON.parse(calls[0].options.body), { username: 'member', password: 'fixture-password-only' }); assert.equal(calls[1].options.headers['X-Parse-Session-Token'], profile.sessionToken); assert.equal(calls[1].url, config.parseUrl + '/users/me'); await client.logout(result.token); assert.equal(calls[2].options.method, 'POST'); assert.ok(calls.every(c => !c.url.includes('fixture-') && c.options.credentials === 'omit' && c.options.redirect === 'error')); assert.ok(calls.every(c => !Object.keys(c.options.headers).some(k => /master|service/i.test(k)))); }); test('账号密码错误不重试,不把平台原始错误内容交给界面', async () => { let calls = 0; const client = createPlatformAuthClient({ ...config, fetchImpl: async () => { calls++;return reply({ code: 101, error: 'sensitive internal diagnostic' }, 404); } }); await assert.rejects(client.login('member', 'fixture-wrong-only'), error => error.code === 'INVALID_CREDENTIALS' && !error.message.includes('sensitive')); assert.equal(calls, 1); }); test('登录返回企业正确也必须以users/me为准,跨企业拒绝并关闭新会话', async () => { const calls = []; const client = createPlatformAuthClient({ ...config, fetchImpl: async (url) => { calls.push(url); return reply(url.endsWith('/login') ? profile : url.endsWith('/logout') ? {} : { ...profile, company: { ...profile.company, objectId: 'another-company' } }); } }); await assert.rejects(client.login('member', 'fixture-password-only'), { code: 'COMPANY_FORBIDDEN' }); assert.deepEqual(calls.map(url => url.slice(config.parseUrl.length)), ['/login', '/users/me', '/logout']); }); test('网络、限流、非JSON响应提示可恢复;不会泄露原始响应', async () => { for (const [fetchImpl, code] of [ [async () => { throw Error('raw connection secret'); }, 'AUTH_UNAVAILABLE'], [async () => reply({ error: 'private limit details' }, 429), 'AUTH_RATE_LIMITED'], [async () => ({ ok: false, status: 502, json: async () => { throw Error('raw proxy body'); } }), 'AUTH_UNAVAILABLE'] ]) { const client = createPlatformAuthClient({ ...config, fetchImpl }); await assert.rejects(client.login('member', 'fixture-password-only'), error => error.code === code && !/private|raw|secret/.test(error.message)); } }); test('刷新从服务端重新核验,不信任浏览器伪造的用户或姓名', async () => { const storage = memory(); storage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ token: 'fixture-session-only', user: { displayName: '伪造管理员' } })); let calls = 0; const session = new PlatformSession({ storage, client: { current: async token => { calls++;assert.equal(token, 'fixture-session-only');return publicUser; } } }); assert.equal(session.state.user, null); await session.restore(); assert.equal(calls, 1); assert.deepEqual(session.state.user, publicUser); assert.equal(session.state.status, 'authenticated'); }); test('登录只持久化会话,不持久化密码/平台档案;退出先清本机再关闭服务端会话', async () => { const storage = memory(), logout = deferred(); const session = new PlatformSession({ storage, client: { login: async () => ({ token: 'fixture-session-only', user: publicUser }), logout: () => logout.promise } }); await session.login('member', 'fixture-password-only'); assert.deepEqual(JSON.parse(storage.getItem(AUTH_STORAGE_KEY)), { token: 'fixture-session-only' }); assert.ok(!JSON.stringify(session.state).includes('fixture-')); const pending = session.logout(); assert.equal(session.state.user, null); assert.equal(storage.getItem(AUTH_STORAGE_KEY), null); logout.resolve({}); assert.deepEqual(await pending, { remoteClosed: true }); }); test('失效会话清除身份,临时网络故障保留可重试凭据但不冒称已登录', async () => { for (const code of ['AUTH_REQUIRED', 'COMPANY_FORBIDDEN', 'AUTH_UNAVAILABLE']) { const storage = memory();storage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ token: 'fixture-session-only' })); let online = false; const session = new PlatformSession({ storage, client: { current: async () => { if (online) return publicUser; throw Object.assign(Error('会话检查失败'), { code }); } } }); await session.restore(); assert.equal(session.state.user, null); assert.equal(session.state.status, code === 'AUTH_UNAVAILABLE' ? 'unavailable' : 'guest'); assert.equal(!!storage.getItem(AUTH_STORAGE_KEY), code === 'AUTH_UNAVAILABLE'); if (code === 'AUTH_UNAVAILABLE') { online = true;await session.restore();assert.deepEqual(session.state.user, publicUser); } } }); test('浏览器禁用存储仍可登录,公开状态不暴露会话', async () => { const storage = { getItem() { throw Error('denied'); }, setItem() { throw Error('denied'); }, removeItem() { throw Error('denied'); } }; const session = new PlatformSession({ storage, client: { login: async () => ({ token: 'fixture-session-only', user: publicUser }), logout: async () => ({}) } }); await session.restore(); await session.login('member', 'fixture-password-only'); assert.equal(session.state.status, 'authenticated'); assert.equal(session.state.retained, false); assert.ok(!JSON.stringify(session.state).includes('fixture-session-only')); await session.logout(); assert.equal(session.state.user, null); }); test('退出后才返回的登录请求不恢复身份,并撤销刚创建的会话', async () => { const response = deferred(), closed = []; const session = new PlatformSession({ storage: memory(), client: { login: () => response.promise, logout: async token => closed.push(token) } }); const pending = session.login('member', 'fixture-password-only'); await session.logout(); response.resolve({ token: 'fixture-late-session', user: publicUser }); assert.equal(await pending, null); assert.equal(session.state.status, 'guest'); assert.deepEqual(closed, ['fixture-late-session']); }); test('并发登录仅保留最后一次明确提交的账号;迟到响应不覆盖', async () => { const first = deferred(), second = deferred(), closed = [], storage = memory(); let count = 0; const session = new PlatformSession({ storage, client: { login: () => (++count === 1 ? first : second).promise, logout: async token => closed.push(token) } }); const a = session.login('member', 'fixture-one'), b = session.login('member-two', 'fixture-two'); second.resolve({ token: 'fixture-second-session', user: { ...publicUser, id: 'user-two', username: 'member-two' } }); await b; first.resolve({ token: 'fixture-first-session', user: publicUser }); await a; assert.equal(session.state.user.id, 'user-two'); assert.equal(JSON.parse(storage.getItem(AUTH_STORAGE_KEY)).token, 'fixture-second-session'); assert.deepEqual(closed, ['fixture-first-session']); }); test('退出中断刷新核验;恢复请求迟到也不能再次变为登录', async () => { const storage = memory(), response = deferred();storage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ token: 'fixture-session-only' })); const session = new PlatformSession({ storage, client: { current: () => response.promise, logout: async () => ({}) } }); const pending = session.restore(); await session.logout();response.resolve(publicUser);await pending; assert.equal(session.state.user, null); assert.equal(storage.getItem(AUTH_STORAGE_KEY), null); });