forked from xiangwang25/whale-town-end-v2
771 lines
25 KiB
TypeScript
771 lines
25 KiB
TypeScript
/**
|
||
* 聊天业务服务测试
|
||
*
|
||
* 测试范围:
|
||
* - 玩家登录/登出流程
|
||
* - 聊天消息发送和广播
|
||
* - 位置更新和会话管理
|
||
* - Token验证和错误处理
|
||
*
|
||
* @author moyin
|
||
* @version 1.0.1
|
||
* @since 2026-01-14
|
||
* @lastModified 2026-01-19
|
||
*
|
||
* 修改记录:
|
||
* - 2026-01-19 moyin: 修复handlePlayerLogout测试,删除不再调用的deleteApiKey断言和过时测试用例
|
||
*/
|
||
|
||
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { Logger } from '@nestjs/common';
|
||
import { ChatService } from './chat.service';
|
||
import { ChatSessionService } from './services/chat_session.service';
|
||
import { ChatFilterService } from './services/chat_filter.service';
|
||
import { LoginCoreService } from '../../core/login_core/login_core.service';
|
||
import { AccountProfileService } from '../auth/account_profile.service';
|
||
import { EconomyService } from '../player/economy.service';
|
||
|
||
describe('ChatService', () => {
|
||
let service: ChatService;
|
||
let sessionService: jest.Mocked<ChatSessionService>;
|
||
let filterService: jest.Mocked<ChatFilterService>;
|
||
let zulipClientPool: any;
|
||
let apiKeySecurityService: any;
|
||
let loginCoreService: jest.Mocked<LoginCoreService>;
|
||
let mockWebSocketGateway: any;
|
||
let economyService: jest.Mocked<Pick<EconomyService, 'spend' | 'earn'>>;
|
||
|
||
beforeEach(async () => {
|
||
// Mock依赖
|
||
const mockSessionService = {
|
||
createSession: jest.fn(),
|
||
getSession: jest.fn(),
|
||
destroySession: jest.fn(),
|
||
updatePlayerPosition: jest.fn(),
|
||
injectContext: jest.fn(),
|
||
getSocketsInMap: jest.fn(),
|
||
getSocketIdByUserId: jest.fn(),
|
||
addFriend: jest.fn(),
|
||
createFriendRequest: jest.fn(),
|
||
acceptFriendRequest: jest.fn(),
|
||
rejectFriendRequest: jest.fn(),
|
||
removeFriend: jest.fn(),
|
||
getFriends: jest.fn(),
|
||
getFriendRequests: jest.fn(),
|
||
};
|
||
|
||
const mockFilterService = {
|
||
validateMessage: jest.fn(),
|
||
filterContent: jest.fn(),
|
||
checkRateLimit: jest.fn(),
|
||
validatePermission: jest.fn(),
|
||
};
|
||
|
||
const mockZulipClientPool = {
|
||
createUserClient: jest.fn(),
|
||
destroyUserClient: jest.fn(),
|
||
sendMessage: jest.fn(),
|
||
getUserClient: jest.fn(),
|
||
};
|
||
|
||
const mockApiKeySecurityService = {
|
||
getApiKey: jest.fn(),
|
||
deleteApiKey: jest.fn(),
|
||
};
|
||
|
||
const mockLoginCoreService = {
|
||
verifyToken: jest.fn(),
|
||
};
|
||
const mockAccountProfileService = {
|
||
getAccountProfile: jest.fn(),
|
||
};
|
||
|
||
const mockEconomyService = {
|
||
spend: jest.fn(),
|
||
earn: jest.fn(),
|
||
};
|
||
|
||
const mockZulipAccountsService = {
|
||
findByGameUserId: jest.fn(),
|
||
};
|
||
|
||
mockWebSocketGateway = {
|
||
broadcastToMap: jest.fn(),
|
||
broadcastToAll: jest.fn(),
|
||
sendToPlayer: jest.fn(),
|
||
};
|
||
|
||
const module: TestingModule = await Test.createTestingModule({
|
||
providers: [
|
||
ChatService,
|
||
{
|
||
provide: ChatSessionService,
|
||
useValue: mockSessionService,
|
||
},
|
||
{
|
||
provide: ChatFilterService,
|
||
useValue: mockFilterService,
|
||
},
|
||
{
|
||
provide: 'ZULIP_CLIENT_POOL_SERVICE',
|
||
useValue: mockZulipClientPool,
|
||
},
|
||
{
|
||
provide: 'API_KEY_SECURITY_SERVICE',
|
||
useValue: mockApiKeySecurityService,
|
||
},
|
||
{
|
||
provide: LoginCoreService,
|
||
useValue: mockLoginCoreService,
|
||
},
|
||
{
|
||
provide: AccountProfileService,
|
||
useValue: mockAccountProfileService,
|
||
},
|
||
{
|
||
provide: EconomyService,
|
||
useValue: mockEconomyService,
|
||
},
|
||
{
|
||
provide: 'ZulipAccountsService',
|
||
useValue: mockZulipAccountsService,
|
||
},
|
||
],
|
||
}).compile();
|
||
|
||
service = module.get<ChatService>(ChatService);
|
||
sessionService = module.get(ChatSessionService);
|
||
filterService = module.get(ChatFilterService);
|
||
zulipClientPool = module.get('ZULIP_CLIENT_POOL_SERVICE');
|
||
apiKeySecurityService = module.get('API_KEY_SECURITY_SERVICE');
|
||
loginCoreService = module.get(LoginCoreService);
|
||
economyService = module.get(EconomyService);
|
||
|
||
// 设置默认的mock行为
|
||
// ZulipAccountsService默认返回null(用户没有Zulip账号)
|
||
const zulipAccountsService = module.get('ZulipAccountsService');
|
||
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
|
||
|
||
// ZulipClientPool的getUserClient默认返回null
|
||
zulipClientPool.getUserClient.mockResolvedValue(null);
|
||
|
||
// 设置WebSocket网关
|
||
service.setWebSocketGateway(mockWebSocketGateway);
|
||
|
||
// 禁用日志输出
|
||
jest.spyOn(Logger.prototype, 'log').mockImplementation();
|
||
jest.spyOn(Logger.prototype, 'error').mockImplementation();
|
||
jest.spyOn(Logger.prototype, 'warn').mockImplementation();
|
||
});
|
||
|
||
afterEach(() => {
|
||
jest.clearAllMocks();
|
||
});
|
||
|
||
describe('初始化', () => {
|
||
it('应该成功创建服务实例', () => {
|
||
expect(service).toBeDefined();
|
||
});
|
||
|
||
it('应该成功设置WebSocket网关', () => {
|
||
const newGateway = { broadcastToMap: jest.fn(), broadcastToAll: jest.fn(), sendToPlayer: jest.fn() };
|
||
service.setWebSocketGateway(newGateway);
|
||
expect(service['websocketGateway']).toBe(newGateway);
|
||
});
|
||
});
|
||
|
||
describe('handlePlayerLogin', () => {
|
||
const validToken = 'valid.jwt.token';
|
||
const socketId = 'socket_123';
|
||
|
||
it('应该成功处理玩家登录', async () => {
|
||
const userInfo = {
|
||
sub: 'user_123',
|
||
username: 'testuser',
|
||
email: 'test@example.com',
|
||
role: 1,
|
||
type: 'access' as 'access' | 'refresh',
|
||
};
|
||
|
||
loginCoreService.verifyToken.mockResolvedValue(userInfo);
|
||
sessionService.createSession.mockResolvedValue({
|
||
socketId,
|
||
userId: userInfo.sub,
|
||
username: userInfo.username,
|
||
zulipQueueId: 'queue_123',
|
||
currentMap: 'whale_port',
|
||
position: { x: 400, y: 300 },
|
||
lastActivity: new Date(),
|
||
createdAt: new Date(),
|
||
});
|
||
|
||
const result = await service.handlePlayerLogin({ token: validToken, socketId });
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.userId).toBe(userInfo.sub);
|
||
expect(result.username).toBe(userInfo.username);
|
||
expect(loginCoreService.verifyToken).toHaveBeenCalledWith(validToken, 'access');
|
||
expect(sessionService.createSession).toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该拒绝空Token', async () => {
|
||
const result = await service.handlePlayerLogin({ token: '', socketId });
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('Token或socketId不能为空');
|
||
expect(loginCoreService.verifyToken).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该拒绝空socketId', async () => {
|
||
const result = await service.handlePlayerLogin({ token: validToken, socketId: '' });
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('Token或socketId不能为空');
|
||
});
|
||
|
||
it('应该处理Token验证失败', async () => {
|
||
loginCoreService.verifyToken.mockResolvedValue(null);
|
||
|
||
const result = await service.handlePlayerLogin({ token: validToken, socketId });
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('Token验证失败');
|
||
});
|
||
|
||
it('应该处理Token验证异常', async () => {
|
||
loginCoreService.verifyToken.mockRejectedValue(new Error('Token expired'));
|
||
|
||
const result = await service.handlePlayerLogin({ token: validToken, socketId });
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('Token验证失败');
|
||
});
|
||
|
||
it('应该处理会话创建失败', async () => {
|
||
const userInfo = { sub: 'user_123', username: 'testuser', email: 'test@example.com', role: 1, type: 'access' as 'access' | 'refresh' };
|
||
loginCoreService.verifyToken.mockResolvedValue(userInfo);
|
||
sessionService.createSession.mockRejectedValue(new Error('Redis error'));
|
||
|
||
const result = await service.handlePlayerLogin({ token: validToken, socketId });
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('登录失败,请稍后重试');
|
||
});
|
||
});
|
||
|
||
describe('handlePlayerLogout', () => {
|
||
const socketId = 'socket_123';
|
||
const userId = 'user_123';
|
||
|
||
it('应该成功处理玩家登出', async () => {
|
||
sessionService.getSession.mockResolvedValue({
|
||
socketId,
|
||
userId,
|
||
username: 'testuser',
|
||
zulipQueueId: 'queue_123',
|
||
currentMap: 'whale_port',
|
||
position: { x: 400, y: 300 },
|
||
lastActivity: new Date(),
|
||
createdAt: new Date(),
|
||
});
|
||
zulipClientPool.destroyUserClient.mockResolvedValue(undefined);
|
||
sessionService.destroySession.mockResolvedValue(true);
|
||
|
||
await service.handlePlayerLogout(socketId, 'manual');
|
||
|
||
expect(sessionService.getSession).toHaveBeenCalledWith(socketId);
|
||
expect(zulipClientPool.destroyUserClient).toHaveBeenCalledWith(userId);
|
||
expect(sessionService.destroySession).toHaveBeenCalledWith(socketId);
|
||
});
|
||
|
||
it('应该处理会话不存在的情况', async () => {
|
||
sessionService.getSession.mockResolvedValue(null);
|
||
|
||
await service.handlePlayerLogout(socketId);
|
||
|
||
expect(sessionService.destroySession).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该处理Zulip客户端清理失败', async () => {
|
||
sessionService.getSession.mockResolvedValue({
|
||
socketId,
|
||
userId,
|
||
username: 'testuser',
|
||
zulipQueueId: 'queue_123',
|
||
currentMap: 'whale_port',
|
||
position: { x: 400, y: 300 },
|
||
lastActivity: new Date(),
|
||
createdAt: new Date(),
|
||
});
|
||
zulipClientPool.destroyUserClient.mockRejectedValue(new Error('Zulip error'));
|
||
sessionService.destroySession.mockResolvedValue(true);
|
||
|
||
await service.handlePlayerLogout(socketId);
|
||
|
||
expect(sessionService.destroySession).toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('sendChatMessage', () => {
|
||
const socketId = 'socket_123';
|
||
const userId = 'user_123';
|
||
const content = 'Hello, world!';
|
||
|
||
beforeEach(() => {
|
||
sessionService.getSession.mockResolvedValue({
|
||
socketId,
|
||
userId,
|
||
username: 'testuser',
|
||
zulipQueueId: 'queue_123',
|
||
currentMap: 'whale_port',
|
||
position: { x: 400, y: 300 },
|
||
lastActivity: new Date(),
|
||
createdAt: new Date(),
|
||
});
|
||
sessionService.injectContext.mockResolvedValue({
|
||
stream: 'Whale Port',
|
||
topic: 'General',
|
||
});
|
||
filterService.validateMessage.mockResolvedValue({
|
||
allowed: true,
|
||
filteredContent: content,
|
||
});
|
||
sessionService.getSocketsInMap.mockResolvedValue([socketId, 'socket_456']);
|
||
apiKeySecurityService.getApiKey.mockResolvedValue({
|
||
success: true,
|
||
apiKey: 'test_api_key',
|
||
});
|
||
});
|
||
|
||
it('应该成功发送聊天消息', async () => {
|
||
const result = await service.sendChatMessage({ socketId, content, scope: 'local' });
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.messageId).toBeDefined();
|
||
expect(sessionService.getSession).toHaveBeenCalledWith(socketId);
|
||
expect(filterService.validateMessage).toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该将世界频道消息广播给所有在线玩家', async () => {
|
||
const result = await service.sendChatMessage({ socketId, content, scope: 'global' });
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(mockWebSocketGateway.broadcastToAll).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
t: 'chat_render',
|
||
from: 'testuser',
|
||
fromUserId: userId,
|
||
txt: content,
|
||
scope: 'global',
|
||
}),
|
||
socketId,
|
||
);
|
||
expect(sessionService.getSocketsInMap).not.toHaveBeenCalled();
|
||
expect(economyService.spend).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该由服务端固定扣除 100 鲸币后发布世界公告', async () => {
|
||
sessionService.getSession.mockResolvedValue({
|
||
socketId,
|
||
userId: '123',
|
||
username: 'testuser',
|
||
zulipQueueId: 'queue_123',
|
||
currentMap: 'whale_port',
|
||
position: { x: 400, y: 300 },
|
||
lastActivity: new Date(),
|
||
createdAt: new Date(),
|
||
});
|
||
economyService.spend.mockResolvedValue({
|
||
user_id: '123',
|
||
balance: 900,
|
||
currency: 'whale_coin',
|
||
});
|
||
|
||
const result = await service.sendChatMessage({
|
||
socketId,
|
||
content,
|
||
scope: 'local',
|
||
worldBulletin: true,
|
||
...({ cost: 1 } as any),
|
||
});
|
||
|
||
expect(result).toEqual(expect.objectContaining({ success: true, charged: 100, balance: 900 }));
|
||
expect(economyService.spend).toHaveBeenCalledWith(
|
||
BigInt(123),
|
||
100,
|
||
'world_bulletin',
|
||
expect.stringMatching(/^game_/),
|
||
'发布世界公告',
|
||
);
|
||
expect(mockWebSocketGateway.broadcastToAll).toHaveBeenCalledWith(
|
||
expect.objectContaining({ scope: 'global', worldBulletin: true }),
|
||
undefined,
|
||
);
|
||
});
|
||
|
||
it('应该在鲸币余额不足时拒绝公告且不广播', async () => {
|
||
sessionService.getSession.mockResolvedValue({
|
||
socketId,
|
||
userId: '123',
|
||
username: 'testuser',
|
||
zulipQueueId: 'queue_123',
|
||
currentMap: 'whale_port',
|
||
position: { x: 400, y: 300 },
|
||
lastActivity: new Date(),
|
||
createdAt: new Date(),
|
||
});
|
||
economyService.spend.mockRejectedValue(new Error('鲸币余额不足'));
|
||
|
||
const result = await service.sendChatMessage({ socketId, content, scope: 'global', worldBulletin: true });
|
||
|
||
expect(result).toEqual({ success: false, error: '鲸币余额不足,发布世界公告需要 100 鲸币' });
|
||
expect(mockWebSocketGateway.broadcastToAll).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该在公告广播失败时退回已扣鲸币', async () => {
|
||
sessionService.getSession.mockResolvedValue({
|
||
socketId,
|
||
userId: '123',
|
||
username: 'testuser',
|
||
zulipQueueId: 'queue_123',
|
||
currentMap: 'whale_port',
|
||
position: { x: 400, y: 300 },
|
||
lastActivity: new Date(),
|
||
createdAt: new Date(),
|
||
});
|
||
economyService.spend.mockResolvedValue({ user_id: '123', balance: 900, currency: 'whale_coin' });
|
||
economyService.earn.mockResolvedValue({ user_id: '123', balance: 1000, currency: 'whale_coin' });
|
||
mockWebSocketGateway.broadcastToAll.mockImplementation(() => {
|
||
throw new Error('广播失败');
|
||
});
|
||
|
||
const result = await service.sendChatMessage({ socketId, content, scope: 'global', worldBulletin: true });
|
||
|
||
expect(result).toEqual({ success: false, error: '广播失败' });
|
||
expect(economyService.earn).toHaveBeenCalledWith(
|
||
BigInt(123),
|
||
100,
|
||
'world_bulletin_refund',
|
||
expect.stringMatching(/^game_/),
|
||
'世界公告发送失败退款',
|
||
);
|
||
});
|
||
|
||
it('应该只在显式请求时广播角色气泡', async () => {
|
||
await service.sendChatMessage({ socketId, content, scope: 'global' });
|
||
expect(mockWebSocketGateway.broadcastToAll).toHaveBeenLastCalledWith(
|
||
expect.objectContaining({
|
||
bubble: false,
|
||
}),
|
||
socketId,
|
||
);
|
||
|
||
await service.sendChatMessage({ socketId, content: 'bubble hello', scope: 'global', bubble: true });
|
||
expect(mockWebSocketGateway.broadcastToAll).toHaveBeenLastCalledWith(
|
||
expect.objectContaining({
|
||
bubble: true,
|
||
}),
|
||
socketId,
|
||
);
|
||
});
|
||
|
||
it('应该将私聊消息只发送给发送者和目标玩家', async () => {
|
||
sessionService.getSocketIdByUserId.mockResolvedValue('socket_target');
|
||
|
||
const result = await service.sendChatMessage({
|
||
socketId,
|
||
content,
|
||
scope: 'private',
|
||
targetUserId: 'user_target',
|
||
targetUsername: 'targetuser',
|
||
});
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(sessionService.getSocketIdByUserId).toHaveBeenCalledWith('user_target');
|
||
expect(mockWebSocketGateway.sendToPlayer).toHaveBeenCalledWith(
|
||
socketId,
|
||
expect.objectContaining({
|
||
scope: 'private',
|
||
fromUserId: userId,
|
||
toUserId: 'user_target',
|
||
toUsername: 'targetuser',
|
||
}),
|
||
);
|
||
expect(mockWebSocketGateway.sendToPlayer).toHaveBeenCalledWith(
|
||
'socket_target',
|
||
expect.objectContaining({
|
||
scope: 'private',
|
||
fromUserId: userId,
|
||
toUserId: 'user_target',
|
||
}),
|
||
);
|
||
expect(mockWebSocketGateway.broadcastToAll).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该拒绝缺少目标用户的私聊', async () => {
|
||
const result = await service.sendChatMessage({ socketId, content, scope: 'private' });
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('请选择悄悄话对象');
|
||
expect(mockWebSocketGateway.sendToPlayer).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该拒绝目标离线的私聊', async () => {
|
||
sessionService.getSocketIdByUserId.mockResolvedValue(null);
|
||
|
||
const result = await service.sendChatMessage({
|
||
socketId,
|
||
content,
|
||
scope: 'private',
|
||
targetUserId: 'user_target',
|
||
});
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('悄悄话对象不在线');
|
||
});
|
||
|
||
it('应该拒绝不存在的会话', async () => {
|
||
sessionService.getSession.mockResolvedValue(null);
|
||
|
||
const result = await service.sendChatMessage({ socketId, content, scope: 'local' });
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('会话不存在,请重新登录');
|
||
});
|
||
|
||
it('应该拒绝被过滤的消息', async () => {
|
||
filterService.validateMessage.mockResolvedValue({
|
||
allowed: false,
|
||
reason: '消息包含敏感词',
|
||
});
|
||
|
||
const result = await service.sendChatMessage({ socketId, content, scope: 'local' });
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('消息包含敏感词');
|
||
});
|
||
|
||
it('应该处理消息发送异常', async () => {
|
||
sessionService.getSession.mockRejectedValue(new Error('Redis error'));
|
||
|
||
const result = await service.sendChatMessage({ socketId, content, scope: 'local' });
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('消息发送失败,请稍后重试');
|
||
});
|
||
});
|
||
|
||
describe('updatePlayerPosition', () => {
|
||
const socketId = 'socket_123';
|
||
const mapId = 'whale_port';
|
||
const x = 500;
|
||
const y = 400;
|
||
|
||
it('应该成功更新玩家位置', async () => {
|
||
sessionService.updatePlayerPosition.mockResolvedValue(true);
|
||
|
||
const result = await service.updatePlayerPosition({ socketId, mapId, x, y });
|
||
|
||
expect(result).toBe(true);
|
||
expect(sessionService.updatePlayerPosition).toHaveBeenCalledWith(socketId, mapId, x, y, {
|
||
appearance: undefined,
|
||
});
|
||
});
|
||
|
||
it('应该拒绝空socketId', async () => {
|
||
const result = await service.updatePlayerPosition({ socketId: '', mapId, x, y });
|
||
|
||
expect(result).toBe(false);
|
||
expect(sessionService.updatePlayerPosition).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该拒绝空mapId', async () => {
|
||
const result = await service.updatePlayerPosition({ socketId, mapId: '', x, y });
|
||
|
||
expect(result).toBe(false);
|
||
expect(sessionService.updatePlayerPosition).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该处理更新失败', async () => {
|
||
sessionService.updatePlayerPosition.mockRejectedValue(new Error('Redis error'));
|
||
|
||
const result = await service.updatePlayerPosition({ socketId, mapId, x, y });
|
||
|
||
expect(result).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('friends', () => {
|
||
const socketId = 'socket_123';
|
||
const userId = 'user_123';
|
||
|
||
beforeEach(() => {
|
||
sessionService.getSession.mockResolvedValue({
|
||
socketId,
|
||
userId,
|
||
username: 'testuser',
|
||
zulipQueueId: 'queue_123',
|
||
currentMap: 'whale_port',
|
||
position: { x: 400, y: 300 },
|
||
lastActivity: new Date(),
|
||
createdAt: new Date(),
|
||
});
|
||
});
|
||
|
||
it('应该添加好友', async () => {
|
||
const friend = { userId: 'user_friend', username: 'friend', online: true };
|
||
sessionService.addFriend.mockResolvedValue(friend);
|
||
|
||
const result = await service.addFriend({
|
||
socketId,
|
||
friendUserId: friend.userId,
|
||
friendUsername: friend.username,
|
||
});
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.friend).toEqual(friend);
|
||
expect(sessionService.addFriend).toHaveBeenCalledWith(userId, friend.userId, friend.username);
|
||
});
|
||
|
||
it('应该获取好友列表', async () => {
|
||
const friends = [{ userId: 'user_friend', username: 'friend', online: false }];
|
||
const requests = [{ userId: 'requester', username: 'requester', createdAt: '2026-07-01T00:00:00.000Z' }];
|
||
sessionService.getFriends.mockResolvedValue(friends);
|
||
sessionService.getFriendRequests.mockResolvedValue(requests);
|
||
|
||
const result = await service.getFriends(socketId);
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.friends).toEqual(friends);
|
||
expect(result.requests).toEqual(requests);
|
||
expect(sessionService.getFriends).toHaveBeenCalledWith(userId);
|
||
expect(sessionService.getFriendRequests).toHaveBeenCalledWith(userId);
|
||
});
|
||
|
||
it('应该发送好友请求并实时通知在线目标', async () => {
|
||
const friendRequest = { userId, username: 'testuser', createdAt: '2026-07-01T00:00:00.000Z' };
|
||
sessionService.createFriendRequest.mockResolvedValue(friendRequest);
|
||
sessionService.getSocketIdByUserId.mockResolvedValue('target_socket');
|
||
|
||
const result = await service.requestFriend({
|
||
socketId,
|
||
friendUserId: 'user_friend',
|
||
});
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(sessionService.createFriendRequest).toHaveBeenCalledWith(userId, 'testuser', 'user_friend');
|
||
expect(mockWebSocketGateway.sendToPlayer).toHaveBeenCalledWith('target_socket', {
|
||
t: 'friend_request_received',
|
||
request: friendRequest,
|
||
});
|
||
});
|
||
|
||
it('应该接受好友请求并通知发起方', async () => {
|
||
const friend = { userId: 'requester', username: 'requester', online: true };
|
||
const reciprocalFriend = { userId, username: 'testuser', online: true };
|
||
sessionService.acceptFriendRequest.mockResolvedValue({ friend, reciprocalFriend });
|
||
sessionService.getSocketIdByUserId.mockResolvedValue('requester_socket');
|
||
|
||
const result = await service.acceptFriendRequest({
|
||
socketId,
|
||
friendUserId: 'requester',
|
||
});
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.friend).toEqual(friend);
|
||
expect(sessionService.acceptFriendRequest).toHaveBeenCalledWith(userId, 'requester', 'testuser');
|
||
expect(mockWebSocketGateway.sendToPlayer).toHaveBeenCalledWith('requester_socket', {
|
||
t: 'friend_request_accepted',
|
||
friend: reciprocalFriend,
|
||
});
|
||
});
|
||
|
||
it('应该拒绝好友请求并通知发起方', async () => {
|
||
sessionService.rejectFriendRequest.mockResolvedValue(undefined);
|
||
sessionService.getSocketIdByUserId.mockResolvedValue('requester_socket');
|
||
|
||
const result = await service.rejectFriendRequest({
|
||
socketId,
|
||
friendUserId: 'requester',
|
||
});
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(sessionService.rejectFriendRequest).toHaveBeenCalledWith(userId, 'requester');
|
||
expect(mockWebSocketGateway.sendToPlayer).toHaveBeenCalledWith('requester_socket', {
|
||
t: 'friend_request_rejected',
|
||
userId,
|
||
username: 'testuser',
|
||
});
|
||
});
|
||
|
||
it('应该移除好友', async () => {
|
||
sessionService.removeFriend.mockResolvedValue(undefined);
|
||
|
||
const result = await service.removeFriend({
|
||
socketId,
|
||
friendUserId: 'user_friend',
|
||
});
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(sessionService.removeFriend).toHaveBeenCalledWith(userId, 'user_friend');
|
||
});
|
||
|
||
it('应该在未登录时拒绝好友操作', async () => {
|
||
sessionService.getSession.mockResolvedValue(null);
|
||
|
||
const result = await service.getFriends(socketId);
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toBe('会话不存在,请重新登录');
|
||
});
|
||
});
|
||
|
||
describe('getChatHistory', () => {
|
||
it('应该返回聊天历史', async () => {
|
||
const result = await service.getChatHistory({ mapId: 'whale_port' });
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.messages).toBeDefined();
|
||
expect(Array.isArray(result.messages)).toBe(true);
|
||
});
|
||
|
||
it('应该支持分页查询', async () => {
|
||
const result = await service.getChatHistory({ mapId: 'whale_port', limit: 10, offset: 0 });
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.count).toBeLessThanOrEqual(10);
|
||
});
|
||
});
|
||
|
||
describe('getSession', () => {
|
||
const socketId = 'socket_123';
|
||
|
||
it('应该返回会话信息', async () => {
|
||
const mockSession = {
|
||
socketId,
|
||
userId: 'user_123',
|
||
username: 'testuser',
|
||
zulipQueueId: 'queue_123',
|
||
currentMap: 'whale_port',
|
||
position: { x: 400, y: 300 },
|
||
lastActivity: new Date(),
|
||
createdAt: new Date(),
|
||
};
|
||
sessionService.getSession.mockResolvedValue(mockSession);
|
||
|
||
const result = await service.getSession(socketId);
|
||
|
||
expect(result).toEqual(mockSession);
|
||
expect(sessionService.getSession).toHaveBeenCalledWith(socketId);
|
||
});
|
||
|
||
it('应该处理会话不存在', async () => {
|
||
sessionService.getSession.mockResolvedValue(null);
|
||
|
||
const result = await service.getSession(socketId);
|
||
|
||
expect(result).toBeNull();
|
||
});
|
||
});
|
||
});
|