refactor: 更新WebSocket相关测试和location_broadcast模块

- 更新location_broadcast网关以支持原生WebSocket
- 修改WebSocket认证守卫和中间件
- 更新相关的测试文件和规范
- 添加WebSocket测试工具
- 完善Zulip服务的测试覆盖

技术改进:
- 统一WebSocket实现架构
- 优化性能监控和限流中间件
- 更新测试用例以适配新的WebSocket实现
This commit is contained in:
moyin
2026-01-09 17:02:43 +08:00
parent e9dc887c59
commit cbf4120ddd
13 changed files with 752 additions and 524 deletions

View File

@@ -13,7 +13,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check';
import { MessageFilterService, ViolationType } from './message_filter.service';
import { IZulipConfigService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
import { IZulipConfigService } from '../../../core/zulip_core/zulip_core.interfaces';
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
import { IRedisService } from '../../../core/redis/redis.interface';

View File

@@ -25,7 +25,7 @@ import {
CleanupResult
} from './session_cleanup.service';
import { SessionManagerService } from './session_manager.service';
import { IZulipClientPoolService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
import { IZulipClientPoolService } from '../../../core/zulip_core/zulip_core.interfaces';
describe('SessionCleanupService', () => {
let service: SessionCleanupService;
@@ -43,8 +43,9 @@ describe('SessionCleanupService', () => {
beforeEach(async () => {
jest.clearAllMocks();
// Only use fake timers for tests that need them
// The concurrent test will use real timers for proper Promise handling
jest.clearAllTimers();
// 确保每个测试开始时都使用真实定时器
jest.useRealTimers();
mockSessionManager = {
cleanupExpiredSessions: jest.fn(),
@@ -85,12 +86,18 @@ describe('SessionCleanupService', () => {
service = module.get<SessionCleanupService>(SessionCleanupService);
});
afterEach(() => {
afterEach(async () => {
// 确保停止所有清理任务
service.stopCleanupTask();
// Only restore timers if they were faked
if (jest.isMockFunction(setTimeout)) {
jest.useRealTimers();
}
// 等待任何正在进行的异步操作完成
await new Promise(resolve => setImmediate(resolve));
// 清理定时器
jest.clearAllTimers();
// 恢复真实定时器
jest.useRealTimers();
});
it('should be defined', () => {
@@ -127,6 +134,8 @@ describe('SessionCleanupService', () => {
expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(30);
// 确保清理任务被停止
service.stopCleanupTask();
jest.useRealTimers();
});
});
@@ -294,46 +303,49 @@ describe('SessionCleanupService', () => {
it('对于任何有效的清理配置,系统应该按配置间隔执行清理', async () => {
await fc.assert(
fc.asyncProperty(
// 生成有效的清理间隔1-10分钟
fc.integer({ min: 1, max: 10 }).map(minutes => minutes * 60 * 1000),
// 生成有效的会话超时时间10-120分钟
fc.integer({ min: 10, max: 120 }),
// 生成有效的清理间隔1-5分钟减少范围
fc.integer({ min: 1, max: 5 }).map(minutes => minutes * 60 * 1000),
// 生成有效的会话超时时间10-60分钟,减少范围
fc.integer({ min: 10, max: 60 }),
async (intervalMs, sessionTimeoutMinutes) => {
// 重置mock以确保每次测试都是干净的状态
jest.clearAllMocks();
jest.useFakeTimers();
const config: Partial<CleanupConfig> = {
intervalMs,
sessionTimeoutMinutes,
enabled: true,
};
try {
const config: Partial<CleanupConfig> = {
intervalMs,
sessionTimeoutMinutes,
enabled: true,
};
// 模拟清理结果
mockSessionManager.cleanupExpiredSessions.mockResolvedValue(
createMockCleanupResult({ cleanedCount: 2 })
);
// 模拟清理结果
mockSessionManager.cleanupExpiredSessions.mockResolvedValue(
createMockCleanupResult({ cleanedCount: 2 })
);
service.updateConfig(config);
service.startCleanupTask();
service.updateConfig(config);
service.startCleanupTask();
// 验证配置被正确设置
const status = service.getStatus();
expect(status.config.intervalMs).toBe(intervalMs);
expect(status.config.sessionTimeoutMinutes).toBe(sessionTimeoutMinutes);
expect(status.isEnabled).toBe(true);
// 验证配置被正确设置
const status = service.getStatus();
expect(status.config.intervalMs).toBe(intervalMs);
expect(status.config.sessionTimeoutMinutes).toBe(sessionTimeoutMinutes);
expect(status.isEnabled).toBe(true);
// 验证立即执行了一次清理
await jest.runOnlyPendingTimersAsync();
expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(sessionTimeoutMinutes);
// 验证立即执行了一次清理
await jest.runOnlyPendingTimersAsync();
expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(sessionTimeoutMinutes);
service.stopCleanupTask();
jest.useRealTimers();
} finally {
service.stopCleanupTask();
jest.useRealTimers();
}
}
),
{ numRuns: 50 }
{ numRuns: 20, timeout: 5000 } // 减少运行次数并添加超时
);
}, 30000);
}, 15000);
/**
* 属性: 对于任何清理操作,都应该记录清理结果和统计信息
@@ -343,11 +355,11 @@ describe('SessionCleanupService', () => {
await fc.assert(
fc.asyncProperty(
// 生成清理的会话数量
fc.integer({ min: 0, max: 20 }),
fc.integer({ min: 0, max: 10 }),
// 生成Zulip队列ID列表
fc.array(
fc.string({ minLength: 5, maxLength: 20 }).filter(s => s.trim().length > 0),
{ minLength: 0, maxLength: 20 }
fc.string({ minLength: 5, maxLength: 15 }).filter(s => s.trim().length > 0),
{ minLength: 0, maxLength: 10 }
),
async (cleanedCount, queueIds) => {
// 重置mock以确保每次测试都是干净的状态
@@ -375,9 +387,9 @@ describe('SessionCleanupService', () => {
expect(lastResult!.cleanedSessions).toBe(cleanedCount);
}
),
{ numRuns: 50 }
{ numRuns: 20, timeout: 3000 } // 减少运行次数并添加超时
);
}, 30000);
}, 10000);
/**
* 属性: 清理过程中发生错误时,系统应该正确处理并记录错误信息
@@ -387,7 +399,7 @@ describe('SessionCleanupService', () => {
await fc.assert(
fc.asyncProperty(
// 生成各种错误消息
fc.string({ minLength: 5, maxLength: 100 }).filter(s => s.trim().length > 0),
fc.string({ minLength: 5, maxLength: 50 }).filter(s => s.trim().length > 0),
async (errorMessage) => {
// 重置mock以确保每次测试都是干净的状态
jest.clearAllMocks();
@@ -411,9 +423,9 @@ describe('SessionCleanupService', () => {
expect(lastResult!.error).toBe(errorMessage.trim());
}
),
{ numRuns: 50 }
{ numRuns: 20, timeout: 3000 } // 减少运行次数并添加超时
);
}, 30000);
}, 10000);
/**
* 属性: 并发清理请求应该被正确处理,避免重复执行
@@ -475,11 +487,11 @@ describe('SessionCleanupService', () => {
await fc.assert(
fc.asyncProperty(
// 生成过期会话数量
fc.integer({ min: 1, max: 10 }),
fc.integer({ min: 1, max: 5 }),
// 生成每个会话对应的Zulip队列ID
fc.array(
fc.string({ minLength: 8, maxLength: 20 }).filter(s => s.trim().length > 0),
{ minLength: 1, maxLength: 10 }
fc.string({ minLength: 8, maxLength: 15 }).filter(s => s.trim().length > 0),
{ minLength: 1, maxLength: 5 }
),
async (sessionCount, queueIds) => {
// 重置mock以确保每次测试都是干净的状态
@@ -506,9 +518,9 @@ describe('SessionCleanupService', () => {
expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(30);
}
),
{ numRuns: 50 }
{ numRuns: 20, timeout: 3000 } // 减少运行次数并添加超时
);
}, 30000);
}, 10000);
/**
* 属性: 清理操作应该是原子性的,要么全部成功要么全部回滚
@@ -520,7 +532,7 @@ describe('SessionCleanupService', () => {
// 生成是否模拟清理失败
fc.boolean(),
// 生成会话数量
fc.integer({ min: 1, max: 5 }),
fc.integer({ min: 1, max: 3 }),
async (shouldFail, sessionCount) => {
// 重置mock以确保每次测试都是干净的状态
jest.clearAllMocks();
@@ -559,9 +571,9 @@ describe('SessionCleanupService', () => {
expect(result.duration).toBeGreaterThanOrEqual(0);
}
),
{ numRuns: 50 }
{ numRuns: 20, timeout: 3000 } // 减少运行次数并添加超时
);
}, 30000);
}, 10000);
/**
* 属性: 清理配置更新应该正确重启清理任务而不丢失状态
@@ -572,41 +584,44 @@ describe('SessionCleanupService', () => {
fc.asyncProperty(
// 生成初始配置
fc.record({
intervalMs: fc.integer({ min: 1, max: 5 }).map(m => m * 60 * 1000),
sessionTimeoutMinutes: fc.integer({ min: 10, max: 60 }),
intervalMs: fc.integer({ min: 1, max: 3 }).map(m => m * 60 * 1000),
sessionTimeoutMinutes: fc.integer({ min: 10, max: 30 }),
}),
// 生成新配置
fc.record({
intervalMs: fc.integer({ min: 1, max: 5 }).map(m => m * 60 * 1000),
sessionTimeoutMinutes: fc.integer({ min: 10, max: 60 }),
intervalMs: fc.integer({ min: 1, max: 3 }).map(m => m * 60 * 1000),
sessionTimeoutMinutes: fc.integer({ min: 10, max: 30 }),
}),
async (initialConfig, newConfig) => {
// 重置mock以确保每次测试都是干净的状态
jest.clearAllMocks();
// 设置初始配置并启动任务
service.updateConfig(initialConfig);
service.startCleanupTask();
try {
// 设置初始配置并启动任务
service.updateConfig(initialConfig);
service.startCleanupTask();
let status = service.getStatus();
expect(status.isEnabled).toBe(true);
expect(status.config.intervalMs).toBe(initialConfig.intervalMs);
let status = service.getStatus();
expect(status.isEnabled).toBe(true);
expect(status.config.intervalMs).toBe(initialConfig.intervalMs);
// 更新配置
service.updateConfig(newConfig);
// 更新配置
service.updateConfig(newConfig);
// 验证配置更新后任务仍在运行
status = service.getStatus();
expect(status.isEnabled).toBe(true);
expect(status.config.intervalMs).toBe(newConfig.intervalMs);
expect(status.config.sessionTimeoutMinutes).toBe(newConfig.sessionTimeoutMinutes);
// 验证配置更新后任务仍在运行
status = service.getStatus();
expect(status.isEnabled).toBe(true);
expect(status.config.intervalMs).toBe(newConfig.intervalMs);
expect(status.config.sessionTimeoutMinutes).toBe(newConfig.sessionTimeoutMinutes);
service.stopCleanupTask();
} finally {
service.stopCleanupTask();
}
}
),
{ numRuns: 30 }
{ numRuns: 15, timeout: 3000 } // 减少运行次数并添加超时
);
}, 30000);
}, 10000);
});
describe('模块生命周期', () => {

View File

@@ -158,6 +158,13 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy {
}
}
/**
* 获取当前定时器引用(用于测试)
*/
getCleanupInterval(): NodeJS.Timeout | null {
return this.cleanupInterval;
}
/**
* 执行一次清理
*

View File

@@ -13,7 +13,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check';
import { SessionManagerService, GameSession, Position } from './session_manager.service';
import { IZulipConfigService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
import { IZulipConfigService } from '../../../core/zulip_core/zulip_core.interfaces';
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
import { IRedisService } from '../../../core/redis/redis.interface';
@@ -154,6 +154,9 @@ describe('SessionManagerService', () => {
// 清理内存存储
memoryStore.clear();
memorySets.clear();
// 等待任何正在进行的异步操作完成
await new Promise(resolve => setImmediate(resolve));
});
it('should be defined', () => {
@@ -399,9 +402,9 @@ describe('SessionManagerService', () => {
expect(retrievedSession?.zulipQueueId).toBe(createdSession.zulipQueueId);
}
),
{ numRuns: 100 }
{ numRuns: 50, timeout: 5000 } // 添加超时控制
);
}, 60000);
}, 30000);
/**
* 属性: 对于任何位置更新,会话应该正确反映新位置
@@ -449,9 +452,9 @@ describe('SessionManagerService', () => {
expect(session?.position.y).toBe(y);
}
),
{ numRuns: 100 }
{ numRuns: 50, timeout: 5000 } // 添加超时控制
);
}, 60000);
}, 30000);
/**
* 属性: 对于任何地图切换,玩家应该从旧地图移除并添加到新地图
@@ -499,9 +502,9 @@ describe('SessionManagerService', () => {
}
}
),
{ numRuns: 100 }
{ numRuns: 50, timeout: 5000 } // 添加超时控制
);
}, 60000);
}, 30000);
/**
* 属性: 对于任何会话销毁,所有相关数据应该被清理
@@ -551,9 +554,9 @@ describe('SessionManagerService', () => {
expect(mapPlayers).not.toContain(socketId.trim());
}
),
{ numRuns: 100 }
{ numRuns: 50, timeout: 5000 } // 添加超时控制
);
}, 60000);
}, 30000);
/**
* 属性: 创建-更新-销毁的完整生命周期应该正确管理会话状态
@@ -613,8 +616,8 @@ describe('SessionManagerService', () => {
expect(finalSession).toBeNull();
}
),
{ numRuns: 100 }
{ numRuns: 50, timeout: 5000 } // 添加超时控制
);
}, 60000);
}, 30000);
});
});

View File

@@ -26,7 +26,7 @@ import {
MessageDistributor,
} from './zulip_event_processor.service';
import { SessionManagerService, GameSession } from './session_manager.service';
import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip_core/zulip_core.interfaces';
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
describe('ZulipEventProcessorService', () => {