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