/** * 会话清理定时任务服务测试 * * 功能描述: * - 测试SessionCleanupService的核心功能 * - 包含属性测试验证定时清理机制 * - 包含属性测试验证资源释放完整性 * * **Feature: zulip-integration, Property 13: 定时清理机制** * **Validates: Requirements 6.1, 6.2, 6.3** * * **Feature: zulip-integration, Property 14: 资源释放完整性** * **Validates: Requirements 6.4, 6.5** * * @author angjustinl * @version 1.0.0 * @since 2025-12-31 */ import { Test, TestingModule } from '@nestjs/testing'; import * as fc from 'fast-check'; import { SessionCleanupService, CleanupConfig, CleanupResult } from './session_cleanup.service'; import { SessionManagerService } from './session_manager.service'; import { IZulipClientPoolService } from '../../../core/zulip_core/zulip_core.interfaces'; describe('SessionCleanupService', () => { let service: SessionCleanupService; let mockSessionManager: jest.Mocked; let mockZulipClientPool: jest.Mocked; // 模拟清理结果 const createMockCleanupResult = (overrides: Partial = {}): any => ({ cleanedCount: 3, zulipQueueIds: ['queue-1', 'queue-2', 'queue-3'], duration: 150, timestamp: new Date(), ...overrides, }); beforeEach(async () => { jest.clearAllMocks(); jest.clearAllTimers(); // 确保每个测试开始时都使用真实定时器 jest.useRealTimers(); mockSessionManager = { cleanupExpiredSessions: jest.fn(), getSession: jest.fn(), destroySession: jest.fn(), createSession: jest.fn(), updatePlayerPosition: jest.fn(), getSocketsInMap: jest.fn(), injectContext: jest.fn(), } as any; mockZulipClientPool = { createUserClient: jest.fn(), getUserClient: jest.fn(), hasUserClient: jest.fn(), sendMessage: jest.fn(), registerEventQueue: jest.fn(), deregisterEventQueue: jest.fn(), destroyUserClient: jest.fn(), getPoolStats: jest.fn(), cleanupIdleClients: jest.fn(), } as any; const module: TestingModule = await Test.createTestingModule({ providers: [ SessionCleanupService, { provide: SessionManagerService, useValue: mockSessionManager, }, { provide: 'ZULIP_CLIENT_POOL_SERVICE', useValue: mockZulipClientPool, }, ], }).compile(); service = module.get(SessionCleanupService); }); afterEach(async () => { // 确保停止所有清理任务 service.stopCleanupTask(); // 等待任何正在进行的异步操作完成 await new Promise(resolve => setImmediate(resolve)); // 清理定时器 jest.clearAllTimers(); // 恢复真实定时器 jest.useRealTimers(); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('startCleanupTask - 启动清理任务', () => { it('应该启动定时清理任务', () => { service.startCleanupTask(); const status = service.getStatus(); expect(status.isEnabled).toBe(true); }); it('应该在已启动时不重复启动', () => { service.startCleanupTask(); service.startCleanupTask(); // 第二次调用 const status = service.getStatus(); expect(status.isEnabled).toBe(true); }); it('应该立即执行一次清理', async () => { jest.useFakeTimers(); mockSessionManager.cleanupExpiredSessions.mockResolvedValue( createMockCleanupResult({ cleanedCount: 2 }) ); service.startCleanupTask(); // 等待立即执行的清理完成 await jest.runOnlyPendingTimersAsync(); expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(30); // 确保清理任务被停止 service.stopCleanupTask(); jest.useRealTimers(); }); }); describe('stopCleanupTask - 停止清理任务', () => { it('应该停止定时清理任务', () => { service.startCleanupTask(); service.stopCleanupTask(); const status = service.getStatus(); expect(status.isEnabled).toBe(false); }); it('应该在未启动时安全停止', () => { service.stopCleanupTask(); const status = service.getStatus(); expect(status.isEnabled).toBe(false); }); }); describe('runCleanup - 执行清理', () => { it('应该成功执行清理并返回结果', async () => { const mockResult = createMockCleanupResult({ cleanedCount: 5, zulipQueueIds: ['queue-1', 'queue-2', 'queue-3', 'queue-4', 'queue-5'], }); mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult); const result = await service.runCleanup(); expect(result.success).toBe(true); expect(result.cleanedSessions).toBe(5); expect(result.deregisteredQueues).toBe(5); expect(result.duration).toBeGreaterThanOrEqual(0); // 修改为 >= 0,因为测试环境可能很快 expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(30); }); it('应该处理清理过程中的错误', async () => { const error = new Error('清理失败'); mockSessionManager.cleanupExpiredSessions.mockRejectedValue(error); const result = await service.runCleanup(); expect(result.success).toBe(false); expect(result.error).toBe('清理失败'); expect(result.cleanedSessions).toBe(0); expect(result.deregisteredQueues).toBe(0); }); it('应该防止并发执行', async () => { let resolveFirst: () => void; const firstPromise = new Promise(resolve => { resolveFirst = () => resolve(createMockCleanupResult()); }); mockSessionManager.cleanupExpiredSessions.mockReturnValueOnce(firstPromise); // 同时启动两个清理任务 const promise1 = service.runCleanup(); const promise2 = service.runCleanup(); // 第二个应该立即返回失败 const result2 = await promise2; expect(result2.success).toBe(false); expect(result2.error).toContain('正在执行中'); // 完成第一个任务 resolveFirst!(); const result1 = await promise1; expect(result1.success).toBe(true); }, 15000); it('应该记录最后一次清理结果', async () => { const mockResult = createMockCleanupResult({ cleanedCount: 3 }); mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult); await service.runCleanup(); const lastResult = service.getLastCleanupResult(); expect(lastResult).not.toBeNull(); expect(lastResult!.cleanedSessions).toBe(3); expect(lastResult!.success).toBe(true); }); }); describe('getStatus - 获取状态', () => { it('应该返回正确的状态信息', () => { const status = service.getStatus(); expect(status).toHaveProperty('isRunning'); expect(status).toHaveProperty('isEnabled'); expect(status).toHaveProperty('config'); expect(status).toHaveProperty('lastResult'); expect(typeof status.isRunning).toBe('boolean'); expect(typeof status.isEnabled).toBe('boolean'); }); it('应该反映任务启动状态', () => { let status = service.getStatus(); expect(status.isEnabled).toBe(false); service.startCleanupTask(); status = service.getStatus(); expect(status.isEnabled).toBe(true); service.stopCleanupTask(); status = service.getStatus(); expect(status.isEnabled).toBe(false); }); }); describe('updateConfig - 更新配置', () => { it('应该更新清理配置', () => { const newConfig: Partial = { intervalMs: 10 * 60 * 1000, // 10分钟 sessionTimeoutMinutes: 60, // 60分钟 }; service.updateConfig(newConfig); const status = service.getStatus(); expect(status.config.intervalMs).toBe(10 * 60 * 1000); expect(status.config.sessionTimeoutMinutes).toBe(60); }); it('应该在配置更改后重启任务', () => { service.startCleanupTask(); const newConfig: Partial = { intervalMs: 2 * 60 * 1000, // 2分钟 }; service.updateConfig(newConfig); const status = service.getStatus(); expect(status.isEnabled).toBe(true); expect(status.config.intervalMs).toBe(2 * 60 * 1000); }); it('应该支持禁用清理任务', () => { service.startCleanupTask(); service.updateConfig({ enabled: false }); const status = service.getStatus(); expect(status.isEnabled).toBe(false); }); }); /** * 属性测试: 定时清理机制 * * **Feature: zulip-integration, Property 13: 定时清理机制** * **Validates: Requirements 6.1, 6.2, 6.3** * * 系统应该定期清理过期的游戏会话,释放相关资源, * 并确保清理过程不影响正常的游戏服务 */ describe('Property 13: 定时清理机制', () => { /** * 属性: 对于任何有效的清理配置,系统应该按配置间隔执行清理 * 验证需求 6.1: 系统应定期检查并清理过期的游戏会话 */ it('对于任何有效的清理配置,系统应该按配置间隔执行清理', async () => { await fc.assert( fc.asyncProperty( // 生成有效的清理间隔(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(); try { const config: Partial = { intervalMs, sessionTimeoutMinutes, enabled: true, }; // 模拟清理结果 mockSessionManager.cleanupExpiredSessions.mockResolvedValue( createMockCleanupResult({ cleanedCount: 2 }) ); 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); // 验证立即执行了一次清理 await jest.runOnlyPendingTimersAsync(); expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(sessionTimeoutMinutes); } finally { service.stopCleanupTask(); jest.useRealTimers(); } } ), { numRuns: 20, timeout: 5000 } // 减少运行次数并添加超时 ); }, 15000); /** * 属性: 对于任何清理操作,都应该记录清理结果和统计信息 * 验证需求 6.2: 清理过程中系统应记录清理的会话数量和释放的资源 */ it('对于任何清理操作,都应该记录清理结果和统计信息', async () => { await fc.assert( fc.asyncProperty( // 生成清理的会话数量 fc.integer({ min: 0, max: 10 }), // 生成Zulip队列ID列表 fc.array( fc.string({ minLength: 5, maxLength: 15 }).filter(s => s.trim().length > 0), { minLength: 0, maxLength: 10 } ), async (cleanedCount, queueIds) => { // 重置mock以确保每次测试都是干净的状态 jest.clearAllMocks(); const mockResult = createMockCleanupResult({ cleanedCount, zulipQueueIds: queueIds.slice(0, cleanedCount), // 确保队列数量不超过清理数量 }); mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult); const result = await service.runCleanup(); // 验证清理结果被正确记录 expect(result.success).toBe(true); expect(result.cleanedSessions).toBe(cleanedCount); expect(result.deregisteredQueues).toBe(Math.min(queueIds.length, cleanedCount)); expect(result.duration).toBeGreaterThanOrEqual(0); expect(result.timestamp).toBeInstanceOf(Date); // 验证最后一次清理结果被保存 const lastResult = service.getLastCleanupResult(); expect(lastResult).not.toBeNull(); expect(lastResult!.cleanedSessions).toBe(cleanedCount); } ), { numRuns: 20, timeout: 3000 } // 减少运行次数并添加超时 ); }, 10000); /** * 属性: 清理过程中发生错误时,系统应该正确处理并记录错误信息 * 验证需求 6.3: 清理过程中出现错误时系统应记录错误信息并继续正常服务 */ it('清理过程中发生错误时,系统应该正确处理并记录错误信息', async () => { await fc.assert( fc.asyncProperty( // 生成各种错误消息 fc.string({ minLength: 5, maxLength: 50 }).filter(s => s.trim().length > 0), async (errorMessage) => { // 重置mock以确保每次测试都是干净的状态 jest.clearAllMocks(); const error = new Error(errorMessage.trim()); mockSessionManager.cleanupExpiredSessions.mockRejectedValue(error); const result = await service.runCleanup(); // 验证错误被正确处理 expect(result.success).toBe(false); expect(result.error).toBe(errorMessage.trim()); expect(result.cleanedSessions).toBe(0); expect(result.deregisteredQueues).toBe(0); expect(result.duration).toBeGreaterThanOrEqual(0); // 验证错误结果被保存 const lastResult = service.getLastCleanupResult(); expect(lastResult).not.toBeNull(); expect(lastResult!.success).toBe(false); expect(lastResult!.error).toBe(errorMessage.trim()); } ), { numRuns: 20, timeout: 3000 } // 减少运行次数并添加超时 ); }, 10000); /** * 属性: 并发清理请求应该被正确处理,避免重复执行 * 验证需求 6.1: 系统应避免同时执行多个清理任务 */ it('并发清理请求应该被正确处理,避免重复执行', async () => { // 重置mock jest.clearAllMocks(); // 创建一个可控的Promise,使用实际的异步行为 let resolveCleanup: (value: any) => void; const cleanupPromise = new Promise(resolve => { resolveCleanup = resolve; }); mockSessionManager.cleanupExpiredSessions.mockReturnValue(cleanupPromise); // 启动第一个清理请求(应该成功) const promise1 = service.runCleanup(); // 等待一个微任务周期,确保第一个请求开始执行 await Promise.resolve(); // 启动第二个和第三个清理请求(应该被拒绝) const promise2 = service.runCleanup(); const promise3 = service.runCleanup(); // 第二个和第三个请求应该立即返回失败 const result2 = await promise2; const result3 = await promise3; expect(result2.success).toBe(false); expect(result2.error).toContain('正在执行中'); expect(result3.success).toBe(false); expect(result3.error).toContain('正在执行中'); // 完成第一个清理操作 resolveCleanup!(createMockCleanupResult({ cleanedCount: 1 })); const result1 = await promise1; expect(result1.success).toBe(true); }, 10000); }); /** * 属性测试: 资源释放完整性 * * **Feature: zulip-integration, Property 14: 资源释放完整性** * **Validates: Requirements 6.4, 6.5** * * 清理过期会话时,系统应该完整释放所有相关资源, * 包括Zulip事件队列、内存缓存等,确保不会造成资源泄漏 */ describe('Property 14: 资源释放完整性', () => { /** * 属性: 对于任何过期会话,清理时应该释放所有相关的Zulip资源 * 验证需求 6.4: 清理会话时系统应注销对应的Zulip事件队列 */ it('对于任何过期会话,清理时应该释放所有相关的Zulip资源', async () => { await fc.assert( fc.asyncProperty( // 生成过期会话数量 fc.integer({ min: 1, max: 5 }), // 生成每个会话对应的Zulip队列ID fc.array( fc.string({ minLength: 8, maxLength: 15 }).filter(s => s.trim().length > 0), { minLength: 1, maxLength: 5 } ), async (sessionCount, queueIds) => { // 重置mock以确保每次测试都是干净的状态 jest.clearAllMocks(); const actualQueueIds = queueIds.slice(0, sessionCount); const mockResult = createMockCleanupResult({ cleanedCount: sessionCount, zulipQueueIds: actualQueueIds, }); mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult); const result = await service.runCleanup(); // 验证清理成功 expect(result.success).toBe(true); expect(result.cleanedSessions).toBe(sessionCount); // 验证Zulip队列被处理(这里简化为计数验证) expect(result.deregisteredQueues).toBe(actualQueueIds.length); // 验证SessionManager被调用清理过期会话 expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(30); } ), { numRuns: 20, timeout: 3000 } // 减少运行次数并添加超时 ); }, 10000); /** * 属性: 清理操作应该是原子性的,要么全部成功要么全部回滚 * 验证需求 6.5: 清理过程应确保数据一致性,避免部分清理导致的不一致状态 */ it('清理操作应该是原子性的,要么全部成功要么全部回滚', async () => { await fc.assert( fc.asyncProperty( // 生成是否模拟清理失败 fc.boolean(), // 生成会话数量 fc.integer({ min: 1, max: 3 }), async (shouldFail, sessionCount) => { // 重置mock以确保每次测试都是干净的状态 jest.clearAllMocks(); if (shouldFail) { // 模拟清理失败 const error = new Error('清理操作失败'); mockSessionManager.cleanupExpiredSessions.mockRejectedValue(error); } else { // 模拟清理成功 const mockResult = createMockCleanupResult({ cleanedCount: sessionCount, zulipQueueIds: Array.from({ length: sessionCount }, (_, i) => `queue-${i}`), }); mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult); } const result = await service.runCleanup(); if (shouldFail) { // 失败时应该没有任何资源被释放 expect(result.success).toBe(false); expect(result.cleanedSessions).toBe(0); expect(result.deregisteredQueues).toBe(0); expect(result.error).toBeDefined(); } else { // 成功时所有资源都应该被正确处理 expect(result.success).toBe(true); expect(result.cleanedSessions).toBe(sessionCount); expect(result.deregisteredQueues).toBe(sessionCount); expect(result.error).toBeUndefined(); } // 验证结果的一致性 expect(result.timestamp).toBeInstanceOf(Date); expect(result.duration).toBeGreaterThanOrEqual(0); } ), { numRuns: 20, timeout: 3000 } // 减少运行次数并添加超时 ); }, 10000); /** * 属性: 清理配置更新应该正确重启清理任务而不丢失状态 * 验证需求 6.5: 配置更新时系统应保持服务连续性 */ it('清理配置更新应该正确重启清理任务而不丢失状态', async () => { await fc.assert( fc.asyncProperty( // 生成初始配置 fc.record({ 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: 3 }).map(m => m * 60 * 1000), sessionTimeoutMinutes: fc.integer({ min: 10, max: 30 }), }), async (initialConfig, newConfig) => { // 重置mock以确保每次测试都是干净的状态 jest.clearAllMocks(); try { // 设置初始配置并启动任务 service.updateConfig(initialConfig); service.startCleanupTask(); let status = service.getStatus(); expect(status.isEnabled).toBe(true); expect(status.config.intervalMs).toBe(initialConfig.intervalMs); // 更新配置 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); } finally { service.stopCleanupTask(); } } ), { numRuns: 15, timeout: 3000 } // 减少运行次数并添加超时 ); }, 10000); }); describe('模块生命周期', () => { it('应该在模块初始化时启动清理任务', async () => { // 重新创建服务实例来测试模块初始化 const module: TestingModule = await Test.createTestingModule({ providers: [ SessionCleanupService, { provide: SessionManagerService, useValue: mockSessionManager, }, { provide: 'ZULIP_CLIENT_POOL_SERVICE', useValue: mockZulipClientPool, }, ], }).compile(); const newService = module.get(SessionCleanupService); // 模拟模块初始化 await newService.onModuleInit(); const status = newService.getStatus(); expect(status.isEnabled).toBe(true); // 清理 await newService.onModuleDestroy(); }); it('应该在模块销毁时停止清理任务', async () => { service.startCleanupTask(); await service.onModuleDestroy(); const status = service.getStatus(); expect(status.isEnabled).toBe(false); }); }); });