- 新增多个模块的单元测试文件,提升测试覆盖率 - 完善AI-Reading文档系统,包含7步代码检查流程 - 新增集成测试和属性测试框架 - 优化项目结构和配置文件 - 清理过时的规范文档,统一使用新的检查标准
340 lines
12 KiB
TypeScript
340 lines
12 KiB
TypeScript
/**
|
||
* 配置验证属性测试
|
||
*
|
||
* 功能描述:
|
||
* - 使用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<AppLoggerService>;
|
||
const mockFs = fs as jest.Mocked<typeof fs>;
|
||
|
||
// 默认有效配置
|
||
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>(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);
|
||
});
|
||
}); |