* **新增 Zulip 模块**:包含完整的集成服务,涵盖客户端池(client pool)、会话管理及事件处理。 * **新增 WebSocket 网关**:用于处理 Zulip 的实时事件监听与双向通信。 * **新增安全服务**:支持 API 密钥加密存储及凭据的安全管理。 * **新增配置管理服务**:支持配置热加载(hot-reload),实现动态配置更新。 * **新增错误处理与监控服务**:提升系统的可靠性与可观测性。 * **新增消息过滤服务**:用于内容校验及速率限制(流控)。 * **新增流初始化与会话清理服务**:优化资源管理与回收。 * **完善测试覆盖**:包含单元测试及端到端(e2e)集成测试。 * **完善详细文档**:包括 API 参考手册、配置指南及集成概述。 * **新增地图配置系统**:实现游戏地点与 Zulip Stream(频道)及 Topic(话题)的逻辑映射。 * **新增环境变量配置**:涵盖 Zulip 服务器地址、身份验证及监控相关设置。 * **更新 App 模块**:注册并启用新的 Zulip 集成模块。 * **更新 Redis 接口**:以支持增强型的会话管理功能。 * **实现 WebSocket 协议支持**:确保与 Zulip 之间的实时双向通信。
615 lines
21 KiB
TypeScript
615 lines
21 KiB
TypeScript
/**
|
||
* 会话管理服务测试
|
||
*
|
||
* 功能描述:
|
||
* - 测试SessionManagerService的核心功能
|
||
* - 包含属性测试验证会话状态一致性
|
||
*
|
||
* @author 开发团队
|
||
* @version 1.0.0
|
||
* @since 2025-12-25
|
||
*/
|
||
|
||
import { Test, TestingModule } from '@nestjs/testing';
|
||
import * as fc from 'fast-check';
|
||
import { SessionManagerService, GameSession, Position } from './session-manager.service';
|
||
import { ConfigManagerService } from './config-manager.service';
|
||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||
import { IRedisService } from '../../../core/redis/redis.interface';
|
||
|
||
describe('SessionManagerService', () => {
|
||
let service: SessionManagerService;
|
||
let mockLogger: jest.Mocked<AppLoggerService>;
|
||
let mockRedisService: jest.Mocked<IRedisService>;
|
||
let mockConfigManager: jest.Mocked<ConfigManagerService>;
|
||
|
||
// 内存存储模拟Redis
|
||
let memoryStore: Map<string, { value: string; expireAt?: number }>;
|
||
let memorySets: Map<string, Set<string>>;
|
||
|
||
beforeEach(async () => {
|
||
jest.clearAllMocks();
|
||
|
||
// 初始化内存存储
|
||
memoryStore = new Map();
|
||
memorySets = new Map();
|
||
|
||
mockLogger = {
|
||
info: jest.fn(),
|
||
warn: jest.fn(),
|
||
error: jest.fn(),
|
||
debug: jest.fn(),
|
||
} as any;
|
||
|
||
mockConfigManager = {
|
||
getStreamByMap: jest.fn().mockImplementation((mapId: string) => {
|
||
const streamMap: Record<string, string> = {
|
||
'whale_port': 'Whale Port',
|
||
'pumpkin_valley': 'Pumpkin Valley',
|
||
'offer_city': 'Offer City',
|
||
'model_factory': 'Model Factory',
|
||
'kernel_island': 'Kernel Island',
|
||
'moyu_beach': 'Moyu Beach',
|
||
'ladder_peak': 'Ladder Peak',
|
||
'galaxy_bay': 'Galaxy Bay',
|
||
'data_ruins': 'Data Ruins',
|
||
'novice_village': 'Novice Village',
|
||
};
|
||
return streamMap[mapId] || 'General';
|
||
}),
|
||
getTopicByObject: jest.fn().mockReturnValue('General'),
|
||
getMapConfig: jest.fn(),
|
||
getAllMaps: jest.fn(),
|
||
} as any;
|
||
|
||
// 创建模拟Redis服务,使用内存存储
|
||
mockRedisService = {
|
||
set: jest.fn().mockImplementation(async (key: string, value: string, ttl?: number) => {
|
||
memoryStore.set(key, {
|
||
value,
|
||
expireAt: ttl ? Date.now() + ttl * 1000 : undefined
|
||
});
|
||
}),
|
||
setex: jest.fn().mockImplementation(async (key: string, ttl: number, value: string) => {
|
||
memoryStore.set(key, {
|
||
value,
|
||
expireAt: Date.now() + ttl * 1000
|
||
});
|
||
}),
|
||
get: jest.fn().mockImplementation(async (key: string) => {
|
||
const item = memoryStore.get(key);
|
||
if (!item) return null;
|
||
if (item.expireAt && item.expireAt <= Date.now()) {
|
||
memoryStore.delete(key);
|
||
return null;
|
||
}
|
||
return item.value;
|
||
}),
|
||
del: jest.fn().mockImplementation(async (key: string) => {
|
||
const existed = memoryStore.has(key);
|
||
memoryStore.delete(key);
|
||
return existed;
|
||
}),
|
||
exists: jest.fn().mockImplementation(async (key: string) => {
|
||
return memoryStore.has(key);
|
||
}),
|
||
expire: jest.fn().mockImplementation(async (key: string, ttl: number) => {
|
||
const item = memoryStore.get(key);
|
||
if (item) {
|
||
item.expireAt = Date.now() + ttl * 1000;
|
||
}
|
||
}),
|
||
ttl: jest.fn().mockResolvedValue(3600),
|
||
incr: jest.fn().mockResolvedValue(1),
|
||
sadd: jest.fn().mockImplementation(async (key: string, member: string) => {
|
||
if (!memorySets.has(key)) {
|
||
memorySets.set(key, new Set());
|
||
}
|
||
memorySets.get(key)!.add(member);
|
||
}),
|
||
srem: jest.fn().mockImplementation(async (key: string, member: string) => {
|
||
const set = memorySets.get(key);
|
||
if (set) {
|
||
set.delete(member);
|
||
}
|
||
}),
|
||
smembers: jest.fn().mockImplementation(async (key: string) => {
|
||
const set = memorySets.get(key);
|
||
return set ? Array.from(set) : [];
|
||
}),
|
||
flushall: jest.fn().mockImplementation(async () => {
|
||
memoryStore.clear();
|
||
memorySets.clear();
|
||
}),
|
||
} as any;
|
||
|
||
const module: TestingModule = await Test.createTestingModule({
|
||
providers: [
|
||
SessionManagerService,
|
||
{
|
||
provide: AppLoggerService,
|
||
useValue: mockLogger,
|
||
},
|
||
{
|
||
provide: 'REDIS_SERVICE',
|
||
useValue: mockRedisService,
|
||
},
|
||
{
|
||
provide: ConfigManagerService,
|
||
useValue: mockConfigManager,
|
||
},
|
||
],
|
||
}).compile();
|
||
|
||
service = module.get<SessionManagerService>(SessionManagerService);
|
||
});
|
||
|
||
afterEach(async () => {
|
||
// 清理内存存储
|
||
memoryStore.clear();
|
||
memorySets.clear();
|
||
});
|
||
|
||
it('should be defined', () => {
|
||
expect(service).toBeDefined();
|
||
});
|
||
|
||
describe('createSession - 创建会话', () => {
|
||
it('应该成功创建新会话', async () => {
|
||
const session = await service.createSession(
|
||
'socket-123',
|
||
'user-456',
|
||
'queue-789',
|
||
'TestUser',
|
||
);
|
||
|
||
expect(session).toBeDefined();
|
||
expect(session.socketId).toBe('socket-123');
|
||
expect(session.userId).toBe('user-456');
|
||
expect(session.zulipQueueId).toBe('queue-789');
|
||
expect(session.username).toBe('TestUser');
|
||
expect(session.currentMap).toBe('novice_village');
|
||
});
|
||
|
||
it('应该在socketId为空时抛出错误', async () => {
|
||
await expect(service.createSession('', 'user-456', 'queue-789'))
|
||
.rejects.toThrow('socketId不能为空');
|
||
});
|
||
|
||
it('应该在userId为空时抛出错误', async () => {
|
||
await expect(service.createSession('socket-123', '', 'queue-789'))
|
||
.rejects.toThrow('userId不能为空');
|
||
});
|
||
|
||
it('应该在zulipQueueId为空时抛出错误', async () => {
|
||
await expect(service.createSession('socket-123', 'user-456', ''))
|
||
.rejects.toThrow('zulipQueueId不能为空');
|
||
});
|
||
|
||
it('应该清理用户已有的旧会话', async () => {
|
||
// 创建第一个会话
|
||
await service.createSession('socket-old', 'user-456', 'queue-old');
|
||
|
||
// 创建第二个会话(同一用户)
|
||
const newSession = await service.createSession('socket-new', 'user-456', 'queue-new');
|
||
|
||
expect(newSession.socketId).toBe('socket-new');
|
||
|
||
// 旧会话应该被清理
|
||
const oldSession = await service.getSession('socket-old');
|
||
expect(oldSession).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('getSession - 获取会话', () => {
|
||
it('应该返回已存在的会话', async () => {
|
||
await service.createSession('socket-123', 'user-456', 'queue-789');
|
||
|
||
const session = await service.getSession('socket-123');
|
||
|
||
expect(session).toBeDefined();
|
||
expect(session?.socketId).toBe('socket-123');
|
||
});
|
||
|
||
it('应该在会话不存在时返回null', async () => {
|
||
const session = await service.getSession('nonexistent');
|
||
|
||
expect(session).toBeNull();
|
||
});
|
||
|
||
it('应该在socketId为空时返回null', async () => {
|
||
const session = await service.getSession('');
|
||
|
||
expect(session).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('getSessionByUserId - 根据用户ID获取会话', () => {
|
||
it('应该返回用户的会话', async () => {
|
||
await service.createSession('socket-123', 'user-456', 'queue-789');
|
||
|
||
const session = await service.getSessionByUserId('user-456');
|
||
|
||
expect(session).toBeDefined();
|
||
expect(session?.userId).toBe('user-456');
|
||
});
|
||
|
||
it('应该在用户没有会话时返回null', async () => {
|
||
const session = await service.getSessionByUserId('nonexistent');
|
||
|
||
expect(session).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('updatePlayerPosition - 更新玩家位置', () => {
|
||
it('应该成功更新位置', async () => {
|
||
await service.createSession('socket-123', 'user-456', 'queue-789');
|
||
|
||
const result = await service.updatePlayerPosition('socket-123', 'novice_village', 100, 200);
|
||
|
||
expect(result).toBe(true);
|
||
|
||
const session = await service.getSession('socket-123');
|
||
expect(session?.position).toEqual({ x: 100, y: 200 });
|
||
});
|
||
|
||
it('应该在切换地图时更新地图玩家列表', async () => {
|
||
await service.createSession('socket-123', 'user-456', 'queue-789');
|
||
|
||
const result = await service.updatePlayerPosition('socket-123', 'tavern', 150, 250);
|
||
|
||
expect(result).toBe(true);
|
||
|
||
const session = await service.getSession('socket-123');
|
||
expect(session?.currentMap).toBe('tavern');
|
||
|
||
// 验证地图玩家列表更新
|
||
const tavernPlayers = await service.getSocketsInMap('tavern');
|
||
expect(tavernPlayers).toContain('socket-123');
|
||
|
||
const villagePlayers = await service.getSocketsInMap('novice_village');
|
||
expect(villagePlayers).not.toContain('socket-123');
|
||
});
|
||
|
||
it('应该在会话不存在时返回false', async () => {
|
||
const result = await service.updatePlayerPosition('nonexistent', 'tavern', 100, 200);
|
||
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('destroySession - 销毁会话', () => {
|
||
it('应该成功销毁会话', async () => {
|
||
await service.createSession('socket-123', 'user-456', 'queue-789');
|
||
|
||
const result = await service.destroySession('socket-123');
|
||
|
||
expect(result).toBe(true);
|
||
|
||
const session = await service.getSession('socket-123');
|
||
expect(session).toBeNull();
|
||
});
|
||
|
||
it('应该在会话不存在时返回true', async () => {
|
||
const result = await service.destroySession('nonexistent');
|
||
|
||
expect(result).toBe(true);
|
||
});
|
||
|
||
it('应该清理用户会话映射', async () => {
|
||
await service.createSession('socket-123', 'user-456', 'queue-789');
|
||
await service.destroySession('socket-123');
|
||
|
||
const session = await service.getSessionByUserId('user-456');
|
||
expect(session).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('getSocketsInMap - 获取地图玩家列表', () => {
|
||
it('应该返回地图中的所有玩家', async () => {
|
||
await service.createSession('socket-1', 'user-1', 'queue-1');
|
||
await service.createSession('socket-2', 'user-2', 'queue-2');
|
||
|
||
const sockets = await service.getSocketsInMap('novice_village');
|
||
|
||
expect(sockets).toHaveLength(2);
|
||
expect(sockets).toContain('socket-1');
|
||
expect(sockets).toContain('socket-2');
|
||
});
|
||
|
||
it('应该在地图为空时返回空数组', async () => {
|
||
const sockets = await service.getSocketsInMap('empty_map');
|
||
|
||
expect(sockets).toHaveLength(0);
|
||
});
|
||
});
|
||
|
||
describe('injectContext - 上下文注入', () => {
|
||
it('应该返回正确的Stream', async () => {
|
||
await service.createSession('socket-123', 'user-456', 'queue-789');
|
||
|
||
const context = await service.injectContext('socket-123');
|
||
|
||
expect(context.stream).toBe('Novice Village');
|
||
});
|
||
|
||
it('应该在会话不存在时返回默认上下文', async () => {
|
||
const context = await service.injectContext('nonexistent');
|
||
|
||
expect(context.stream).toBe('General');
|
||
});
|
||
});
|
||
|
||
|
||
/**
|
||
* 属性测试: 会话状态一致性
|
||
*
|
||
* **Feature: zulip-integration, Property 6: 会话状态一致性**
|
||
* **Validates: Requirements 6.1, 6.2, 6.3, 6.5**
|
||
*
|
||
* 对于任何玩家会话,系统应该在Redis中正确维护WebSocket ID与Zulip队列ID的映射关系,
|
||
* 及时更新位置信息,并支持服务重启后的状态恢复
|
||
*/
|
||
describe('Property 6: 会话状态一致性', () => {
|
||
/**
|
||
* 属性: 对于任何有效的会话参数,创建会话后应该能够正确获取
|
||
* 验证需求 6.1: 玩家登录成功后系统应在Redis中存储WebSocket ID与Zulip队列ID的映射关系
|
||
*/
|
||
it('对于任何有效的会话参数,创建会话后应该能够正确获取', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
// 生成有效的socketId
|
||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||
// 生成有效的userId
|
||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||
// 生成有效的zulipQueueId
|
||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||
// 生成有效的username
|
||
fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
|
||
async (socketId, userId, zulipQueueId, username) => {
|
||
// 清理之前的数据
|
||
memoryStore.clear();
|
||
memorySets.clear();
|
||
|
||
// 创建会话
|
||
const createdSession = await service.createSession(
|
||
socketId.trim(),
|
||
userId.trim(),
|
||
zulipQueueId.trim(),
|
||
username.trim(),
|
||
);
|
||
|
||
// 验证创建的会话
|
||
expect(createdSession.socketId).toBe(socketId.trim());
|
||
expect(createdSession.userId).toBe(userId.trim());
|
||
expect(createdSession.zulipQueueId).toBe(zulipQueueId.trim());
|
||
expect(createdSession.username).toBe(username.trim());
|
||
|
||
// 获取会话并验证一致性
|
||
const retrievedSession = await service.getSession(socketId.trim());
|
||
expect(retrievedSession).not.toBeNull();
|
||
expect(retrievedSession?.socketId).toBe(createdSession.socketId);
|
||
expect(retrievedSession?.userId).toBe(createdSession.userId);
|
||
expect(retrievedSession?.zulipQueueId).toBe(createdSession.zulipQueueId);
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
);
|
||
}, 60000);
|
||
|
||
/**
|
||
* 属性: 对于任何位置更新,会话应该正确反映新位置
|
||
* 验证需求 6.2: 玩家切换地图时系统应更新玩家的当前位置信息
|
||
*/
|
||
it('对于任何位置更新,会话应该正确反映新位置', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
// 生成有效的socketId
|
||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||
// 生成有效的userId
|
||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||
// 生成有效的地图ID
|
||
fc.constantFrom('novice_village', 'tavern', 'market'),
|
||
// 生成有效的坐标
|
||
fc.integer({ min: 0, max: 1000 }),
|
||
fc.integer({ min: 0, max: 1000 }),
|
||
async (socketId, userId, mapId, x, y) => {
|
||
// 清理之前的数据
|
||
memoryStore.clear();
|
||
memorySets.clear();
|
||
|
||
// 创建会话
|
||
await service.createSession(
|
||
socketId.trim(),
|
||
userId.trim(),
|
||
'queue-test',
|
||
);
|
||
|
||
// 更新位置
|
||
const updateResult = await service.updatePlayerPosition(
|
||
socketId.trim(),
|
||
mapId,
|
||
x,
|
||
y,
|
||
);
|
||
|
||
expect(updateResult).toBe(true);
|
||
|
||
// 验证位置更新
|
||
const session = await service.getSession(socketId.trim());
|
||
expect(session).not.toBeNull();
|
||
expect(session?.currentMap).toBe(mapId);
|
||
expect(session?.position.x).toBe(x);
|
||
expect(session?.position.y).toBe(y);
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
);
|
||
}, 60000);
|
||
|
||
/**
|
||
* 属性: 对于任何地图切换,玩家应该从旧地图移除并添加到新地图
|
||
* 验证需求 6.3: 查询在线玩家时系统应从Redis中获取当前活跃的会话列表
|
||
*/
|
||
it('对于任何地图切换,玩家应该从旧地图移除并添加到新地图', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
// 生成有效的socketId
|
||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||
// 生成有效的userId
|
||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||
// 生成初始地图和目标地图(确保不同)
|
||
fc.constantFrom('novice_village', 'tavern', 'market'),
|
||
fc.constantFrom('novice_village', 'tavern', 'market'),
|
||
async (socketId, userId, initialMap, targetMap) => {
|
||
// 清理之前的数据
|
||
memoryStore.clear();
|
||
memorySets.clear();
|
||
|
||
// 创建会话(使用初始地图)
|
||
await service.createSession(
|
||
socketId.trim(),
|
||
userId.trim(),
|
||
'queue-test',
|
||
'TestUser',
|
||
initialMap,
|
||
);
|
||
|
||
// 验证初始地图包含玩家
|
||
const initialPlayers = await service.getSocketsInMap(initialMap);
|
||
expect(initialPlayers).toContain(socketId.trim());
|
||
|
||
// 如果目标地图不同,切换地图
|
||
if (initialMap !== targetMap) {
|
||
await service.updatePlayerPosition(socketId.trim(), targetMap, 100, 100);
|
||
|
||
// 验证旧地图不再包含玩家
|
||
const oldMapPlayers = await service.getSocketsInMap(initialMap);
|
||
expect(oldMapPlayers).not.toContain(socketId.trim());
|
||
|
||
// 验证新地图包含玩家
|
||
const newMapPlayers = await service.getSocketsInMap(targetMap);
|
||
expect(newMapPlayers).toContain(socketId.trim());
|
||
}
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
);
|
||
}, 60000);
|
||
|
||
/**
|
||
* 属性: 对于任何会话销毁,所有相关数据应该被清理
|
||
* 验证需求 6.5: 服务器重启时系统应能够从Redis中恢复会话状态(通过验证销毁后数据被正确清理)
|
||
*/
|
||
it('对于任何会话销毁,所有相关数据应该被清理', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
// 生成有效的socketId
|
||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||
// 生成有效的userId
|
||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||
// 生成有效的地图ID
|
||
fc.constantFrom('novice_village', 'tavern', 'market'),
|
||
async (socketId, userId, mapId) => {
|
||
// 清理之前的数据
|
||
memoryStore.clear();
|
||
memorySets.clear();
|
||
|
||
// 创建会话
|
||
await service.createSession(
|
||
socketId.trim(),
|
||
userId.trim(),
|
||
'queue-test',
|
||
'TestUser',
|
||
mapId,
|
||
);
|
||
|
||
// 验证会话存在
|
||
const sessionBefore = await service.getSession(socketId.trim());
|
||
expect(sessionBefore).not.toBeNull();
|
||
|
||
// 销毁会话
|
||
const destroyResult = await service.destroySession(socketId.trim());
|
||
expect(destroyResult).toBe(true);
|
||
|
||
// 验证会话被清理
|
||
const sessionAfter = await service.getSession(socketId.trim());
|
||
expect(sessionAfter).toBeNull();
|
||
|
||
// 验证用户会话映射被清理
|
||
const userSession = await service.getSessionByUserId(userId.trim());
|
||
expect(userSession).toBeNull();
|
||
|
||
// 验证地图玩家列表被清理
|
||
const mapPlayers = await service.getSocketsInMap(mapId);
|
||
expect(mapPlayers).not.toContain(socketId.trim());
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
);
|
||
}, 60000);
|
||
|
||
/**
|
||
* 属性: 创建-更新-销毁的完整生命周期应该正确管理会话状态
|
||
*/
|
||
it('创建-更新-销毁的完整生命周期应该正确管理会话状态', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
// 生成有效的socketId
|
||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||
// 生成有效的userId
|
||
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||
// 生成位置更新序列
|
||
fc.array(
|
||
fc.record({
|
||
mapId: fc.constantFrom('novice_village', 'tavern', 'market'),
|
||
x: fc.integer({ min: 0, max: 1000 }),
|
||
y: fc.integer({ min: 0, max: 1000 }),
|
||
}),
|
||
{ minLength: 1, maxLength: 5 }
|
||
),
|
||
async (socketId, userId, positionUpdates) => {
|
||
// 清理之前的数据
|
||
memoryStore.clear();
|
||
memorySets.clear();
|
||
|
||
// 1. 创建会话
|
||
const session = await service.createSession(
|
||
socketId.trim(),
|
||
userId.trim(),
|
||
'queue-test',
|
||
);
|
||
expect(session).toBeDefined();
|
||
|
||
// 2. 执行位置更新序列
|
||
for (const update of positionUpdates) {
|
||
const result = await service.updatePlayerPosition(
|
||
socketId.trim(),
|
||
update.mapId,
|
||
update.x,
|
||
update.y,
|
||
);
|
||
expect(result).toBe(true);
|
||
|
||
// 验证每次更新后的状态
|
||
const currentSession = await service.getSession(socketId.trim());
|
||
expect(currentSession?.currentMap).toBe(update.mapId);
|
||
expect(currentSession?.position.x).toBe(update.x);
|
||
expect(currentSession?.position.y).toBe(update.y);
|
||
}
|
||
|
||
// 3. 销毁会话
|
||
const destroyResult = await service.destroySession(socketId.trim());
|
||
expect(destroyResult).toBe(true);
|
||
|
||
// 4. 验证所有数据被清理
|
||
const finalSession = await service.getSession(socketId.trim());
|
||
expect(finalSession).toBeNull();
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
);
|
||
}, 60000);
|
||
});
|
||
});
|