refactor:重构Zulip模块按业务功能模块化架构

- 将技术实现服务从business层迁移到core层
- 创建src/core/zulip/核心服务模块,包含API客户端、连接池等技术服务
- 保留src/business/zulip/业务逻辑,专注游戏相关的业务规则
- 通过依赖注入实现业务层与核心层的解耦
- 更新模块导入关系,确保架构分层清晰

重构后的架构符合单一职责原则,提高了代码的可维护性和可测试性
This commit is contained in:
moyin
2025-12-31 15:44:36 +08:00
parent 5140bd1a54
commit 2d10131838
36 changed files with 2773 additions and 125 deletions

View 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);
});
});