/** * 配置管理服务测试 * * 功能描述: * - 测试ConfigManagerService的核心功能 * - 包含属性测试验证配置验证正确性 * * @author angjustinl, moyin * @version 1.0.0 * @since 2025-12-25 */ import { Test, TestingModule } from '@nestjs/testing'; import * as fc from 'fast-check'; import { ConfigManagerService, MapConfig, ZulipConfig } from './config_manager.service'; import { AppLoggerService } from '../../utils/logger/logger.service'; import * as fs from 'fs'; import * as path from 'path'; // Mock fs module jest.mock('fs'); describe('ConfigManagerService', () => { let service: ConfigManagerService; let mockLogger: jest.Mocked; const mockFs = fs as jest.Mocked; // 默认有效配置 const validMapConfig = { maps: [ { mapId: 'novice_village', mapName: '新手村', zulipStream: 'Novice Village', interactionObjects: [ { objectId: 'notice_board', objectName: '公告板', zulipTopic: 'Notice Board', position: { x: 100, y: 150 } } ] }, { mapId: 'tavern', mapName: '酒馆', zulipStream: 'Tavern', interactionObjects: [ { objectId: 'bar_counter', objectName: '吧台', zulipTopic: 'Bar Counter', position: { x: 150, y: 100 } } ] } ] }; beforeEach(async () => { jest.clearAllMocks(); // 设置测试环境变量 process.env.NODE_ENV = 'test'; process.env.ZULIP_SERVER_URL = 'https://test-zulip.com'; process.env.ZULIP_BOT_EMAIL = 'test-bot@test.com'; process.env.ZULIP_BOT_API_KEY = 'test-api-key'; process.env.ZULIP_API_KEY_ENCRYPTION_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; mockLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn(), } as any; // 默认mock fs行为 mockFs.existsSync.mockReturnValue(true); mockFs.readFileSync.mockReturnValue(JSON.stringify(validMapConfig)); mockFs.writeFileSync.mockImplementation(() => {}); mockFs.mkdirSync.mockImplementation(() => undefined); const module: TestingModule = await Test.createTestingModule({ providers: [ ConfigManagerService, { provide: AppLoggerService, useValue: mockLogger, }, ], }).compile(); service = module.get(ConfigManagerService); }); afterEach(() => { jest.restoreAllMocks(); // 清理环境变量 delete process.env.NODE_ENV; delete process.env.ZULIP_SERVER_URL; delete process.env.ZULIP_BOT_EMAIL; delete process.env.ZULIP_BOT_API_KEY; delete process.env.ZULIP_API_KEY_ENCRYPTION_KEY; }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('loadMapConfig - 加载地图配置', () => { it('应该成功加载有效的地图配置', async () => { await service.loadMapConfig(); const mapIds = service.getAllMapIds(); expect(mapIds).toContain('novice_village'); expect(mapIds).toContain('tavern'); }); it('应该在配置文件不存在时创建默认配置', async () => { mockFs.existsSync.mockReturnValue(false); await service.loadMapConfig(); expect(mockFs.writeFileSync).toHaveBeenCalled(); }); it('应该在配置格式无效时抛出错误', async () => { mockFs.readFileSync.mockReturnValue(JSON.stringify({ invalid: 'config' })); await expect(service.loadMapConfig()).rejects.toThrow('配置格式无效'); }); }); describe('getStreamByMap - 根据地图获取Stream', () => { it('应该返回正确的Stream名称', () => { const stream = service.getStreamByMap('novice_village'); expect(stream).toBe('Novice Village'); }); it('应该在地图不存在时返回null', () => { const stream = service.getStreamByMap('nonexistent'); expect(stream).toBeNull(); }); it('应该在mapId为空时返回null', () => { const stream = service.getStreamByMap(''); expect(stream).toBeNull(); }); }); describe('getMapConfig - 获取地图配置', () => { it('应该返回完整的地图配置', () => { const config = service.getMapConfig('novice_village'); expect(config).toBeDefined(); expect(config?.mapId).toBe('novice_village'); expect(config?.mapName).toBe('新手村'); expect(config?.zulipStream).toBe('Novice Village'); }); it('应该在地图不存在时返回null', () => { const config = service.getMapConfig('nonexistent'); expect(config).toBeNull(); }); }); describe('getTopicByObject - 根据交互对象获取Topic', () => { it('应该返回正确的Topic名称', () => { const topic = service.getTopicByObject('novice_village', 'notice_board'); expect(topic).toBe('Notice Board'); }); it('应该在对象不存在时返回null', () => { const topic = service.getTopicByObject('novice_village', 'nonexistent'); expect(topic).toBeNull(); }); it('应该在地图不存在时返回null', () => { const topic = service.getTopicByObject('nonexistent', 'notice_board'); expect(topic).toBeNull(); }); }); describe('findNearbyObject - 查找附近的交互对象', () => { it('应该找到半径内的交互对象', () => { const obj = service.findNearbyObject('novice_village', 110, 160, 50); expect(obj).toBeDefined(); expect(obj?.objectId).toBe('notice_board'); }); it('应该在没有附近对象时返回null', () => { const obj = service.findNearbyObject('novice_village', 500, 500, 50); expect(obj).toBeNull(); }); }); describe('getMapIdByStream - 根据Stream获取地图ID', () => { it('应该返回正确的地图ID', () => { const mapId = service.getMapIdByStream('Novice Village'); expect(mapId).toBe('novice_village'); }); it('应该支持大小写不敏感查询', () => { const mapId = service.getMapIdByStream('novice village'); expect(mapId).toBe('novice_village'); }); it('应该在Stream不存在时返回null', () => { const mapId = service.getMapIdByStream('nonexistent'); expect(mapId).toBeNull(); }); }); describe('validateConfig - 验证配置', () => { it('应该对有效配置返回valid=true(除了API Key警告)', async () => { const result = await service.validateConfig(); // 由于测试环境没有设置API Key,会有一个错误 // 但地图配置应该是有效的 expect(result.errors.some(e => e.includes('地图配置无效'))).toBe(false); }); }); describe('validateMapConfigDetailed - 详细验证地图配置', () => { it('应该对有效配置返回valid=true', () => { const config = { mapId: 'test_map', mapName: '测试地图', zulipStream: 'Test Stream', interactionObjects: [] as any[] }; const result = service.validateMapConfigDetailed(config); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('应该检测缺少mapId的错误', () => { const config = { mapName: '测试地图', zulipStream: 'Test Stream', interactionObjects: [] as any[] }; const result = service.validateMapConfigDetailed(config); expect(result.valid).toBe(false); expect(result.errors).toContain('缺少mapId字段'); }); it('应该检测缺少mapName的错误', () => { const config = { mapId: 'test_map', zulipStream: 'Test Stream', interactionObjects: [] as any[] }; const result = service.validateMapConfigDetailed(config); expect(result.valid).toBe(false); expect(result.errors).toContain('缺少mapName字段'); }); it('应该检测缺少zulipStream的错误', () => { const config = { mapId: 'test_map', mapName: '测试地图', interactionObjects: [] as any[] }; const result = service.validateMapConfigDetailed(config); expect(result.valid).toBe(false); expect(result.errors).toContain('缺少zulipStream字段'); }); it('应该检测交互对象中缺少字段的错误', () => { const config = { mapId: 'test_map', mapName: '测试地图', zulipStream: 'Test Stream', interactionObjects: [ { objectId: 'test_obj', // 缺少objectName, zulipTopic, position } ] }; const result = service.validateMapConfigDetailed(config); expect(result.valid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); }); }); describe('getConfigStats - 获取配置统计', () => { it('应该返回正确的统计信息', () => { const stats = service.getConfigStats(); expect(stats.mapCount).toBe(2); expect(stats.totalObjects).toBe(2); expect(stats.isValid).toBe(true); }); }); /** * 属性测试: 配置验证 * * **Feature: zulip-integration, Property 12: 配置验证** * **Validates: Requirements 10.5** * * 对于任何系统配置,系统应该在启动时验证配置的有效性, * 并在发现无效配置时报告详细的错误信息 */ describe('Property 12: 配置验证', () => { /** * 属性: 对于任何有效的地图配置,验证应该返回valid=true * 验证需求 10.5: 验证配置时系统应在启动时检查配置的有效性并报告错误 */ it('对于任何有效的地图配置,验证应该返回valid=true', async () => { await fc.assert( fc.asyncProperty( // 生成有效的mapId fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // 生成有效的mapName fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // 生成有效的zulipStream fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // 生成有效的交互对象数组 fc.array( fc.record({ objectId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), objectName: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), zulipTopic: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), position: fc.record({ x: fc.integer({ min: 0, max: 10000 }), y: fc.integer({ min: 0, max: 10000 }), }), }), { minLength: 0, maxLength: 10 } ), async (mapId, mapName, zulipStream, interactionObjects) => { const config = { mapId: mapId.trim(), mapName: mapName.trim(), zulipStream: zulipStream.trim(), interactionObjects: interactionObjects.map(obj => ({ objectId: obj.objectId.trim(), objectName: obj.objectName.trim(), zulipTopic: obj.zulipTopic.trim(), position: obj.position, })), }; const result = service.validateMapConfigDetailed(config); // 有效配置应该通过验证 expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何缺少必填字段的配置,验证应该返回valid=false并包含错误信息 * 验证需求 10.5: 验证配置时系统应在启动时检查配置的有效性并报告错误 */ it('对于任何缺少mapId的配置,验证应该返回valid=false', async () => { await fc.assert( fc.asyncProperty( // 生成有效的mapName fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // 生成有效的zulipStream fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), async (mapName, zulipStream) => { const config = { // 缺少mapId mapName: mapName.trim(), zulipStream: zulipStream.trim(), interactionObjects: [] as any[], }; const result = service.validateMapConfigDetailed(config); // 缺少mapId应该验证失败 expect(result.valid).toBe(false); expect(result.errors.some(e => e.includes('mapId'))).toBe(true); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何缺少mapName的配置,验证应该返回valid=false */ it('对于任何缺少mapName的配置,验证应该返回valid=false', async () => { await fc.assert( fc.asyncProperty( // 生成有效的mapId fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // 生成有效的zulipStream fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), async (mapId, zulipStream) => { const config = { mapId: mapId.trim(), // 缺少mapName zulipStream: zulipStream.trim(), interactionObjects: [] as any[], }; const result = service.validateMapConfigDetailed(config); // 缺少mapName应该验证失败 expect(result.valid).toBe(false); expect(result.errors.some(e => e.includes('mapName'))).toBe(true); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何缺少zulipStream的配置,验证应该返回valid=false */ it('对于任何缺少zulipStream的配置,验证应该返回valid=false', async () => { await fc.assert( fc.asyncProperty( // 生成有效的mapId fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // 生成有效的mapName fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), async (mapId, mapName) => { const config = { mapId: mapId.trim(), mapName: mapName.trim(), // 缺少zulipStream interactionObjects: [] as any[], }; const result = service.validateMapConfigDetailed(config); // 缺少zulipStream应该验证失败 expect(result.valid).toBe(false); expect(result.errors.some(e => e.includes('zulipStream'))).toBe(true); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何交互对象缺少必填字段的配置,验证应该返回valid=false */ it('对于任何交互对象缺少objectId的配置,验证应该返回valid=false', async () => { await fc.assert( fc.asyncProperty( // 生成有效的地图配置 fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // 生成有效的交互对象(但缺少objectId) fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), fc.integer({ min: 0, max: 10000 }), fc.integer({ min: 0, max: 10000 }), async (mapId, mapName, zulipStream, objectName, zulipTopic, x, y) => { const config = { mapId: mapId.trim(), mapName: mapName.trim(), zulipStream: zulipStream.trim(), interactionObjects: [ { // 缺少objectId objectName: objectName.trim(), zulipTopic: zulipTopic.trim(), position: { x, y }, } ], }; const result = service.validateMapConfigDetailed(config); // 缺少objectId应该验证失败 expect(result.valid).toBe(false); expect(result.errors.some(e => e.includes('objectId'))).toBe(true); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何交互对象position无效的配置,验证应该返回valid=false */ it('对于任何交互对象position无效的配置,验证应该返回valid=false', async () => { await fc.assert( fc.asyncProperty( // 生成有效的地图配置 fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // 生成有效的交互对象字段 fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), async (mapId, mapName, zulipStream, objectId, objectName, zulipTopic) => { const config = { mapId: mapId.trim(), mapName: mapName.trim(), zulipStream: zulipStream.trim(), interactionObjects: [ { objectId: objectId.trim(), objectName: objectName.trim(), zulipTopic: zulipTopic.trim(), // 缺少position } ], }; const result = service.validateMapConfigDetailed(config); // 缺少position应该验证失败 expect(result.valid).toBe(false); expect(result.errors.some(e => e.includes('position'))).toBe(true); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 验证结果的错误数量应该与实际错误数量一致 */ it('验证结果的错误数量应该与实际错误数量一致', async () => { await fc.assert( fc.asyncProperty( // 随机决定是否包含各个字段 fc.boolean(), fc.boolean(), fc.boolean(), // 生成字段值 fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), async (includeMapId, includeMapName, includeZulipStream, mapId, mapName, zulipStream) => { const config: any = { interactionObjects: [] as any[], }; let expectedErrors = 0; if (includeMapId) { config.mapId = mapId.trim(); } else { expectedErrors++; } if (includeMapName) { config.mapName = mapName.trim(); } else { expectedErrors++; } if (includeZulipStream) { config.zulipStream = zulipStream.trim(); } else { expectedErrors++; } const result = service.validateMapConfigDetailed(config); // 错误数量应该与预期一致 expect(result.errors.length).toBe(expectedErrors); expect(result.valid).toBe(expectedErrors === 0); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何空字符串字段,验证应该返回valid=false */ it('对于任何空字符串字段,验证应该返回valid=false', async () => { await fc.assert( fc.asyncProperty( // 随机选择哪个字段为空 fc.constantFrom('mapId', 'mapName', 'zulipStream'), // 生成有效的字段值 fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), async (emptyField, mapId, mapName, zulipStream) => { const config: any = { mapId: emptyField === 'mapId' ? '' : mapId.trim(), mapName: emptyField === 'mapName' ? '' : mapName.trim(), zulipStream: emptyField === 'zulipStream' ? '' : zulipStream.trim(), interactionObjects: [] as any[], }; const result = service.validateMapConfigDetailed(config); // 空字符串字段应该验证失败 expect(result.valid).toBe(false); expect(result.errors.some(e => e.includes(emptyField))).toBe(true); } ), { numRuns: 100 } ); }, 60000); }); // ==================== 补充测试用例 ==================== describe('hasMap - 检查地图是否存在', () => { it('应该返回true当地图存在时', () => { const exists = service.hasMap('novice_village'); expect(exists).toBe(true); }); it('应该返回false当地图不存在时', () => { const exists = service.hasMap('nonexistent'); expect(exists).toBe(false); }); it('应该处理空字符串输入', () => { const exists = service.hasMap(''); expect(exists).toBe(false); }); it('应该处理null/undefined输入', () => { const exists1 = service.hasMap(null as any); const exists2 = service.hasMap(undefined as any); expect(exists1).toBe(false); expect(exists2).toBe(false); }); }); describe('getAllMapIds - 获取所有地图ID', () => { it('应该返回所有地图ID列表', () => { const mapIds = service.getAllMapIds(); expect(mapIds).toContain('novice_village'); expect(mapIds).toContain('tavern'); expect(mapIds.length).toBe(2); }); }); describe('getMapConfigByStream - 根据Stream获取地图配置', () => { it('应该返回正确的地图配置', () => { const config = service.getMapConfigByStream('Novice Village'); expect(config).toBeDefined(); expect(config?.mapId).toBe('novice_village'); }); it('应该支持大小写不敏感查询', () => { const config = service.getMapConfigByStream('novice village'); expect(config).toBeDefined(); expect(config?.mapId).toBe('novice_village'); }); it('应该在Stream不存在时返回null', () => { const config = service.getMapConfigByStream('nonexistent'); expect(config).toBeNull(); }); }); describe('getAllStreams - 获取所有Stream名称', () => { it('应该返回所有Stream名称列表', () => { const streams = service.getAllStreams(); expect(streams).toContain('Novice Village'); expect(streams).toContain('Tavern'); expect(streams.length).toBe(2); }); }); describe('hasStream - 检查Stream是否存在', () => { it('应该返回true当Stream存在时', () => { const exists = service.hasStream('Novice Village'); expect(exists).toBe(true); }); it('应该支持大小写不敏感查询', () => { const exists = service.hasStream('novice village'); expect(exists).toBe(true); }); it('应该返回false当Stream不存在时', () => { const exists = service.hasStream('nonexistent'); expect(exists).toBe(false); }); it('应该处理空字符串输入', () => { const exists = service.hasStream(''); expect(exists).toBe(false); }); }); describe('findObjectByTopic - 根据Topic查找交互对象', () => { it('应该找到正确的交互对象', () => { const obj = service.findObjectByTopic('Notice Board'); expect(obj).toBeDefined(); expect(obj?.objectId).toBe('notice_board'); expect(obj?.mapId).toBe('novice_village'); }); it('应该支持大小写不敏感查询', () => { const obj = service.findObjectByTopic('notice board'); expect(obj).toBeDefined(); expect(obj?.objectId).toBe('notice_board'); }); it('应该在Topic不存在时返回null', () => { const obj = service.findObjectByTopic('nonexistent'); expect(obj).toBeNull(); }); it('应该处理空字符串输入', () => { const obj = service.findObjectByTopic(''); expect(obj).toBeNull(); }); }); describe('getObjectsInMap - 获取地图中的所有交互对象', () => { it('应该返回地图中的所有交互对象', () => { const objects = service.getObjectsInMap('novice_village'); expect(objects.length).toBe(1); expect(objects[0].objectId).toBe('notice_board'); expect(objects[0].mapId).toBe('novice_village'); }); it('应该在地图不存在时返回空数组', () => { const objects = service.getObjectsInMap('nonexistent'); expect(objects).toEqual([]); }); }); describe('getConfigFilePath - 获取配置文件路径', () => { it('应该返回正确的配置文件路径', () => { const filePath = service.getConfigFilePath(); expect(filePath).toContain('map-config.json'); }); }); describe('configFileExists - 检查配置文件是否存在', () => { it('应该返回true当配置文件存在时', () => { mockFs.existsSync.mockReturnValue(true); const exists = service.configFileExists(); expect(exists).toBe(true); }); it('应该返回false当配置文件不存在时', () => { mockFs.existsSync.mockReturnValue(false); const exists = service.configFileExists(); expect(exists).toBe(false); }); }); describe('reloadConfig - 热重载配置', () => { it('应该成功重载配置', async () => { await expect(service.reloadConfig()).resolves.not.toThrow(); }); it('应该在配置文件读取失败时抛出错误', async () => { mockFs.readFileSync.mockImplementation(() => { throw new Error('File read error'); }); await expect(service.reloadConfig()).rejects.toThrow(); }); }); describe('getZulipConfig - 获取Zulip配置', () => { it('应该返回Zulip配置对象', () => { const config = service.getZulipConfig(); expect(config).toBeDefined(); expect(config.zulipServerUrl).toBeDefined(); expect(config.websocketPort).toBeDefined(); }); }); describe('getAllMapConfigs - 获取所有地图配置', () => { it('应该返回所有地图配置列表', () => { const configs = service.getAllMapConfigs(); expect(configs.length).toBe(2); expect(configs.some(c => c.mapId === 'novice_village')).toBe(true); expect(configs.some(c => c.mapId === 'tavern')).toBe(true); }); }); describe('配置文件监听功能', () => { let mockWatcher: any; beforeEach(() => { mockWatcher = { close: jest.fn(), }; (fs.watch as jest.Mock).mockReturnValue(mockWatcher); }); describe('enableConfigWatcher - 启用配置文件监听', () => { it('应该成功启用配置文件监听', () => { const result = service.enableConfigWatcher(); expect(result).toBe(true); expect(fs.watch).toHaveBeenCalled(); }); it('应该在配置文件不存在时返回false', () => { mockFs.existsSync.mockReturnValue(false); const result = service.enableConfigWatcher(); expect(result).toBe(false); }); it('应该在已启用时跳过重复启用', () => { service.enableConfigWatcher(); (fs.watch as jest.Mock).mockClear(); const result = service.enableConfigWatcher(); expect(result).toBe(true); expect(fs.watch).not.toHaveBeenCalled(); }); it('应该处理fs.watch抛出的错误', () => { (fs.watch as jest.Mock).mockImplementation(() => { throw new Error('Watch error'); }); const result = service.enableConfigWatcher(); expect(result).toBe(false); }); }); describe('disableConfigWatcher - 禁用配置文件监听', () => { it('应该成功禁用配置文件监听', () => { service.enableConfigWatcher(); service.disableConfigWatcher(); expect(mockWatcher.close).toHaveBeenCalled(); }); it('应该处理未启用监听的情况', () => { // 不应该抛出错误 expect(() => service.disableConfigWatcher()).not.toThrow(); }); }); describe('isConfigWatcherEnabled - 检查监听状态', () => { it('应该返回正确的监听状态', () => { expect(service.isConfigWatcherEnabled()).toBe(false); service.enableConfigWatcher(); expect(service.isConfigWatcherEnabled()).toBe(true); service.disableConfigWatcher(); expect(service.isConfigWatcherEnabled()).toBe(false); }); }); }); describe('getFullConfiguration - 获取完整配置', () => { it('应该返回完整的配置对象', () => { const config = service.getFullConfiguration(); expect(config).toBeDefined(); }); }); describe('updateConfigValue - 更新配置值', () => { it('应该成功更新有效的配置值', () => { // 这个测试需要模拟fullConfig存在 const result = service.updateConfigValue('message.rateLimit', 20); // 由于测试环境中fullConfig可能未初始化,这里主要测试不抛出异常 expect(typeof result).toBe('boolean'); }); it('应该在配置键不存在时返回false', () => { const result = service.updateConfigValue('nonexistent.key', 'value'); expect(result).toBe(false); }); it('应该处理无效的键路径', () => { const result = service.updateConfigValue('', 'value'); expect(result).toBe(false); }); }); describe('exportMapConfig - 导出地图配置', () => { it('应该成功导出配置到文件', () => { const result = service.exportMapConfig(); expect(result).toBe(true); expect(mockFs.writeFileSync).toHaveBeenCalled(); }); it('应该处理文件写入错误', () => { mockFs.writeFileSync.mockImplementation(() => { throw new Error('Write error'); }); const result = service.exportMapConfig(); expect(result).toBe(false); }); it('应该支持自定义文件路径', () => { const customPath = '/custom/path/config.json'; const result = service.exportMapConfig(customPath); expect(result).toBe(true); expect(mockFs.writeFileSync).toHaveBeenCalledWith( customPath, expect.any(String), 'utf-8' ); }); }); describe('错误处理测试', () => { it('应该处理JSON解析错误', async () => { mockFs.readFileSync.mockReturnValue('invalid json'); await expect(service.loadMapConfig()).rejects.toThrow(); }); it('应该处理文件系统错误', async () => { mockFs.readFileSync.mockImplementation(() => { throw new Error('File system error'); }); await expect(service.loadMapConfig()).rejects.toThrow(); }); it('应该处理配置验证过程中的错误', async () => { // 模拟验证过程中抛出异常 const originalValidateMapConfig = (service as any).validateMapConfig; (service as any).validateMapConfig = jest.fn().mockImplementation(() => { throw new Error('Validation error'); }); const result = await service.validateConfig(); expect(result.valid).toBe(false); expect(result.errors.some(e => e.includes('验证过程出错'))).toBe(true); // 恢复原方法 (service as any).validateMapConfig = originalValidateMapConfig; }); }); describe('边界条件测试', () => { it('应该处理空的地图配置', async () => { const emptyConfig = { maps: [] }; mockFs.readFileSync.mockReturnValue(JSON.stringify(emptyConfig)); await service.loadMapConfig(); const mapIds = service.getAllMapIds(); expect(mapIds).toEqual([]); }); it('应该处理大量地图配置', async () => { const largeConfig = { maps: Array.from({ length: 1000 }, (_, i) => ({ mapId: `map_${i}`, mapName: `地图${i}`, zulipStream: `Stream${i}`, interactionObjects: [] })) }; mockFs.readFileSync.mockReturnValue(JSON.stringify(largeConfig)); await service.loadMapConfig(); const mapIds = service.getAllMapIds(); expect(mapIds.length).toBe(1000); }); it('应该处理极长的字符串输入', () => { const longString = 'a'.repeat(10000); const stream = service.getStreamByMap(longString); expect(stream).toBeNull(); }); it('应该处理特殊字符输入', () => { const specialChars = '!@#$%^&*()[]{}|;:,.<>?'; const stream = service.getStreamByMap(specialChars); expect(stream).toBeNull(); }); }); describe('并发操作测试', () => { it('应该处理并发的配置查询', async () => { const promises = Array.from({ length: 100 }, () => Promise.resolve(service.getStreamByMap('novice_village')) ); const results = await Promise.all(promises); results.forEach(result => { expect(result).toBe('Novice Village'); }); }); it('应该处理并发的配置重载', async () => { const promises = Array.from({ length: 10 }, () => service.reloadConfig()); // 不应该抛出异常 await expect(Promise.all(promises)).resolves.not.toThrow(); }); }); describe('内存管理测试', () => { it('应该正确清理资源', () => { service.enableConfigWatcher(); // 模拟模块销毁 service.onModuleDestroy(); expect(service.isConfigWatcherEnabled()).toBe(false); }); }); describe('属性测试 - 配置查询一致性', () => { /** * 属性测试: 配置查询的一致性 * 验证双向查询的一致性(mapId <-> stream) */ it('mapId和stream之间的双向查询应该保持一致', async () => { await fc.assert( fc.asyncProperty( // 从现有的mapId中选择 fc.constantFrom('novice_village', 'tavern'), async (mapId) => { // 通过mapId获取stream const stream = service.getStreamByMap(mapId); expect(stream).not.toBeNull(); // 通过stream反向获取mapId const retrievedMapId = service.getMapIdByStream(stream!); expect(retrievedMapId).toBe(mapId); // 通过stream获取配置 const config = service.getMapConfigByStream(stream!); expect(config).not.toBeNull(); expect(config!.mapId).toBe(mapId); } ), { numRuns: 50 } ); }, 30000); /** * 属性测试: 交互对象查询的一致性 */ it('交互对象的不同查询方式应该返回一致的结果', async () => { await fc.assert( fc.asyncProperty( fc.constantFrom('novice_village', 'tavern'), async (mapId) => { // 获取地图中的所有对象 const objectsInMap = service.getObjectsInMap(mapId); for (const obj of objectsInMap) { // 通过topic查找对象 const objByTopic = service.findObjectByTopic(obj.zulipTopic); expect(objByTopic).not.toBeNull(); expect(objByTopic!.objectId).toBe(obj.objectId); expect(objByTopic!.mapId).toBe(mapId); // 通过mapId和objectId获取topic const topic = service.getTopicByObject(mapId, obj.objectId); expect(topic).toBe(obj.zulipTopic); } } ), { numRuns: 50 } ); }, 30000); /** * 属性测试: 配置验证的幂等性 */ it('配置验证应该是幂等的', async () => { await fc.assert( fc.asyncProperty( fc.record({ mapId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), mapName: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), zulipStream: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), interactionObjects: fc.array( fc.record({ objectId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), objectName: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), zulipTopic: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), position: fc.record({ x: fc.integer({ min: 0, max: 10000 }), y: fc.integer({ min: 0, max: 10000 }), }), }), { maxLength: 5 } ), }), async (config) => { // 多次验证同一个配置应该返回相同结果 const result1 = service.validateMapConfigDetailed(config); const result2 = service.validateMapConfigDetailed(config); const result3 = service.validateMapConfigDetailed(config); expect(result1.valid).toBe(result2.valid); expect(result2.valid).toBe(result3.valid); expect(result1.errors).toEqual(result2.errors); expect(result2.errors).toEqual(result3.errors); } ), { numRuns: 100 } ); }, 60000); }); });