552 lines
17 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|
|
}); |