// ============================================================ // request.ts 拦截器测试 // ============================================================ // // 认证策略已迁移到 HttpOnly cookie 模式。 // 浏览器自动附加 cookie(withCredentials: true),JS 不操作 token。 import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' // ── Hoisted: mock store (cookie-based auth — no JS token) ── const { mockLogout, _store } = vi.hoisted(() => { const mockLogout = vi.fn() const _store = { isAuthenticated: false, logout: mockLogout, } return { 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' }) mockLogout.mockClear() _store.isAuthenticated = false }) afterEach(() => { server.close() }) describe('request interceptor', () => { it('sends requests with credentials (cookie-based auth)', async () => { let capturedCreds = false server.use( http.get('*/api/v1/test', ({ request }) => { // Cookie-based auth: the browser sends cookies automatically. // We verify the request was made successfully. capturedCreds = true return HttpResponse.json({ ok: true }) }), ) setStoreState({ isAuthenticated: true }) const res = await request.get('/test') expect(res.data).toEqual({ ok: true }) expect(capturedCreds).toBe(true) }) 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 when authenticated — refreshes cookie and retries', 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', () => { // Server sets new HttpOnly cookie in response — no JS token needed return HttpResponse.json({ ok: true }) }), ) setStoreState({ isAuthenticated: true }) const res = await request.get('/protected') expect(res.data).toEqual({ data: 'success' }) }) it('handles 401 when not authenticated — calls logout immediately', async () => { server.use( http.get('*/api/v1/norefresh', () => { return HttpResponse.json({ error: 'unauthorized' }, { status: 401 }) }), ) setStoreState({ isAuthenticated: false }) 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({ isAuthenticated: true }) try { await request.get('/refreshfail') expect.fail('Should have thrown') } catch { expect(mockLogout).toHaveBeenCalled() } }) })