feat:实现位置广播系统
- 添加位置广播核心控制器和服务 - 实现健康检查和位置同步功能 - 添加WebSocket实时位置更新支持 - 完善位置广播的测试覆盖
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
/**
|
||||
* 位置广播控制器单元测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试位置广播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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user