Files
whale-town-end/test/zulip_integration/chat_message_e2e.spec.ts
moyin ed04b8c92d docs(zulip): 完善Zulip业务模块功能文档
范围: src/business/zulip/README.md
- 补充对外提供的接口章节(14个公共方法)
- 添加使用的项目内部依赖说明(7个依赖)
- 完善核心特性描述(5个特性)
- 添加潜在风险评估(4个风险及缓解措施)
- 优化文档结构和内容完整性
2026-01-15 10:53:04 +08:00

455 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.
/**
* 聊天消息端到端集成测试
*
* 功能描述:
* - 测试从WebSocket接收消息到Zulip服务器发送的完整流程
* - 验证消息路由、过滤、认证等中间环节
* - 测试真实的网络请求和响应处理
*
* 测试范围:
* - WebSocket → ChatService → ZulipClientPool → ZulipClient → Zulip API
*
* 更新记录:
* - 2026-01-14: 重构后更新 - 使用新的四层架构模块
* - ChatService 替代 ZulipService
* - ChatSessionService 替代 SessionManagerService
*
* @author moyin
* @version 2.0.0
* @since 2026-01-10
* @lastModified 2026-01-14
*/
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { ChatService } from '../../src/business/chat/chat.service';
import { ChatSessionService } from '../../src/business/chat/services/chat_session.service';
import { ZulipClientPoolService } from '../../src/core/zulip_core/services/zulip_client_pool.service';
import { ZulipClientService, ZulipClientInstance } from '../../src/core/zulip_core/services/zulip_client.service';
import { AppModule } from '../../src/app.module';
describe('ChatMessage E2E Integration', () => {
let app: INestApplication;
let chatService: ChatService;
let zulipClientPool: ZulipClientPoolService;
let zulipClient: ZulipClientService;
let sessionManager: ChatSessionService;
// 模拟的Zulip客户端
let mockZulipSdkClient: any;
// 测试数据
const testUserId = 'test-user-12345';
const testSocketId = 'ws_test_socket_123';
const testConfig = {
username: 'test-bot@example.com',
apiKey: 'test-api-key-abcdef',
realm: 'https://test-zulip.example.com',
};
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
// 获取服务实例(使用新的四层架构模块)
chatService = moduleFixture.get<ChatService>(ChatService);
zulipClientPool = moduleFixture.get<ZulipClientPoolService>(ZulipClientPoolService);
zulipClient = moduleFixture.get<ZulipClientService>(ZulipClientService);
sessionManager = moduleFixture.get<ChatSessionService>(ChatSessionService);
await app.init();
});
afterAll(async () => {
await app.close();
});
beforeEach(() => {
// 创建模拟的zulip-js客户端
mockZulipSdkClient = {
config: testConfig,
users: {
me: {
getProfile: jest.fn().mockResolvedValue({
result: 'success',
email: testConfig.username,
full_name: 'Test Bot',
user_id: 123,
}),
},
},
messages: {
send: jest.fn().mockResolvedValue({
result: 'success',
id: 12345,
}),
},
queues: {
register: jest.fn().mockResolvedValue({
result: 'success',
queue_id: 'test-queue-123',
last_event_id: 0,
}),
deregister: jest.fn().mockResolvedValue({
result: 'success',
}),
},
events: {
retrieve: jest.fn().mockResolvedValue({
result: 'success',
events: [],
}),
},
};
// Mock zulip-js模块
jest.spyOn(zulipClient as any, 'loadZulipModule').mockResolvedValue(() => mockZulipSdkClient);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('完整的聊天消息流程', () => {
it('应该成功处理从登录到消息发送的完整流程', async () => {
// 1. 模拟用户登录
const loginResult = await chatService.handlePlayerLogin({
socketId: testSocketId,
token: 'valid-jwt-token', // 这里需要有效的JWT token
});
// 验证登录成功可能需要根据实际JWT验证逻辑调整
if (loginResult.success) {
expect(loginResult.userId).toBeDefined();
expect(loginResult.sessionId).toBeDefined();
// 2. 发送聊天消息
const chatResult = await chatService.sendChatMessage({
socketId: testSocketId,
content: 'Hello from E2E test!',
scope: 'local',
});
// 验证消息发送成功
expect(chatResult.success).toBe(true);
expect(chatResult.messageId).toBeDefined();
// 验证Zulip API被正确调用
expect(mockZulipSdkClient.messages.send).toHaveBeenCalledWith({
type: 'stream',
to: expect.any(String), // Stream名称
subject: expect.any(String), // Topic名称
content: 'Hello from E2E test!',
});
} else {
// 如果登录失败,跳过测试或使用模拟会话
console.warn('登录失败,使用模拟会话进行测试');
// 创建模拟会话
await sessionManager.createSession(
testSocketId,
testUserId,
'test-queue-123',
'TestUser',
'whale_port',
{ x: 0, y: 0 }
);
// 发送消息
const chatResult = await chatService.sendChatMessage({
socketId: testSocketId,
content: 'Hello from E2E test with mock session!',
scope: 'local',
});
expect(chatResult.success).toBe(true);
}
}, 15000);
it('应该正确处理不同消息范围的路由', async () => {
// 创建测试会话
await sessionManager.createSession(
testSocketId,
testUserId,
'test-queue-123',
'TestUser',
'whale_port',
{ x: 0, y: 0 }
);
// 测试本地消息
const localResult = await chatService.sendChatMessage({
socketId: testSocketId,
content: 'Local message test',
scope: 'local',
});
expect(localResult.success).toBe(true);
// 验证消息被发送到正确的Stream
const localCall = mockZulipSdkClient.messages.send.mock.calls.find(
(call: any) => call[0].content === 'Local message test'
);
expect(localCall).toBeDefined();
expect(localCall[0].to).toBe('Whale Port'); // 应该路由到地图对应的Stream
// 测试全局消息
const globalResult = await chatService.sendChatMessage({
socketId: testSocketId,
content: 'Global message test',
scope: 'global',
});
expect(globalResult.success).toBe(true);
// 验证全局消息路由
const globalCall = mockZulipSdkClient.messages.send.mock.calls.find(
(call: any) => call[0].content === 'Global message test'
);
expect(globalCall).toBeDefined();
});
it('应该处理消息过滤和验证', async () => {
// 创建测试会话
await sessionManager.createSession(
testSocketId,
testUserId,
'test-queue-123',
'TestUser',
'whale_port',
{ x: 0, y: 0 }
);
// 测试正常消息
const normalResult = await chatService.sendChatMessage({
socketId: testSocketId,
content: 'This is a normal message',
scope: 'local',
});
expect(normalResult.success).toBe(true);
// 测试空消息
const emptyResult = await chatService.sendChatMessage({
socketId: testSocketId,
content: '',
scope: 'local',
});
expect(emptyResult.success).toBe(false);
// 测试过长消息
const longMessage = 'A'.repeat(2000); // 假设限制是1000字符
const longResult = await chatService.sendChatMessage({
socketId: testSocketId,
content: longMessage,
scope: 'local',
});
// 根据实际过滤规则验证结果
console.log('Long message result:', longResult);
});
it('应该处理Zulip API错误', async () => {
// 创建测试会话
await sessionManager.createSession(
testSocketId,
testUserId,
'test-queue-123',
'TestUser',
'whale_port',
{ x: 0, y: 0 }
);
// 模拟Zulip API错误
mockZulipSdkClient.messages.send.mockResolvedValueOnce({
result: 'error',
msg: 'Stream does not exist',
code: 'STREAM_NOT_FOUND',
});
const result = await chatService.sendChatMessage({
socketId: testSocketId,
content: 'This message will fail',
scope: 'local',
});
// 验证错误处理(根据实际业务逻辑,可能返回成功但记录错误)
expect(result).toBeDefined();
});
it('应该处理网络异常', async () => {
// 创建测试会话
await sessionManager.createSession(
testSocketId,
testUserId,
'test-queue-123',
'TestUser',
'whale_port',
{ x: 0, y: 0 }
);
// 模拟网络异常
mockZulipSdkClient.messages.send.mockRejectedValueOnce(new Error('Network timeout'));
const result = await chatService.sendChatMessage({
socketId: testSocketId,
content: 'This will timeout',
scope: 'local',
});
// 验证网络异常处理
expect(result).toBeDefined();
});
});
describe('客户端池管理', () => {
it('应该正确管理用户的Zulip客户端', async () => {
// 创建用户客户端
const clientInstance = await zulipClientPool.createUserClient(testUserId, testConfig);
expect(clientInstance).toBeDefined();
expect(clientInstance.userId).toBe(testUserId);
expect(clientInstance.isValid).toBe(true);
// 验证客户端可以发送消息
const sendResult = await zulipClientPool.sendMessage(
testUserId,
'test-stream',
'test-topic',
'Test message from pool'
);
expect(sendResult.success).toBe(true);
expect(mockZulipSdkClient.messages.send).toHaveBeenCalledWith({
type: 'stream',
to: 'test-stream',
subject: 'test-topic',
content: 'Test message from pool',
});
// 清理客户端
await zulipClientPool.destroyUserClient(testUserId);
});
it('应该处理多用户并发', async () => {
const userIds = ['user1', 'user2', 'user3'];
const clients: ZulipClientInstance[] = [];
// 创建多个用户客户端
for (const userId of userIds) {
const client = await zulipClientPool.createUserClient(userId, {
...testConfig,
username: `${userId}@example.com`,
});
clients.push(client);
}
// 并发发送消息
const sendPromises = userIds.map(userId =>
zulipClientPool.sendMessage(
userId,
'concurrent-stream',
'concurrent-topic',
`Message from ${userId}`
)
);
const results = await Promise.all(sendPromises);
// 验证所有消息都成功发送
results.forEach(result => {
expect(result.success).toBe(true);
});
// 清理所有客户端
for (const userId of userIds) {
await zulipClientPool.destroyUserClient(userId);
}
});
});
describe('事件队列集成', () => {
it('应该正确处理事件队列生命周期', async () => {
// 创建客户端
const clientInstance = await zulipClientPool.createUserClient(testUserId, testConfig);
// 验证队列已注册
expect(clientInstance.queueId).toBeDefined();
expect(mockZulipSdkClient.queues.register).toHaveBeenCalled();
// 模拟接收事件
const mockEvents = [
{
id: 1,
type: 'message',
message: {
id: 98765,
sender_email: 'other-user@example.com',
content: 'Hello from other user',
stream_id: 1,
subject: 'Test Topic',
},
},
];
mockZulipSdkClient.events.retrieve.mockResolvedValueOnce({
result: 'success',
events: mockEvents,
});
// 获取事件
const userClient: ZulipClientInstance | null = await zulipClientPool.getUserClient(testUserId);
if (userClient) {
const eventsResult = await zulipClient.getEvents(userClient, true);
expect(eventsResult.success).toBe(true);
expect(eventsResult.events).toEqual(mockEvents);
}
// 清理
await zulipClientPool.destroyUserClient(testUserId);
expect(mockZulipSdkClient.queues.deregister).toHaveBeenCalled();
});
});
describe('性能测试', () => {
it('应该在高负载下保持性能', async () => {
const messageCount = 50;
const startTime = Date.now();
// 创建测试会话
await sessionManager.createSession(
testSocketId,
testUserId,
'test-queue-123',
'TestUser',
'whale_port',
{ x: 0, y: 0 }
);
// 发送大量消息
const promises = Array.from({ length: messageCount }, (_, i) =>
chatService.sendChatMessage({
socketId: testSocketId,
content: `Performance test message ${i}`,
scope: 'local',
})
);
const results = await Promise.all(promises);
const endTime = Date.now();
const duration = endTime - startTime;
// 验证所有消息处理完成
expect(results).toHaveLength(messageCount);
// 性能检查
const avgTimePerMessage = duration / messageCount;
console.log(`处理${messageCount}条消息耗时: ${duration}ms, 平均每条: ${avgTimePerMessage.toFixed(2)}ms`);
// 期望平均每条消息处理时间不超过100ms
expect(avgTimePerMessage).toBeLessThan(100);
}, 30000);
});
});