/** * Zulip消息发送集成测试 * * 功能描述: * - 测试消息发送到真实Zulip服务器的完整流程 * - 验证HTTP请求、响应处理和错误场景 * - 包含网络异常和API错误的测试 * * 注意:这些测试需要真实的Zulip服务器配置 * * 最近修改: * - 2026-01-12: 架构优化 - 从src/core/zulip_core/services/移动到test/integration/,符合测试分离规范 (修改者: moyin) * - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin) * - 2026-01-10: 测试新增 - 创建Zulip消息发送集成测试 (修改者: moyin) * * @author moyin * @version 1.1.0 * @since 2026-01-10 * @lastModified 2026-01-12 */ import { Test, TestingModule } from '@nestjs/testing'; import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from '../../src/core/zulip_core/services/zulip_client.service'; import * as nock from 'nock'; describe('ZulipMessageIntegration', () => { let service: ZulipClientService; let mockZulipClient: any; let clientInstance: ZulipClientInstance; const testConfig: ZulipClientConfig = { username: 'test-bot@example.com', apiKey: 'test-api-key-12345', realm: 'https://test-zulip.example.com', }; beforeEach(async () => { // 清理所有HTTP拦截 nock.cleanAll(); const module: TestingModule = await Test.createTestingModule({ providers: [ZulipClientService], }).compile(); service = module.get(ZulipClientService); // 创建模拟的zulip-js客户端 mockZulipClient = { config: testConfig, users: { me: { getProfile: jest.fn(), }, }, messages: { send: jest.fn(), }, queues: { register: jest.fn(), deregister: jest.fn(), }, events: { retrieve: jest.fn(), }, }; // 模拟客户端实例 clientInstance = { userId: 'test-user-123', config: testConfig, client: mockZulipClient, lastEventId: -1, createdAt: new Date(), lastActivity: new Date(), isValid: true, }; // Mock zulip-js模块加载 jest.spyOn(service as any, 'loadZulipModule').mockResolvedValue(() => mockZulipClient); }); afterEach(() => { nock.cleanAll(); jest.clearAllMocks(); }); describe('消息发送到Zulip服务器', () => { it('应该成功发送消息到Zulip API', async () => { // 模拟成功的API响应 mockZulipClient.messages.send.mockResolvedValue({ result: 'success', id: 12345, msg: '', }); const result = await service.sendMessage( clientInstance, 'test-stream', 'test-topic', 'Hello from integration test!' ); expect(result.success).toBe(true); expect(result.messageId).toBe(12345); expect(mockZulipClient.messages.send).toHaveBeenCalledWith({ type: 'stream', to: 'test-stream', subject: 'test-topic', content: 'Hello from integration test!', }); }); it('应该处理Zulip API错误响应', async () => { // 模拟API错误响应 mockZulipClient.messages.send.mockResolvedValue({ result: 'error', msg: 'Stream does not exist', code: 'STREAM_NOT_FOUND', }); const result = await service.sendMessage( clientInstance, 'nonexistent-stream', 'test-topic', 'This should fail' ); expect(result.success).toBe(false); expect(result.error).toBe('Stream does not exist'); }); it('应该处理网络连接异常', async () => { // 模拟网络异常 mockZulipClient.messages.send.mockRejectedValue(new Error('Network timeout')); const result = await service.sendMessage( clientInstance, 'test-stream', 'test-topic', 'This will timeout' ); expect(result.success).toBe(false); expect(result.error).toBe('Network timeout'); }); it('应该处理认证失败', async () => { // 模拟认证失败 mockZulipClient.messages.send.mockResolvedValue({ result: 'error', msg: 'Invalid API key', code: 'BAD_REQUEST', }); const result = await service.sendMessage( clientInstance, 'test-stream', 'test-topic', 'Authentication test' ); expect(result.success).toBe(false); expect(result.error).toBe('Invalid API key'); }); it('应该正确处理特殊字符和长消息', async () => { const longMessage = 'A'.repeat(1000) + '特殊字符测试: 🎮🎯🚀 @#$%^&*()'; mockZulipClient.messages.send.mockResolvedValue({ result: 'success', id: 67890, }); const result = await service.sendMessage( clientInstance, 'test-stream', 'special-chars-topic', longMessage ); expect(result.success).toBe(true); expect(result.messageId).toBe(67890); expect(mockZulipClient.messages.send).toHaveBeenCalledWith({ type: 'stream', to: 'test-stream', subject: 'special-chars-topic', content: longMessage, }); }); it('应该更新客户端最后活动时间', async () => { const initialTime = new Date('2026-01-01T00:00:00Z'); clientInstance.lastActivity = initialTime; mockZulipClient.messages.send.mockResolvedValue({ result: 'success', id: 11111, }); await service.sendMessage( clientInstance, 'test-stream', 'test-topic', 'Activity test' ); expect(clientInstance.lastActivity.getTime()).toBeGreaterThan(initialTime.getTime()); }); }); describe('事件队列与Zulip服务器交互', () => { it('应该成功注册事件队列', async () => { mockZulipClient.queues.register.mockResolvedValue({ result: 'success', queue_id: 'test-queue-123', last_event_id: 42, }); const result = await service.registerQueue(clientInstance, ['message', 'typing']); expect(result.success).toBe(true); expect(result.queueId).toBe('test-queue-123'); expect(result.lastEventId).toBe(42); expect(clientInstance.queueId).toBe('test-queue-123'); expect(clientInstance.lastEventId).toBe(42); }); it('应该处理队列注册失败', async () => { mockZulipClient.queues.register.mockResolvedValue({ result: 'error', msg: 'Rate limit exceeded', }); const result = await service.registerQueue(clientInstance); expect(result.success).toBe(false); expect(result.error).toBe('Rate limit exceeded'); }); it('应该成功获取事件', async () => { clientInstance.queueId = 'test-queue-123'; clientInstance.lastEventId = 10; const mockEvents = [ { id: 11, type: 'message', message: { id: 98765, sender_email: 'user@example.com', content: 'Test message from Zulip', stream_id: 1, subject: 'Test Topic', }, }, { id: 12, type: 'typing', sender: { user_id: 123 }, }, ]; mockZulipClient.events.retrieve.mockResolvedValue({ result: 'success', events: mockEvents, }); const result = await service.getEvents(clientInstance, true); expect(result.success).toBe(true); expect(result.events).toEqual(mockEvents); expect(clientInstance.lastEventId).toBe(12); // 更新为最后一个事件的ID }); it('应该处理空事件队列', async () => { clientInstance.queueId = 'test-queue-123'; mockZulipClient.events.retrieve.mockResolvedValue({ result: 'success', events: [], }); const result = await service.getEvents(clientInstance, true); expect(result.success).toBe(true); expect(result.events).toEqual([]); }); it('应该成功注销事件队列', async () => { clientInstance.queueId = 'test-queue-123'; mockZulipClient.queues.deregister.mockResolvedValue({ result: 'success', }); const result = await service.deregisterQueue(clientInstance); expect(result).toBe(true); expect(clientInstance.queueId).toBeUndefined(); expect(clientInstance.lastEventId).toBe(-1); }); it('应该处理队列过期情况', async () => { clientInstance.queueId = 'expired-queue'; // 模拟队列过期的JSON解析错误 mockZulipClient.queues.deregister.mockRejectedValue( new Error('invalid json response body at https://zulip.example.com/api/v1/events reason: Unexpected token') ); const result = await service.deregisterQueue(clientInstance); expect(result).toBe(true); // 应该返回true,因为队列已过期 expect(clientInstance.queueId).toBeUndefined(); expect(clientInstance.lastEventId).toBe(-1); }); }); describe('API Key验证', () => { it('应该成功验证有效的API Key', async () => { mockZulipClient.users.me.getProfile.mockResolvedValue({ result: 'success', email: 'test-bot@example.com', full_name: 'Test Bot', user_id: 123, }); const isValid = await service.validateApiKey(clientInstance); expect(isValid).toBe(true); expect(clientInstance.isValid).toBe(true); }); it('应该拒绝无效的API Key', async () => { mockZulipClient.users.me.getProfile.mockResolvedValue({ result: 'error', msg: 'Invalid API key', }); const isValid = await service.validateApiKey(clientInstance); expect(isValid).toBe(false); expect(clientInstance.isValid).toBe(false); }); it('应该处理API Key验证网络异常', async () => { mockZulipClient.users.me.getProfile.mockRejectedValue(new Error('Connection refused')); const isValid = await service.validateApiKey(clientInstance); expect(isValid).toBe(false); expect(clientInstance.isValid).toBe(false); }); }); describe('错误恢复和重试机制', () => { it('应该在临时网络错误后恢复', async () => { // 第一次调用失败,第二次成功 mockZulipClient.messages.send .mockRejectedValueOnce(new Error('Temporary network error')) .mockResolvedValueOnce({ result: 'success', id: 99999, }); // 第一次调用应该失败 const firstResult = await service.sendMessage( clientInstance, 'test-stream', 'test-topic', 'First attempt' ); expect(firstResult.success).toBe(false); // 第二次调用应该成功 const secondResult = await service.sendMessage( clientInstance, 'test-stream', 'test-topic', 'Second attempt' ); expect(secondResult.success).toBe(true); expect(secondResult.messageId).toBe(99999); }); it('应该处理服务器5xx错误', async () => { mockZulipClient.messages.send.mockRejectedValue(new Error('Internal Server Error (500)')); const result = await service.sendMessage( clientInstance, 'test-stream', 'test-topic', 'Server error test' ); expect(result.success).toBe(false); expect(result.error).toBe('Internal Server Error (500)'); }); }); describe('性能和并发测试', () => { it('应该处理并发消息发送', async () => { // 模拟多个并发消息 - 设置一次mock,让它返回不同的ID mockZulipClient.messages.send.mockImplementation(() => { const id = Math.floor(Math.random() * 10000) + 1000; return Promise.resolve({ result: 'success', id: id, }); }); // 创建并发消息发送的Promise数组 const messagePromises: Promise[] = []; for (let i = 0; i < 10; i++) { messagePromises.push( service.sendMessage( clientInstance, 'test-stream', 'concurrent-topic', `Concurrent message ${i}` ) ); } const results = await Promise.all(messagePromises); results.forEach((result) => { expect(result.success).toBe(true); expect(result.messageId).toBeGreaterThan(999); }); }); it('应该在大量消息发送时保持性能', async () => { const startTime = Date.now(); const messageCount = 100; mockZulipClient.messages.send.mockImplementation(() => Promise.resolve({ result: 'success', id: Math.floor(Math.random() * 100000), }) ); const promises = Array.from({ length: messageCount }, (_, i) => service.sendMessage( clientInstance, 'performance-stream', 'performance-topic', `Performance test message ${i}` ) ); const results = await Promise.all(promises); const endTime = Date.now(); const duration = endTime - startTime; // 验证所有消息都成功发送 results.forEach(result => { expect(result.success).toBe(true); }); // 性能检查:100条消息应该在合理时间内完成(这里设为5秒) expect(duration).toBeLessThan(5000); console.log(`发送${messageCount}条消息耗时: ${duration}ms`); }, 10000); }); });