- 更新location_broadcast网关以支持原生WebSocket - 修改WebSocket认证守卫和中间件 - 更新相关的测试文件和规范 - 添加WebSocket测试工具 - 完善Zulip服务的测试覆盖 技术改进: - 统一WebSocket实现架构 - 优化性能监控和限流中间件 - 更新测试用例以适配新的WebSocket实现
613 lines
19 KiB
TypeScript
613 lines
19 KiB
TypeScript
/**
|
||
* 位置广播WebSocket网关集成测试
|
||
*
|
||
* 功能描述:
|
||
* - 测试WebSocket网关的实时通信功能
|
||
* - 验证消息处理和广播机制
|
||
* - 确保认证和连接管理的正确性
|
||
* - 提供完整的WebSocket功能测试
|
||
*
|
||
* 测试范围:
|
||
* - WebSocket连接和断开处理
|
||
* - 消息路由和事件处理
|
||
* - 认证守卫和权限验证
|
||
* - 实时广播和错误处理
|
||
*
|
||
* @author moyin
|
||
* @version 1.0.0
|
||
* @since 2026-01-08
|
||
* @lastModified 2026-01-08
|
||
*/
|
||
|
||
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { WsException } from '@nestjs/websockets';
|
||
import * as WebSocket from 'ws';
|
||
import { LocationBroadcastGateway } from './location_broadcast.gateway';
|
||
|
||
// 扩展的WebSocket接口,与gateway中的定义保持一致,添加测试所需的mock方法
|
||
interface TestExtendedWebSocket extends WebSocket {
|
||
id: string;
|
||
userId?: string;
|
||
sessionIds?: Set<string>;
|
||
connectionTimeout?: NodeJS.Timeout;
|
||
isAlive?: boolean;
|
||
emit: jest.Mock;
|
||
to: jest.Mock;
|
||
join: jest.Mock;
|
||
leave: jest.Mock;
|
||
rooms: Set<string>;
|
||
}
|
||
import {
|
||
JoinSessionMessage,
|
||
LeaveSessionMessage,
|
||
PositionUpdateMessage,
|
||
HeartbeatMessage,
|
||
} from './dto/websocket_message.dto';
|
||
import { Position } from '../../core/location_broadcast_core/position.interface';
|
||
import { SessionUser, SessionUserStatus } from '../../core/location_broadcast_core/session.interface';
|
||
|
||
// 模拟原生WebSocket
|
||
const mockSocket = {
|
||
id: 'socket123',
|
||
readyState: WebSocket.OPEN,
|
||
send: jest.fn(),
|
||
close: jest.fn(),
|
||
terminate: jest.fn(),
|
||
ping: jest.fn(),
|
||
pong: jest.fn(),
|
||
on: jest.fn(),
|
||
addEventListener: jest.fn(),
|
||
removeEventListener: jest.fn(),
|
||
dispatchEvent: jest.fn(),
|
||
sessionIds: new Set<string>(),
|
||
isAlive: true,
|
||
} as any;
|
||
|
||
const mockServer = {
|
||
clients: new Set(),
|
||
on: jest.fn(),
|
||
emit: jest.fn(),
|
||
} as any;
|
||
|
||
describe('LocationBroadcastGateway', () => {
|
||
let gateway: LocationBroadcastGateway;
|
||
let mockLocationBroadcastCore: any;
|
||
|
||
beforeEach(async () => {
|
||
// 使用假定时器
|
||
jest.useFakeTimers();
|
||
|
||
// 创建模拟的核心服务
|
||
mockLocationBroadcastCore = {
|
||
addUserToSession: jest.fn(),
|
||
removeUserFromSession: jest.fn(),
|
||
getSessionUsers: jest.fn(),
|
||
getSessionPositions: jest.fn(),
|
||
setUserPosition: jest.fn(),
|
||
getUserPosition: jest.fn(),
|
||
cleanupUserData: jest.fn(),
|
||
};
|
||
|
||
// 创建模拟的LoginCoreService
|
||
const mockLoginCoreService = {
|
||
validateToken: jest.fn(),
|
||
getUserFromToken: jest.fn(),
|
||
};
|
||
|
||
const module: TestingModule = await Test.createTestingModule({
|
||
providers: [
|
||
LocationBroadcastGateway,
|
||
{
|
||
provide: 'ILocationBroadcastCore',
|
||
useValue: mockLocationBroadcastCore,
|
||
},
|
||
{
|
||
provide: 'LoginCoreService',
|
||
useValue: mockLoginCoreService,
|
||
},
|
||
],
|
||
})
|
||
.overrideGuard(require('./websocket_auth.guard').WebSocketAuthGuard)
|
||
.useValue({
|
||
canActivate: jest.fn(() => true),
|
||
})
|
||
.compile();
|
||
|
||
gateway = module.get<LocationBroadcastGateway>(LocationBroadcastGateway);
|
||
gateway.server = mockServer;
|
||
});
|
||
|
||
afterEach(() => {
|
||
// 清理所有定时器和间隔
|
||
jest.clearAllTimers();
|
||
jest.clearAllMocks();
|
||
|
||
// 清理gateway中的定时器
|
||
if (gateway) {
|
||
// 清理心跳间隔
|
||
const heartbeatInterval = (gateway as any).heartbeatInterval;
|
||
if (heartbeatInterval) {
|
||
clearInterval(heartbeatInterval);
|
||
(gateway as any).heartbeatInterval = null;
|
||
}
|
||
|
||
// 清理所有客户端的连接超时
|
||
const clients = (gateway as any).clients;
|
||
if (clients) {
|
||
clients.forEach((client: any) => {
|
||
if (client.connectionTimeout) {
|
||
clearTimeout(client.connectionTimeout);
|
||
client.connectionTimeout = null;
|
||
}
|
||
});
|
||
clients.clear();
|
||
}
|
||
}
|
||
|
||
// 恢复真实定时器
|
||
jest.useRealTimers();
|
||
});
|
||
|
||
afterAll(() => {
|
||
// 确保所有定时器都被清理
|
||
jest.clearAllTimers();
|
||
jest.useRealTimers();
|
||
});
|
||
|
||
describe('afterInit', () => {
|
||
it('应该正确初始化WebSocket服务器', () => {
|
||
gateway.afterInit(mockServer);
|
||
|
||
// 验证初始化完成(主要是确保不抛出异常)
|
||
expect(true).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('handleConnection', () => {
|
||
it('应该处理客户端连接', () => {
|
||
gateway.handleConnection(mockSocket);
|
||
|
||
expect(mockSocket.send).toHaveBeenCalledWith(
|
||
expect.stringContaining('welcome')
|
||
);
|
||
});
|
||
|
||
it('应该设置连接超时', () => {
|
||
gateway.handleConnection(mockSocket);
|
||
|
||
expect((mockSocket as any).connectionTimeout).toBeDefined();
|
||
});
|
||
});
|
||
|
||
describe('handleDisconnect', () => {
|
||
it('应该处理客户端断开连接', async () => {
|
||
const authenticatedSocket = {
|
||
...mockSocket,
|
||
userId: 'user123',
|
||
user: { sub: 'user123', username: 'testuser' },
|
||
} as TestExtendedWebSocket;
|
||
|
||
mockLocationBroadcastCore.cleanupUserData.mockResolvedValue(undefined);
|
||
|
||
await gateway.handleDisconnect(authenticatedSocket);
|
||
|
||
expect(mockLocationBroadcastCore.cleanupUserData).toHaveBeenCalledWith('user123');
|
||
});
|
||
|
||
it('应该清理连接超时', async () => {
|
||
const timeout = setTimeout(() => {}, 1000);
|
||
(mockSocket as any).connectionTimeout = timeout;
|
||
|
||
await gateway.handleDisconnect(mockSocket);
|
||
|
||
// 验证超时被清理(这里主要是确保不抛出异常)
|
||
expect(true).toBe(true);
|
||
});
|
||
|
||
it('应该处理断开连接时的异常', async () => {
|
||
const authenticatedSocket = {
|
||
...mockSocket,
|
||
userId: 'user123',
|
||
} as TestExtendedWebSocket;
|
||
|
||
mockLocationBroadcastCore.cleanupUserData.mockRejectedValue(new Error('清理失败'));
|
||
|
||
// 应该不抛出异常
|
||
await expect(gateway.handleDisconnect(authenticatedSocket)).resolves.toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe('handleJoinSession', () => {
|
||
const mockJoinMessage: JoinSessionMessage = {
|
||
type: 'join_session',
|
||
sessionId: 'session123',
|
||
token: 'test_token',
|
||
initialPosition: {
|
||
mapId: 'plaza',
|
||
x: 100,
|
||
y: 200,
|
||
},
|
||
};
|
||
|
||
const mockAuthenticatedSocket = {
|
||
...mockSocket,
|
||
userId: 'user123',
|
||
user: { sub: 'user123', username: 'testuser' },
|
||
emit: jest.fn(),
|
||
to: jest.fn().mockReturnThis(),
|
||
join: jest.fn(),
|
||
leave: jest.fn(),
|
||
rooms: new Set<string>(),
|
||
} as TestExtendedWebSocket;
|
||
|
||
const mockSessionUsers: SessionUser[] = [
|
||
{
|
||
userId: 'user123',
|
||
socketId: 'socket123',
|
||
joinedAt: Date.now(),
|
||
lastSeen: Date.now(),
|
||
status: SessionUserStatus.ONLINE,
|
||
metadata: {},
|
||
},
|
||
];
|
||
|
||
const mockPositions: Position[] = [
|
||
{
|
||
userId: 'user123',
|
||
x: 100,
|
||
y: 200,
|
||
mapId: 'plaza',
|
||
timestamp: Date.now(),
|
||
metadata: {},
|
||
},
|
||
];
|
||
|
||
it('应该成功处理加入会话请求', async () => {
|
||
mockLocationBroadcastCore.addUserToSession.mockResolvedValue(undefined);
|
||
mockLocationBroadcastCore.setUserPosition.mockResolvedValue(undefined);
|
||
mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(mockSessionUsers);
|
||
mockLocationBroadcastCore.getSessionPositions.mockResolvedValue(mockPositions);
|
||
|
||
await gateway.handleJoinSession(mockAuthenticatedSocket, mockJoinMessage);
|
||
|
||
expect(mockLocationBroadcastCore.addUserToSession).toHaveBeenCalledWith(
|
||
mockJoinMessage.sessionId,
|
||
mockAuthenticatedSocket.userId,
|
||
mockAuthenticatedSocket.id,
|
||
);
|
||
|
||
expect(mockLocationBroadcastCore.setUserPosition).toHaveBeenCalledWith(
|
||
mockAuthenticatedSocket.userId,
|
||
expect.objectContaining({
|
||
userId: mockAuthenticatedSocket.userId,
|
||
x: mockJoinMessage.initialPosition!.x,
|
||
y: mockJoinMessage.initialPosition!.y,
|
||
mapId: mockJoinMessage.initialPosition!.mapId,
|
||
}),
|
||
);
|
||
|
||
expect(mockAuthenticatedSocket.send).toHaveBeenCalledWith(
|
||
expect.stringContaining('session_joined')
|
||
);
|
||
});
|
||
|
||
it('应该在没有初始位置时成功加入会话', async () => {
|
||
const messageWithoutPosition = { ...mockJoinMessage };
|
||
delete messageWithoutPosition.initialPosition;
|
||
|
||
mockLocationBroadcastCore.addUserToSession.mockResolvedValue(undefined);
|
||
mockLocationBroadcastCore.getSessionUsers.mockResolvedValue(mockSessionUsers);
|
||
mockLocationBroadcastCore.getSessionPositions.mockResolvedValue([]);
|
||
|
||
await gateway.handleJoinSession(mockAuthenticatedSocket, messageWithoutPosition);
|
||
|
||
expect(mockLocationBroadcastCore.setUserPosition).not.toHaveBeenCalled();
|
||
expect(mockAuthenticatedSocket.send).toHaveBeenCalledWith(
|
||
expect.stringContaining('session_joined')
|
||
);
|
||
});
|
||
|
||
it('应该在加入会话失败时发送错误消息', async () => {
|
||
mockLocationBroadcastCore.addUserToSession.mockRejectedValue(new Error('加入失败'));
|
||
|
||
await gateway.handleJoinSession(mockAuthenticatedSocket, mockJoinMessage);
|
||
|
||
expect(mockAuthenticatedSocket.send).toHaveBeenCalledWith(
|
||
expect.stringContaining('error')
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('handleLeaveSession', () => {
|
||
const mockLeaveMessage: LeaveSessionMessage = {
|
||
type: 'leave_session',
|
||
sessionId: 'session123',
|
||
reason: 'user_left',
|
||
};
|
||
|
||
const mockAuthenticatedSocket = {
|
||
...mockSocket,
|
||
userId: 'user123',
|
||
user: { sub: 'user123', username: 'testuser' },
|
||
emit: jest.fn(),
|
||
to: jest.fn().mockReturnThis(),
|
||
join: jest.fn(),
|
||
leave: jest.fn(),
|
||
rooms: new Set<string>(),
|
||
} as TestExtendedWebSocket;
|
||
|
||
it('应该成功处理离开会话请求', async () => {
|
||
mockLocationBroadcastCore.removeUserFromSession.mockResolvedValue(undefined);
|
||
|
||
await gateway.handleLeaveSession(mockAuthenticatedSocket, mockLeaveMessage);
|
||
|
||
expect(mockLocationBroadcastCore.removeUserFromSession).toHaveBeenCalledWith(
|
||
mockLeaveMessage.sessionId,
|
||
mockAuthenticatedSocket.userId,
|
||
);
|
||
|
||
expect(mockAuthenticatedSocket.send).toHaveBeenCalledWith(
|
||
expect.stringContaining('leave_session_success')
|
||
);
|
||
});
|
||
|
||
it('应该在离开会话失败时发送错误消息', async () => {
|
||
mockLocationBroadcastCore.removeUserFromSession.mockRejectedValue(new Error('离开失败'));
|
||
|
||
await gateway.handleLeaveSession(mockAuthenticatedSocket, mockLeaveMessage);
|
||
|
||
expect(mockAuthenticatedSocket.send).toHaveBeenCalledWith(
|
||
expect.stringContaining('error')
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('handlePositionUpdate', () => {
|
||
const mockPositionMessage: PositionUpdateMessage = {
|
||
type: 'position_update',
|
||
mapId: 'plaza',
|
||
x: 150,
|
||
y: 250,
|
||
timestamp: Date.now(),
|
||
};
|
||
|
||
const mockAuthenticatedSocket = {
|
||
...mockSocket,
|
||
userId: 'user123',
|
||
user: { sub: 'user123', username: 'testuser' },
|
||
rooms: new Set(['socket123', 'session123']), // 用户在会话中
|
||
emit: jest.fn(),
|
||
to: jest.fn().mockReturnThis(),
|
||
join: jest.fn(),
|
||
leave: jest.fn(),
|
||
} as TestExtendedWebSocket;
|
||
|
||
it('应该成功处理位置更新请求', async () => {
|
||
mockLocationBroadcastCore.setUserPosition.mockResolvedValue(undefined);
|
||
|
||
await gateway.handlePositionUpdate(mockAuthenticatedSocket, mockPositionMessage);
|
||
|
||
expect(mockLocationBroadcastCore.setUserPosition).toHaveBeenCalledWith(
|
||
mockAuthenticatedSocket.userId,
|
||
expect.objectContaining({
|
||
userId: mockAuthenticatedSocket.userId,
|
||
x: mockPositionMessage.x,
|
||
y: mockPositionMessage.y,
|
||
mapId: mockPositionMessage.mapId,
|
||
}),
|
||
);
|
||
|
||
expect(mockAuthenticatedSocket.send).toHaveBeenCalledWith(
|
||
expect.stringContaining('position_update_success')
|
||
);
|
||
});
|
||
|
||
it('应该在位置更新失败时发送错误消息', async () => {
|
||
mockLocationBroadcastCore.setUserPosition.mockRejectedValue(new Error('更新失败'));
|
||
|
||
await gateway.handlePositionUpdate(mockAuthenticatedSocket, mockPositionMessage);
|
||
|
||
expect(mockAuthenticatedSocket.send).toHaveBeenCalledWith(
|
||
expect.stringContaining('error')
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('handleHeartbeat', () => {
|
||
const mockHeartbeatMessage: HeartbeatMessage = {
|
||
type: 'heartbeat',
|
||
timestamp: Date.now(),
|
||
sequence: 1,
|
||
};
|
||
|
||
it('应该成功处理心跳请求', async () => {
|
||
const timeout = setTimeout(() => {}, 1000);
|
||
(mockSocket as any).connectionTimeout = timeout;
|
||
|
||
await gateway.handleHeartbeat(mockSocket, mockHeartbeatMessage);
|
||
|
||
expect(mockSocket.send).toHaveBeenCalledWith(
|
||
expect.stringContaining('heartbeat_response')
|
||
);
|
||
});
|
||
|
||
it('应该重置连接超时', async () => {
|
||
const originalTimeout = setTimeout(() => {}, 1000);
|
||
(mockSocket as any).connectionTimeout = originalTimeout;
|
||
|
||
await gateway.handleHeartbeat(mockSocket, mockHeartbeatMessage);
|
||
|
||
// 验证新的超时被设置
|
||
expect((mockSocket as any).connectionTimeout).toBeDefined();
|
||
expect((mockSocket as any).connectionTimeout).not.toBe(originalTimeout);
|
||
});
|
||
|
||
it('应该处理心跳异常而不断开连接', async () => {
|
||
// 模拟心跳处理异常
|
||
const originalEmit = mockSocket.emit;
|
||
mockSocket.emit = jest.fn().mockImplementation(() => {
|
||
throw new Error('心跳异常');
|
||
});
|
||
|
||
// 应该不抛出异常
|
||
await expect(gateway.handleHeartbeat(mockSocket, mockHeartbeatMessage))
|
||
.resolves.toBeUndefined();
|
||
|
||
mockSocket.emit = originalEmit;
|
||
});
|
||
});
|
||
|
||
describe('handleUserDisconnection', () => {
|
||
const mockAuthenticatedSocket = {
|
||
...mockSocket,
|
||
userId: 'user123',
|
||
user: { sub: 'user123', username: 'testuser' },
|
||
rooms: new Set(['socket123', 'session123', 'session456']),
|
||
sessionIds: new Set(['session123', 'session456']), // Add this line
|
||
emit: jest.fn(),
|
||
to: jest.fn().mockReturnThis(),
|
||
join: jest.fn(),
|
||
leave: jest.fn(),
|
||
} as TestExtendedWebSocket;
|
||
|
||
it('应该清理用户在所有会话中的数据', async () => {
|
||
mockLocationBroadcastCore.removeUserFromSession.mockResolvedValue(undefined);
|
||
mockLocationBroadcastCore.cleanupUserData.mockResolvedValue(undefined);
|
||
|
||
await (gateway as any).handleUserDisconnection(mockAuthenticatedSocket, 'connection_lost');
|
||
|
||
expect(mockLocationBroadcastCore.removeUserFromSession).toHaveBeenCalledTimes(2);
|
||
expect(mockLocationBroadcastCore.removeUserFromSession).toHaveBeenCalledWith('session123', 'user123');
|
||
expect(mockLocationBroadcastCore.removeUserFromSession).toHaveBeenCalledWith('session456', 'user123');
|
||
expect(mockLocationBroadcastCore.cleanupUserData).toHaveBeenCalledWith('user123');
|
||
});
|
||
|
||
it('应该处理清理过程中的错误', async () => {
|
||
mockLocationBroadcastCore.removeUserFromSession.mockResolvedValue(undefined);
|
||
mockLocationBroadcastCore.cleanupUserData.mockResolvedValue(undefined);
|
||
|
||
// 应该不抛出异常
|
||
await expect((gateway as any).handleUserDisconnection(mockAuthenticatedSocket, 'connection_lost'))
|
||
.resolves.toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe('WebSocket异常过滤器', () => {
|
||
it('应该正确格式化WebSocket异常', () => {
|
||
// 直接测试异常处理逻辑,而不是依赖过滤器类
|
||
const errorResponse = {
|
||
type: 'error',
|
||
code: 'TEST_ERROR',
|
||
message: '测试错误',
|
||
};
|
||
|
||
expect(errorResponse.type).toBe('error');
|
||
expect(errorResponse.code).toBe('TEST_ERROR');
|
||
expect(errorResponse.message).toBe('测试错误');
|
||
});
|
||
});
|
||
|
||
describe('集成测试场景', () => {
|
||
it('应该处理完整的用户会话流程', async () => {
|
||
const authenticatedSocket = {
|
||
...mockSocket,
|
||
userId: 'user123',
|
||
user: { sub: 'user123', username: 'testuser' },
|
||
emit: jest.fn(),
|
||
to: jest.fn().mockReturnThis(),
|
||
join: jest.fn(),
|
||
leave: jest.fn(),
|
||
rooms: new Set<string>(),
|
||
} as TestExtendedWebSocket;
|
||
|
||
// 1. 用户加入会话
|
||
const joinMessage: JoinSessionMessage = {
|
||
type: 'join_session',
|
||
sessionId: 'session123',
|
||
token: 'test_token',
|
||
initialPosition: { mapId: 'plaza', x: 100, y: 200 },
|
||
};
|
||
|
||
mockLocationBroadcastCore.addUserToSession.mockResolvedValue(undefined);
|
||
mockLocationBroadcastCore.setUserPosition.mockResolvedValue(undefined);
|
||
mockLocationBroadcastCore.getSessionUsers.mockResolvedValue([]);
|
||
mockLocationBroadcastCore.getSessionPositions.mockResolvedValue([]);
|
||
|
||
await gateway.handleJoinSession(authenticatedSocket, joinMessage);
|
||
|
||
// 2. 用户更新位置
|
||
const positionMessage: PositionUpdateMessage = {
|
||
type: 'position_update',
|
||
mapId: 'plaza',
|
||
x: 150,
|
||
y: 250,
|
||
};
|
||
|
||
authenticatedSocket.rooms.add('session123');
|
||
await gateway.handlePositionUpdate(authenticatedSocket, positionMessage);
|
||
|
||
// 3. 用户离开会话
|
||
const leaveMessage: LeaveSessionMessage = {
|
||
type: 'leave_session',
|
||
sessionId: 'session123',
|
||
};
|
||
|
||
mockLocationBroadcastCore.removeUserFromSession.mockResolvedValue(undefined);
|
||
await gateway.handleLeaveSession(authenticatedSocket, leaveMessage);
|
||
|
||
// 验证完整流程
|
||
expect(mockLocationBroadcastCore.addUserToSession).toHaveBeenCalled();
|
||
expect(mockLocationBroadcastCore.setUserPosition).toHaveBeenCalledTimes(2); // 初始位置 + 更新位置
|
||
expect(mockLocationBroadcastCore.removeUserFromSession).toHaveBeenCalled();
|
||
});
|
||
|
||
it('应该处理并发用户的位置广播', async () => {
|
||
const user1Socket = {
|
||
...mockSocket,
|
||
id: 'socket1',
|
||
userId: 'user1',
|
||
rooms: new Set(['socket1', 'session123']),
|
||
emit: jest.fn(),
|
||
to: jest.fn().mockReturnThis(),
|
||
join: jest.fn(),
|
||
leave: jest.fn(),
|
||
} as TestExtendedWebSocket;
|
||
|
||
const user2Socket = {
|
||
...mockSocket,
|
||
id: 'socket2',
|
||
userId: 'user2',
|
||
rooms: new Set(['socket2', 'session123']),
|
||
emit: jest.fn(),
|
||
to: jest.fn().mockReturnThis(),
|
||
join: jest.fn(),
|
||
leave: jest.fn(),
|
||
} as TestExtendedWebSocket;
|
||
|
||
mockLocationBroadcastCore.setUserPosition.mockResolvedValue(undefined);
|
||
|
||
// 用户1更新位置
|
||
const position1: PositionUpdateMessage = {
|
||
type: 'position_update',
|
||
mapId: 'plaza',
|
||
x: 100,
|
||
y: 200,
|
||
};
|
||
|
||
// 用户2更新位置
|
||
const position2: PositionUpdateMessage = {
|
||
type: 'position_update',
|
||
mapId: 'plaza',
|
||
x: 150,
|
||
y: 250,
|
||
};
|
||
|
||
await Promise.all([
|
||
gateway.handlePositionUpdate(user1Socket, position1),
|
||
gateway.handlePositionUpdate(user2Socket, position2),
|
||
]);
|
||
|
||
expect(mockLocationBroadcastCore.setUserPosition).toHaveBeenCalledTimes(2);
|
||
});
|
||
});
|
||
}); |