forked from xiangwang25/whale-town-end-v2
feat: integrate invitation access, world NPCs, and deployment
This commit is contained in:
863
src/business/chat/services/chat_session.service.spec.ts
Normal file
863
src/business/chat/services/chat_session.service.spec.ts
Normal file
@@ -0,0 +1,863 @@
|
||||
/**
|
||||
* 聊天会话管理服务测试
|
||||
*
|
||||
* 测试范围:
|
||||
* - 会话创建和销毁
|
||||
* - 位置更新和地图切换
|
||||
* - 上下文注入和Stream/Topic映射
|
||||
* - 过期会话清理
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { ChatSessionService } from './chat_session.service';
|
||||
|
||||
describe('ChatSessionService', () => {
|
||||
let service: ChatSessionService;
|
||||
let redisService: any;
|
||||
let configManager: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockRedisService = {
|
||||
set: jest.fn(),
|
||||
get: jest.fn(),
|
||||
setex: jest.fn(),
|
||||
del: jest.fn(),
|
||||
sadd: jest.fn(),
|
||||
srem: jest.fn(),
|
||||
smembers: jest.fn(),
|
||||
expire: jest.fn(),
|
||||
};
|
||||
|
||||
const mockConfigManager = {
|
||||
getStreamByMap: jest.fn(),
|
||||
findNearbyObject: jest.fn(),
|
||||
getAllMapIds: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ChatSessionService,
|
||||
{
|
||||
provide: 'REDIS_SERVICE',
|
||||
useValue: mockRedisService,
|
||||
},
|
||||
{
|
||||
provide: 'ZULIP_CONFIG_SERVICE',
|
||||
useValue: mockConfigManager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ChatSessionService>(ChatSessionService);
|
||||
redisService = module.get('REDIS_SERVICE');
|
||||
configManager = module.get('ZULIP_CONFIG_SERVICE');
|
||||
|
||||
// 禁用日志输出
|
||||
jest.spyOn(Logger.prototype, 'log').mockImplementation();
|
||||
jest.spyOn(Logger.prototype, 'error').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('初始化', () => {
|
||||
it('应该成功创建服务实例', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSession', () => {
|
||||
const socketId = 'socket_123';
|
||||
const userId = 'user_123';
|
||||
const zulipQueueId = 'queue_123';
|
||||
const username = 'testuser';
|
||||
|
||||
beforeEach(() => {
|
||||
redisService.get.mockResolvedValue(null);
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
redisService.sadd.mockResolvedValue(1);
|
||||
redisService.expire.mockResolvedValue(1);
|
||||
});
|
||||
|
||||
it('应该成功创建会话', async () => {
|
||||
const session = await service.createSession(socketId, userId, zulipQueueId, username);
|
||||
|
||||
expect(session).toBeDefined();
|
||||
expect(session.socketId).toBe(socketId);
|
||||
expect(session.userId).toBe(userId);
|
||||
expect(session.username).toBe(username);
|
||||
expect(session.zulipQueueId).toBe(zulipQueueId);
|
||||
expect(redisService.setex).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该使用默认地图和位置', async () => {
|
||||
const session = await service.createSession(socketId, userId, zulipQueueId);
|
||||
|
||||
expect(session.currentMap).toBe('novice_village');
|
||||
expect(session.position).toEqual({ x: 400, y: 300 });
|
||||
});
|
||||
|
||||
it('应该使用提供的初始地图和位置', async () => {
|
||||
const initialMap = 'whale_port';
|
||||
const initialPosition = { x: 500, y: 400 };
|
||||
|
||||
const session = await service.createSession(
|
||||
socketId,
|
||||
userId,
|
||||
zulipQueueId,
|
||||
username,
|
||||
initialMap,
|
||||
initialPosition
|
||||
);
|
||||
|
||||
expect(session.currentMap).toBe(initialMap);
|
||||
expect(session.position).toEqual(initialPosition);
|
||||
});
|
||||
|
||||
it('应该保存初始外观到在线会话', async () => {
|
||||
const appearance = {
|
||||
skinId: 'girl_sailor_turnaround_v2_8x4',
|
||||
avatarId: 'default',
|
||||
};
|
||||
|
||||
const session = await service.createSession(
|
||||
socketId,
|
||||
userId,
|
||||
zulipQueueId,
|
||||
username,
|
||||
'whale_port',
|
||||
{ x: 500, y: 400 },
|
||||
appearance
|
||||
);
|
||||
|
||||
expect(session.appearance).toEqual(appearance);
|
||||
});
|
||||
|
||||
it('应该拒绝空socketId', async () => {
|
||||
await expect(service.createSession('', userId, zulipQueueId)).rejects.toThrow('参数不能为空');
|
||||
});
|
||||
|
||||
it('应该拒绝空userId', async () => {
|
||||
await expect(service.createSession(socketId, '', zulipQueueId)).rejects.toThrow('参数不能为空');
|
||||
});
|
||||
|
||||
it('应该拒绝空zulipQueueId', async () => {
|
||||
await expect(service.createSession(socketId, userId, '')).rejects.toThrow('参数不能为空');
|
||||
});
|
||||
|
||||
it('应该清理旧会话', async () => {
|
||||
const oldSocketId = 'old_socket_123';
|
||||
redisService.get.mockResolvedValueOnce(oldSocketId);
|
||||
redisService.get.mockResolvedValueOnce(JSON.stringify({
|
||||
socketId: oldSocketId,
|
||||
userId,
|
||||
username,
|
||||
zulipQueueId,
|
||||
currentMap: 'novice_village',
|
||||
position: { x: 400, y: 300 },
|
||||
lastActivity: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
await service.createSession(socketId, userId, zulipQueueId, username);
|
||||
|
||||
expect(redisService.del).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该添加到地图玩家列表', async () => {
|
||||
await service.createSession(socketId, userId, zulipQueueId, username);
|
||||
|
||||
expect(redisService.sadd).toHaveBeenCalledWith(
|
||||
expect.stringContaining('chat:map_players:'),
|
||||
socketId
|
||||
);
|
||||
});
|
||||
|
||||
it('应该生成默认用户名', async () => {
|
||||
const session = await service.createSession(socketId, userId, zulipQueueId);
|
||||
|
||||
expect(session.username).toBe(`user_${userId}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSession', () => {
|
||||
const socketId = 'socket_123';
|
||||
const mockSessionData = {
|
||||
socketId,
|
||||
userId: 'user_123',
|
||||
username: 'testuser',
|
||||
zulipQueueId: 'queue_123',
|
||||
currentMap: 'whale_port',
|
||||
position: { x: 400, y: 300 },
|
||||
lastActivity: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
it('应该返回会话信息', async () => {
|
||||
redisService.get.mockResolvedValue(JSON.stringify(mockSessionData));
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
|
||||
const session = await service.getSession(socketId);
|
||||
|
||||
expect(session).toBeDefined();
|
||||
expect(session?.socketId).toBe(socketId);
|
||||
expect(session?.userId).toBe(mockSessionData.userId);
|
||||
});
|
||||
|
||||
it('应该更新最后活动时间', async () => {
|
||||
redisService.get.mockResolvedValue(JSON.stringify(mockSessionData));
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
|
||||
await service.getSession(socketId);
|
||||
|
||||
expect(redisService.setex).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该处理会话不存在', async () => {
|
||||
redisService.get.mockResolvedValue(null);
|
||||
|
||||
const session = await service.getSession(socketId);
|
||||
|
||||
expect(session).toBeNull();
|
||||
});
|
||||
|
||||
it('应该拒绝空socketId', async () => {
|
||||
const session = await service.getSession('');
|
||||
|
||||
expect(session).toBeNull();
|
||||
});
|
||||
|
||||
it('应该处理Redis错误', async () => {
|
||||
redisService.get.mockRejectedValue(new Error('Redis error'));
|
||||
|
||||
const session = await service.getSession(socketId);
|
||||
|
||||
expect(session).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSocketIdByUserId', () => {
|
||||
const socketId = 'socket_123';
|
||||
const userId = 'user_123';
|
||||
const mockSessionData = {
|
||||
socketId,
|
||||
userId,
|
||||
username: 'testuser',
|
||||
zulipQueueId: 'queue_123',
|
||||
currentMap: 'whale_port',
|
||||
position: { x: 400, y: 300 },
|
||||
lastActivity: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
it('应该返回在线用户的Socket ID', async () => {
|
||||
redisService.get
|
||||
.mockResolvedValueOnce(socketId)
|
||||
.mockResolvedValueOnce(JSON.stringify(mockSessionData));
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
|
||||
const result = await service.getSocketIdByUserId(userId);
|
||||
|
||||
expect(result).toBe(socketId);
|
||||
expect(redisService.get).toHaveBeenCalledWith(expect.stringContaining(`chat:user_session:${userId}`));
|
||||
});
|
||||
|
||||
it('应该在用户没有在线映射时返回null', async () => {
|
||||
redisService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await service.getSocketIdByUserId(userId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('应该清理失效的用户会话映射', async () => {
|
||||
redisService.get
|
||||
.mockResolvedValueOnce(socketId)
|
||||
.mockResolvedValueOnce(null);
|
||||
|
||||
const result = await service.getSocketIdByUserId(userId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(redisService.del).toHaveBeenCalledWith(expect.stringContaining(`chat:user_session:${userId}`));
|
||||
});
|
||||
|
||||
it('应该拒绝空userId', async () => {
|
||||
const result = await service.getSocketIdByUserId('');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(redisService.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('friends', () => {
|
||||
const userId = 'user_123';
|
||||
const friendUserId = 'user_friend';
|
||||
|
||||
it('应该添加好友并返回在线状态', async () => {
|
||||
redisService.get
|
||||
.mockResolvedValueOnce('friend_socket')
|
||||
.mockResolvedValueOnce(JSON.stringify({
|
||||
socketId: 'friend_socket',
|
||||
userId: friendUserId,
|
||||
username: 'friend',
|
||||
zulipQueueId: 'queue_friend',
|
||||
currentMap: 'whale_port',
|
||||
position: { x: 100, y: 100 },
|
||||
lastActivity: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
}));
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
redisService.sadd.mockResolvedValue(1);
|
||||
redisService.set.mockResolvedValue(undefined);
|
||||
|
||||
const friend = await service.addFriend(userId, friendUserId, 'friend');
|
||||
|
||||
expect(friend).toEqual({ userId: friendUserId, username: 'friend', online: true });
|
||||
expect(redisService.sadd).toHaveBeenCalledWith(expect.stringContaining(`chat:friends:${userId}`), friendUserId);
|
||||
expect(redisService.set).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`chat:friend_data:${userId}:${friendUserId}`),
|
||||
expect.stringContaining('"username":"friend"'),
|
||||
);
|
||||
});
|
||||
|
||||
it('应该拒绝添加自己为好友', async () => {
|
||||
await expect(service.addFriend(userId, userId, 'self')).rejects.toThrow('不能添加自己为好友');
|
||||
});
|
||||
|
||||
it('应该创建好友请求并保存到目标用户待处理列表', async () => {
|
||||
redisService.smembers.mockResolvedValue([]);
|
||||
redisService.sadd.mockResolvedValue(1);
|
||||
redisService.expire.mockResolvedValue(1);
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
|
||||
const request = await service.createFriendRequest(userId, 'testuser', friendUserId);
|
||||
|
||||
expect(request.userId).toBe(userId);
|
||||
expect(request.username).toBe('testuser');
|
||||
expect(redisService.sadd).toHaveBeenCalledWith(expect.stringContaining(`chat:friend_requests:${friendUserId}`), userId);
|
||||
expect(redisService.setex).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`chat:friend_request_data:${friendUserId}:${userId}`),
|
||||
expect.any(Number),
|
||||
expect.stringContaining('"username":"testuser"'),
|
||||
);
|
||||
});
|
||||
|
||||
it('应该接受好友请求并建立双向好友关系', async () => {
|
||||
redisService.get
|
||||
.mockResolvedValueOnce(JSON.stringify({
|
||||
userId,
|
||||
username: 'testuser',
|
||||
createdAt: '2026-07-01T00:00:00.000Z',
|
||||
}))
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce(null);
|
||||
redisService.sadd.mockResolvedValue(1);
|
||||
redisService.set.mockResolvedValue(undefined);
|
||||
redisService.srem.mockResolvedValue(1);
|
||||
redisService.del.mockResolvedValue(true);
|
||||
|
||||
const result = await service.acceptFriendRequest(friendUserId, userId, 'friend');
|
||||
|
||||
expect(result.friend).toEqual({ userId, username: 'testuser', online: false });
|
||||
expect(result.reciprocalFriend).toEqual({ userId: friendUserId, username: 'friend', online: false });
|
||||
expect(redisService.sadd).toHaveBeenCalledWith(expect.stringContaining(`chat:friends:${friendUserId}`), userId);
|
||||
expect(redisService.sadd).toHaveBeenCalledWith(expect.stringContaining(`chat:friends:${userId}`), friendUserId);
|
||||
expect(redisService.srem).toHaveBeenCalledWith(expect.stringContaining(`chat:friend_requests:${friendUserId}`), userId);
|
||||
});
|
||||
|
||||
it('应该获取待处理好友请求', async () => {
|
||||
redisService.smembers.mockResolvedValue([userId]);
|
||||
redisService.get.mockResolvedValue(JSON.stringify({
|
||||
userId,
|
||||
username: 'testuser',
|
||||
createdAt: '2026-07-01T00:00:00.000Z',
|
||||
}));
|
||||
|
||||
const requests = await service.getFriendRequests(friendUserId);
|
||||
|
||||
expect(requests).toEqual([
|
||||
{ userId, username: 'testuser', createdAt: '2026-07-01T00:00:00.000Z' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该移除好友', async () => {
|
||||
redisService.srem.mockResolvedValue(1);
|
||||
redisService.del.mockResolvedValue(true);
|
||||
|
||||
await service.removeFriend(userId, friendUserId);
|
||||
|
||||
expect(redisService.srem).toHaveBeenCalledWith(expect.stringContaining(`chat:friends:${userId}`), friendUserId);
|
||||
expect(redisService.del).toHaveBeenCalledWith(expect.stringContaining(`chat:friend_data:${userId}:${friendUserId}`));
|
||||
});
|
||||
|
||||
it('应该按在线状态和用户名返回好友列表', async () => {
|
||||
redisService.smembers.mockResolvedValue(['offline_friend', 'online_friend']);
|
||||
redisService.get
|
||||
.mockResolvedValueOnce(JSON.stringify({ userId: 'offline_friend', username: 'Beta' }))
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce(JSON.stringify({ userId: 'online_friend', username: 'Alpha' }))
|
||||
.mockResolvedValueOnce('online_socket')
|
||||
.mockResolvedValueOnce(JSON.stringify({
|
||||
socketId: 'online_socket',
|
||||
userId: 'online_friend',
|
||||
username: 'Alpha',
|
||||
zulipQueueId: 'queue_online',
|
||||
currentMap: 'whale_port',
|
||||
position: { x: 100, y: 100 },
|
||||
lastActivity: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
}));
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
|
||||
const friends = await service.getFriends(userId);
|
||||
|
||||
expect(friends).toEqual([
|
||||
{ userId: 'online_friend', username: 'Alpha', online: true },
|
||||
{ userId: 'offline_friend', username: 'Beta', online: false },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectContext', () => {
|
||||
const socketId = 'socket_123';
|
||||
const mockSessionData = {
|
||||
socketId,
|
||||
userId: 'user_123',
|
||||
username: 'testuser',
|
||||
zulipQueueId: 'queue_123',
|
||||
currentMap: 'whale_port',
|
||||
position: { x: 400, y: 300 },
|
||||
lastActivity: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
redisService.get.mockResolvedValue(JSON.stringify(mockSessionData));
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
configManager.getStreamByMap.mockReturnValue('Whale Port');
|
||||
configManager.findNearbyObject.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it('应该返回正确的Stream', async () => {
|
||||
const context = await service.injectContext(socketId);
|
||||
|
||||
expect(context.stream).toBe('Whale Port');
|
||||
});
|
||||
|
||||
it('应该使用默认Topic', async () => {
|
||||
const context = await service.injectContext(socketId);
|
||||
|
||||
expect(context.topic).toBe('General');
|
||||
});
|
||||
|
||||
it('应该根据附近对象设置Topic', async () => {
|
||||
configManager.findNearbyObject.mockReturnValue({
|
||||
zulipTopic: 'Tavern',
|
||||
});
|
||||
|
||||
const context = await service.injectContext(socketId);
|
||||
|
||||
expect(context.topic).toBe('Tavern');
|
||||
});
|
||||
|
||||
it('应该支持指定地图ID', async () => {
|
||||
configManager.getStreamByMap.mockReturnValue('Market');
|
||||
|
||||
const context = await service.injectContext(socketId, 'market');
|
||||
|
||||
expect(configManager.getStreamByMap).toHaveBeenCalledWith('market');
|
||||
});
|
||||
|
||||
it('应该处理会话不存在', async () => {
|
||||
redisService.get.mockResolvedValue(null);
|
||||
|
||||
const context = await service.injectContext(socketId);
|
||||
|
||||
expect(context.stream).toBe('General');
|
||||
});
|
||||
|
||||
it('应该处理地图没有对应Stream', async () => {
|
||||
configManager.getStreamByMap.mockReturnValue(null);
|
||||
|
||||
const context = await service.injectContext(socketId);
|
||||
|
||||
expect(context.stream).toBe('General');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSocketsInMap', () => {
|
||||
const mapId = 'whale_port';
|
||||
|
||||
it('应该返回地图中的所有Socket', async () => {
|
||||
const sockets = ['socket_1', 'socket_2', 'socket_3'];
|
||||
redisService.smembers.mockResolvedValue(sockets);
|
||||
|
||||
const result = await service.getSocketsInMap(mapId);
|
||||
|
||||
expect(result).toEqual(sockets);
|
||||
});
|
||||
|
||||
it('应该处理空地图', async () => {
|
||||
redisService.smembers.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getSocketsInMap(mapId);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该处理Redis错误', async () => {
|
||||
redisService.smembers.mockRejectedValue(new Error('Redis error'));
|
||||
|
||||
const result = await service.getSocketsInMap(mapId);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlayersInMap', () => {
|
||||
it('应该只返回同账号的当前会话并清理旧会话', async () => {
|
||||
const mapId = 'whale_port';
|
||||
const activeSocketId = 'socket_active';
|
||||
const staleSocketId = 'socket_stale';
|
||||
const createSessionData = (socketId: string, skinId: string) => JSON.stringify({
|
||||
socketId,
|
||||
userId: 'user_123',
|
||||
username: 'testuser',
|
||||
zulipQueueId: `queue_${socketId}`,
|
||||
currentMap: mapId,
|
||||
position: { x: 100, y: 200 },
|
||||
appearance: { skinId },
|
||||
lastActivity: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
redisService.smembers.mockResolvedValue([staleSocketId, activeSocketId]);
|
||||
redisService.get.mockImplementation(async (key: string) => {
|
||||
if (key === `chat:session:${staleSocketId}`) return createSessionData(staleSocketId, 'old_skin');
|
||||
if (key === `chat:session:${activeSocketId}`) return createSessionData(activeSocketId, 'generated_skin_1');
|
||||
if (key === 'chat:user_session:user_123') return activeSocketId;
|
||||
return null;
|
||||
});
|
||||
|
||||
const players = await service.getPlayersInMap(mapId);
|
||||
|
||||
expect(players).toHaveLength(1);
|
||||
expect(players[0]).toMatchObject({ socketId: activeSocketId, userId: 'user_123' });
|
||||
expect(players[0].appearance?.skinId).toBe('generated_skin_1');
|
||||
expect(redisService.srem).toHaveBeenCalledWith('chat:map_players:whale_port', staleSocketId);
|
||||
expect(redisService.del).toHaveBeenCalledWith(`chat:session:${staleSocketId}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePlayerPosition', () => {
|
||||
const socketId = 'socket_123';
|
||||
const mapId = 'whale_port';
|
||||
const x = 500;
|
||||
const y = 400;
|
||||
const mockSessionData = {
|
||||
socketId,
|
||||
userId: 'user_123',
|
||||
username: 'testuser',
|
||||
zulipQueueId: 'queue_123',
|
||||
currentMap: 'novice_village',
|
||||
position: { x: 400, y: 300 },
|
||||
lastActivity: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
redisService.get.mockResolvedValue(JSON.stringify(mockSessionData));
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
redisService.srem.mockResolvedValue(1);
|
||||
redisService.sadd.mockResolvedValue(1);
|
||||
redisService.expire.mockResolvedValue(1);
|
||||
});
|
||||
|
||||
it('应该成功更新位置', async () => {
|
||||
const result = await service.updatePlayerPosition(socketId, mapId, x, y);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(redisService.setex).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该更新地图玩家列表当切换地图', async () => {
|
||||
await service.updatePlayerPosition(socketId, mapId, x, y);
|
||||
|
||||
expect(redisService.srem).toHaveBeenCalled();
|
||||
expect(redisService.sadd).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该不更新地图玩家列表当在同一地图', async () => {
|
||||
const sameMapData = { ...mockSessionData, currentMap: mapId };
|
||||
redisService.get.mockResolvedValue(JSON.stringify(sameMapData));
|
||||
|
||||
await service.updatePlayerPosition(socketId, mapId, x, y);
|
||||
|
||||
expect(redisService.srem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该拒绝空socketId', async () => {
|
||||
const result = await service.updatePlayerPosition('', mapId, x, y);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('应该拒绝空mapId', async () => {
|
||||
const result = await service.updatePlayerPosition(socketId, '', x, y);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('应该处理会话不存在', async () => {
|
||||
redisService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await service.updatePlayerPosition(socketId, mapId, x, y);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('应该处理Redis错误', async () => {
|
||||
redisService.get.mockRejectedValue(new Error('Redis error'));
|
||||
|
||||
const result = await service.updatePlayerPosition(socketId, mapId, x, y);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroySession', () => {
|
||||
const socketId = 'socket_123';
|
||||
const mockSessionData = {
|
||||
socketId,
|
||||
userId: 'user_123',
|
||||
username: 'testuser',
|
||||
zulipQueueId: 'queue_123',
|
||||
currentMap: 'whale_port',
|
||||
position: { x: 400, y: 300 },
|
||||
lastActivity: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
it('旧连接销毁时不应删除同账号的新会话映射', async () => {
|
||||
redisService.get.mockImplementation(async (key: string) => {
|
||||
if (key === `chat:session:${socketId}`) return JSON.stringify(mockSessionData);
|
||||
if (key === 'chat:user_session:user_123') return 'socket_new';
|
||||
return null;
|
||||
});
|
||||
|
||||
const result = await service.destroySession(socketId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(redisService.del).toHaveBeenCalledWith(`chat:session:${socketId}`);
|
||||
expect(redisService.del).not.toHaveBeenCalledWith('chat:user_session:user_123');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
redisService.get.mockImplementation(async (key: string) => {
|
||||
if (key === `chat:session:${socketId}`) return JSON.stringify(mockSessionData);
|
||||
if (key === 'chat:user_session:user_123') return socketId;
|
||||
return null;
|
||||
});
|
||||
redisService.srem.mockResolvedValue(1);
|
||||
redisService.del.mockResolvedValue(1);
|
||||
});
|
||||
|
||||
it('应该成功销毁会话', async () => {
|
||||
const result = await service.destroySession(socketId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(redisService.del).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('应该从地图玩家列表移除', async () => {
|
||||
await service.destroySession(socketId);
|
||||
|
||||
expect(redisService.srem).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该删除用户会话映射', async () => {
|
||||
await service.destroySession(socketId);
|
||||
|
||||
expect(redisService.del).toHaveBeenCalledWith(
|
||||
expect.stringContaining('chat:user_session:')
|
||||
);
|
||||
});
|
||||
|
||||
it('应该处理会话不存在', async () => {
|
||||
redisService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await service.destroySession(socketId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝空socketId', async () => {
|
||||
const result = await service.destroySession('');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('应该处理Redis错误', async () => {
|
||||
redisService.get.mockRejectedValue(new Error('Redis error'));
|
||||
|
||||
const result = await service.destroySession(socketId);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanupExpiredSessions', () => {
|
||||
beforeEach(() => {
|
||||
configManager.getAllMapIds.mockReturnValue(['novice_village', 'whale_port']);
|
||||
});
|
||||
|
||||
it('应该清理过期会话', async () => {
|
||||
const expiredSession = {
|
||||
socketId: 'socket_123',
|
||||
userId: 'user_123',
|
||||
username: 'testuser',
|
||||
zulipQueueId: 'queue_123',
|
||||
currentMap: 'whale_port',
|
||||
position: { x: 400, y: 300 },
|
||||
lastActivity: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
redisService.smembers.mockResolvedValue(['socket_123']);
|
||||
redisService.get.mockResolvedValueOnce(JSON.stringify(expiredSession));
|
||||
redisService.get.mockResolvedValueOnce(JSON.stringify(expiredSession));
|
||||
redisService.srem.mockResolvedValue(1);
|
||||
redisService.del.mockResolvedValue(1);
|
||||
|
||||
const result = await service.cleanupExpiredSessions(30);
|
||||
|
||||
expect(result.cleanedCount).toBeGreaterThanOrEqual(1);
|
||||
expect(result.zulipQueueIds).toContain('queue_123');
|
||||
});
|
||||
|
||||
it('应该不清理未过期会话', async () => {
|
||||
const activeSession = {
|
||||
socketId: 'socket_123',
|
||||
userId: 'user_123',
|
||||
username: 'testuser',
|
||||
zulipQueueId: 'queue_123',
|
||||
currentMap: 'whale_port',
|
||||
position: { x: 400, y: 300 },
|
||||
lastActivity: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
redisService.smembers.mockResolvedValue(['socket_123']);
|
||||
redisService.get.mockResolvedValue(JSON.stringify(activeSession));
|
||||
|
||||
const result = await service.cleanupExpiredSessions(30);
|
||||
|
||||
expect(result.cleanedCount).toBe(0);
|
||||
});
|
||||
|
||||
it('应该处理多个地图', async () => {
|
||||
redisService.smembers.mockResolvedValue([]);
|
||||
|
||||
const result = await service.cleanupExpiredSessions(30);
|
||||
|
||||
expect(redisService.smembers).toHaveBeenCalledTimes(2);
|
||||
expect(result.cleanedCount).toBe(0);
|
||||
});
|
||||
|
||||
it('应该使用默认地图当配置为空', async () => {
|
||||
configManager.getAllMapIds.mockReturnValue([]);
|
||||
redisService.smembers.mockResolvedValue([]);
|
||||
|
||||
const result = await service.cleanupExpiredSessions(30);
|
||||
|
||||
expect(result.cleanedCount).toBe(0);
|
||||
});
|
||||
|
||||
it('应该处理清理过程中的错误', async () => {
|
||||
redisService.smembers.mockRejectedValue(new Error('Redis error'));
|
||||
|
||||
const result = await service.cleanupExpiredSessions(30);
|
||||
|
||||
expect(result.cleanedCount).toBe(0);
|
||||
expect(result.zulipQueueIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该清理不存在的会话数据', async () => {
|
||||
redisService.smembers.mockResolvedValue(['socket_123']);
|
||||
redisService.get.mockResolvedValue(null);
|
||||
redisService.srem.mockResolvedValue(1);
|
||||
|
||||
const result = await service.cleanupExpiredSessions(30);
|
||||
|
||||
expect(redisService.srem).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
it('应该处理极大的坐标值', async () => {
|
||||
const socketId = 'socket_123';
|
||||
const userId = 'user_123';
|
||||
const zulipQueueId = 'queue_123';
|
||||
|
||||
redisService.get.mockResolvedValue(null);
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
redisService.sadd.mockResolvedValue(1);
|
||||
redisService.expire.mockResolvedValue(1);
|
||||
|
||||
const session = await service.createSession(
|
||||
socketId,
|
||||
userId,
|
||||
zulipQueueId,
|
||||
'testuser',
|
||||
'whale_port',
|
||||
{ x: 999999, y: 999999 }
|
||||
);
|
||||
|
||||
expect(session.position).toEqual({ x: 999999, y: 999999 });
|
||||
});
|
||||
|
||||
it('应该处理负坐标值', async () => {
|
||||
const socketId = 'socket_123';
|
||||
const userId = 'user_123';
|
||||
const zulipQueueId = 'queue_123';
|
||||
|
||||
redisService.get.mockResolvedValue(null);
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
redisService.sadd.mockResolvedValue(1);
|
||||
redisService.expire.mockResolvedValue(1);
|
||||
|
||||
const session = await service.createSession(
|
||||
socketId,
|
||||
userId,
|
||||
zulipQueueId,
|
||||
'testuser',
|
||||
'whale_port',
|
||||
{ x: -100, y: -100 }
|
||||
);
|
||||
|
||||
expect(session.position).toEqual({ x: -100, y: -100 });
|
||||
});
|
||||
|
||||
it('应该处理特殊字符的用户名', async () => {
|
||||
const socketId = 'socket_123';
|
||||
const userId = 'user_123';
|
||||
const zulipQueueId = 'queue_123';
|
||||
const username = 'test@user#123';
|
||||
|
||||
redisService.get.mockResolvedValue(null);
|
||||
redisService.setex.mockResolvedValue('OK');
|
||||
redisService.sadd.mockResolvedValue(1);
|
||||
redisService.expire.mockResolvedValue(1);
|
||||
|
||||
const session = await service.createSession(socketId, userId, zulipQueueId, username);
|
||||
|
||||
expect(session.username).toBe(username);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user