forked from datawhale/whale-town-end
feat(gateway/chat): 新增聊天网关模块
范围:src/gateway/chat/ - 新增 ChatWebSocketGateway WebSocket 网关,处理实时聊天通信 - 新增 ChatController HTTP 控制器,提供聊天历史和系统状态接口 - 新增 ChatGatewayModule 模块配置,整合网关层组件 - 新增请求/响应 DTO 定义,提供数据验证和类型约束 - 新增完整的单元测试覆盖 - 新增模块 README 文档,包含接口说明、核心特性和风险评估
This commit is contained in:
193
src/gateway/chat/chat.gateway.spec.ts
Normal file
193
src/gateway/chat/chat.gateway.spec.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 聊天 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user