Files
whale-town-end/src/core/zulip/services/zulip_client.service.spec.ts
moyin 2d10131838 refactor:重构Zulip模块按业务功能模块化架构
- 将技术实现服务从business层迁移到core层
- 创建src/core/zulip/核心服务模块,包含API客户端、连接池等技术服务
- 保留src/business/zulip/业务逻辑,专注游戏相关的业务规则
- 通过依赖注入实现业务层与核心层的解耦
- 更新模块导入关系,确保架构分层清晰

重构后的架构符合单一职责原则,提高了代码的可维护性和可测试性
2025-12-31 15:44:36 +08:00

411 lines
12 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.
/**
* 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<AppLoggerService>;
// 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>(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);
});
});