/** * 聊天业务服务测试 * * 测试范围: * - 玩家登录/登出流程 * - 聊天消息发送和广播 * - 位置更新和会话管理 * - Token验证和错误处理 * * @author moyin * @version 1.0.1 * @since 2026-01-14 * @lastModified 2026-01-19 * * 修改记录: * - 2026-01-19 moyin: 修复handlePlayerLogout测试,删除不再调用的deleteApiKey断言和过时测试用例 */ import { Test, TestingModule } from '@nestjs/testing'; import { Logger } from '@nestjs/common'; import { ChatService } from './chat.service'; import { ChatSessionService } from './services/chat_session.service'; import { ChatFilterService } from './services/chat_filter.service'; import { LoginCoreService } from '../../core/login_core/login_core.service'; describe('ChatService', () => { let service: ChatService; let sessionService: jest.Mocked; let filterService: jest.Mocked; let zulipClientPool: any; let apiKeySecurityService: any; let loginCoreService: jest.Mocked; let mockWebSocketGateway: any; beforeEach(async () => { // Mock依赖 const mockSessionService = { createSession: jest.fn(), getSession: jest.fn(), destroySession: jest.fn(), updatePlayerPosition: jest.fn(), injectContext: jest.fn(), getSocketsInMap: jest.fn(), }; const mockFilterService = { validateMessage: jest.fn(), filterContent: jest.fn(), checkRateLimit: jest.fn(), validatePermission: jest.fn(), }; const mockZulipClientPool = { createUserClient: jest.fn(), destroyUserClient: jest.fn(), sendMessage: jest.fn(), getUserClient: jest.fn(), }; const mockApiKeySecurityService = { getApiKey: jest.fn(), deleteApiKey: jest.fn(), }; const mockLoginCoreService = { verifyToken: jest.fn(), }; const mockZulipAccountsService = { findByGameUserId: jest.fn(), }; mockWebSocketGateway = { broadcastToMap: jest.fn(), sendToPlayer: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ ChatService, { provide: ChatSessionService, useValue: mockSessionService, }, { provide: ChatFilterService, useValue: mockFilterService, }, { provide: 'ZULIP_CLIENT_POOL_SERVICE', useValue: mockZulipClientPool, }, { provide: 'API_KEY_SECURITY_SERVICE', useValue: mockApiKeySecurityService, }, { provide: LoginCoreService, useValue: mockLoginCoreService, }, { provide: 'ZulipAccountsService', useValue: mockZulipAccountsService, }, ], }).compile(); service = module.get(ChatService); sessionService = module.get(ChatSessionService); filterService = module.get(ChatFilterService); zulipClientPool = module.get('ZULIP_CLIENT_POOL_SERVICE'); apiKeySecurityService = module.get('API_KEY_SECURITY_SERVICE'); loginCoreService = module.get(LoginCoreService); // 设置默认的mock行为 // ZulipAccountsService默认返回null(用户没有Zulip账号) const zulipAccountsService = module.get('ZulipAccountsService'); zulipAccountsService.findByGameUserId.mockResolvedValue(null); // ZulipClientPool的getUserClient默认返回null zulipClientPool.getUserClient.mockResolvedValue(null); // 设置WebSocket网关 service.setWebSocketGateway(mockWebSocketGateway); // 禁用日志输出 jest.spyOn(Logger.prototype, 'log').mockImplementation(); jest.spyOn(Logger.prototype, 'error').mockImplementation(); jest.spyOn(Logger.prototype, 'warn').mockImplementation(); }); afterEach(() => { jest.clearAllMocks(); }); describe('初始化', () => { it('应该成功创建服务实例', () => { expect(service).toBeDefined(); }); it('应该成功设置WebSocket网关', () => { const newGateway = { broadcastToMap: jest.fn(), sendToPlayer: jest.fn() }; service.setWebSocketGateway(newGateway); expect(service['websocketGateway']).toBe(newGateway); }); }); describe('handlePlayerLogin', () => { const validToken = 'valid.jwt.token'; const socketId = 'socket_123'; it('应该成功处理玩家登录', async () => { const userInfo = { sub: 'user_123', username: 'testuser', email: 'test@example.com', role: 1, type: 'access' as 'access' | 'refresh', }; loginCoreService.verifyToken.mockResolvedValue(userInfo); sessionService.createSession.mockResolvedValue({ socketId, userId: userInfo.sub, username: userInfo.username, zulipQueueId: 'queue_123', currentMap: 'whale_port', position: { x: 400, y: 300 }, lastActivity: new Date(), createdAt: new Date(), }); const result = await service.handlePlayerLogin({ token: validToken, socketId }); expect(result.success).toBe(true); expect(result.userId).toBe(userInfo.sub); expect(result.username).toBe(userInfo.username); expect(loginCoreService.verifyToken).toHaveBeenCalledWith(validToken, 'access'); expect(sessionService.createSession).toHaveBeenCalled(); }); it('应该拒绝空Token', async () => { const result = await service.handlePlayerLogin({ token: '', socketId }); expect(result.success).toBe(false); expect(result.error).toBe('Token或socketId不能为空'); expect(loginCoreService.verifyToken).not.toHaveBeenCalled(); }); it('应该拒绝空socketId', async () => { const result = await service.handlePlayerLogin({ token: validToken, socketId: '' }); expect(result.success).toBe(false); expect(result.error).toBe('Token或socketId不能为空'); }); it('应该处理Token验证失败', async () => { loginCoreService.verifyToken.mockResolvedValue(null); const result = await service.handlePlayerLogin({ token: validToken, socketId }); expect(result.success).toBe(false); expect(result.error).toBe('Token验证失败'); }); it('应该处理Token验证异常', async () => { loginCoreService.verifyToken.mockRejectedValue(new Error('Token expired')); const result = await service.handlePlayerLogin({ token: validToken, socketId }); expect(result.success).toBe(false); expect(result.error).toBe('Token验证失败'); }); it('应该处理会话创建失败', async () => { const userInfo = { sub: 'user_123', username: 'testuser', email: 'test@example.com', role: 1, type: 'access' as 'access' | 'refresh' }; loginCoreService.verifyToken.mockResolvedValue(userInfo); sessionService.createSession.mockRejectedValue(new Error('Redis error')); const result = await service.handlePlayerLogin({ token: validToken, socketId }); expect(result.success).toBe(false); expect(result.error).toBe('登录失败,请稍后重试'); }); }); describe('handlePlayerLogout', () => { const socketId = 'socket_123'; const userId = 'user_123'; it('应该成功处理玩家登出', async () => { sessionService.getSession.mockResolvedValue({ socketId, userId, username: 'testuser', zulipQueueId: 'queue_123', currentMap: 'whale_port', position: { x: 400, y: 300 }, lastActivity: new Date(), createdAt: new Date(), }); zulipClientPool.destroyUserClient.mockResolvedValue(undefined); sessionService.destroySession.mockResolvedValue(true); await service.handlePlayerLogout(socketId, 'manual'); expect(sessionService.getSession).toHaveBeenCalledWith(socketId); expect(zulipClientPool.destroyUserClient).toHaveBeenCalledWith(userId); expect(sessionService.destroySession).toHaveBeenCalledWith(socketId); }); it('应该处理会话不存在的情况', async () => { sessionService.getSession.mockResolvedValue(null); await service.handlePlayerLogout(socketId); expect(sessionService.destroySession).not.toHaveBeenCalled(); }); it('应该处理Zulip客户端清理失败', async () => { sessionService.getSession.mockResolvedValue({ socketId, userId, username: 'testuser', zulipQueueId: 'queue_123', currentMap: 'whale_port', position: { x: 400, y: 300 }, lastActivity: new Date(), createdAt: new Date(), }); zulipClientPool.destroyUserClient.mockRejectedValue(new Error('Zulip error')); sessionService.destroySession.mockResolvedValue(true); await service.handlePlayerLogout(socketId); expect(sessionService.destroySession).toHaveBeenCalled(); }); }); describe('sendChatMessage', () => { const socketId = 'socket_123'; const userId = 'user_123'; const content = 'Hello, world!'; beforeEach(() => { sessionService.getSession.mockResolvedValue({ socketId, userId, username: 'testuser', zulipQueueId: 'queue_123', currentMap: 'whale_port', position: { x: 400, y: 300 }, lastActivity: new Date(), createdAt: new Date(), }); sessionService.injectContext.mockResolvedValue({ stream: 'Whale Port', topic: 'General', }); filterService.validateMessage.mockResolvedValue({ allowed: true, filteredContent: content, }); sessionService.getSocketsInMap.mockResolvedValue([socketId, 'socket_456']); apiKeySecurityService.getApiKey.mockResolvedValue({ success: true, apiKey: 'test_api_key', }); }); it('应该成功发送聊天消息', async () => { const result = await service.sendChatMessage({ socketId, content, scope: 'local' }); expect(result.success).toBe(true); expect(result.messageId).toBeDefined(); expect(sessionService.getSession).toHaveBeenCalledWith(socketId); expect(filterService.validateMessage).toHaveBeenCalled(); }); it('应该拒绝不存在的会话', async () => { sessionService.getSession.mockResolvedValue(null); const result = await service.sendChatMessage({ socketId, content, scope: 'local' }); expect(result.success).toBe(false); expect(result.error).toBe('会话不存在,请重新登录'); }); it('应该拒绝被过滤的消息', async () => { filterService.validateMessage.mockResolvedValue({ allowed: false, reason: '消息包含敏感词', }); const result = await service.sendChatMessage({ socketId, content, scope: 'local' }); expect(result.success).toBe(false); expect(result.error).toBe('消息包含敏感词'); }); it('应该处理消息发送异常', async () => { sessionService.getSession.mockRejectedValue(new Error('Redis error')); const result = await service.sendChatMessage({ socketId, content, scope: 'local' }); expect(result.success).toBe(false); expect(result.error).toBe('消息发送失败,请稍后重试'); }); }); describe('updatePlayerPosition', () => { const socketId = 'socket_123'; const mapId = 'whale_port'; const x = 500; const y = 400; it('应该成功更新玩家位置', async () => { sessionService.updatePlayerPosition.mockResolvedValue(true); const result = await service.updatePlayerPosition({ socketId, mapId, x, y }); expect(result).toBe(true); expect(sessionService.updatePlayerPosition).toHaveBeenCalledWith(socketId, mapId, x, y); }); it('应该拒绝空socketId', async () => { const result = await service.updatePlayerPosition({ socketId: '', mapId, x, y }); expect(result).toBe(false); expect(sessionService.updatePlayerPosition).not.toHaveBeenCalled(); }); it('应该拒绝空mapId', async () => { const result = await service.updatePlayerPosition({ socketId, mapId: '', x, y }); expect(result).toBe(false); expect(sessionService.updatePlayerPosition).not.toHaveBeenCalled(); }); it('应该处理更新失败', async () => { sessionService.updatePlayerPosition.mockRejectedValue(new Error('Redis error')); const result = await service.updatePlayerPosition({ socketId, mapId, x, y }); expect(result).toBe(false); }); }); describe('getChatHistory', () => { it('应该返回聊天历史', async () => { const result = await service.getChatHistory({ mapId: 'whale_port' }); expect(result.success).toBe(true); expect(result.messages).toBeDefined(); expect(Array.isArray(result.messages)).toBe(true); }); it('应该支持分页查询', async () => { const result = await service.getChatHistory({ mapId: 'whale_port', limit: 10, offset: 0 }); expect(result.success).toBe(true); expect(result.count).toBeLessThanOrEqual(10); }); }); describe('getSession', () => { const socketId = 'socket_123'; it('应该返回会话信息', async () => { const mockSession = { socketId, userId: 'user_123', username: 'testuser', zulipQueueId: 'queue_123', currentMap: 'whale_port', position: { x: 400, y: 300 }, lastActivity: new Date(), createdAt: new Date(), }; sessionService.getSession.mockResolvedValue(mockSession); const result = await service.getSession(socketId); expect(result).toEqual(mockSession); expect(sessionService.getSession).toHaveBeenCalledWith(socketId); }); it('应该处理会话不存在', async () => { sessionService.getSession.mockResolvedValue(null); const result = await service.getSession(socketId); expect(result).toBeNull(); }); }); });