Files
knowai/test/unit/api/client.test.ts
tobegold574 6a81b7bb13
Some checks reported errors
continuous-integration/drone/push Build was killed
feat(image): 新建 knowai-core:1.0.0 镜像并完成推送
- 搭建 api、auth、utils 等逻辑模块
- 通过 tsc、eslint、vitest 测试验证

BREAKING CHANGE: 新镜像分支
2025-11-10 20:20:25 +08:00

465 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);
});
});
});