Files
whale-town-end/src/business/location_broadcast/location_broadcast.controller.spec.ts
moyin c31cbe559d feat:实现位置广播系统
- 添加位置广播核心控制器和服务
- 实现健康检查和位置同步功能
- 添加WebSocket实时位置更新支持
- 完善位置广播的测试覆盖
2026-01-08 23:05:52 +08:00

552 lines
17 KiB
TypeScript

/**
* 位置广播控制器单元测试
*
* 功能描述:
* - 测试位置广播HTTP API控制器的功能
* - 验证API端点的请求处理和响应格式
* - 确保权限验证和错误处理的正确性
* - 提供完整的API测试覆盖率
*
* 测试范围:
* - HTTP API端点的功能测试
* - 请求参数验证和响应格式
* - 权限控制和安全验证
* - 异常处理和错误响应
*
* @author moyin
* @version 1.0.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { Test, TestingModule } from '@nestjs/testing';
import { HttpException, HttpStatus } from '@nestjs/common';
import { LocationBroadcastController } from './location_broadcast.controller';
import { LocationBroadcastService } from './services/location_broadcast.service';
import { LocationSessionService } from './services/location_session.service';
import { LocationPositionService } from './services/location_position.service';
import { JwtPayload } from '../../core/login_core/login_core.service';
import { CreateSessionDto, SessionQueryDto, PositionQueryDto, UpdateSessionConfigDto } from './dto/api.dto';
import { GameSession, SessionStatus } from '../../core/location_broadcast_core/session.interface';
describe('LocationBroadcastController', () => {
let controller: LocationBroadcastController;
let mockLocationBroadcastService: any;
let mockLocationSessionService: any;
let mockLocationPositionService: any;
const mockUser: JwtPayload = {
sub: 'user123',
username: 'testuser',
role: 1,
email: 'test@example.com',
type: 'access',
};
const mockAdminUser: JwtPayload = {
sub: 'admin123',
username: 'admin',
role: 2,
email: 'admin@example.com',
type: 'access',
};
beforeEach(async () => {
// 创建模拟服务
mockLocationBroadcastService = {
cleanupUserData: jest.fn(),
};
mockLocationSessionService = {
createSession: jest.fn(),
querySessions: jest.fn(),
getSessionDetail: jest.fn(),
updateSessionConfig: jest.fn(),
endSession: jest.fn(),
};
mockLocationPositionService = {
queryPositions: jest.fn(),
getPositionStats: jest.fn(),
getPositionHistory: jest.fn(),
};
// 创建模拟的LoginCoreService
const mockLoginCoreService = {
validateToken: jest.fn(),
getUserFromToken: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [LocationBroadcastController],
providers: [
{
provide: LocationBroadcastService,
useValue: mockLocationBroadcastService,
},
{
provide: LocationSessionService,
useValue: mockLocationSessionService,
},
{
provide: LocationPositionService,
useValue: mockLocationPositionService,
},
{
provide: 'LoginCoreService',
useValue: mockLoginCoreService,
},
],
})
.overrideGuard(require('../../business/auth/jwt_auth.guard').JwtAuthGuard)
.useValue({
canActivate: jest.fn(() => true),
})
.compile();
controller = module.get<LocationBroadcastController>(LocationBroadcastController);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('createSession', () => {
const mockCreateSessionDto: CreateSessionDto = {
sessionId: 'session123',
name: '测试会话',
description: '这是一个测试会话',
maxUsers: 50,
allowObservers: true,
broadcastRange: 1000,
};
const mockSession: GameSession = {
sessionId: 'session123',
users: [],
createdAt: Date.now(),
lastActivity: Date.now(),
status: SessionStatus.ACTIVE,
config: {
maxUsers: 50,
timeoutSeconds: 3600,
allowObservers: true,
requirePassword: false,
broadcastRange: 1000,
},
metadata: {
name: '测试会话',
description: '这是一个测试会话',
creatorId: 'user123',
},
};
it('应该成功创建会话', async () => {
mockLocationSessionService.createSession.mockResolvedValue(mockSession);
const result = await controller.createSession(mockCreateSessionDto, mockUser);
expect(result.success).toBe(true);
expect(result.data.sessionId).toBe('session123');
expect(result.message).toBe('会话创建成功');
expect(mockLocationSessionService.createSession).toHaveBeenCalledWith({
sessionId: mockCreateSessionDto.sessionId,
creatorId: mockUser.sub,
name: mockCreateSessionDto.name,
description: mockCreateSessionDto.description,
maxUsers: mockCreateSessionDto.maxUsers,
allowObservers: mockCreateSessionDto.allowObservers,
broadcastRange: mockCreateSessionDto.broadcastRange,
metadata: mockCreateSessionDto.metadata,
});
});
it('应该处理会话创建失败', async () => {
mockLocationSessionService.createSession.mockRejectedValue(new Error('创建失败'));
await expect(controller.createSession(mockCreateSessionDto, mockUser))
.rejects.toThrow(HttpException);
});
it('应该处理HTTP异常', async () => {
const httpException = new HttpException('会话ID已存在', HttpStatus.CONFLICT);
mockLocationSessionService.createSession.mockRejectedValue(httpException);
await expect(controller.createSession(mockCreateSessionDto, mockUser))
.rejects.toThrow(httpException);
});
});
describe('querySessions', () => {
const mockQueryDto: SessionQueryDto = {
status: 'active',
minUsers: 1,
maxUsers: 100,
offset: 0,
limit: 10,
};
const mockQueryResult = {
sessions: [],
total: 0,
page: 1,
pageSize: 10,
};
it('应该成功查询会话列表', async () => {
mockLocationSessionService.querySessions.mockResolvedValue(mockQueryResult);
const result = await controller.querySessions(mockQueryDto);
expect(result.success).toBe(true);
expect(result.data).toEqual(mockQueryResult);
expect(mockLocationSessionService.querySessions).toHaveBeenCalledWith({
status: mockQueryDto.status,
minUsers: mockQueryDto.minUsers,
maxUsers: mockQueryDto.maxUsers,
publicOnly: mockQueryDto.publicOnly,
offset: 0,
limit: 10,
});
});
it('应该处理查询失败', async () => {
mockLocationSessionService.querySessions.mockRejectedValue(new Error('查询失败'));
await expect(controller.querySessions(mockQueryDto))
.rejects.toThrow(HttpException);
});
});
describe('getSessionDetail', () => {
const mockSessionDetail = {
session: {
sessionId: 'session123',
users: [],
createdAt: Date.now(),
lastActivity: Date.now(),
status: SessionStatus.ACTIVE,
config: { maxUsers: 100, timeoutSeconds: 3600, allowObservers: true, requirePassword: false },
metadata: {},
},
users: [],
onlineCount: 0,
activeMaps: [],
};
it('应该成功获取会话详情', async () => {
mockLocationSessionService.getSessionDetail.mockResolvedValue(mockSessionDetail);
const result = await controller.getSessionDetail('session123', mockUser);
expect(result.success).toBe(true);
expect(result.data).toEqual(mockSessionDetail);
expect(mockLocationSessionService.getSessionDetail).toHaveBeenCalledWith('session123', mockUser.sub);
});
it('应该处理会话不存在', async () => {
const notFoundException = new HttpException('会话不存在', HttpStatus.NOT_FOUND);
mockLocationSessionService.getSessionDetail.mockRejectedValue(notFoundException);
await expect(controller.getSessionDetail('nonexistent', mockUser))
.rejects.toThrow(notFoundException);
});
});
describe('updateSessionConfig', () => {
const mockUpdateConfigDto: UpdateSessionConfigDto = {
maxUsers: 150,
allowObservers: false,
broadcastRange: 1500,
};
const mockUpdatedSession: GameSession = {
sessionId: 'session123',
users: [],
createdAt: Date.now(),
lastActivity: Date.now(),
status: SessionStatus.ACTIVE,
config: {
maxUsers: 150,
timeoutSeconds: 3600,
allowObservers: false,
requirePassword: false,
broadcastRange: 1500,
},
metadata: {},
};
it('应该成功更新会话配置', async () => {
mockLocationSessionService.updateSessionConfig.mockResolvedValue(mockUpdatedSession);
const result = await controller.updateSessionConfig('session123', mockUpdateConfigDto, mockUser);
expect(result.success).toBe(true);
expect(result.data).toEqual(mockUpdatedSession);
expect(result.message).toBe('会话配置更新成功');
expect(mockLocationSessionService.updateSessionConfig).toHaveBeenCalledWith(
'session123',
mockUpdateConfigDto,
mockUser.sub,
);
});
it('应该处理权限不足', async () => {
const forbiddenException = new HttpException('权限不足', HttpStatus.FORBIDDEN);
mockLocationSessionService.updateSessionConfig.mockRejectedValue(forbiddenException);
await expect(controller.updateSessionConfig('session123', mockUpdateConfigDto, mockUser))
.rejects.toThrow(forbiddenException);
});
});
describe('endSession', () => {
it('应该成功结束会话', async () => {
mockLocationSessionService.endSession.mockResolvedValue(true);
const result = await controller.endSession('session123', mockUser);
expect(result.success).toBe(true);
expect(result.message).toBe('会话结束成功');
expect(mockLocationSessionService.endSession).toHaveBeenCalledWith('session123', mockUser.sub);
});
it('应该处理结束会话失败', async () => {
mockLocationSessionService.endSession.mockRejectedValue(new Error('结束失败'));
await expect(controller.endSession('session123', mockUser))
.rejects.toThrow(HttpException);
});
});
describe('queryPositions', () => {
const mockQueryDto: PositionQueryDto = {
mapId: 'plaza',
limit: 50,
offset: 0,
};
const mockQueryResult = {
positions: [
{
userId: 'user1',
x: 100,
y: 200,
mapId: 'plaza',
timestamp: Date.now(),
metadata: {},
},
],
total: 1,
timestamp: Date.now(),
};
it('应该成功查询位置信息', async () => {
mockLocationPositionService.queryPositions.mockResolvedValue(mockQueryResult);
const result = await controller.queryPositions(mockQueryDto);
expect(result.success).toBe(true);
expect(result.data).toEqual(mockQueryResult);
expect(mockLocationPositionService.queryPositions).toHaveBeenCalledWith({
userIds: undefined,
mapId: mockQueryDto.mapId,
sessionId: mockQueryDto.sessionId,
range: undefined,
pagination: {
offset: 0,
limit: 50,
},
});
});
it('应该处理用户ID列表', async () => {
const queryWithUserIds = { ...mockQueryDto, userIds: 'user1,user2,user3' };
mockLocationPositionService.queryPositions.mockResolvedValue(mockQueryResult);
await controller.queryPositions(queryWithUserIds);
expect(mockLocationPositionService.queryPositions).toHaveBeenCalledWith(
expect.objectContaining({
userIds: ['user1', 'user2', 'user3'],
}),
);
});
it('应该处理范围查询', async () => {
const queryWithRange = {
...mockQueryDto,
centerX: 100,
centerY: 200,
radius: 50,
};
mockLocationPositionService.queryPositions.mockResolvedValue(mockQueryResult);
await controller.queryPositions(queryWithRange);
expect(mockLocationPositionService.queryPositions).toHaveBeenCalledWith(
expect.objectContaining({
range: {
centerX: 100,
centerY: 200,
radius: 50,
},
}),
);
});
});
describe('getPositionStats', () => {
const mockStatsResult = {
totalUsers: 100,
onlineUsers: 85,
activeMaps: 5,
mapDistribution: { plaza: 30, forest: 25, mountain: 30 },
updateFrequency: 2.5,
timestamp: Date.now(),
};
it('应该成功获取位置统计', async () => {
mockLocationPositionService.getPositionStats.mockResolvedValue(mockStatsResult);
const result = await controller.getPositionStats('plaza', 'session123');
expect(result.success).toBe(true);
expect(result.data).toEqual(mockStatsResult);
expect(mockLocationPositionService.getPositionStats).toHaveBeenCalledWith({
mapId: 'plaza',
sessionId: 'session123',
});
});
it('应该处理统计获取失败', async () => {
mockLocationPositionService.getPositionStats.mockRejectedValue(new Error('统计失败'));
await expect(controller.getPositionStats())
.rejects.toThrow(HttpException);
});
});
describe('getUserPositionHistory', () => {
const mockHistoryResult = [
{
userId: 'user123',
x: 100,
y: 200,
mapId: 'plaza',
timestamp: Date.now() - 60000,
sessionId: 'session123',
metadata: {},
},
];
it('应该允许用户查看自己的位置历史', async () => {
mockLocationPositionService.getPositionHistory.mockResolvedValue(mockHistoryResult);
const result = await controller.getUserPositionHistory('user123', mockUser, 'plaza', 100);
expect(result.success).toBe(true);
expect(result.data).toEqual(mockHistoryResult);
expect(mockLocationPositionService.getPositionHistory).toHaveBeenCalledWith({
userId: 'user123',
mapId: 'plaza',
limit: 100,
});
});
it('应该允许管理员查看任何用户的位置历史', async () => {
mockLocationPositionService.getPositionHistory.mockResolvedValue(mockHistoryResult);
const result = await controller.getUserPositionHistory('user456', mockAdminUser, 'plaza', 100);
expect(result.success).toBe(true);
expect(result.data).toEqual(mockHistoryResult);
});
it('应该拒绝普通用户查看其他用户的位置历史', async () => {
await expect(controller.getUserPositionHistory('user456', mockUser, 'plaza', 100))
.rejects.toThrow(HttpException);
expect(mockLocationPositionService.getPositionHistory).not.toHaveBeenCalled();
});
it('应该处理历史获取失败', async () => {
mockLocationPositionService.getPositionHistory.mockRejectedValue(new Error('获取失败'));
await expect(controller.getUserPositionHistory('user123', mockUser))
.rejects.toThrow(HttpException);
});
});
describe('cleanupUserData', () => {
it('应该允许用户清理自己的数据', async () => {
mockLocationBroadcastService.cleanupUserData.mockResolvedValue(true);
const result = await controller.cleanupUserData('user123', mockUser);
expect(result.success).toBe(true);
expect(result.message).toBe('用户数据清理成功');
expect(mockLocationBroadcastService.cleanupUserData).toHaveBeenCalledWith('user123');
});
it('应该允许管理员清理任何用户的数据', async () => {
mockLocationBroadcastService.cleanupUserData.mockResolvedValue(true);
const result = await controller.cleanupUserData('user456', mockAdminUser);
expect(result.success).toBe(true);
expect(result.message).toBe('用户数据清理成功');
});
it('应该拒绝普通用户清理其他用户的数据', async () => {
await expect(controller.cleanupUserData('user456', mockUser))
.rejects.toThrow(HttpException);
expect(mockLocationBroadcastService.cleanupUserData).not.toHaveBeenCalled();
});
it('应该处理清理失败', async () => {
mockLocationBroadcastService.cleanupUserData.mockResolvedValue(false);
await expect(controller.cleanupUserData('user123', mockUser))
.rejects.toThrow(HttpException);
});
it('应该处理清理异常', async () => {
mockLocationBroadcastService.cleanupUserData.mockRejectedValue(new Error('清理异常'));
await expect(controller.cleanupUserData('user123', mockUser))
.rejects.toThrow(HttpException);
});
});
describe('错误处理', () => {
it('应该正确处理HTTP异常', async () => {
const httpException = new HttpException('测试异常', HttpStatus.BAD_REQUEST);
mockLocationSessionService.createSession.mockRejectedValue(httpException);
const createSessionDto: CreateSessionDto = {
sessionId: 'test',
};
await expect(controller.createSession(createSessionDto, mockUser))
.rejects.toThrow(httpException);
});
it('应该将普通异常转换为HTTP异常', async () => {
const normalError = new Error('普通错误');
mockLocationSessionService.createSession.mockRejectedValue(normalError);
const createSessionDto: CreateSessionDto = {
sessionId: 'test',
};
try {
await controller.createSession(createSessionDto, mockUser);
} catch (error) {
expect(error).toBeInstanceOf(HttpException);
expect((error as HttpException).getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
}
});
});
});