feat:实现位置广播系统

- 添加位置广播核心控制器和服务
- 实现健康检查和位置同步功能
- 添加WebSocket实时位置更新支持
- 完善位置广播的测试覆盖
This commit is contained in:
moyin
2026-01-08 23:05:52 +08:00
parent 6924416bbd
commit c31cbe559d
27 changed files with 12212 additions and 0 deletions

View File

@@ -0,0 +1,464 @@
/**
* 会话管理服务单元测试
*
* 功能描述:
* - 测试会话管理服务的核心功能
* - 验证会话创建、查询、配置等业务逻辑
* - 确保权限验证和数据验证的正确性
* - 提供完整的测试覆盖率
*
* 测试范围:
* - 会话创建和配置管理
* - 会话查询和详情获取
* - 权限验证和访问控制
* - 数据验证和错误处理
*
* @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>(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();
});
});
});
});