/** * 位置广播控制器单元测试 * * 功能描述: * - 测试位置广播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); }); 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); } }); }); });