Some checks reported errors
continuous-integration/drone/push Build was killed
- 搭建 api、auth、utils 等逻辑模块 - 通过 tsc、eslint、vitest 测试验证 BREAKING CHANGE: 新镜像分支
465 lines
15 KiB
TypeScript
465 lines
15 KiB
TypeScript
/**
|
||
* API客户端测试
|
||
* 测试API客户端的核心功能,包括请求、响应处理、拦截器管理等
|
||
*/
|
||
|
||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { createMockAxiosResponse, createMockAxiosError } from '@/test/mocks/http-client';
|
||
import { createMockApiRequestConfig } from '@/test/mocks/data-factory';
|
||
import axios from 'axios';
|
||
|
||
// 使用vi.hoisted提升createMockAxios,避免初始化顺序问题
|
||
const { createMockAxios } = vi.hoisted(() => {
|
||
let interceptorIdCounter = 0;
|
||
|
||
const mockAxiosInstance = {
|
||
get: vi.fn(),
|
||
post: vi.fn(),
|
||
put: vi.fn(),
|
||
delete: vi.fn(),
|
||
patch: vi.fn(),
|
||
request: vi.fn(),
|
||
interceptors: {
|
||
request: {
|
||
use: vi.fn(() => ++interceptorIdCounter),
|
||
eject: vi.fn()
|
||
},
|
||
response: {
|
||
use: vi.fn(() => ++interceptorIdCounter),
|
||
eject: vi.fn()
|
||
}
|
||
},
|
||
defaults: {
|
||
headers: {
|
||
common: {},
|
||
get: {},
|
||
post: {},
|
||
put: {},
|
||
delete: {}
|
||
}
|
||
}
|
||
};
|
||
|
||
return { createMockAxios: () => mockAxiosInstance };
|
||
});
|
||
|
||
// 模拟axios模块
|
||
vi.mock('axios', () => ({
|
||
default: {
|
||
create: vi.fn(() => createMockAxios())
|
||
},
|
||
AxiosError: class extends Error {
|
||
code?: string;
|
||
config?: any;
|
||
request?: any;
|
||
response?: any;
|
||
isAxiosError?: boolean;
|
||
|
||
constructor(message: string, code?: string, config?: any, request?: any, response?: any) {
|
||
super(message);
|
||
this.name = 'AxiosError';
|
||
this.code = code;
|
||
this.config = config;
|
||
this.request = request;
|
||
this.response = response;
|
||
this.isAxiosError = true;
|
||
}
|
||
}
|
||
}));
|
||
|
||
// 在模拟设置后导入createApiClient
|
||
import { createApiClient } from '@/api/client';
|
||
|
||
// 获取模拟的axios类型
|
||
type MockedAxios = ReturnType<typeof createMockAxios> & {
|
||
get: ReturnType<typeof vi.fn>;
|
||
post: ReturnType<typeof vi.fn>;
|
||
put: ReturnType<typeof vi.fn>;
|
||
delete: ReturnType<typeof vi.fn>;
|
||
patch: ReturnType<typeof vi.fn>;
|
||
request: ReturnType<typeof vi.fn>;
|
||
interceptors: {
|
||
request: {
|
||
use: ReturnType<typeof vi.fn>;
|
||
eject: ReturnType<typeof vi.fn>;
|
||
};
|
||
response: {
|
||
use: ReturnType<typeof vi.fn>;
|
||
eject: ReturnType<typeof vi.fn>;
|
||
};
|
||
};
|
||
defaults: {
|
||
timeout?: number;
|
||
baseURL?: string;
|
||
};
|
||
};
|
||
|
||
describe('API客户端', () => {
|
||
let apiClient: ReturnType<typeof createApiClient>;
|
||
let mockAxios: MockedAxios;
|
||
|
||
beforeEach(() => {
|
||
// 重置所有模拟
|
||
vi.clearAllMocks();
|
||
|
||
// 创建新的API客户端实例
|
||
apiClient = createApiClient();
|
||
|
||
// 获取模拟的axios实例
|
||
mockAxios = vi.mocked(axios).create() as MockedAxios;
|
||
});
|
||
|
||
describe('基础请求方法', () => {
|
||
it('应该能够发送GET请求', async () => {
|
||
const mockData = { id: 1, name: 'Test' };
|
||
mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData));
|
||
|
||
const result = await apiClient.get('/test');
|
||
|
||
expect(mockAxios.request).toHaveBeenCalledWith({ method: 'GET', url: '/test' });
|
||
expect(result).toEqual(mockData);
|
||
});
|
||
|
||
it('应该能够发送POST请求', async () => {
|
||
const mockData = { id: 1, name: 'Test' };
|
||
const requestData = { name: 'New Item' };
|
||
mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData));
|
||
|
||
const result = await apiClient.post('/test', requestData);
|
||
|
||
expect(mockAxios.request).toHaveBeenCalledWith({ method: 'POST', url: '/test', data: requestData });
|
||
expect(result).toEqual(mockData);
|
||
});
|
||
|
||
it('应该能够发送PUT请求', async () => {
|
||
const mockData = { id: 1, name: 'Updated Test' };
|
||
const requestData = { name: 'Updated Item' };
|
||
mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData));
|
||
|
||
const result = await apiClient.put('/test/1', requestData);
|
||
|
||
expect(mockAxios.request).toHaveBeenCalledWith({ method: 'PUT', url: '/test/1', data: requestData });
|
||
expect(result).toEqual(mockData);
|
||
});
|
||
|
||
it('应该能够发送DELETE请求', async () => {
|
||
const mockData = { success: true };
|
||
mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData));
|
||
|
||
const result = await apiClient.delete('/test/1');
|
||
|
||
expect(mockAxios.request).toHaveBeenCalledWith({ method: 'DELETE', url: '/test/1' });
|
||
expect(result).toEqual(mockData);
|
||
});
|
||
|
||
it('应该能够发送PATCH请求', async () => {
|
||
const mockData = { id: 1, name: 'Patched Test' };
|
||
const requestData = { name: 'Patched Item' };
|
||
mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData));
|
||
|
||
const result = await apiClient.patch('/test/1', requestData);
|
||
|
||
expect(mockAxios.request).toHaveBeenCalledWith({ method: 'PATCH', url: '/test/1', data: requestData });
|
||
expect(result).toEqual(mockData);
|
||
});
|
||
|
||
it('应该能够发送通用请求', async () => {
|
||
const mockData = { id: 1, name: 'Test' };
|
||
const config = createMockApiRequestConfig({ url: '/test', method: 'GET' });
|
||
mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData));
|
||
|
||
const result = await apiClient.request(config);
|
||
|
||
expect(mockAxios.request).toHaveBeenCalledWith(config);
|
||
expect(result).toEqual(mockData);
|
||
});
|
||
});
|
||
|
||
describe('返回完整响应的请求方法', () => {
|
||
it('应该能够返回GET请求的完整响应', async () => {
|
||
const mockData = { id: 1, name: 'Test' };
|
||
const mockResponse = createMockAxiosResponse(mockData);
|
||
mockAxios.request.mockResolvedValue(mockResponse);
|
||
|
||
const result = await apiClient.request({ url: '/test', method: 'GET' });
|
||
|
||
expect(mockAxios.request).toHaveBeenCalled();
|
||
expect(result).toEqual(mockData);
|
||
});
|
||
|
||
it('应该能够返回POST请求的完整响应', async () => {
|
||
const mockData = { id: 1, name: 'Test' };
|
||
const requestData = { name: 'New Item' };
|
||
const mockResponse = createMockAxiosResponse(mockData);
|
||
mockAxios.request.mockResolvedValue(mockResponse);
|
||
|
||
const result = await apiClient.request({ url: '/test', method: 'POST', data: requestData });
|
||
|
||
expect(mockAxios.request).toHaveBeenCalled();
|
||
expect(result).toEqual(mockData);
|
||
});
|
||
});
|
||
|
||
describe('拦截器管理', () => {
|
||
it('应该能够添加请求拦截器', () => {
|
||
const onRequest = (config: any) => config;
|
||
const onRequestError = (error: any) => error;
|
||
|
||
const interceptorId = apiClient.addRequestInterceptor(onRequest, onRequestError);
|
||
|
||
expect(mockAxios.interceptors.request.use).toHaveBeenCalled();
|
||
expect(typeof interceptorId).toBe('number');
|
||
});
|
||
|
||
it('应该能够添加响应拦截器', () => {
|
||
const onResponse = (response: any) => response;
|
||
const onResponseError = (error: any) => error;
|
||
|
||
const interceptorId = apiClient.addResponseInterceptor(onResponse, onResponseError);
|
||
|
||
expect(mockAxios.interceptors.response.use).toHaveBeenCalled();
|
||
expect(typeof interceptorId).toBe('number');
|
||
});
|
||
|
||
it('应该能够移除请求拦截器', () => {
|
||
const onRequest = (config: any) => config;
|
||
|
||
const interceptorId = apiClient.addRequestInterceptor(onRequest);
|
||
apiClient.removeRequestInterceptor(interceptorId);
|
||
|
||
expect(mockAxios.interceptors.request.eject).toHaveBeenCalledWith(interceptorId);
|
||
});
|
||
|
||
it('应该能够处理移除不存在的请求拦截器', () => {
|
||
const nonExistentId = 999;
|
||
|
||
// 不应该抛出错误
|
||
expect(() => {
|
||
apiClient.removeRequestInterceptor(nonExistentId);
|
||
}).not.toThrow();
|
||
|
||
// 不应该调用eject方法,因为拦截器不存在
|
||
expect(mockAxios.interceptors.request.eject).not.toHaveBeenCalledWith(nonExistentId);
|
||
});
|
||
|
||
it('应该能够移除响应拦截器', () => {
|
||
const onResponse = (response: any) => response;
|
||
|
||
const interceptorId = apiClient.addResponseInterceptor(onResponse);
|
||
apiClient.removeResponseInterceptor(interceptorId);
|
||
|
||
expect(mockAxios.interceptors.response.eject).toHaveBeenCalledWith(interceptorId);
|
||
});
|
||
|
||
it('应该能够处理移除不存在的响应拦截器', () => {
|
||
const nonExistentId = 999;
|
||
|
||
// 不应该抛出错误
|
||
expect(() => {
|
||
apiClient.removeResponseInterceptor(nonExistentId);
|
||
}).not.toThrow();
|
||
|
||
// 不应该调用eject方法,因为拦截器不存在
|
||
expect(mockAxios.interceptors.response.eject).not.toHaveBeenCalledWith(nonExistentId);
|
||
});
|
||
|
||
it('应该能够添加和移除多个请求拦截器', () => {
|
||
const onRequest1 = (config: any) => config;
|
||
const onRequest2 = (config: any) => config;
|
||
const onRequest3 = (config: any) => config;
|
||
|
||
// 添加三个拦截器
|
||
const id1 = apiClient.addRequestInterceptor(onRequest1);
|
||
const id2 = apiClient.addRequestInterceptor(onRequest2);
|
||
const id3 = apiClient.addRequestInterceptor(onRequest3);
|
||
|
||
// 移除中间的拦截器
|
||
apiClient.removeRequestInterceptor(id2);
|
||
|
||
// 验证eject被调用
|
||
expect(mockAxios.interceptors.request.eject).toHaveBeenCalledWith(id2);
|
||
|
||
// 移除剩余的拦截器
|
||
apiClient.removeRequestInterceptor(id1);
|
||
apiClient.removeRequestInterceptor(id3);
|
||
|
||
// 验证所有eject都被调用
|
||
expect(mockAxios.interceptors.request.eject).toHaveBeenCalledWith(id1);
|
||
expect(mockAxios.interceptors.request.eject).toHaveBeenCalledWith(id3);
|
||
});
|
||
|
||
it('应该能够添加和移除多个响应拦截器', () => {
|
||
const onResponse1 = (response: any) => response;
|
||
const onResponse2 = (response: any) => response;
|
||
const onResponse3 = (response: any) => response;
|
||
|
||
// 添加三个拦截器
|
||
const id1 = apiClient.addResponseInterceptor(onResponse1);
|
||
const id2 = apiClient.addResponseInterceptor(onResponse2);
|
||
const id3 = apiClient.addResponseInterceptor(onResponse3);
|
||
|
||
// 移除第一个和第三个拦截器
|
||
apiClient.removeResponseInterceptor(id1);
|
||
apiClient.removeResponseInterceptor(id3);
|
||
|
||
// 验证eject被调用
|
||
expect(mockAxios.interceptors.response.eject).toHaveBeenCalledWith(id1);
|
||
expect(mockAxios.interceptors.response.eject).toHaveBeenCalledWith(id3);
|
||
|
||
// 移除剩余的拦截器
|
||
apiClient.removeResponseInterceptor(id2);
|
||
|
||
// 验证所有eject都被调用
|
||
expect(mockAxios.interceptors.response.eject).toHaveBeenCalledWith(id2);
|
||
});
|
||
});
|
||
|
||
describe('配置管理', () => {
|
||
it('应该能够设置默认配置', () => {
|
||
const config = { timeout: 5000 };
|
||
apiClient.setDefaults(config);
|
||
|
||
expect(mockAxios.defaults.timeout).toBe(5000);
|
||
});
|
||
|
||
it('应该能够设置基础URL', () => {
|
||
const baseURL = 'https://api.example.com';
|
||
apiClient.setBaseURL(baseURL);
|
||
|
||
expect(mockAxios.defaults.baseURL).toBe(baseURL);
|
||
});
|
||
});
|
||
|
||
describe('实例创建', () => {
|
||
it('应该能够创建新实例', () => {
|
||
const newConfig = { timeout: 8000 };
|
||
const newInstance = apiClient.createInstance(newConfig);
|
||
|
||
expect(newInstance).toBeDefined();
|
||
expect(typeof newInstance.get).toBe('function');
|
||
expect(typeof newInstance.post).toBe('function');
|
||
});
|
||
|
||
it('应该能够复制拦截器到新实例', () => {
|
||
// 添加请求拦截器
|
||
const onRequest = (config: any) => ({ ...config, intercepted: true });
|
||
const onRequestError = (error: any) => error;
|
||
const requestInterceptorId = apiClient.addRequestInterceptor(onRequest, onRequestError);
|
||
|
||
// 添加响应拦截器
|
||
const onResponse = (response: any) => ({ ...response, intercepted: true });
|
||
const onResponseError = (error: any) => error;
|
||
const responseInterceptorId = apiClient.addResponseInterceptor(onResponse, onResponseError);
|
||
|
||
// 创建新实例
|
||
const newInstance = apiClient.createInstance({ timeout: 5000 });
|
||
|
||
// 验证新实例有拦截器方法
|
||
expect(typeof newInstance.addRequestInterceptor).toBe('function');
|
||
expect(typeof newInstance.addResponseInterceptor).toBe('function');
|
||
expect(typeof newInstance.removeRequestInterceptor).toBe('function');
|
||
expect(typeof newInstance.removeResponseInterceptor).toBe('function');
|
||
|
||
// 清理
|
||
apiClient.removeRequestInterceptor(requestInterceptorId);
|
||
apiClient.removeResponseInterceptor(responseInterceptorId);
|
||
});
|
||
|
||
it('应该能够处理headers类型转换', () => {
|
||
// 设置原始实例的headers
|
||
apiClient.setDefaults({
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Custom-Header': 'custom-value',
|
||
'X-Number-Header': 123,
|
||
'X-Boolean-Header': true,
|
||
'X-Object-Header': { key: 'value' }, // 这个应该被过滤掉
|
||
'X-Function-Header': () => {} // 这个应该被过滤掉
|
||
}
|
||
});
|
||
|
||
// 创建新实例
|
||
const newInstance = apiClient.createInstance({ timeout: 5000 });
|
||
|
||
// 验证新实例创建成功
|
||
expect(newInstance).toBeDefined();
|
||
});
|
||
|
||
it('应该能够创建嵌套实例', () => {
|
||
// 创建第一层新实例
|
||
const firstLevelInstance = apiClient.createInstance({ timeout: 3000 });
|
||
|
||
// 从第一层实例创建第二层实例
|
||
const secondLevelInstance = firstLevelInstance.createInstance({ baseURL: 'https://api.example.com' });
|
||
|
||
// 验证第二层实例创建成功
|
||
expect(secondLevelInstance).toBeDefined();
|
||
expect(typeof secondLevelInstance.get).toBe('function');
|
||
expect(typeof secondLevelInstance.post).toBe('function');
|
||
});
|
||
|
||
it('应该能够处理空配置创建实例', () => {
|
||
const newInstance = apiClient.createInstance();
|
||
|
||
expect(newInstance).toBeDefined();
|
||
expect(typeof newInstance.get).toBe('function');
|
||
expect(typeof newInstance.post).toBe('function');
|
||
});
|
||
|
||
it('应该能够处理没有headers的实例', () => {
|
||
// 创建一个没有headers的实例
|
||
const noHeadersInstance = apiClient.createInstance({ timeout: 2000 });
|
||
|
||
// 从没有headers的实例创建新实例
|
||
const newInstance = noHeadersInstance.createInstance({ baseURL: 'https://test.api.com' });
|
||
|
||
expect(newInstance).toBeDefined();
|
||
});
|
||
});
|
||
|
||
describe('错误处理', () => {
|
||
it('应该能够处理请求错误', async () => {
|
||
const errorMessage = 'Request failed';
|
||
mockAxios.request.mockRejectedValue(createMockAxiosError(errorMessage));
|
||
|
||
await expect(apiClient.get('/test')).rejects.toThrow(errorMessage);
|
||
});
|
||
|
||
it('应该能够处理网络错误', async () => {
|
||
const networkError = new Error('Network Error');
|
||
mockAxios.request.mockRejectedValue(networkError);
|
||
|
||
await expect(apiClient.get('/test')).rejects.toThrow('Network Error');
|
||
});
|
||
});
|
||
|
||
describe('类型转换', () => {
|
||
it('应该正确处理请求配置转换', async () => {
|
||
const config = createMockApiRequestConfig({
|
||
url: '/test',
|
||
method: 'POST',
|
||
data: { name: 'Test' },
|
||
headers: { 'Custom-Header': 'value' }
|
||
});
|
||
|
||
const mockData = { success: true };
|
||
mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData));
|
||
|
||
await apiClient.request(config);
|
||
|
||
// 验证axios被调用时接收到了正确的配置
|
||
expect(mockAxios.request).toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该正确处理响应数据转换', async () => {
|
||
const mockData = { id: 1, name: 'Test' };
|
||
mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData));
|
||
|
||
const result = await apiClient.get('/test');
|
||
|
||
// 验证返回的数据是正确的
|
||
expect(result).toEqual(mockData);
|
||
});
|
||
});
|
||
});
|