Files
whale-town-end/src/gateway/chat/chat.gateway.spec.ts
moyin 5bcf3cb678 feat(gateway/chat): 新增聊天网关模块
范围:src/gateway/chat/
- 新增 ChatWebSocketGateway WebSocket 网关,处理实时聊天通信
- 新增 ChatController HTTP 控制器,提供聊天历史和系统状态接口
- 新增 ChatGatewayModule 模块配置,整合网关层组件
- 新增请求/响应 DTO 定义,提供数据验证和类型约束
- 新增完整的单元测试覆盖
- 新增模块 README 文档,包含接口说明、核心特性和风险评估
2026-01-14 19:11:25 +08:00

194 lines
5.5 KiB
TypeScript

/**
* 聊天 WebSocket 网关单元测试
*
* 功能描述:
* - 测试 ChatWebSocketGateway 的 WebSocket 连接管理
* - 验证消息路由和处理逻辑
* - 测试房间管理和广播功能
*
* 测试范围:
* - onModuleInit() - 模块初始化
* - onModuleDestroy() - 模块销毁
* - getConnectionCount() - 获取连接数
* - getAuthenticatedConnectionCount() - 获取认证连接数
* - getMapPlayerCounts() - 获取地图玩家数
* - getMapPlayers() - 获取地图玩家列表
* - sendToPlayer() - 单播消息
* - broadcastToMap() - 地图广播
*
* @author moyin
* @version 1.0.0
* @since 2026-01-14
*/
import { Test, TestingModule } from '@nestjs/testing';
import { ChatWebSocketGateway } from './chat.gateway';
import { ChatService } from '../../business/chat/chat.service';
// Mock ws module
jest.mock('ws', () => {
const mockServerInstance = {
on: jest.fn(),
close: jest.fn(),
};
const MockServer = jest.fn(() => mockServerInstance);
return {
Server: MockServer,
OPEN: 1,
__mockServerInstance: mockServerInstance,
};
});
describe('ChatWebSocketGateway', () => {
let gateway: ChatWebSocketGateway;
let mockChatService: jest.Mocked<Partial<ChatService>>;
beforeEach(async () => {
// Reset mocks
jest.clearAllMocks();
mockChatService = {
setWebSocketGateway: jest.fn(),
handlePlayerLogin: jest.fn(),
handlePlayerLogout: jest.fn(),
sendChatMessage: jest.fn(),
updatePlayerPosition: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
ChatWebSocketGateway,
{ provide: ChatService, useValue: mockChatService },
],
}).compile();
gateway = module.get<ChatWebSocketGateway>(ChatWebSocketGateway);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('onModuleInit', () => {
it('should initialize WebSocket server and set gateway reference', async () => {
await gateway.onModuleInit();
expect(mockChatService.setWebSocketGateway).toHaveBeenCalledWith(gateway);
});
it('should use default port 3001 when WEBSOCKET_PORT is not set', async () => {
delete process.env.WEBSOCKET_PORT;
await gateway.onModuleInit();
// Verify server was created (mock was called)
const ws = require('ws');
expect(ws.Server).toHaveBeenCalledWith(
expect.objectContaining({
port: 3001,
path: '/game',
})
);
});
it('should use custom port from environment variable', async () => {
process.env.WEBSOCKET_PORT = '4000';
// Create new gateway instance to pick up env change
const module: TestingModule = await Test.createTestingModule({
providers: [
ChatWebSocketGateway,
{ provide: ChatService, useValue: mockChatService },
],
}).compile();
const newGateway = module.get<ChatWebSocketGateway>(ChatWebSocketGateway);
await newGateway.onModuleInit();
const ws = require('ws');
expect(ws.Server).toHaveBeenCalledWith(
expect.objectContaining({
port: 4000,
path: '/game',
})
);
delete process.env.WEBSOCKET_PORT;
});
});
describe('onModuleDestroy', () => {
it('should close WebSocket server when it exists', async () => {
await gateway.onModuleInit();
await gateway.onModuleDestroy();
const ws = require('ws');
expect(ws.__mockServerInstance.close).toHaveBeenCalled();
});
it('should not throw when server does not exist', async () => {
// Don't call onModuleInit, so server is undefined
await expect(gateway.onModuleDestroy()).resolves.not.toThrow();
});
});
describe('getConnectionCount', () => {
it('should return 0 when no clients connected', () => {
expect(gateway.getConnectionCount()).toBe(0);
});
});
describe('getAuthenticatedConnectionCount', () => {
it('should return 0 when no authenticated clients', () => {
expect(gateway.getAuthenticatedConnectionCount()).toBe(0);
});
});
describe('getMapPlayerCounts', () => {
it('should return empty object when no rooms exist', () => {
expect(gateway.getMapPlayerCounts()).toEqual({});
});
});
describe('getMapPlayers', () => {
it('should return empty array for non-existent room', () => {
expect(gateway.getMapPlayers('non_existent_map')).toEqual([]);
});
});
describe('sendToPlayer', () => {
it('should not throw when client does not exist', () => {
expect(() => {
gateway.sendToPlayer('non_existent_id', { type: 'test' });
}).not.toThrow();
});
});
describe('broadcastToMap', () => {
it('should not throw when room does not exist', () => {
expect(() => {
gateway.broadcastToMap('non_existent_map', { type: 'test' });
}).not.toThrow();
});
it('should handle excludeId parameter', () => {
expect(() => {
gateway.broadcastToMap('non_existent_map', { type: 'test' }, 'exclude_id');
}).not.toThrow();
});
});
describe('IChatWebSocketGateway interface', () => {
it('should implement all interface methods', () => {
expect(typeof gateway.sendToPlayer).toBe('function');
expect(typeof gateway.broadcastToMap).toBe('function');
expect(typeof gateway.getConnectionCount).toBe('function');
expect(typeof gateway.getAuthenticatedConnectionCount).toBe('function');
expect(typeof gateway.getMapPlayerCounts).toBe('function');
expect(typeof gateway.getMapPlayers).toBe('function');
});
});
});