// ============================================================ // request.ts 拦截器测试 // ============================================================ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' // ── Hoisted: mock functions + store (accessible in vi.mock factory) ── const { mockSetToken, mockSetRefreshToken, mockLogout, _store } = vi.hoisted(() => { const mockSetToken = vi.fn() const mockSetRefreshToken = vi.fn() const mockLogout = vi.fn() const _store = { token: null as string | null, refreshToken: null as string | null, setToken: mockSetToken, setRefreshToken: mockSetRefreshToken, logout: mockLogout, } return { mockSetToken, mockSetRefreshToken, mockLogout, _store } }) vi.mock('@/stores/authStore', () => ({ useAuthStore: { getState: () => _store, }, })) import request, { ApiRequestError } from '@/services/request' function setStoreState(overrides: Partial) { Object.assign(_store, overrides) } // ── MSW server ────────────────────────────────────────────── const server = setupServer() beforeEach(() => { server.listen({ onUnhandledRequest: 'bypass' }) mockSetToken.mockClear() mockSetRefreshToken.mockClear() mockLogout.mockClear() _store.token = null _store.refreshToken = null }) afterEach(() => { server.close() }) describe('request interceptor', () => { it('attaches Authorization header when token exists', async () => { let capturedAuth: string | null = null server.use( http.get('*/api/v1/test', ({ request }) => { capturedAuth = request.headers.get('Authorization') return HttpResponse.json({ ok: true }) }), ) setStoreState({ token: 'test-jwt-token' }) await request.get('/test') expect(capturedAuth).toBe('Bearer test-jwt-token') }) it('does not attach Authorization header when no token', async () => { let capturedAuth: string | null = null server.use( http.get('*/api/v1/test', ({ request }) => { capturedAuth = request.headers.get('Authorization') return HttpResponse.json({ ok: true }) }), ) setStoreState({ token: null }) await request.get('/test') expect(capturedAuth).toBeNull() }) it('wraps non-401 errors as ApiRequestError', async () => { server.use( http.get('*/api/v1/test', () => { return HttpResponse.json( { error: 'not_found', message: 'Resource not found' }, { status: 404 }, ) }), ) try { await request.get('/test') expect.fail('Should have thrown') } catch (err) { expect(err).toBeInstanceOf(ApiRequestError) expect((err as ApiRequestError).status).toBe(404) expect((err as ApiRequestError).body.message).toBe('Resource not found') } }) it('wraps network errors as ApiRequestError with status 0', async () => { server.use( http.get('*/api/v1/test', () => { return HttpResponse.error() }), ) try { await request.get('/test') expect.fail('Should have thrown') } catch (err) { expect(err).toBeInstanceOf(ApiRequestError) expect((err as ApiRequestError).status).toBe(0) } }) it('handles 401 with refresh token success', async () => { let callCount = 0 server.use( http.get('*/api/v1/protected', () => { callCount++ if (callCount === 1) { return HttpResponse.json({ error: 'unauthorized' }, { status: 401 }) } return HttpResponse.json({ data: 'success' }) }), http.post('*/api/v1/auth/refresh', () => { return HttpResponse.json({ token: 'new-jwt', refresh_token: 'new-refresh' }) }), ) setStoreState({ token: 'old-jwt', refreshToken: 'old-refresh' }) const res = await request.get('/protected') expect(res.data).toEqual({ data: 'success' }) expect(mockSetToken).toHaveBeenCalledWith('new-jwt') expect(mockSetRefreshToken).toHaveBeenCalledWith('new-refresh') }) it('handles 401 with no refresh token — calls logout immediately', async () => { server.use( http.get('*/api/v1/norefresh', () => { return HttpResponse.json({ error: 'unauthorized' }, { status: 401 }) }), ) setStoreState({ token: 'old-jwt', refreshToken: null }) try { await request.get('/norefresh') expect.fail('Should have thrown') } catch { expect(mockLogout).toHaveBeenCalled() } }) it('handles 401 with refresh failure — calls logout', async () => { server.use( http.get('*/api/v1/refreshfail', () => { return HttpResponse.json({ error: 'unauthorized' }, { status: 401 }) }), http.post('*/api/v1/auth/refresh', () => { return HttpResponse.json({ error: 'invalid' }, { status: 401 }) }), ) setStoreState({ token: 'old-jwt', refreshToken: 'old-refresh' }) try { await request.get('/refreshfail') expect.fail('Should have thrown') } catch { expect(mockLogout).toHaveBeenCalled() } }) })