/** * Zulip客户端核心服务测试 * * 功能描述: * - 测试ZulipClientService的核心功能 * - 包含属性测试验证客户端生命周期管理 * * @author angjustinl * @version 1.0.0 * @since 2025-12-25 */ import { Test, TestingModule } from '@nestjs/testing'; import * as fc from 'fast-check'; import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip_client.service'; import { AppLoggerService } from '../../../core/utils/logger/logger.service'; describe('ZulipClientService', () => { let service: ZulipClientService; let mockLogger: jest.Mocked; // Mock zulip-js模块 const mockZulipClient = { users: { me: { getProfile: jest.fn(), }, }, messages: { send: jest.fn(), }, queues: { register: jest.fn(), deregister: jest.fn(), }, events: { retrieve: jest.fn(), }, }; const mockZulipInit = jest.fn().mockResolvedValue(mockZulipClient); beforeEach(async () => { // 重置所有mock jest.clearAllMocks(); mockLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn(), } as any; const module: TestingModule = await Test.createTestingModule({ providers: [ ZulipClientService, { provide: AppLoggerService, useValue: mockLogger, }, ], }).compile(); service = module.get(ZulipClientService); // Mock动态导入 jest.spyOn(service as any, 'loadZulipModule').mockResolvedValue(mockZulipInit); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('配置验证', () => { it('应该拒绝空的username', async () => { const config: ZulipClientConfig = { username: '', apiKey: 'valid-api-key', realm: 'https://zulip.example.com', }; await expect(service.createClient('user1', config)).rejects.toThrow('无效的username配置'); }); it('应该拒绝空的apiKey', async () => { const config: ZulipClientConfig = { username: 'user@example.com', apiKey: '', realm: 'https://zulip.example.com', }; await expect(service.createClient('user1', config)).rejects.toThrow('无效的apiKey配置'); }); it('应该拒绝无效的realm URL', async () => { const config: ZulipClientConfig = { username: 'user@example.com', apiKey: 'valid-api-key', realm: 'not-a-valid-url', }; await expect(service.createClient('user1', config)).rejects.toThrow('realm必须是有效的URL'); }); }); describe('客户端创建', () => { it('应该成功创建客户端', async () => { mockZulipClient.users.me.getProfile.mockResolvedValue({ result: 'success', email: 'user@example.com', }); const config: ZulipClientConfig = { username: 'user@example.com', apiKey: 'valid-api-key', realm: 'https://zulip.example.com', }; const client = await service.createClient('user1', config); expect(client).toBeDefined(); expect(client.userId).toBe('user1'); expect(client.isValid).toBe(true); expect(client.config).toEqual(config); }); it('应该在API Key验证失败时抛出错误', async () => { mockZulipClient.users.me.getProfile.mockResolvedValue({ result: 'error', msg: 'Invalid API key', }); const config: ZulipClientConfig = { username: 'user@example.com', apiKey: 'invalid-api-key', realm: 'https://zulip.example.com', }; await expect(service.createClient('user1', config)).rejects.toThrow('API Key验证失败'); }); }); describe('消息发送', () => { let clientInstance: ZulipClientInstance; beforeEach(() => { clientInstance = { userId: 'user1', config: { username: 'user@example.com', apiKey: 'valid-api-key', realm: 'https://zulip.example.com', }, client: mockZulipClient, lastEventId: -1, createdAt: new Date(), lastActivity: new Date(), isValid: true, }; }); it('应该成功发送消息', async () => { mockZulipClient.messages.send.mockResolvedValue({ result: 'success', id: 12345, }); const result = await service.sendMessage(clientInstance, 'test-stream', 'test-topic', 'Hello World'); expect(result.success).toBe(true); expect(result.messageId).toBe(12345); }); it('应该在客户端无效时返回错误', async () => { clientInstance.isValid = false; const result = await service.sendMessage(clientInstance, 'test-stream', 'test-topic', 'Hello World'); expect(result.success).toBe(false); expect(result.error).toContain('无效'); }); }); describe('事件队列管理', () => { let clientInstance: ZulipClientInstance; beforeEach(() => { clientInstance = { userId: 'user1', config: { username: 'user@example.com', apiKey: 'valid-api-key', realm: 'https://zulip.example.com', }, client: mockZulipClient, lastEventId: -1, createdAt: new Date(), lastActivity: new Date(), isValid: true, }; }); it('应该成功注册事件队列', async () => { mockZulipClient.queues.register.mockResolvedValue({ result: 'success', queue_id: 'queue-123', last_event_id: 0, }); const result = await service.registerQueue(clientInstance); expect(result.success).toBe(true); expect(result.queueId).toBe('queue-123'); expect(clientInstance.queueId).toBe('queue-123'); }); it('应该成功注销事件队列', async () => { clientInstance.queueId = 'queue-123'; mockZulipClient.queues.deregister.mockResolvedValue({ result: 'success', }); const result = await service.deregisterQueue(clientInstance); expect(result).toBe(true); expect(clientInstance.queueId).toBeUndefined(); }); }); /** * 属性测试: Zulip客户端生命周期管理 * * **Feature: zulip-integration, Property 2: Zulip客户端生命周期管理** * **Validates: Requirements 2.1, 2.2, 2.5** * * 对于任何用户的Zulip API Key,系统应该创建专用的Zulip客户端实例, * 注册事件队列,并在用户登出时完全清理客户端和队列资源 */ describe('Property 2: Zulip客户端生命周期管理', () => { /** * 属性: 对于任何有效的配置,创建客户端后应该处于有效状态 */ it('对于任何有效配置,创建的客户端应该处于有效状态', async () => { mockZulipClient.users.me.getProfile.mockResolvedValue({ result: 'success', email: 'user@example.com', }); await fc.assert( fc.asyncProperty( // 生成有效的用户ID fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // 生成有效的邮箱格式 fc.emailAddress(), // 生成有效的API Key fc.string({ minLength: 10, maxLength: 100 }).filter(s => s.trim().length >= 10), async (userId, email, apiKey) => { const config: ZulipClientConfig = { username: email, apiKey: apiKey, realm: 'https://zulip.example.com', }; const client = await service.createClient(userId, config); // 验证客户端状态 expect(client.userId).toBe(userId); expect(client.isValid).toBe(true); expect(client.config.username).toBe(email); expect(client.config.apiKey).toBe(apiKey); expect(client.createdAt).toBeInstanceOf(Date); expect(client.lastActivity).toBeInstanceOf(Date); } ), { numRuns: 100 } ); }, 30000); /** * 属性: 对于任何客户端,注册队列后应该有有效的队列ID */ it('对于任何客户端,注册队列后应该有有效的队列ID', async () => { await fc.assert( fc.asyncProperty( fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), fc.string({ minLength: 5, maxLength: 50 }).filter(s => s.trim().length >= 5), fc.integer({ min: 0, max: 1000 }), async (userId, queueId, lastEventId) => { mockZulipClient.queues.register.mockResolvedValue({ result: 'success', queue_id: queueId, last_event_id: lastEventId, }); const clientInstance: ZulipClientInstance = { userId, config: { username: 'user@example.com', apiKey: 'valid-api-key', realm: 'https://zulip.example.com', }, client: mockZulipClient, lastEventId: -1, createdAt: new Date(), lastActivity: new Date(), isValid: true, }; const result = await service.registerQueue(clientInstance); // 验证队列注册结果 expect(result.success).toBe(true); expect(result.queueId).toBe(queueId); expect(clientInstance.queueId).toBe(queueId); expect(clientInstance.lastEventId).toBe(lastEventId); } ), { numRuns: 100 } ); }, 30000); /** * 属性: 对于任何客户端,销毁后应该处于无效状态且队列被清理 */ it('对于任何客户端,销毁后应该处于无效状态且队列被清理', async () => { mockZulipClient.queues.deregister.mockResolvedValue({ result: 'success', }); await fc.assert( fc.asyncProperty( fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), fc.string({ minLength: 5, maxLength: 50 }).filter(s => s.trim().length >= 5), async (userId, queueId) => { const clientInstance: ZulipClientInstance = { userId, config: { username: 'user@example.com', apiKey: 'valid-api-key', realm: 'https://zulip.example.com', }, client: mockZulipClient, queueId: queueId, lastEventId: 10, createdAt: new Date(), lastActivity: new Date(), isValid: true, }; await service.destroyClient(clientInstance); // 验证客户端被正确销毁 expect(clientInstance.isValid).toBe(false); expect(clientInstance.queueId).toBeUndefined(); expect(clientInstance.client).toBeNull(); } ), { numRuns: 100 } ); }, 30000); /** * 属性: 创建-注册-销毁的完整生命周期应该正确管理资源 */ it('创建-注册-销毁的完整生命周期应该正确管理资源', async () => { mockZulipClient.users.me.getProfile.mockResolvedValue({ result: 'success', email: 'user@example.com', }); mockZulipClient.queues.register.mockResolvedValue({ result: 'success', queue_id: 'queue-123', last_event_id: 0, }); mockZulipClient.queues.deregister.mockResolvedValue({ result: 'success', }); await fc.assert( fc.asyncProperty( fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), fc.emailAddress(), fc.string({ minLength: 10, maxLength: 100 }).filter(s => s.trim().length >= 10), async (userId, email, apiKey) => { const config: ZulipClientConfig = { username: email, apiKey: apiKey, realm: 'https://zulip.example.com', }; // 1. 创建客户端 const client = await service.createClient(userId, config); expect(client.isValid).toBe(true); // 2. 注册事件队列 const registerResult = await service.registerQueue(client); expect(registerResult.success).toBe(true); expect(client.queueId).toBeDefined(); // 3. 销毁客户端 await service.destroyClient(client); expect(client.isValid).toBe(false); expect(client.queueId).toBeUndefined(); } ), { numRuns: 100 } ); }, 30000); }); });