/** * 配置验证属性测试 * * 功能描述: * - 使用fast-check进行配置验证的属性测试 * - 验证配置验证逻辑的正确性和完整性 * - 测试各种边界情况和随机输入 * * 职责分离: * - 属性测试:验证配置验证的数学属性 * - 随机测试:使用随机生成的数据验证逻辑 * - 边界测试:测试各种边界条件 * * 最近修改: * - 2026-01-12: 代码规范优化 - 从单元测试中分离属性测试 (修改者: moyin) * * @author moyin * @version 1.0.1 * @since 2026-01-12 * @lastModified 2026-01-12 */ import { Test, TestingModule } from '@nestjs/testing'; import * as fc from 'fast-check'; import { ConfigManagerService } from '../../src/core/zulip_core/services/config_manager.service'; import { AppLoggerService } from '../../src/core/utils/logger/logger.service'; import * as fs from 'fs'; // Mock fs module jest.mock('fs'); describe('ConfigManagerService Property Tests', () => { 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 } } ] } ] }; 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); await service.loadMapConfig(); }); 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; }); /** * 属性测试: 配置验证 * * **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); /** * 属性: 验证结果的错误数量应该与实际错误数量一致 */ 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); /** * 属性: 配置验证的幂等性 */ 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); }); });