Files
whale-town-end/src/business/location_broadcast/services/location_session.service.spec.ts
moyin c31cbe559d feat:实现位置广播系统
- 添加位置广播核心控制器和服务
- 实现健康检查和位置同步功能
- 添加WebSocket实时位置更新支持
- 完善位置广播的测试覆盖
2026-01-08 23:05:52 +08:00

464 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 会话管理服务单元测试
*
* 功能描述:
* - 测试会话管理服务的核心功能
* - 验证会话创建、查询、配置等业务逻辑
* - 确保权限验证和数据验证的正确性
* - 提供完整的测试覆盖率
*
* 测试范围:
* - 会话创建和配置管理
* - 会话查询和详情获取
* - 权限验证和访问控制
* - 数据验证和错误处理
*
* @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();
});
});
});
});