forked from datawhale/whale-town-end
- API密钥安全服务:新增422个测试用例,覆盖加密、解密、验证等核心功能 - 配置管理服务:新增515个测试用例,覆盖配置加载、验证、更新等场景 - 错误处理服务:新增455个测试用例,覆盖各种错误场景和恢复机制 - 监控服务:新增360个测试用例,覆盖性能监控、健康检查等功能 总计新增1752个测试用例,显著提升代码质量和可靠性
1112 lines
38 KiB
TypeScript
1112 lines
38 KiB
TypeScript
/**
|
||
* 配置管理服务测试
|
||
*
|
||
* 功能描述:
|
||
* - 测试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<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 }
|
||
}
|
||
]
|
||
},
|
||
{
|
||
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>(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);
|
||
});
|
||
});
|