/** @vitest-environment jsdom */ import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import type { AuthUser } from '../../../packages/shared/src/contracts/auth'; import { requestClientApi, setStoredAuthAccessToken, } from '../src/services/clientApi'; import { beginPlatformSessionTransition, commitAuthenticatedPlatformSession, currentPlatformSessionGeneration, resetPlatformSessionStateForTests, } from '../src/services/platformSession'; vi.mock('@tauri-apps/plugin-http', () => ({ fetch: vi.fn() })); vi.mock('../src/services/errorReporting', () => ({ captureClientError: vi.fn(), })); const user = { id: 'session-user' } as AuthUser; const nativeInvoke = vi.fn(async () => null); const catalog = { models: [{ id: 'quality', displayName: '高质量' }] }; const json = (value: unknown, status = 200) => new Response(JSON.stringify(value), { status }); beforeEach(async () => { resetPlatformSessionStateForTests(); window.localStorage.clear(); nativeInvoke.mockClear(); window.__TAURI__ = { core: { invoke: nativeInvoke } }; setStoredAuthAccessToken('expired-token'); await commitAuthenticatedPlatformSession( user, currentPlatformSessionGeneration(), ); nativeInvoke.mockClear(); }); afterEach(() => { resetPlatformSessionStateForTests(); window.localStorage.clear(); delete window.__TAURI__; vi.restoreAllMocks(); }); it('并发模型请求共享续期,并在安装 Rust 会话后使用新 token 重试', async () => { let refreshCalls = 0; let modelCalls = 0; const fetch = vi .spyOn(globalThis, 'fetch') .mockImplementation(async (input, init) => { if (input === '/api/auth/refresh') { refreshCalls += 1; return json({ token: 'fresh-token' }); } if (input === '/api/auth/me') return json({ user }); modelCalls += 1; const token = new Headers(init?.headers).get('Authorization'); if (token === 'Bearer expired-token') return json({}, 401); expect(token).toBe('Bearer fresh-token'); expect(nativeInvoke).toHaveBeenCalledWith( 'install_platform_account_session', expect.objectContaining({ accessToken: 'fresh-token', userId: user.id, }), ); return json(catalog); }); const results = await Promise.all([ requestClientApi('/api/llm/models', { method: 'GET' }, '读取失败'), requestClientApi('/api/llm/models', { method: 'GET' }, '读取失败'), ]); expect(results).toEqual([catalog, catalog]); expect(refreshCalls).toBe(1); expect(modelCalls).toBe(4); expect(fetch).toHaveBeenCalledTimes(6); }); it.each([401])('续期失败保留原 HTTP %s,且不重发业务请求', async (status) => { const fetch = vi .spyOn(globalThis, 'fetch') .mockResolvedValueOnce(json({}, status)) .mockResolvedValueOnce(json({}, 401)); await expect( requestClientApi('/api/llm/models', { method: 'GET' }, '读取失败'), ).rejects.toMatchObject({ status }); expect(fetch).toHaveBeenCalledTimes(2); }); it('跳过鉴权的请求不触发续期', async () => { const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(json({}, 401)); await expect( requestClientApi('/api/example', {}, '读取失败', { skipAuth: true }), ).rejects.toMatchObject({ status: 401 }); expect(fetch).toHaveBeenCalledTimes(1); }); it('403 权限拒绝不触发续期或重发写请求', async () => { const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(json({}, 403)); await expect( requestClientApi( '/api/example', { method: 'POST', body: '{}' }, '权限不足', ), ).rejects.toMatchObject({ status: 403 }); expect(fetch).toHaveBeenCalledTimes(1); expect(nativeInvoke).not.toHaveBeenCalled(); }); it('续期成功后的再次未授权不循环重试', async () => { const fetch = vi .spyOn(globalThis, 'fetch') .mockResolvedValueOnce(json({}, 401)) .mockResolvedValueOnce(json({ token: 'fresh-token' })) .mockResolvedValueOnce(json({ user })) .mockResolvedValueOnce(json({}, 401)); await expect( requestClientApi('/api/llm/models', {}, '读取失败'), ).rejects.toMatchObject({ status: 401 }); expect(fetch).toHaveBeenCalledTimes(4); }); it('请求期间账号切换后,不替新账号续期或重发旧请求', async () => { let finish!: (response: Response) => void; const fetch = vi.spyOn(globalThis, 'fetch').mockImplementation( () => new Promise((resolve) => { finish = resolve; }), ); const pending = requestClientApi('/api/llm/models', {}, '读取失败'); const rejection = expect(pending).rejects.toMatchObject({ status: 401 }); const generation = beginPlatformSessionTransition(); setStoredAuthAccessToken('other-token'); await commitAuthenticatedPlatformSession( { ...user, id: 'other-user' }, generation, ); finish(json({}, 401)); await rejection; expect(fetch).toHaveBeenCalledTimes(1); });