forked from datawhale/whale-town-end
范围:src/gateway/chat/ - 新增 ChatWebSocketGateway WebSocket 网关,处理实时聊天通信 - 新增 ChatController HTTP 控制器,提供聊天历史和系统状态接口 - 新增 ChatGatewayModule 模块配置,整合网关层组件 - 新增请求/响应 DTO 定义,提供数据验证和类型约束 - 新增完整的单元测试覆盖 - 新增模块 README 文档,包含接口说明、核心特性和风险评估
194 lines
5.5 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|