/** * 会话管理服务单元测试 * * 功能描述: * - 测试会话管理服务的核心功能 * - 验证会话创建、查询、配置等业务逻辑 * - 确保权限验证和数据验证的正确性 * - 提供完整的测试覆盖率 * * 测试范围: * - 会话创建和配置管理 * - 会话查询和详情获取 * - 权限验证和访问控制 * - 数据验证和错误处理 * * @author moyin * @version 1.0.0 * @since 2026-01-08 * @lastModified 2026-01-08 */ import { Test, TestingModule } from '@nestjs/testing'; import { BadRequestException, NotFoundException, ConflictException, ForbiddenException } from '@nestjs/common'; import { LocationSessionService, CreateSessionRequest, SessionQueryRequest } from './location_session.service'; import { GameSession, SessionUser, SessionUserStatus, SessionStatus } from '../../../core/location_broadcast_core/session.interface'; describe('LocationSessionService', () => { let service: LocationSessionService; let mockLocationBroadcastCore: any; beforeEach(async () => { // 创建模拟的核心服务 mockLocationBroadcastCore = { getSessionUsers: jest.fn(), getSessionPositions: jest.fn(), removeUserFromSession: jest.fn(), cleanupEmptySession: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ LocationSessionService, { provide: 'ILocationBroadcastCore', useValue: mockLocationBroadcastCore, }, ], }).compile(); service = module.get(LocationSessionService); }); afterEach(() => { jest.clearAllMocks(); }); describe('createSession', () => { const mockCreateRequest: CreateSessionRequest = { sessionId: 'session123', creatorId: 'user456', name: '测试会话', description: '这是一个测试会话', maxUsers: 50, allowObservers: true, broadcastRange: 1000, }; it('应该成功创建会话', async () => { // 模拟会话不存在(返回空用户列表) mockLocationBroadcastCore.getSessionUsers.mockResolvedValue([]); const result = await service.createSession(mockCreateRequest); expect(result.sessionId).toBe(mockCreateRequest.sessionId); expect(result.users).toEqual([]); expect(result.status).toBe(SessionStatus.ACTIVE); expect(result.config.maxUsers).toBe(mockCreateRequest.maxUsers); expect(result.config.allowObservers).toBe(mockCreateRequest.allowObservers); expect(result.metadata.name).toBe(mockCreateRequest.name); expect(result.metadata.description).toBe(mockCreateRequest.description); expect(result.metadata.creatorId).toBe(mockCreateRequest.creatorId); }); it('应该在会话ID已存在时抛出冲突异常', async () => { // 模拟会话已存在(返回非空用户列表) const existingUsers: SessionUser[] = [ { userId: 'user789', socketId: 'socket123', joinedAt: Date.now(), lastSeen: Date.now(), status: SessionUserStatus.ONLINE, metadata: {}, }, ]; mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(existingUsers); await expect(service.createSession(mockCreateRequest)).rejects.toThrow(ConflictException); }); it('应该验证必填参数', async () => { const invalidRequests = [ { ...mockCreateRequest, sessionId: '' }, { ...mockCreateRequest, creatorId: '' }, ]; for (const request of invalidRequests) { await expect(service.createSession(request)).rejects.toThrow(BadRequestException); } }); it('应该验证参数范围', async () => { const invalidRequests = [ { ...mockCreateRequest, maxUsers: 0 }, { ...mockCreateRequest, maxUsers: 1001 }, { ...mockCreateRequest, broadcastRange: -1 }, { ...mockCreateRequest, broadcastRange: 10001 }, ]; // 为每个无效请求设置Mock返回值 mockLocationBroadcastCore.getSessionUsers.mockResolvedValue([]); for (const request of invalidRequests) { await expect(service.createSession(request)).rejects.toThrow(BadRequestException); } }); it('应该正确处理可选参数', async () => { const minimalRequest: CreateSessionRequest = { sessionId: 'session123', creatorId: 'user456', }; mockLocationBroadcastCore.getSessionUsers.mockResolvedValue([]); const result = await service.createSession(minimalRequest); expect(result.config.maxUsers).toBe(100); // 默认值 expect(result.config.allowObservers).toBe(true); // 默认值 expect(result.config.broadcastRange).toBe(1000); // 默认值 expect(result.metadata.name).toBe(minimalRequest.sessionId); // 默认使用sessionId }); it('应该正确设置密码相关配置', async () => { const requestWithPassword = { ...mockCreateRequest, password: 'secret123', }; mockLocationBroadcastCore.getSessionUsers.mockResolvedValue([]); const result = await service.createSession(requestWithPassword); expect(result.config.requirePassword).toBe(true); expect(result.config.password).toBe('secret123'); expect(result.metadata.isPublic).toBe(false); }); }); describe('getSessionDetail', () => { const mockUsers: SessionUser[] = [ { userId: 'user1', socketId: 'socket1', joinedAt: Date.now() - 60000, lastSeen: Date.now(), status: SessionUserStatus.ONLINE, metadata: {}, }, { userId: 'user2', socketId: 'socket2', joinedAt: Date.now() - 30000, lastSeen: Date.now() - 5000, status: SessionUserStatus.AWAY, metadata: {}, }, ]; const mockPositions = [ { userId: 'user1', x: 100, y: 200, mapId: 'plaza', timestamp: Date.now(), metadata: {} }, { userId: 'user2', x: 150, y: 250, mapId: 'forest', timestamp: Date.now(), metadata: {} }, ]; it('应该返回完整的会话详情', async () => { mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(mockUsers); mockLocationBroadcastCore.getSessionPositions.mockResolvedValue(mockPositions); const result = await service.getSessionDetail('session123', 'user456'); expect(result.session).toBeDefined(); expect(result.session.sessionId).toBe('session123'); expect(result.users).toEqual(mockUsers); expect(result.onlineCount).toBe(1); // 只有一个在线用户 expect(result.activeMaps).toEqual(['plaza', 'forest']); }); it('应该在会话不存在时抛出异常', async () => { mockLocationBroadcastCore.getSessionUsers.mockResolvedValue([]); await expect(service.getSessionDetail('nonexistent', 'user456')).rejects.toThrow(NotFoundException); }); it('应该正确统计在线用户数', async () => { const allOnlineUsers = mockUsers.map(user => ({ ...user, status: SessionUserStatus.ONLINE })); mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(allOnlineUsers); mockLocationBroadcastCore.getSessionPositions.mockResolvedValue(mockPositions); const result = await service.getSessionDetail('session123'); expect(result.onlineCount).toBe(2); }); }); describe('querySessions', () => { it('应该返回空的会话列表(当前实现)', async () => { const query: SessionQueryRequest = { status: SessionStatus.ACTIVE, minUsers: 1, maxUsers: 100, offset: 0, limit: 10, }; const result = await service.querySessions(query); expect(result.sessions).toEqual([]); expect(result.total).toBe(0); expect(result.page).toBe(1); expect(result.pageSize).toBe(10); }); it('应该正确计算分页信息', async () => { const query: SessionQueryRequest = { offset: 20, limit: 5, }; const result = await service.querySessions(query); expect(result.page).toBe(5); // (20 / 5) + 1 expect(result.pageSize).toBe(5); }); }); describe('updateSessionConfig', () => { it('应该成功更新会话配置', async () => { const mockUsers: SessionUser[] = [ { userId: 'user1', socketId: 'socket1', joinedAt: Date.now(), lastSeen: Date.now(), status: SessionUserStatus.ONLINE, metadata: {}, }, ]; mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(mockUsers); mockLocationBroadcastCore.getSessionPositions.mockResolvedValue([]); const newConfig = { maxUsers: 150, allowObservers: false, broadcastRange: 1500, }; const result = await service.updateSessionConfig('session123', newConfig, 'user456'); expect(result).toBeDefined(); expect(result.sessionId).toBe('session123'); }); it('应该验证配置参数', async () => { const invalidConfigs = [ { maxUsers: 0 }, { maxUsers: 1001 }, { broadcastRange: -1 }, { broadcastRange: 10001 }, { autoCleanupMinutes: 0 }, { autoCleanupMinutes: 1441 }, ]; for (const config of invalidConfigs) { await expect(service.updateSessionConfig('session123', config, 'user456')).rejects.toThrow(BadRequestException); } }); }); describe('endSession', () => { it('应该成功结束会话', async () => { const mockUsers: SessionUser[] = [ { userId: 'user1', socketId: 'socket1', joinedAt: Date.now(), lastSeen: Date.now(), status: SessionUserStatus.ONLINE, metadata: {}, }, { userId: 'user2', socketId: 'socket2', joinedAt: Date.now(), lastSeen: Date.now(), status: SessionUserStatus.ONLINE, metadata: {}, }, ]; mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(mockUsers); mockLocationBroadcastCore.removeUserFromSession.mockResolvedValue(undefined); mockLocationBroadcastCore.cleanupEmptySession.mockResolvedValue(undefined); const result = await service.endSession('session123', 'user456', 'manual_end'); expect(result).toBe(true); expect(mockLocationBroadcastCore.removeUserFromSession).toHaveBeenCalledTimes(2); expect(mockLocationBroadcastCore.cleanupEmptySession).toHaveBeenCalledWith('session123'); }); it('应该在移除用户失败时继续处理其他用户', async () => { const mockUsers: SessionUser[] = [ { userId: 'user1', socketId: 'socket1', joinedAt: Date.now(), lastSeen: Date.now(), status: SessionUserStatus.ONLINE, metadata: {} }, { userId: 'user2', socketId: 'socket2', joinedAt: Date.now(), lastSeen: Date.now(), status: SessionUserStatus.ONLINE, metadata: {} }, ]; mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(mockUsers); mockLocationBroadcastCore.removeUserFromSession .mockResolvedValueOnce(undefined) // 第一个用户成功 .mockRejectedValueOnce(new Error('移除失败')); // 第二个用户失败 mockLocationBroadcastCore.cleanupEmptySession.mockResolvedValue(undefined); const result = await service.endSession('session123', 'user456'); expect(result).toBe(true); expect(mockLocationBroadcastCore.removeUserFromSession).toHaveBeenCalledTimes(2); expect(mockLocationBroadcastCore.cleanupEmptySession).toHaveBeenCalled(); }); }); describe('validateSessionPassword', () => { it('应该返回验证成功(当前实现)', async () => { const result = await service.validateSessionPassword('session123', 'password'); expect(result).toBe(true); }); it('应该处理验证失败的情况', async () => { // 当前实现总是返回true,这里测试异常处理 const result = await service.validateSessionPassword('session123', ''); expect(result).toBe(true); }); }); describe('canUserJoinSession', () => { it('应该允许用户加入活跃会话', async () => { const mockUsers: SessionUser[] = [ { userId: 'user1', socketId: 'socket1', joinedAt: Date.now(), lastSeen: Date.now(), status: SessionUserStatus.ONLINE, metadata: {}, }, ]; mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(mockUsers); mockLocationBroadcastCore.getSessionPositions.mockResolvedValue([]); const result = await service.canUserJoinSession('session123', 'user2'); expect(result.canJoin).toBe(true); }); it('应该拒绝用户加入已满的会话', async () => { // 创建一个满员的用户列表(假设最大用户数为100) const mockUsers: SessionUser[] = Array.from({ length: 100 }, (_, i) => ({ userId: `user${i}`, socketId: `socket${i}`, joinedAt: Date.now(), lastSeen: Date.now(), status: SessionUserStatus.ONLINE, metadata: {}, })); mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(mockUsers); mockLocationBroadcastCore.getSessionPositions.mockResolvedValue([]); const result = await service.canUserJoinSession('session123', 'newuser'); expect(result.canJoin).toBe(false); expect(result.reason).toBe('会话已满'); }); it('应该拒绝已在会话中的用户重复加入', async () => { const mockUsers: SessionUser[] = [ { userId: 'user1', socketId: 'socket1', joinedAt: Date.now(), lastSeen: Date.now(), status: SessionUserStatus.ONLINE, metadata: {}, }, ]; mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(mockUsers); mockLocationBroadcastCore.getSessionPositions.mockResolvedValue([]); const result = await service.canUserJoinSession('session123', 'user1'); expect(result.canJoin).toBe(false); expect(result.reason).toBe('用户已在会话中'); }); it('应该在检查失败时返回拒绝', async () => { mockLocationBroadcastCore.getSessionUsers.mockRejectedValue(new Error('检查失败')); const result = await service.canUserJoinSession('session123', 'user1'); expect(result.canJoin).toBe(false); expect(result.reason).toBe('权限检查失败'); }); }); describe('私有方法测试', () => { describe('validateCreateSessionRequest', () => { it('应该验证会话ID长度', async () => { const longSessionId = 'a'.repeat(101); const request: CreateSessionRequest = { sessionId: longSessionId, creatorId: 'user123', }; mockLocationBroadcastCore.getSessionUsers.mockResolvedValue([]); await expect(service.createSession(request)).rejects.toThrow(BadRequestException); }); }); describe('validateSessionOperatorPermission', () => { it('应该通过权限验证(当前实现)', async () => { // 当前实现不进行实际的权限验证,这里测试不抛出异常 const mockUsers: SessionUser[] = [ { userId: 'user456', socketId: 'socket123', joinedAt: Date.now(), lastSeen: Date.now(), status: SessionUserStatus.ONLINE, metadata: {} } ]; mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(mockUsers); mockLocationBroadcastCore.getSessionPositions.mockResolvedValue([]); await expect(service.updateSessionConfig('session123', {}, 'user456')).resolves.toBeDefined(); }); }); }); });