| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' |
| import axios from 'axios' |
| import type { AxiosInstance } from 'axios' |
|
|
| |
| vi.mock('@/i18n', () => ({ |
| getLocale: () => 'zh-CN', |
| })) |
|
|
| describe('API Client', () => { |
| let apiClient: AxiosInstance |
|
|
| beforeEach(async () => { |
| localStorage.clear() |
| |
| vi.resetModules() |
| const mod = await import('@/api/client') |
| apiClient = mod.apiClient |
| }) |
|
|
| afterEach(() => { |
| vi.restoreAllMocks() |
| }) |
|
|
| |
|
|
| describe('请求拦截器', () => { |
| it('自动附加 Authorization 头', async () => { |
| localStorage.setItem('auth_token', 'my-jwt-token') |
|
|
| |
| const adapter = vi.fn().mockResolvedValue({ |
| status: 200, |
| data: { code: 0, data: {} }, |
| headers: {}, |
| config: {}, |
| statusText: 'OK', |
| }) |
| apiClient.defaults.adapter = adapter |
|
|
| await apiClient.get('/test') |
|
|
| const config = adapter.mock.calls[0][0] |
| expect(config.headers.get('Authorization')).toBe('Bearer my-jwt-token') |
| }) |
|
|
| it('无 token 时不附加 Authorization 头', async () => { |
| const adapter = vi.fn().mockResolvedValue({ |
| status: 200, |
| data: { code: 0, data: {} }, |
| headers: {}, |
| config: {}, |
| statusText: 'OK', |
| }) |
| apiClient.defaults.adapter = adapter |
|
|
| await apiClient.get('/test') |
|
|
| const config = adapter.mock.calls[0][0] |
| expect(config.headers.get('Authorization')).toBeFalsy() |
| }) |
|
|
| it('GET 请求自动附加 timezone 参数', async () => { |
| const adapter = vi.fn().mockResolvedValue({ |
| status: 200, |
| data: { code: 0, data: {} }, |
| headers: {}, |
| config: {}, |
| statusText: 'OK', |
| }) |
| apiClient.defaults.adapter = adapter |
|
|
| await apiClient.get('/test') |
|
|
| const config = adapter.mock.calls[0][0] |
| expect(config.params).toHaveProperty('timezone') |
| }) |
|
|
| it('POST 请求不附加 timezone 参数', async () => { |
| const adapter = vi.fn().mockResolvedValue({ |
| status: 200, |
| data: { code: 0, data: {} }, |
| headers: {}, |
| config: {}, |
| statusText: 'OK', |
| }) |
| apiClient.defaults.adapter = adapter |
|
|
| await apiClient.post('/test', { foo: 'bar' }) |
|
|
| const config = adapter.mock.calls[0][0] |
| expect(config.params?.timezone).toBeUndefined() |
| }) |
| }) |
|
|
| |
|
|
| describe('响应拦截器', () => { |
| it('code=0 时解包 data 字段', async () => { |
| const adapter = vi.fn().mockResolvedValue({ |
| status: 200, |
| data: { code: 0, data: { name: 'test' }, message: 'ok' }, |
| headers: {}, |
| config: {}, |
| statusText: 'OK', |
| }) |
| apiClient.defaults.adapter = adapter |
|
|
| const response = await apiClient.get('/test') |
| expect(response.data).toEqual({ name: 'test' }) |
| }) |
|
|
| it('code!=0 时拒绝并返回结构化错误', async () => { |
| const adapter = vi.fn().mockResolvedValue({ |
| status: 200, |
| data: { code: 1001, message: '参数错误', data: null }, |
| headers: {}, |
| config: {}, |
| statusText: 'OK', |
| }) |
| apiClient.defaults.adapter = adapter |
|
|
| await expect(apiClient.get('/test')).rejects.toEqual( |
| expect.objectContaining({ |
| code: 1001, |
| message: '参数错误', |
| }) |
| ) |
| }) |
| }) |
|
|
| |
|
|
| describe('401 Token 刷新', () => { |
| it('无 refresh_token 时 401 清除 localStorage', async () => { |
| localStorage.setItem('auth_token', 'expired-token') |
| |
|
|
| |
| const originalLocation = window.location |
| Object.defineProperty(window, 'location', { |
| value: { ...originalLocation, pathname: '/dashboard', href: '/dashboard' }, |
| writable: true, |
| }) |
|
|
| const adapter = vi.fn().mockRejectedValue({ |
| response: { |
| status: 401, |
| data: { code: 'TOKEN_EXPIRED', message: 'Token expired' }, |
| }, |
| config: { |
| url: '/test', |
| headers: { Authorization: 'Bearer expired-token' }, |
| }, |
| code: 'ERR_BAD_REQUEST', |
| }) |
| apiClient.defaults.adapter = adapter |
|
|
| await expect(apiClient.get('/test')).rejects.toBeDefined() |
|
|
| expect(localStorage.getItem('auth_token')).toBeNull() |
|
|
| |
| Object.defineProperty(window, 'location', { |
| value: originalLocation, |
| writable: true, |
| }) |
| }) |
| }) |
|
|
| |
|
|
| describe('网络错误', () => { |
| it('网络错误返回 status 0 的错误', async () => { |
| const adapter = vi.fn().mockRejectedValue({ |
| code: 'ERR_NETWORK', |
| message: 'Network Error', |
| config: { url: '/test' }, |
| |
| }) |
| apiClient.defaults.adapter = adapter |
|
|
| await expect(apiClient.get('/test')).rejects.toEqual( |
| expect.objectContaining({ |
| status: 0, |
| message: 'Network error. Please check your connection.', |
| }) |
| ) |
| }) |
| }) |
|
|
| |
|
|
| describe('请求取消', () => { |
| it('取消的请求保持原始取消错误', async () => { |
| const source = axios.CancelToken.source() |
|
|
| const adapter = vi.fn().mockRejectedValue( |
| new axios.Cancel('Operation canceled') |
| ) |
| apiClient.defaults.adapter = adapter |
|
|
| await expect( |
| apiClient.get('/test', { cancelToken: source.token }) |
| ).rejects.toBeDefined() |
| }) |
| }) |
| }) |
|
|