feat(image): 新建 knowai-core:1.0.0 镜像并完成推送
Some checks reported errors
continuous-integration/drone/push Build was killed

- 搭建 api、auth、utils 等逻辑模块
- 通过 tsc、eslint、vitest 测试验证

BREAKING CHANGE: 新镜像分支
This commit is contained in:
tobegold574
2025-11-10 20:20:25 +08:00
commit 6a81b7bb13
73 changed files with 10511 additions and 0 deletions

View File

@@ -0,0 +1,464 @@
/**
* 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);
});
});
});