354 lines
10 KiB
JavaScript
354 lines
10 KiB
JavaScript
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|||
|
|
import { api, clearCache, cancelAllRequests } from '@/services/api'
|
|||
|
|
import axios from 'axios'
|
|||
|
|
|
|||
|
|
// 模拟axios
|
|||
|
|
vi.mock('axios')
|
|||
|
|
const mockedAxios = vi.mocked(axios)
|
|||
|
|
|
|||
|
|
// 模拟localStorage
|
|||
|
|
const localStorageMock = {
|
|||
|
|
getItem: vi.fn(),
|
|||
|
|
setItem: vi.fn(),
|
|||
|
|
removeItem: vi.fn(),
|
|||
|
|
clear: vi.fn()
|
|||
|
|
}
|
|||
|
|
global.localStorage = localStorageMock
|
|||
|
|
|
|||
|
|
// 模拟console方法
|
|||
|
|
const consoleSpy = {
|
|||
|
|
error: vi.spyOn(console, 'error').mockImplementation(() => {}),
|
|||
|
|
warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
|
|||
|
|
log: vi.spyOn(console, 'log').mockImplementation(() => {})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟window.performance
|
|||
|
|
Object.defineProperty(window, 'performance', {
|
|||
|
|
value: {
|
|||
|
|
now: vi.fn(() => Date.now())
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
describe('API Service', () => {
|
|||
|
|
beforeEach(() => {
|
|||
|
|
// 重置所有模拟
|
|||
|
|
vi.clearAllMocks()
|
|||
|
|
|
|||
|
|
// 模拟axios.create返回值
|
|||
|
|
mockedAxios.create.mockReturnValue({
|
|||
|
|
request: vi.fn(),
|
|||
|
|
interceptors: {
|
|||
|
|
request: {
|
|||
|
|
use: vi.fn()
|
|||
|
|
},
|
|||
|
|
response: {
|
|||
|
|
use: vi.fn()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
afterEach(() => {
|
|||
|
|
// 清理所有请求
|
|||
|
|
cancelAllRequests()
|
|||
|
|
clearCache()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
describe('API实例', () => {
|
|||
|
|
it('应该正确创建API实例', () => {
|
|||
|
|
expect(api).toBeDefined()
|
|||
|
|
expect(typeof api.get).toBe('function')
|
|||
|
|
expect(typeof api.post).toBe('function')
|
|||
|
|
expect(typeof api.put).toBe('function')
|
|||
|
|
expect(typeof api.delete).toBe('function')
|
|||
|
|
expect(typeof api.request).toBe('function')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('应该设置正确的baseURL', () => {
|
|||
|
|
// 由于我们模拟了axios.create,这里我们检查调用参数
|
|||
|
|
expect(mockedAxios.create).toHaveBeenCalledWith(
|
|||
|
|
expect.objectContaining({
|
|||
|
|
baseURL: 'https://localhost:7001/api',
|
|||
|
|
timeout: 15000
|
|||
|
|
})
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
describe('请求拦截器', () => {
|
|||
|
|
it('应该在请求前添加时间戳', async () => {
|
|||
|
|
// 模拟请求
|
|||
|
|
const mockRequest = {
|
|||
|
|
url: '/test',
|
|||
|
|
method: 'get',
|
|||
|
|
headers: {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟请求拦截器
|
|||
|
|
const requestInterceptor = mockedAxios.create().interceptors.request.use.mock.calls[0][0]
|
|||
|
|
const processedRequest = requestInterceptor(mockRequest)
|
|||
|
|
|
|||
|
|
// 检查是否添加了时间戳
|
|||
|
|
expect(processedRequest.metadata).toBeDefined()
|
|||
|
|
expect(processedRequest.metadata.startTime).toBeDefined()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('应该在请求前添加CSRF令牌', async () => {
|
|||
|
|
// 模拟localStorage中的CSRF令牌
|
|||
|
|
localStorageMock.getItem.mockReturnValue('test-csrf-token')
|
|||
|
|
|
|||
|
|
// 模拟请求
|
|||
|
|
const mockRequest = {
|
|||
|
|
url: '/test',
|
|||
|
|
method: 'post',
|
|||
|
|
headers: {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟请求拦截器
|
|||
|
|
const requestInterceptor = mockedAxios.create().interceptors.request.use.mock.calls[0][0]
|
|||
|
|
const processedRequest = requestInterceptor(mockRequest)
|
|||
|
|
|
|||
|
|
// 检查是否添加了CSRF令牌
|
|||
|
|
expect(processedRequest.headers['X-CSRF-Token']).toBe('test-csrf-token')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('应该在请求前添加认证令牌', async () => {
|
|||
|
|
// 模拟localStorage中的认证令牌
|
|||
|
|
localStorageMock.getItem.mockImplementation((key) => {
|
|||
|
|
if (key === 'auth_token') return 'test-auth-token'
|
|||
|
|
return null
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 模拟请求
|
|||
|
|
const mockRequest = {
|
|||
|
|
url: '/test',
|
|||
|
|
method: 'get',
|
|||
|
|
headers: {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟请求拦截器
|
|||
|
|
const requestInterceptor = mockedAxios.create().interceptors.request.use.mock.calls[0][0]
|
|||
|
|
const processedRequest = requestInterceptor(mockRequest)
|
|||
|
|
|
|||
|
|
// 检查是否添加了认证令牌
|
|||
|
|
expect(processedRequest.headers.Authorization).toBe('Bearer test-auth-token')
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
describe('响应拦截器', () => {
|
|||
|
|
it('应该正确处理成功响应', async () => {
|
|||
|
|
// 模拟响应
|
|||
|
|
const mockResponse = {
|
|||
|
|
data: { success: true, data: { id: 1, name: 'Test' } },
|
|||
|
|
status: 200,
|
|||
|
|
statusText: 'OK',
|
|||
|
|
headers: {},
|
|||
|
|
config: {
|
|||
|
|
url: '/test',
|
|||
|
|
method: 'get',
|
|||
|
|
metadata: { startTime: Date.now() - 100 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟响应拦截器
|
|||
|
|
const responseInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][0]
|
|||
|
|
const processedResponse = responseInterceptor(mockResponse)
|
|||
|
|
|
|||
|
|
// 检查响应处理
|
|||
|
|
expect(processedResponse.data).toEqual({ success: true, data: { id: 1, name: 'Test' } })
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('应该记录请求耗时', async () => {
|
|||
|
|
// 模拟响应
|
|||
|
|
const mockResponse = {
|
|||
|
|
data: { success: true, data: { id: 1, name: 'Test' } },
|
|||
|
|
status: 200,
|
|||
|
|
statusText: 'OK',
|
|||
|
|
headers: {},
|
|||
|
|
config: {
|
|||
|
|
url: '/test',
|
|||
|
|
method: 'get',
|
|||
|
|
metadata: { startTime: Date.now() - 100 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟响应拦截器
|
|||
|
|
const responseInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][0]
|
|||
|
|
responseInterceptor(mockResponse)
|
|||
|
|
|
|||
|
|
// 检查是否记录了请求耗时
|
|||
|
|
expect(consoleSpy.log).toHaveBeenCalledWith(
|
|||
|
|
expect.stringContaining('请求完成'),
|
|||
|
|
expect.anything()
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('应该缓存GET请求响应', async () => {
|
|||
|
|
// 模拟响应
|
|||
|
|
const mockResponse = {
|
|||
|
|
data: { success: true, data: { id: 1, name: 'Test' } },
|
|||
|
|
status: 200,
|
|||
|
|
statusText: 'OK',
|
|||
|
|
headers: {},
|
|||
|
|
config: {
|
|||
|
|
url: '/test',
|
|||
|
|
method: 'get',
|
|||
|
|
metadata: { startTime: Date.now() - 100 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟响应拦截器
|
|||
|
|
const responseInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][0]
|
|||
|
|
responseInterceptor(mockResponse)
|
|||
|
|
|
|||
|
|
// 检查是否缓存了响应
|
|||
|
|
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
|||
|
|
expect.stringContaining('api_cache_'),
|
|||
|
|
expect.any(String)
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
describe('错误处理', () => {
|
|||
|
|
it('应该正确处理网络错误', async () => {
|
|||
|
|
// 模拟网络错误
|
|||
|
|
const networkError = new Error('Network Error')
|
|||
|
|
networkError.code = 'NETWORK_ERROR'
|
|||
|
|
|
|||
|
|
// 模拟错误拦截器
|
|||
|
|
const errorInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][1]
|
|||
|
|
|
|||
|
|
// 处理错误
|
|||
|
|
const result = errorInterceptor(networkError)
|
|||
|
|
|
|||
|
|
// 检查错误处理
|
|||
|
|
expect(result).rejects.toThrow()
|
|||
|
|
expect(consoleSpy.error).toHaveBeenCalledWith(
|
|||
|
|
'网络错误',
|
|||
|
|
expect.any(Error)
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('应该正确处理超时错误', async () => {
|
|||
|
|
// 模拟超时错误
|
|||
|
|
const timeoutError = new Error('Timeout')
|
|||
|
|
timeoutError.code = 'ECONNABORTED'
|
|||
|
|
|
|||
|
|
// 模拟错误拦截器
|
|||
|
|
const errorInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][1]
|
|||
|
|
|
|||
|
|
// 处理错误
|
|||
|
|
const result = errorInterceptor(timeoutError)
|
|||
|
|
|
|||
|
|
// 检查错误处理
|
|||
|
|
expect(result).rejects.toThrow()
|
|||
|
|
expect(consoleSpy.error).toHaveBeenCalledWith(
|
|||
|
|
'请求超时',
|
|||
|
|
expect.any(Error)
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('应该正确处理服务器错误', async () => {
|
|||
|
|
// 模拟服务器错误响应
|
|||
|
|
const serverError = {
|
|||
|
|
response: {
|
|||
|
|
status: 500,
|
|||
|
|
statusText: 'Internal Server Error',
|
|||
|
|
data: { message: '服务器内部错误' }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟错误拦截器
|
|||
|
|
const errorInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][1]
|
|||
|
|
|
|||
|
|
// 处理错误
|
|||
|
|
const result = errorInterceptor(serverError)
|
|||
|
|
|
|||
|
|
// 检查错误处理
|
|||
|
|
expect(result).rejects.toThrow()
|
|||
|
|
expect(consoleSpy.error).toHaveBeenCalledWith(
|
|||
|
|
'服务器错误',
|
|||
|
|
expect.any(Object)
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('应该正确处理401未授权错误', async () => {
|
|||
|
|
// 模拟401错误响应
|
|||
|
|
const unauthorizedError = {
|
|||
|
|
response: {
|
|||
|
|
status: 401,
|
|||
|
|
statusText: 'Unauthorized',
|
|||
|
|
data: { message: '未授权访问' }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟错误拦截器
|
|||
|
|
const errorInterceptor = mockedAxios.create().interceptors.response.use.mock.calls[0][1]
|
|||
|
|
|
|||
|
|
// 处理错误
|
|||
|
|
const result = errorInterceptor(unauthorizedError)
|
|||
|
|
|
|||
|
|
// 检查错误处理
|
|||
|
|
expect(result).rejects.toThrow()
|
|||
|
|
expect(consoleSpy.error).toHaveBeenCalledWith(
|
|||
|
|
'未授权访问',
|
|||
|
|
expect.any(Object)
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
describe('缓存管理', () => {
|
|||
|
|
it('应该正确缓存GET请求', () => {
|
|||
|
|
// 模拟缓存数据
|
|||
|
|
const cacheKey = 'api_cache_/test'
|
|||
|
|
const cacheData = {
|
|||
|
|
data: { id: 1, name: 'Test' },
|
|||
|
|
timestamp: Date.now(),
|
|||
|
|
expiry: Date.now() + 300000 // 5分钟后过期
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置缓存
|
|||
|
|
localStorageMock.setItem.mockReturnValue()
|
|||
|
|
localStorageMock.getItem.mockReturnValue(JSON.stringify(cacheData))
|
|||
|
|
|
|||
|
|
// 检查缓存设置
|
|||
|
|
localStorageMock.setItem(cacheKey, JSON.stringify(cacheData))
|
|||
|
|
expect(localStorageMock.setItem).toHaveBeenCalledWith(cacheKey, JSON.stringify(cacheData))
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('应该正确清除缓存', () => {
|
|||
|
|
// 调用清除缓存函数
|
|||
|
|
clearCache()
|
|||
|
|
|
|||
|
|
// 检查是否清除了所有缓存
|
|||
|
|
expect(localStorageMock.removeItem).toHaveBeenCalledWith('api_cache_/test')
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
describe('请求取消', () => {
|
|||
|
|
it('应该正确取消重复请求', () => {
|
|||
|
|
// 模拟请求配置
|
|||
|
|
const requestConfig = {
|
|||
|
|
url: '/test',
|
|||
|
|
method: 'get'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模拟CancelToken
|
|||
|
|
const cancelToken = {
|
|||
|
|
token: 'test-token',
|
|||
|
|
cancel: vi.fn()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否创建了CancelToken
|
|||
|
|
expect(cancelToken.token).toBe('test-token')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('应该正确取消所有请求', () => {
|
|||
|
|
// 调用取消所有请求函数
|
|||
|
|
cancelAllRequests()
|
|||
|
|
|
|||
|
|
// 检查是否取消了所有请求
|
|||
|
|
// 这里需要根据实际实现进行检查
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
})
|