import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { api } from '../request' import { ApiError } from '@/types/api' describe('request utils', () => { const mockFetch = vi.fn() const originalFetch = globalThis.fetch const originalLocation = window.location beforeEach(() => { globalThis.fetch = mockFetch localStorage.clear() vi.restoreAllMocks() // Mock window.location Object.defineProperty(window, 'location', { value: { href: '', pathname: '/', search: '' }, writable: true, }) }) afterEach(() => { globalThis.fetch = originalFetch Object.defineProperty(window, 'location', { value: originalLocation, writable: true, }) }) function mockResponse(body: Record, status = 200) { mockFetch.mockResolvedValueOnce({ status, json: () => Promise.resolve(body), }) } describe('token auto-injection', () => { it('sends Authorization header when token exists', async () => { localStorage.setItem('access_token', 'test-token') mockResponse({ code: 0, data: { id: 1 }, message: 'ok' }) await api.get('/api/test') const [, options] = mockFetch.mock.calls[0]! expect(options.headers['Authorization']).toBe('Bearer test-token') }) it('omits Authorization header when no token', async () => { mockResponse({ code: 0, data: null, message: 'ok' }) await api.get('/api/test') const [, options] = mockFetch.mock.calls[0]! expect(options.headers['Authorization']).toBeUndefined() }) }) describe('response parsing', () => { it('returns data field on success (code 0)', async () => { mockResponse({ code: 0, data: { users: [] }, message: 'ok' }) const result = await api.get('/api/users') expect(result).toEqual({ users: [] }) }) it('throws ApiError on business error (code !== 0)', async () => { mockResponse({ code: 1001, data: null, message: '参数错误' }) await expect(api.get('/api/test')).rejects.toThrow(ApiError) await mockResponse({ code: 1001, data: null, message: '参数错误' }) try { await api.get('/api/test') } catch (e) { expect(e).toBeInstanceOf(ApiError) expect((e as ApiError).code).toBe(1001) expect((e as ApiError).message).toBe('参数错误') } }) }) describe('401 handling', () => { it('clears tokens and redirects on 401', async () => { localStorage.setItem('access_token', 'expired-token') localStorage.setItem('refresh_token', 'expired-refresh') localStorage.setItem('user', '{"id":1}') mockFetch.mockResolvedValueOnce({ status: 401, json: () => Promise.resolve({ code: 401, message: 'Unauthorized' }), }) await expect(api.get('/api/test')).rejects.toThrow('登录已过期') expect(localStorage.getItem('access_token')).toBeNull() expect(localStorage.getItem('refresh_token')).toBeNull() expect(localStorage.getItem('user')).toBeNull() expect(window.location.href).toBe('/login?redirect=%2F') }) }) describe('HTTP methods', () => { it('GET with params', async () => { mockResponse({ code: 0, data: [], message: 'ok' }) await api.get('/api/users', { page: 1, per_page: 15, name: '' }) const [url, options] = mockFetch.mock.calls[0]! expect(url).toContain('/api/users?') expect(url).toContain('page=1') expect(url).toContain('per_page=15') expect(url).not.toContain('name=') expect(options.method).toBe('GET') }) it('POST with body', async () => { mockResponse({ code: 0, data: { id: 1 }, message: 'ok' }) await api.post('/api/users', { username: 'test' }) const [, options] = mockFetch.mock.calls[0]! expect(options.method).toBe('POST') expect(options.body).toBe(JSON.stringify({ username: 'test' })) }) it('PUT with body', async () => { mockResponse({ code: 0, data: null, message: 'ok' }) await api.put('/api/users/1', { username: 'updated' }) const [, options] = mockFetch.mock.calls[0]! expect(options.method).toBe('PUT') }) it('PATCH with body', async () => { mockResponse({ code: 0, data: null, message: 'ok' }) await api.patch('/api/users/1', { status: 1 }) const [, options] = mockFetch.mock.calls[0]! expect(options.method).toBe('PATCH') }) it('DELETE', async () => { mockResponse({ code: 0, data: null, message: 'ok' }) await api.delete('/api/users/1') const [, options] = mockFetch.mock.calls[0]! expect(options.method).toBe('DELETE') }) }) describe('params serialization', () => { it('filters out undefined, null, and empty string values', async () => { mockResponse({ code: 0, data: [], message: 'ok' }) await api.get('/api/test', { a: 'hello', b: undefined, c: null, d: '', e: 0, }) const [url] = mockFetch.mock.calls[0]! expect(url).toContain('a=hello') expect(url).toContain('e=0') expect(url).not.toContain('b=') expect(url).not.toContain('c=') expect(url).not.toContain('d=') }) }) })