/** * Providers Route 测试 * * 测试模型提供商管理 REST API 端点 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { Hono } from 'hono'; // Use vi.hoisted to create mocks const mockRegistry = vi.hoisted(() => ({ listForApi: vi.fn(), getDetail: vi.fn(), getModels: vi.fn(), has: vi.fn(), testConnection: vi.fn(), registerCustom: vi.fn(), setConfig: vi.fn(), saveConfig: vi.fn(), removeCustom: vi.fn(), addCustomModel: vi.fn(), removeCustomModel: vi.fn(), })); const mockCoreModule = vi.hoisted(() => ({ getProviderRegistry: vi.fn(() => mockRegistry), })); vi.mock('@ai-assistant/core', () => mockCoreModule); import { providersRouter } from '../../../src/routes/providers.js'; // Create test app const app = new Hono(); app.route('/providers', providersRouter); describe('Providers Route', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('GET /providers - 列出所有提供商', () => { it('返回提供商列表', async () => { const providers = [ { id: 'anthropic', name: 'Anthropic', builtin: true, enabled: true, hasApiKey: true, modelCount: 5 }, { id: 'openai', name: 'OpenAI', builtin: true, enabled: false, hasApiKey: false, modelCount: 10 }, ]; mockRegistry.listForApi.mockReturnValue(providers); const res = await app.request('/providers'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toEqual(providers); }); it('返回空列表', async () => { mockRegistry.listForApi.mockReturnValue([]); const res = await app.request('/providers'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data).toEqual([]); }); }); describe('GET /providers/:id - 获取提供商详情', () => { it('返回提供商详情', async () => { const detail = { id: 'anthropic', name: 'Anthropic', builtin: true, models: [{ id: 'claude-3', name: 'Claude 3' }], config: { enabled: true, hasApiKey: true }, }; mockRegistry.getDetail.mockReturnValue(detail); const res = await app.request('/providers/anthropic'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toEqual(detail); }); it('提供商不存在返回 404', async () => { mockRegistry.getDetail.mockReturnValue(undefined); const res = await app.request('/providers/non-existent'); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); expect(json.error).toContain('not found'); }); }); describe('GET /providers/:id/models - 获取模型列表', () => { it('返回模型列表', async () => { const models = [ { id: 'claude-3-opus', name: 'Claude 3 Opus', contextWindow: 200000 }, { id: 'claude-3-sonnet', name: 'Claude 3 Sonnet', contextWindow: 200000 }, ]; mockRegistry.has.mockReturnValue(true); mockRegistry.getModels.mockReturnValue(models); const res = await app.request('/providers/anthropic/models'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toEqual(models); }); it('提供商不存在返回 404', async () => { mockRegistry.has.mockReturnValue(false); const res = await app.request('/providers/non-existent/models'); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); }); }); describe('POST /providers/:id/test - 测试连接', () => { it('测试连接成功', async () => { mockRegistry.testConnection.mockResolvedValue({ success: true, latency: 150, }); const res = await app.request('/providers/anthropic/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: 'test-key' }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data.success).toBe(true); expect(json.data.latency).toBe(150); }); it('测试连接失败', async () => { mockRegistry.testConnection.mockResolvedValue({ success: false, error: 'Invalid API key', }); const res = await app.request('/providers/anthropic/test', { method: 'POST', }); const json = await res.json(); expect(res.status).toBe(200); expect(json.data.success).toBe(false); expect(json.data.error).toBe('Invalid API key'); }); }); describe('POST /providers - 注册自定义提供商', () => { it('注册成功', async () => { mockRegistry.registerCustom.mockReturnValue(undefined); mockRegistry.saveConfig.mockResolvedValue(undefined); const res = await app.request('/providers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'custom-provider', name: 'Custom Provider', baseUrl: 'https://api.custom.com', }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(mockRegistry.registerCustom).toHaveBeenCalled(); expect(mockRegistry.saveConfig).toHaveBeenCalled(); }); it('缺少必需字段返回 400', async () => { const res = await app.request('/providers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'custom' }), }); const json = await res.json(); expect(res.status).toBe(400); expect(json.success).toBe(false); expect(json.error).toContain('Missing required fields'); }); }); describe('PUT /providers/:id - 更新配置', () => { it('更新成功', async () => { mockRegistry.has.mockReturnValue(true); mockRegistry.setConfig.mockReturnValue(undefined); mockRegistry.saveConfig.mockResolvedValue(undefined); const res = await app.request('/providers/anthropic', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: true, apiKey: 'new-key' }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(mockRegistry.setConfig).toHaveBeenCalled(); }); it('提供商不存在返回 404', async () => { mockRegistry.has.mockReturnValue(false); const res = await app.request('/providers/non-existent', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: true }), }); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); }); }); describe('DELETE /providers/:id - 删除自定义提供商', () => { it('删除成功', async () => { mockRegistry.removeCustom.mockReturnValue(true); mockRegistry.saveConfig.mockResolvedValue(undefined); const res = await app.request('/providers/custom-provider', { method: 'DELETE', }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); it('提供商不存在返回 404', async () => { mockRegistry.removeCustom.mockReturnValue(false); const res = await app.request('/providers/non-existent', { method: 'DELETE', }); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); }); }); describe('POST /providers/:id/models - 添加自定义模型', () => { it('添加成功', async () => { mockRegistry.addCustomModel.mockReturnValue(undefined); mockRegistry.saveConfig.mockResolvedValue(undefined); const res = await app.request('/providers/anthropic/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'custom-model', name: 'Custom Model', contextWindow: 100000, }), }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(mockRegistry.addCustomModel).toHaveBeenCalledWith('anthropic', expect.any(Object)); }); it('缺少必需字段返回 400', async () => { const res = await app.request('/providers/anthropic/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'model' }), }); const json = await res.json(); expect(res.status).toBe(400); expect(json.error).toContain('Missing required fields'); }); }); describe('DELETE /providers/:id/models/:modelId - 删除自定义模型', () => { it('删除成功', async () => { mockRegistry.removeCustomModel.mockReturnValue(true); mockRegistry.saveConfig.mockResolvedValue(undefined); const res = await app.request('/providers/anthropic/models/custom-model', { method: 'DELETE', }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); }); it('模型不存在返回 404', async () => { mockRegistry.removeCustomModel.mockReturnValue(false); const res = await app.request('/providers/anthropic/models/non-existent', { method: 'DELETE', }); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); }); }); });