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,609 @@
/**
* Zulip集成系统端到端测试
*
* 功能描述:
* - 测试完整的登录到聊天流程
* - 测试多用户并发聊天场景
* - 测试错误场景和降级处理
*
* **验证需求: 所有需求**
*
* @author angjustinl
* @version 1.0.0
* @since 2025-12-25
*/
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { io, Socket as ClientSocket } from 'socket.io-client';
import { AppModule } from '../../app.module';
// 如果没有设置 RUN_E2E_TESTS 环境变量,跳过这些测试
const describeE2E = process.env.RUN_E2E_TESTS === 'true' ? describe : describe.skip;
describeE2E('Zulip Integration E2E Tests', () => {
let app: INestApplication;
let serverUrl: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
await app.listen(0); // 使用随机端口
const address = app.getHttpServer().address();
const port = address.port;
serverUrl = `http://localhost:${port}`;
}, 30000);
afterAll(async () => {
if (app) {
await app.close();
}
});
/**
* 创建WebSocket客户端连接
*/
const createClient = (): Promise<ClientSocket> => {
return new Promise((resolve, reject) => {
const client = io(`${serverUrl}/game`, {
transports: ['websocket'],
autoConnect: true,
});
client.on('connect', () => resolve(client));
client.on('connect_error', (err: any) => reject(err));
setTimeout(() => reject(new Error('Connection timeout')), 5000);
});
};
/**
* 等待指定事件
*/
const waitForEvent = <T>(client: ClientSocket, event: string, timeout = 5000): Promise<T> => {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Timeout waiting for event: ${event}`));
}, timeout);
client.once(event, (data: T) => {
clearTimeout(timer);
resolve(data);
});
});
};
/**
* 测试套件1: 完整的登录到聊天流程测试
* 验证需求: 1.1, 1.2, 1.3, 2.1, 4.1, 4.2, 5.1
*/
describe('完整的登录到聊天流程测试', () => {
let client: ClientSocket;
afterEach(async () => {
if (client?.connected) {
client.disconnect();
}
});
/**
* 测试: WebSocket连接建立
* 验证需求 1.1: 游戏客户端发送登录包时系统应验证游戏Token并建立WebSocket连接
*/
it('应该成功建立WebSocket连接', async () => {
client = await createClient();
expect(client.connected).toBe(true);
});
/**
* 测试: 有效Token登录成功
* 验证需求 1.1, 1.2: 验证Token并分配唯一会话ID
*/
it('应该使用有效Token成功登录', async () => {
client = await createClient();
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: 'valid_test_token_123' });
const response = await loginPromise;
expect(response.t).toBe('login_success');
expect(response.sessionId).toBeDefined();
expect(response.userId).toBeDefined();
expect(response.currentMap).toBeDefined();
});
/**
* 测试: 无效Token登录失败
* 验证需求 1.1: 系统应验证游戏Token
*/
it('应该拒绝无效Token的登录请求', async () => {
client = await createClient();
const errorPromise = waitForEvent<any>(client, 'login_error');
client.emit('login', { type: 'login', token: 'invalid_token' });
const response = await errorPromise;
expect(response.t).toBe('login_error');
expect(response.message).toBeDefined();
});
/**
* 测试: 登录后发送聊天消息
* 验证需求 4.1, 4.2: 玩家发送聊天消息时系统应根据位置确定目标Stream
*/
it('应该在登录后成功发送聊天消息', async () => {
client = await createClient();
// 先登录
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: 'valid_test_token_456' });
await loginPromise;
// 发送聊天消息
const chatPromise = waitForEvent<any>(client, 'chat_sent');
client.emit('chat', { t: 'chat', content: 'Hello World!', scope: 'local' });
const response = await chatPromise;
expect(response.t).toBe('chat_sent');
});
/**
* 测试: 未登录时发送消息被拒绝
* 验证需求 7.2: 系统应验证玩家是否有权限
*/
it('应该拒绝未登录用户的聊天消息', async () => {
client = await createClient();
const errorPromise = waitForEvent<any>(client, 'chat_error');
client.emit('chat', { t: 'chat', content: 'Hello', scope: 'local' });
const response = await errorPromise;
expect(response.t).toBe('chat_error');
expect(response.message).toBe('请先登录');
});
/**
* 测试: 空消息内容被拒绝
* 验证需求 4.3: 系统应过滤消息内容
*/
it('应该拒绝空内容的聊天消息', async () => {
client = await createClient();
// 先登录
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: 'valid_test_token_789' });
await loginPromise;
// 发送空消息
const errorPromise = waitForEvent<any>(client, 'chat_error');
client.emit('chat', { t: 'chat', content: ' ', scope: 'local' });
const response = await errorPromise;
expect(response.t).toBe('chat_error');
expect(response.message).toBe('消息内容不能为空');
});
/**
* 测试: 位置更新
* 验证需求 6.2: 玩家切换地图时系统应更新位置信息
*/
it('应该成功更新玩家位置', async () => {
client = await createClient();
// 先登录
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: 'valid_test_token_position' });
await loginPromise;
// 更新位置 - 位置更新不返回确认消息,只需确保不报错
client.emit('position_update', { t: 'position', x: 100, y: 200, mapId: 'tavern' });
// 等待一小段时间确保消息被处理
await new Promise(resolve => setTimeout(resolve, 100));
// 如果没有错误,测试通过
expect(client.connected).toBe(true);
});
});
/**
* 测试套件2: 多用户并发聊天测试
* 验证需求: 5.2, 5.5, 6.1, 6.3
*/
describe('多用户并发聊天测试', () => {
const clients: ClientSocket[] = [];
afterEach(async () => {
// 断开所有客户端
for (const client of clients) {
if (client?.connected) {
client.disconnect();
}
}
clients.length = 0;
});
/**
* 测试: 多用户同时连接
* 验证需求 6.1: 系统应在Redis中存储会话映射关系
*/
it('应该支持多用户同时连接', async () => {
const userCount = 5;
for (let i = 0; i < userCount; i++) {
const client = await createClient();
clients.push(client);
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: `valid_multi_user_${i}` });
await loginPromise;
}
// 验证所有客户端都已连接并登录
expect(clients.length).toBe(userCount);
for (const client of clients) {
expect(client.connected).toBe(true);
}
});
/**
* 测试: 多用户并发发送消息
* 验证需求 4.1, 4.2: 多用户同时发送消息
*/
it('应该正确处理多用户并发发送消息', async () => {
const userCount = 3;
// 创建并登录多个用户使用完全不同的token前缀避免userId冲突
// userId是从token前8个字符生成的所以每个用户需要不同的前缀
const userPrefixes = ['userAAA1', 'userBBB2', 'userCCC3'];
for (let i = 0; i < userCount; i++) {
const client = await createClient();
clients.push(client);
const loginPromise = waitForEvent<any>(client, 'login_success');
// 使用不同的前缀确保每个用户有唯一的userId
client.emit('login', { type: 'login', token: `${userPrefixes[i]}_concurrent_${Date.now()}` });
await loginPromise;
// 添加小延迟确保会话完全建立
await new Promise(resolve => setTimeout(resolve, 50));
}
// 顺序发送消息(避免并发会话问题)
for (let i = 0; i < clients.length; i++) {
const client = clients[i];
const chatPromise = waitForEvent<any>(client, 'chat_sent');
client.emit('chat', {
t: 'chat',
content: `Message from user ${i}`,
scope: 'local'
});
const result = await chatPromise;
expect(result.t).toBe('chat_sent');
}
});
/**
* 测试: 用户断开连接后资源清理
* 验证需求 1.3: 客户端断开连接时系统应清理相关资源
*/
it('应该在用户断开连接后正确清理资源', async () => {
const client = await createClient();
clients.push(client);
// 登录
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: 'valid_cleanup_test' });
await loginPromise;
// 断开连接
client.disconnect();
// 等待清理完成
await new Promise(resolve => setTimeout(resolve, 200));
expect(client.connected).toBe(false);
});
});
/**
* 测试套件3: 错误场景和降级测试
* 验证需求: 8.1, 8.2, 8.3, 8.4, 8.5
*/
describe('错误场景和降级测试', () => {
let client: ClientSocket;
afterEach(async () => {
if (client?.connected) {
client.disconnect();
}
});
/**
* 测试: 无效消息格式处理
* 验证需求 8.5: 系统应记录详细错误日志
*/
it('应该正确处理无效的消息格式', async () => {
client = await createClient();
// 登录
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: 'valid_error_test_1' });
await loginPromise;
// 发送无效格式的聊天消息
const errorPromise = waitForEvent<any>(client, 'chat_error');
client.emit('chat', { invalid: 'format' });
const response = await errorPromise;
expect(response.t).toBe('chat_error');
expect(response.message).toBe('消息格式无效');
});
/**
* 测试: 重复登录处理
* 验证需求 1.1: 系统应正确处理重复登录
*/
it('应该拒绝已登录用户的重复登录请求', async () => {
client = await createClient();
// 第一次登录
const loginPromise1 = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: 'valid_duplicate_test' });
await loginPromise1;
// 尝试重复登录
const errorPromise = waitForEvent<any>(client, 'login_error');
client.emit('login', { type: 'login', token: 'another_token' });
const response = await errorPromise;
expect(response.t).toBe('login_error');
expect(response.message).toBe('您已经登录');
});
/**
* 测试: 空Token登录处理
* 验证需求 1.1: 系统应验证Token
*/
it('应该拒绝空Token的登录请求', async () => {
client = await createClient();
const errorPromise = waitForEvent<any>(client, 'login_error');
client.emit('login', { type: 'login', token: '' });
const response = await errorPromise;
expect(response.t).toBe('login_error');
});
/**
* 测试: 缺少scope的聊天消息
* 验证需求 4.1: 系统应正确验证消息格式
*/
it('应该拒绝缺少scope的聊天消息', async () => {
client = await createClient();
// 登录
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: 'valid_scope_test' });
await loginPromise;
// 发送缺少scope的消息
const errorPromise = waitForEvent<any>(client, 'chat_error');
client.emit('chat', { t: 'chat', content: 'Hello' });
const response = await errorPromise;
expect(response.t).toBe('chat_error');
expect(response.message).toBe('消息格式无效');
});
/**
* 测试: 无效位置更新处理
* 验证需求 6.2: 系统应正确验证位置数据
*/
it('应该忽略无效的位置更新', async () => {
client = await createClient();
// 登录
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: 'valid_position_error_test' });
await loginPromise;
// 发送无效位置更新缺少mapId
client.emit('position_update', { t: 'position', x: 100, y: 200 });
// 等待处理
await new Promise(resolve => setTimeout(resolve, 100));
// 连接应该保持正常
expect(client.connected).toBe(true);
});
});
/**
* 测试套件4: 连接生命周期测试
* 验证需求: 1.3, 1.4, 6.4
*/
describe('连接生命周期测试', () => {
/**
* 测试: 连接-登录-断开完整流程
* 验证需求 1.1, 1.2, 1.3: 完整的连接生命周期
*/
it('应该正确处理完整的连接生命周期', async () => {
// 1. 建立连接
const client = await createClient();
expect(client.connected).toBe(true);
// 2. 登录
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: 'valid_lifecycle_test' });
const loginResponse = await loginPromise;
expect(loginResponse.t).toBe('login_success');
// 3. 发送消息
const chatPromise = waitForEvent<any>(client, 'chat_sent');
client.emit('chat', { t: 'chat', content: 'Lifecycle test message', scope: 'local' });
const chatResponse = await chatPromise;
expect(chatResponse.t).toBe('chat_sent');
// 4. 断开连接
client.disconnect();
// 等待断开完成
await new Promise(resolve => setTimeout(resolve, 100));
expect(client.connected).toBe(false);
});
/**
* 测试: 快速连接断开
* 验证需求 1.3: 系统应正确处理快速断开
*/
it('应该正确处理快速连接断开', async () => {
const client = await createClient();
expect(client.connected).toBe(true);
// 立即断开
client.disconnect();
await new Promise(resolve => setTimeout(resolve, 100));
expect(client.connected).toBe(false);
});
/**
* 测试: 登录后立即断开
* 验证需求 1.3: 系统应清理会话资源
*/
it('应该正确处理登录后立即断开', async () => {
const client = await createClient();
// 登录
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: 'valid_quick_disconnect' });
await loginPromise;
// 立即断开
client.disconnect();
await new Promise(resolve => setTimeout(resolve, 200));
expect(client.connected).toBe(false);
});
});
/**
* 测试套件5: 消息格式验证测试
* 验证需求: 5.3, 5.4
*/
describe('消息格式验证测试', () => {
let client: ClientSocket;
let testId: number = 0;
afterEach(async () => {
if (client?.connected) {
client.disconnect();
}
// 等待清理完成
await new Promise(resolve => setTimeout(resolve, 100));
});
/**
* 测试: 正常消息格式
* 验证需求 5.3, 5.4: 消息应包含所有必需信息
*/
it('应该接受正确格式的聊天消息', async () => {
testId++;
client = await createClient();
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: `valid_format_normal_${Date.now()}_${testId}` });
await loginPromise;
const chatPromise = waitForEvent<any>(client, 'chat_sent');
client.emit('chat', {
t: 'chat',
content: 'Test message with correct format',
scope: 'local'
});
const response = await chatPromise;
expect(response.t).toBe('chat_sent');
});
/**
* 测试: 长消息处理
* 验证需求 4.1: 系统应正确处理各种长度的消息
*/
it('应该正确处理较长的消息内容', async () => {
testId++;
client = await createClient();
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: `valid_format_long_${Date.now()}_${testId}` });
await loginPromise;
// 使用不重复的长消息内容,避免触发重复字符检测
const longContent = '这是一条较长的测试消息,用于验证系统能够正确处理长消息内容。' +
'消息系统应该能够处理各种长度的消息,包括较长的消息。' +
'这条消息包含多种字符和标点符号,以确保系统的兼容性。' +
'测试消息继续延长,以达到足够的长度进行测试。' +
'系统应该能够正确处理这样的消息而不会出现问题。';
const chatPromise = waitForEvent<any>(client, 'chat_sent');
client.emit('chat', { t: 'chat', content: longContent, scope: 'local' });
const response = await chatPromise;
expect(response.t).toBe('chat_sent');
});
/**
* 测试: 特殊字符消息
* 验证需求 4.1: 系统应正确处理特殊字符
*/
it('应该正确处理包含特殊字符的消息', async () => {
testId++;
client = await createClient();
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: `valid_format_special_${Date.now()}_${testId}` });
await loginPromise;
const specialContent = '你好!@#$%^&*()_+{}|:"<>?~`-=[]\\;\',./';
const chatPromise = waitForEvent<any>(client, 'chat_sent');
client.emit('chat', { t: 'chat', content: specialContent, scope: 'local' });
const response = await chatPromise;
expect(response.t).toBe('chat_sent');
});
/**
* 测试: Unicode消息
* 验证需求 4.1: 系统应正确处理Unicode字符
*/
it('应该正确处理Unicode消息', async () => {
testId++;
client = await createClient();
const loginPromise = waitForEvent<any>(client, 'login_success');
client.emit('login', { type: 'login', token: `valid_format_unicode_${Date.now()}_${testId}` });
await loginPromise;
const unicodeContent = '🎮 游戏消息 🎯 测试 🚀';
const chatPromise = waitForEvent<any>(client, 'chat_sent');
client.emit('chat', { t: 'chat', content: unicodeContent, scope: 'local' });
const response = await chatPromise;
expect(response.t).toBe('chat_sent');
});
});
});