Files
whale-town-end/test/integration/zulip_message_integration.spec.ts
moyin 5af44f95d5 style: 完善代码规范和测试覆盖
- 新增多个模块的单元测试文件,提升测试覆盖率
- 完善AI-Reading文档系统,包含7步代码检查流程
- 新增集成测试和属性测试框架
- 优化项目结构和配置文件
- 清理过时的规范文档,统一使用新的检查标准
2026-01-12 20:09:03 +08:00

469 lines
13 KiB
TypeScript
Raw Permalink 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消息发送集成测试
*
* 功能描述:
* - 测试消息发送到真实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);
});
});