forked from datawhale/whale-town-end
432 lines
11 KiB
TypeScript
432 lines
11 KiB
TypeScript
/**
|
|
* 位置广播端到端测试
|
|
*
|
|
* 功能描述:
|
|
* - 测试位置广播系统的完整功能
|
|
* - 验证WebSocket连接和消息传递
|
|
* - 确保会话管理和用户状态同步
|
|
* - 测试位置更新和广播机制
|
|
*
|
|
* 最近修改:
|
|
* - 2026-01-08: 文件重命名 - 修正kebab-case为snake_case命名规范 (修改者: moyin)
|
|
*
|
|
* @author original
|
|
* @version 1.0.1
|
|
* @since 2025-01-01
|
|
* @lastModified 2026-01-08
|
|
*/
|
|
|
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { INestApplication } from '@nestjs/common';
|
|
import { io, Socket } from 'socket.io-client';
|
|
import { LocationBroadcastModule } from '../../location_broadcast.module';
|
|
|
|
describe('LocationBroadcast (e2e)', () => {
|
|
let app: INestApplication;
|
|
let authToken: string;
|
|
|
|
beforeAll(async () => {
|
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
imports: [LocationBroadcastModule],
|
|
}).compile();
|
|
|
|
app = moduleFixture.createNestApplication();
|
|
await app.init();
|
|
await app.listen(0);
|
|
|
|
authToken = 'test-jwt-token';
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
describe('WebSocket连接测试', () => {
|
|
it('应该成功建立WebSocket连接', (done) => {
|
|
const port = app.getHttpServer().address().port;
|
|
const client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
client.on('connect', () => {
|
|
expect(client.connected).toBe(true);
|
|
client.disconnect();
|
|
done();
|
|
});
|
|
|
|
client.on('connect_error', (error) => {
|
|
done(error);
|
|
});
|
|
});
|
|
|
|
it('应该拒绝无效的认证令牌', (done) => {
|
|
const port = app.getHttpServer().address().port;
|
|
const client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: 'invalid-token' },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
client.on('connect_error', (error) => {
|
|
expect(error).toBeDefined();
|
|
done();
|
|
});
|
|
|
|
client.on('connect', () => {
|
|
client.disconnect();
|
|
done(new Error('应该拒绝无效令牌'));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('会话管理测试', () => {
|
|
let client: Socket;
|
|
|
|
beforeEach((done) => {
|
|
const port = app.getHttpServer().address().port;
|
|
client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
client.on('connect', () => {
|
|
done();
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (client) {
|
|
client.disconnect();
|
|
}
|
|
});
|
|
|
|
it('应该成功加入会话', (done) => {
|
|
client.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId: 'test-session-001',
|
|
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
|
});
|
|
|
|
client.on('session_joined', (response) => {
|
|
expect(response.success).toBe(true);
|
|
expect(response.sessionId).toBe('test-session-001');
|
|
done();
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
done(error);
|
|
});
|
|
});
|
|
|
|
it('应该成功离开会话', (done) => {
|
|
const sessionId = 'test-session-leave';
|
|
|
|
// 先加入会话
|
|
client.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId,
|
|
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
|
});
|
|
|
|
client.on('session_joined', () => {
|
|
// 然后离开会话
|
|
client.emit('leave_session', {
|
|
type: 'leave_session',
|
|
sessionId,
|
|
});
|
|
});
|
|
|
|
client.on('session_left', (response) => {
|
|
expect(response.success).toBe(true);
|
|
expect(response.sessionId).toBe(sessionId);
|
|
done();
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
done(error);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('位置更新测试', () => {
|
|
let client: Socket;
|
|
const sessionId = 'position-test-session';
|
|
|
|
beforeEach((done) => {
|
|
const port = app.getHttpServer().address().port;
|
|
client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
client.on('connect', () => {
|
|
client.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId,
|
|
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
|
});
|
|
});
|
|
|
|
client.on('session_joined', () => {
|
|
done();
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (client) {
|
|
client.disconnect();
|
|
}
|
|
});
|
|
|
|
it('应该成功更新位置', (done) => {
|
|
client.emit('position_update', {
|
|
type: 'position_update',
|
|
x: 150,
|
|
y: 250,
|
|
mapId: 'plaza',
|
|
});
|
|
|
|
client.on('position_update_success', (response) => {
|
|
expect(response.success).toBe(true);
|
|
expect(response.position.x).toBe(150);
|
|
expect(response.position.y).toBe(250);
|
|
done();
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
done(error);
|
|
});
|
|
});
|
|
|
|
it('应该拒绝无效的位置数据', (done) => {
|
|
client.emit('position_update', {
|
|
type: 'position_update',
|
|
x: 'invalid',
|
|
y: 250,
|
|
mapId: 'plaza',
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
expect(error.message).toContain('Invalid position data');
|
|
done();
|
|
});
|
|
|
|
client.on('position_update_success', () => {
|
|
done(new Error('应该拒绝无效位置数据'));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('位置广播测试', () => {
|
|
let client1: Socket;
|
|
let client2: Socket;
|
|
const sessionId = 'broadcast-test-session';
|
|
|
|
beforeEach((done) => {
|
|
const port = app.getHttpServer().address().port;
|
|
let connectedClients = 0;
|
|
|
|
client1 = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
client2 = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
const checkConnections = () => {
|
|
connectedClients++;
|
|
if (connectedClients === 2) {
|
|
// 两个客户端都加入同一会话
|
|
client1.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId,
|
|
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
|
});
|
|
|
|
client2.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId,
|
|
initialPosition: { x: 300, y: 400, mapId: 'plaza' },
|
|
});
|
|
}
|
|
};
|
|
|
|
let joinedClients = 0;
|
|
const checkJoins = () => {
|
|
joinedClients++;
|
|
if (joinedClients === 2) {
|
|
done();
|
|
}
|
|
};
|
|
|
|
client1.on('connect', checkConnections);
|
|
client2.on('connect', checkConnections);
|
|
client1.on('session_joined', checkJoins);
|
|
client2.on('session_joined', checkJoins);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (client1) client1.disconnect();
|
|
if (client2) client2.disconnect();
|
|
});
|
|
|
|
it('应该向同会话的其他用户广播位置更新', (done) => {
|
|
// client2 监听广播
|
|
client2.on('position_broadcast', (broadcast) => {
|
|
expect(broadcast.userId).toBeDefined();
|
|
expect(broadcast.position.x).toBe(150);
|
|
expect(broadcast.position.y).toBe(250);
|
|
done();
|
|
});
|
|
|
|
// client1 更新位置
|
|
client1.emit('position_update', {
|
|
type: 'position_update',
|
|
x: 150,
|
|
y: 250,
|
|
mapId: 'plaza',
|
|
});
|
|
});
|
|
|
|
it('不应该向不同会话的用户广播位置更新', (done) => {
|
|
const differentSessionId = 'different-session';
|
|
let broadcastReceived = false;
|
|
|
|
// client2 加入不同会话
|
|
client2.emit('leave_session', {
|
|
type: 'leave_session',
|
|
sessionId,
|
|
});
|
|
|
|
client2.on('session_left', () => {
|
|
client2.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId: differentSessionId,
|
|
initialPosition: { x: 300, y: 400, mapId: 'plaza' },
|
|
});
|
|
});
|
|
|
|
client2.on('session_joined', () => {
|
|
// 监听广播
|
|
client2.on('position_broadcast', () => {
|
|
broadcastReceived = true;
|
|
});
|
|
|
|
// client1 更新位置
|
|
client1.emit('position_update', {
|
|
type: 'position_update',
|
|
x: 150,
|
|
y: 250,
|
|
mapId: 'plaza',
|
|
});
|
|
|
|
// 等待一段时间确认没有收到广播
|
|
const timeoutId = setTimeout(() => {
|
|
expect(broadcastReceived).toBe(false);
|
|
done();
|
|
}, 1000);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('心跳机制测试', () => {
|
|
let client: Socket;
|
|
|
|
beforeEach((done) => {
|
|
const port = app.getHttpServer().address().port;
|
|
client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
client.on('connect', () => {
|
|
done();
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (client) {
|
|
client.disconnect();
|
|
}
|
|
});
|
|
|
|
it('应该响应心跳消息', (done) => {
|
|
const timestamp = Date.now();
|
|
|
|
client.emit('heartbeat', {
|
|
type: 'heartbeat',
|
|
timestamp,
|
|
});
|
|
|
|
client.on('heartbeat_response', (response) => {
|
|
expect(response.timestamp).toBe(timestamp);
|
|
expect(response.serverTime).toBeDefined();
|
|
done();
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
done(error);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('错误处理测试', () => {
|
|
let client: Socket;
|
|
|
|
beforeEach((done) => {
|
|
const port = app.getHttpServer().address().port;
|
|
client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
client.on('connect', () => {
|
|
done();
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (client) {
|
|
client.disconnect();
|
|
}
|
|
});
|
|
|
|
it('应该处理无效的消息格式', (done) => {
|
|
client.emit('invalid_message', 'not an object');
|
|
|
|
client.on('error', (error) => {
|
|
expect(error.message).toContain('Invalid message format');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('应该处理未知的消息类型', (done) => {
|
|
client.emit('unknown_type', {
|
|
type: 'unknown_message_type',
|
|
data: 'test',
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
expect(error.message).toContain('Unknown message type');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('应该处理在未加入会话时的位置更新', (done) => {
|
|
client.emit('position_update', {
|
|
type: 'position_update',
|
|
x: 100,
|
|
y: 200,
|
|
mapId: 'plaza',
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
expect(error.message).toContain('Not in session');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
}); |