forked from datawhale/whale-town-end
- 将技术实现服务从business层迁移到core层 - 创建src/core/zulip/核心服务模块,包含API客户端、连接池等技术服务 - 保留src/business/zulip/业务逻辑,专注游戏相关的业务规则 - 通过依赖注入实现业务层与核心层的解耦 - 更新模块导入关系,确保架构分层清晰 重构后的架构符合单一职责原则,提高了代码的可维护性和可测试性
411 lines
12 KiB
TypeScript
411 lines
12 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|