- 新增多个模块的单元测试文件,提升测试覆盖率 - 完善AI-Reading文档系统,包含7步代码检查流程 - 新增集成测试和属性测试框架 - 优化项目结构和配置文件 - 清理过时的规范文档,统一使用新的检查标准
469 lines
13 KiB
TypeScript
469 lines
13 KiB
TypeScript
/**
|
||
* Zulip消息发送集成测试
|
||
*
|
||
* 功能描述:
|
||
* - 测试消息发送到真实Zulip服务器的完整流程
|
||
* - 验证HTTP请求、响应处理和错误场景
|
||
* - 包含网络异常和API错误的测试
|
||
*
|
||
* 注意:这些测试需要真实的Zulip服务器配置
|
||
*
|
||
* 最近修改:
|
||
* - 2026-01-12: 架构优化 - 从src/core/zulip_core/services/移动到test/integration/,符合测试分离规范 (修改者: moyin)
|
||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||
* - 2026-01-10: 测试新增 - 创建Zulip消息发送集成测试 (修改者: moyin)
|
||
*
|
||
* @author moyin
|
||
* @version 1.1.0
|
||
* @since 2026-01-10
|
||
* @lastModified 2026-01-12
|
||
*/
|
||
|
||
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from '../../src/core/zulip_core/services/zulip_client.service';
|
||
import * as nock from 'nock';
|
||
|
||
describe('ZulipMessageIntegration', () => {
|
||
let service: ZulipClientService;
|
||
let mockZulipClient: any;
|
||
let clientInstance: ZulipClientInstance;
|
||
|
||
const testConfig: ZulipClientConfig = {
|
||
username: 'test-bot@example.com',
|
||
apiKey: 'test-api-key-12345',
|
||
realm: 'https://test-zulip.example.com',
|
||
};
|
||
|
||
beforeEach(async () => {
|
||
// 清理所有HTTP拦截
|
||
nock.cleanAll();
|
||
|
||
const module: TestingModule = await Test.createTestingModule({
|
||
providers: [ZulipClientService],
|
||
}).compile();
|
||
|
||
service = module.get<ZulipClientService>(ZulipClientService);
|
||
|
||
// 创建模拟的zulip-js客户端
|
||
mockZulipClient = {
|
||
config: testConfig,
|
||
users: {
|
||
me: {
|
||
getProfile: jest.fn(),
|
||
},
|
||
},
|
||
messages: {
|
||
send: jest.fn(),
|
||
},
|
||
queues: {
|
||
register: jest.fn(),
|
||
deregister: jest.fn(),
|
||
},
|
||
events: {
|
||
retrieve: jest.fn(),
|
||
},
|
||
};
|
||
|
||
// 模拟客户端实例
|
||
clientInstance = {
|
||
userId: 'test-user-123',
|
||
config: testConfig,
|
||
client: mockZulipClient,
|
||
lastEventId: -1,
|
||
createdAt: new Date(),
|
||
lastActivity: new Date(),
|
||
isValid: true,
|
||
};
|
||
|
||
// Mock zulip-js模块加载
|
||
jest.spyOn(service as any, 'loadZulipModule').mockResolvedValue(() => mockZulipClient);
|
||
});
|
||
|
||
afterEach(() => {
|
||
nock.cleanAll();
|
||
jest.clearAllMocks();
|
||
});
|
||
|
||
describe('消息发送到Zulip服务器', () => {
|
||
it('应该成功发送消息到Zulip API', async () => {
|
||
// 模拟成功的API响应
|
||
mockZulipClient.messages.send.mockResolvedValue({
|
||
result: 'success',
|
||
id: 12345,
|
||
msg: '',
|
||
});
|
||
|
||
const result = await service.sendMessage(
|
||
clientInstance,
|
||
'test-stream',
|
||
'test-topic',
|
||
'Hello from integration test!'
|
||
);
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.messageId).toBe(12345);
|
||
expect(mockZulipClient.messages.send).toHaveBeenCalledWith({
|
||
type: 'stream',
|
||
to: 'test-stream',
|
||
subject: 'test-topic',
|
||
content: 'Hello from integration test!',
|
||
});
|
||
});
|
||
|
||
it('应该处理Zulip API错误响应', async () => {
|
||
// 模拟API错误响应
|
||
mockZulipClient.messages.send.mockResolvedValue({
|
||
result: 'error',
|
||
msg: 'Stream does not exist',
|
||
code: 'STREAM_NOT_FOUND',
|
||
});
|
||
|
||
const result = await service.sendMessage(
|
||
clientInstance,
|
||
'nonexistent-stream',
|
||
'test-topic',
|
||
'This should fail'
|
||
);
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('Stream does not exist');
|
||
});
|
||
|
||
it('应该处理网络连接异常', async () => {
|
||
// 模拟网络异常
|
||
mockZulipClient.messages.send.mockRejectedValue(new Error('Network timeout'));
|
||
|
||
const result = await service.sendMessage(
|
||
clientInstance,
|
||
'test-stream',
|
||
'test-topic',
|
||
'This will timeout'
|
||
);
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('Network timeout');
|
||
});
|
||
|
||
it('应该处理认证失败', async () => {
|
||
// 模拟认证失败
|
||
mockZulipClient.messages.send.mockResolvedValue({
|
||
result: 'error',
|
||
msg: 'Invalid API key',
|
||
code: 'BAD_REQUEST',
|
||
});
|
||
|
||
const result = await service.sendMessage(
|
||
clientInstance,
|
||
'test-stream',
|
||
'test-topic',
|
||
'Authentication test'
|
||
);
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('Invalid API key');
|
||
});
|
||
|
||
it('应该正确处理特殊字符和长消息', async () => {
|
||
const longMessage = 'A'.repeat(1000) + '特殊字符测试: 🎮🎯🚀 @#$%^&*()';
|
||
|
||
mockZulipClient.messages.send.mockResolvedValue({
|
||
result: 'success',
|
||
id: 67890,
|
||
});
|
||
|
||
const result = await service.sendMessage(
|
||
clientInstance,
|
||
'test-stream',
|
||
'special-chars-topic',
|
||
longMessage
|
||
);
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.messageId).toBe(67890);
|
||
expect(mockZulipClient.messages.send).toHaveBeenCalledWith({
|
||
type: 'stream',
|
||
to: 'test-stream',
|
||
subject: 'special-chars-topic',
|
||
content: longMessage,
|
||
});
|
||
});
|
||
|
||
it('应该更新客户端最后活动时间', async () => {
|
||
const initialTime = new Date('2026-01-01T00:00:00Z');
|
||
clientInstance.lastActivity = initialTime;
|
||
|
||
mockZulipClient.messages.send.mockResolvedValue({
|
||
result: 'success',
|
||
id: 11111,
|
||
});
|
||
|
||
await service.sendMessage(
|
||
clientInstance,
|
||
'test-stream',
|
||
'test-topic',
|
||
'Activity test'
|
||
);
|
||
|
||
expect(clientInstance.lastActivity.getTime()).toBeGreaterThan(initialTime.getTime());
|
||
});
|
||
});
|
||
|
||
describe('事件队列与Zulip服务器交互', () => {
|
||
it('应该成功注册事件队列', async () => {
|
||
mockZulipClient.queues.register.mockResolvedValue({
|
||
result: 'success',
|
||
queue_id: 'test-queue-123',
|
||
last_event_id: 42,
|
||
});
|
||
|
||
const result = await service.registerQueue(clientInstance, ['message', 'typing']);
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.queueId).toBe('test-queue-123');
|
||
expect(result.lastEventId).toBe(42);
|
||
expect(clientInstance.queueId).toBe('test-queue-123');
|
||
expect(clientInstance.lastEventId).toBe(42);
|
||
});
|
||
|
||
it('应该处理队列注册失败', async () => {
|
||
mockZulipClient.queues.register.mockResolvedValue({
|
||
result: 'error',
|
||
msg: 'Rate limit exceeded',
|
||
});
|
||
|
||
const result = await service.registerQueue(clientInstance);
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('Rate limit exceeded');
|
||
});
|
||
|
||
it('应该成功获取事件', async () => {
|
||
clientInstance.queueId = 'test-queue-123';
|
||
clientInstance.lastEventId = 10;
|
||
|
||
const mockEvents = [
|
||
{
|
||
id: 11,
|
||
type: 'message',
|
||
message: {
|
||
id: 98765,
|
||
sender_email: 'user@example.com',
|
||
content: 'Test message from Zulip',
|
||
stream_id: 1,
|
||
subject: 'Test Topic',
|
||
},
|
||
},
|
||
{
|
||
id: 12,
|
||
type: 'typing',
|
||
sender: { user_id: 123 },
|
||
},
|
||
];
|
||
|
||
mockZulipClient.events.retrieve.mockResolvedValue({
|
||
result: 'success',
|
||
events: mockEvents,
|
||
});
|
||
|
||
const result = await service.getEvents(clientInstance, true);
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.events).toEqual(mockEvents);
|
||
expect(clientInstance.lastEventId).toBe(12); // 更新为最后一个事件的ID
|
||
});
|
||
|
||
it('应该处理空事件队列', async () => {
|
||
clientInstance.queueId = 'test-queue-123';
|
||
|
||
mockZulipClient.events.retrieve.mockResolvedValue({
|
||
result: 'success',
|
||
events: [],
|
||
});
|
||
|
||
const result = await service.getEvents(clientInstance, true);
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.events).toEqual([]);
|
||
});
|
||
|
||
it('应该成功注销事件队列', async () => {
|
||
clientInstance.queueId = 'test-queue-123';
|
||
|
||
mockZulipClient.queues.deregister.mockResolvedValue({
|
||
result: 'success',
|
||
});
|
||
|
||
const result = await service.deregisterQueue(clientInstance);
|
||
|
||
expect(result).toBe(true);
|
||
expect(clientInstance.queueId).toBeUndefined();
|
||
expect(clientInstance.lastEventId).toBe(-1);
|
||
});
|
||
|
||
it('应该处理队列过期情况', async () => {
|
||
clientInstance.queueId = 'expired-queue';
|
||
|
||
// 模拟队列过期的JSON解析错误
|
||
mockZulipClient.queues.deregister.mockRejectedValue(
|
||
new Error('invalid json response body at https://zulip.example.com/api/v1/events reason: Unexpected token')
|
||
);
|
||
|
||
const result = await service.deregisterQueue(clientInstance);
|
||
|
||
expect(result).toBe(true); // 应该返回true,因为队列已过期
|
||
expect(clientInstance.queueId).toBeUndefined();
|
||
expect(clientInstance.lastEventId).toBe(-1);
|
||
});
|
||
});
|
||
|
||
describe('API Key验证', () => {
|
||
it('应该成功验证有效的API Key', async () => {
|
||
mockZulipClient.users.me.getProfile.mockResolvedValue({
|
||
result: 'success',
|
||
email: 'test-bot@example.com',
|
||
full_name: 'Test Bot',
|
||
user_id: 123,
|
||
});
|
||
|
||
const isValid = await service.validateApiKey(clientInstance);
|
||
|
||
expect(isValid).toBe(true);
|
||
expect(clientInstance.isValid).toBe(true);
|
||
});
|
||
|
||
it('应该拒绝无效的API Key', async () => {
|
||
mockZulipClient.users.me.getProfile.mockResolvedValue({
|
||
result: 'error',
|
||
msg: 'Invalid API key',
|
||
});
|
||
|
||
const isValid = await service.validateApiKey(clientInstance);
|
||
|
||
expect(isValid).toBe(false);
|
||
expect(clientInstance.isValid).toBe(false);
|
||
});
|
||
|
||
it('应该处理API Key验证网络异常', async () => {
|
||
mockZulipClient.users.me.getProfile.mockRejectedValue(new Error('Connection refused'));
|
||
|
||
const isValid = await service.validateApiKey(clientInstance);
|
||
|
||
expect(isValid).toBe(false);
|
||
expect(clientInstance.isValid).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('错误恢复和重试机制', () => {
|
||
it('应该在临时网络错误后恢复', async () => {
|
||
// 第一次调用失败,第二次成功
|
||
mockZulipClient.messages.send
|
||
.mockRejectedValueOnce(new Error('Temporary network error'))
|
||
.mockResolvedValueOnce({
|
||
result: 'success',
|
||
id: 99999,
|
||
});
|
||
|
||
// 第一次调用应该失败
|
||
const firstResult = await service.sendMessage(
|
||
clientInstance,
|
||
'test-stream',
|
||
'test-topic',
|
||
'First attempt'
|
||
);
|
||
expect(firstResult.success).toBe(false);
|
||
|
||
// 第二次调用应该成功
|
||
const secondResult = await service.sendMessage(
|
||
clientInstance,
|
||
'test-stream',
|
||
'test-topic',
|
||
'Second attempt'
|
||
);
|
||
expect(secondResult.success).toBe(true);
|
||
expect(secondResult.messageId).toBe(99999);
|
||
});
|
||
|
||
it('应该处理服务器5xx错误', async () => {
|
||
mockZulipClient.messages.send.mockRejectedValue(new Error('Internal Server Error (500)'));
|
||
|
||
const result = await service.sendMessage(
|
||
clientInstance,
|
||
'test-stream',
|
||
'test-topic',
|
||
'Server error test'
|
||
);
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('Internal Server Error (500)');
|
||
});
|
||
});
|
||
|
||
describe('性能和并发测试', () => {
|
||
it('应该处理并发消息发送', async () => {
|
||
// 模拟多个并发消息 - 设置一次mock,让它返回不同的ID
|
||
mockZulipClient.messages.send.mockImplementation(() => {
|
||
const id = Math.floor(Math.random() * 10000) + 1000;
|
||
return Promise.resolve({
|
||
result: 'success',
|
||
id: id,
|
||
});
|
||
});
|
||
|
||
// 创建并发消息发送的Promise数组
|
||
const messagePromises: Promise<any>[] = [];
|
||
|
||
for (let i = 0; i < 10; i++) {
|
||
messagePromises.push(
|
||
service.sendMessage(
|
||
clientInstance,
|
||
'test-stream',
|
||
'concurrent-topic',
|
||
`Concurrent message ${i}`
|
||
)
|
||
);
|
||
}
|
||
|
||
const results = await Promise.all(messagePromises);
|
||
|
||
results.forEach((result) => {
|
||
expect(result.success).toBe(true);
|
||
expect(result.messageId).toBeGreaterThan(999);
|
||
});
|
||
});
|
||
|
||
it('应该在大量消息发送时保持性能', async () => {
|
||
const startTime = Date.now();
|
||
const messageCount = 100;
|
||
|
||
mockZulipClient.messages.send.mockImplementation(() =>
|
||
Promise.resolve({
|
||
result: 'success',
|
||
id: Math.floor(Math.random() * 100000),
|
||
})
|
||
);
|
||
|
||
const promises = Array.from({ length: messageCount }, (_, i) =>
|
||
service.sendMessage(
|
||
clientInstance,
|
||
'performance-stream',
|
||
'performance-topic',
|
||
`Performance test message ${i}`
|
||
)
|
||
);
|
||
|
||
const results = await Promise.all(promises);
|
||
const endTime = Date.now();
|
||
const duration = endTime - startTime;
|
||
|
||
// 验证所有消息都成功发送
|
||
results.forEach(result => {
|
||
expect(result.success).toBe(true);
|
||
});
|
||
|
||
// 性能检查:100条消息应该在合理时间内完成(这里设为5秒)
|
||
expect(duration).toBeLessThan(5000);
|
||
|
||
console.log(`发送${messageCount}条消息耗时: ${duration}ms`);
|
||
}, 10000);
|
||
});
|
||
}); |