forked from datawhale/whale-town-end
refactor:重构Zulip模块按业务功能模块化架构
- 将技术实现服务从business层迁移到core层 - 创建src/core/zulip/核心服务模块,包含API客户端、连接池等技术服务 - 保留src/business/zulip/业务逻辑,专注游戏相关的业务规则 - 通过依赖注入实现业务层与核心层的解耦 - 更新模块导入关系,确保架构分层清晰 重构后的架构符合单一职责原则,提高了代码的可维护性和可测试性
This commit is contained in:
410
src/core/zulip/services/zulip_client.service.spec.ts
Normal file
410
src/core/zulip/services/zulip_client.service.spec.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user