范围: src/business/zulip/README.md - 补充对外提供的接口章节(14个公共方法) - 添加使用的项目内部依赖说明(7个依赖) - 完善核心特性描述(5个特性) - 添加潜在风险评估(4个风险及缓解措施) - 优化文档结构和内容完整性
463 lines
15 KiB
TypeScript
463 lines
15 KiB
TypeScript
/**
|
|
* 动态配置控制器测试
|
|
*
|
|
* 功能描述:
|
|
* - 测试动态配置管理的REST API控制器
|
|
* - 验证配置获取、同步和管理功能
|
|
* - 测试配置状态查询和备份管理
|
|
* - 测试错误处理和异常情况
|
|
* - 确保配置管理API的正确性和健壮性
|
|
*
|
|
* 最近修改:
|
|
* - 2026-01-12: 代码规范优化 - 创建缺失的测试文件,确保测试覆盖完整性 (修改者: moyin)
|
|
*
|
|
* @author moyin
|
|
* @version 1.0.0
|
|
* @since 2026-01-12
|
|
* @lastModified 2026-01-12
|
|
*/
|
|
|
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
|
import { DynamicConfigController } from './dynamic_config.controller';
|
|
import { DynamicConfigManagerService } from '../../core/zulip_core/services/dynamic_config_manager.service';
|
|
|
|
describe('DynamicConfigController', () => {
|
|
let controller: DynamicConfigController;
|
|
let configManagerService: jest.Mocked<DynamicConfigManagerService>;
|
|
|
|
const mockConfig = {
|
|
version: '2.0.0',
|
|
lastModified: '2026-01-12T00:00:00.000Z',
|
|
description: '测试配置',
|
|
source: 'remote',
|
|
maps: [
|
|
{
|
|
mapId: 'whale_port',
|
|
mapName: '鲸之港',
|
|
zulipStream: 'Whale Port',
|
|
zulipStreamId: 5,
|
|
description: '中心城区',
|
|
isPublic: true,
|
|
isWebPublic: false,
|
|
interactionObjects: [
|
|
{
|
|
objectId: 'whale_port_general',
|
|
objectName: 'General讨论区',
|
|
zulipTopic: 'General',
|
|
position: { x: 100, y: 100 },
|
|
lastMessageId: 0
|
|
}
|
|
]
|
|
}
|
|
]
|
|
};
|
|
|
|
const mockSyncResult = {
|
|
success: true,
|
|
source: 'remote' as const,
|
|
mapCount: 1,
|
|
objectCount: 1,
|
|
lastUpdated: new Date(),
|
|
backupCreated: true
|
|
};
|
|
|
|
const mockConfigStatus = {
|
|
hasRemoteCredentials: true,
|
|
lastSyncTime: new Date(),
|
|
hasLocalConfig: true,
|
|
configSource: 'remote',
|
|
configVersion: '2.0.0',
|
|
mapCount: 1,
|
|
objectCount: 1,
|
|
syncIntervalMinutes: 30,
|
|
configFile: '/path/to/config.json',
|
|
backupDir: '/path/to/backups'
|
|
};
|
|
|
|
const mockBackupFiles = [
|
|
{
|
|
name: 'map-config-backup-2026-01-12T10-00-00-000Z.json',
|
|
path: '/path/to/backup.json',
|
|
size: 1024,
|
|
created: new Date('2026-01-12T10:00:00.000Z')
|
|
}
|
|
];
|
|
|
|
beforeEach(async () => {
|
|
const mockConfigManager = {
|
|
getConfig: jest.fn(),
|
|
syncConfig: jest.fn(),
|
|
getConfigStatus: jest.fn(),
|
|
getBackupFiles: jest.fn(),
|
|
restoreFromBackup: jest.fn(),
|
|
testZulipConnection: jest.fn(),
|
|
getZulipStreams: jest.fn(),
|
|
getZulipTopics: jest.fn(),
|
|
getStreamByMap: jest.fn(),
|
|
getMapIdByStream: jest.fn(),
|
|
getAllMapConfigs: jest.fn(),
|
|
};
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
controllers: [DynamicConfigController],
|
|
providers: [
|
|
{
|
|
provide: DynamicConfigManagerService,
|
|
useValue: mockConfigManager,
|
|
},
|
|
],
|
|
}).compile();
|
|
|
|
controller = module.get<DynamicConfigController>(DynamicConfigController);
|
|
configManagerService = module.get(DynamicConfigManagerService);
|
|
});
|
|
|
|
it('should be defined', () => {
|
|
expect(controller).toBeDefined();
|
|
});
|
|
|
|
describe('getCurrentConfig', () => {
|
|
it('should return current configuration', async () => {
|
|
configManagerService.getConfig.mockResolvedValue(mockConfig);
|
|
configManagerService.getConfigStatus.mockReturnValue(mockConfigStatus);
|
|
|
|
const result = await controller.getCurrentConfig();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toEqual(mockConfig);
|
|
expect(result.source).toBe(mockConfig.source);
|
|
expect(configManagerService.getConfig).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle errors gracefully', async () => {
|
|
configManagerService.getConfig.mockRejectedValue(new Error('Config error'));
|
|
|
|
await expect(controller.getCurrentConfig()).rejects.toThrow(HttpException);
|
|
});
|
|
});
|
|
|
|
describe('syncConfig', () => {
|
|
it('should sync configuration successfully', async () => {
|
|
configManagerService.syncConfig.mockResolvedValue(mockSyncResult);
|
|
|
|
const result = await controller.syncConfig();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.source).toBe(mockSyncResult.source);
|
|
expect(result.data.mapCount).toBe(mockSyncResult.mapCount);
|
|
expect(configManagerService.syncConfig).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle sync failures', async () => {
|
|
const failedSyncResult = {
|
|
...mockSyncResult,
|
|
success: false,
|
|
error: 'Sync failed'
|
|
};
|
|
configManagerService.syncConfig.mockResolvedValue(failedSyncResult);
|
|
|
|
const result = await controller.syncConfig();
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Sync failed');
|
|
});
|
|
|
|
it('should handle sync errors', async () => {
|
|
configManagerService.syncConfig.mockRejectedValue(new Error('Network error'));
|
|
|
|
await expect(controller.syncConfig()).rejects.toThrow(HttpException);
|
|
});
|
|
});
|
|
|
|
describe('getConfigStatus', () => {
|
|
it('should return configuration status', async () => {
|
|
configManagerService.getConfigStatus.mockReturnValue(mockConfigStatus);
|
|
|
|
const result = await controller.getConfigStatus();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toMatchObject(mockConfigStatus);
|
|
expect(configManagerService.getConfigStatus).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle status retrieval errors', async () => {
|
|
configManagerService.getConfigStatus.mockImplementation(() => {
|
|
throw new Error('Status error');
|
|
});
|
|
|
|
await expect(controller.getConfigStatus()).rejects.toThrow(HttpException);
|
|
});
|
|
});
|
|
|
|
describe('getBackups', () => {
|
|
it('should return list of backup files', async () => {
|
|
configManagerService.getBackupFiles.mockReturnValue(mockBackupFiles);
|
|
|
|
const result = await controller.getBackups();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.backups).toHaveLength(1);
|
|
expect(result.data.count).toBe(1);
|
|
expect(configManagerService.getBackupFiles).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return empty array when no backups exist', async () => {
|
|
configManagerService.getBackupFiles.mockReturnValue([]);
|
|
|
|
const result = await controller.getBackups();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.backups).toEqual([]);
|
|
expect(result.data.count).toBe(0);
|
|
});
|
|
|
|
it('should handle backup listing errors', async () => {
|
|
configManagerService.getBackupFiles.mockImplementation(() => {
|
|
throw new Error('Backup error');
|
|
});
|
|
|
|
await expect(controller.getBackups()).rejects.toThrow(HttpException);
|
|
});
|
|
});
|
|
|
|
describe('restoreFromBackup', () => {
|
|
const backupFileName = 'map-config-backup-2026-01-12T10-00-00-000Z.json';
|
|
|
|
it('should restore from backup successfully', async () => {
|
|
configManagerService.restoreFromBackup.mockResolvedValue(true);
|
|
|
|
const result = await controller.restoreFromBackup(backupFileName);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.backupFile).toBe(backupFileName);
|
|
expect(result.data.message).toBe('配置恢复成功');
|
|
expect(configManagerService.restoreFromBackup).toHaveBeenCalledWith(backupFileName);
|
|
});
|
|
|
|
it('should handle restore failure', async () => {
|
|
configManagerService.restoreFromBackup.mockResolvedValue(false);
|
|
|
|
const result = await controller.restoreFromBackup(backupFileName);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.data.message).toBe('配置恢复失败');
|
|
});
|
|
|
|
it('should handle restore errors', async () => {
|
|
configManagerService.restoreFromBackup.mockRejectedValue(new Error('Restore error'));
|
|
|
|
await expect(controller.restoreFromBackup(backupFileName)).rejects.toThrow(HttpException);
|
|
});
|
|
});
|
|
|
|
describe('testConnection', () => {
|
|
it('should test Zulip connection successfully', async () => {
|
|
configManagerService.testZulipConnection.mockResolvedValue(true);
|
|
|
|
const result = await controller.testConnection();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.connected).toBe(true);
|
|
expect(result.data.message).toBe('Zulip连接正常');
|
|
expect(configManagerService.testZulipConnection).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle connection failure', async () => {
|
|
configManagerService.testZulipConnection.mockResolvedValue(false);
|
|
|
|
const result = await controller.testConnection();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.connected).toBe(false);
|
|
expect(result.data.message).toBe('Zulip连接失败');
|
|
});
|
|
|
|
it('should handle connection test errors', async () => {
|
|
configManagerService.testZulipConnection.mockRejectedValue(new Error('Connection error'));
|
|
|
|
const result = await controller.testConnection();
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.data.connected).toBe(false);
|
|
expect(result.error).toBe('Connection error');
|
|
});
|
|
});
|
|
|
|
describe('getStreams', () => {
|
|
const mockStreams = [
|
|
{
|
|
stream_id: 5,
|
|
name: 'Whale Port',
|
|
description: 'Main port area',
|
|
invite_only: false,
|
|
is_web_public: false,
|
|
stream_post_policy: 1,
|
|
message_retention_days: null,
|
|
history_public_to_subscribers: true,
|
|
first_message_id: null,
|
|
is_announcement_only: false
|
|
}
|
|
];
|
|
|
|
it('should return Zulip streams', async () => {
|
|
configManagerService.getZulipStreams.mockResolvedValue(mockStreams);
|
|
|
|
const result = await controller.getStreams();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.streams).toHaveLength(1);
|
|
expect(result.data.count).toBe(1);
|
|
expect(configManagerService.getZulipStreams).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle stream retrieval errors', async () => {
|
|
configManagerService.getZulipStreams.mockRejectedValue(new Error('Stream error'));
|
|
|
|
await expect(controller.getStreams()).rejects.toThrow(HttpException);
|
|
});
|
|
});
|
|
|
|
describe('getTopics', () => {
|
|
const streamId = 5;
|
|
const mockTopics = [
|
|
{ name: 'General', max_id: 123 },
|
|
{ name: 'Random', max_id: 456 }
|
|
];
|
|
|
|
it('should return topics for a stream', async () => {
|
|
configManagerService.getZulipTopics.mockResolvedValue(mockTopics);
|
|
|
|
const result = await controller.getTopics(streamId.toString());
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.streamId).toBe(streamId);
|
|
expect(result.data.topics).toHaveLength(2);
|
|
expect(result.data.count).toBe(2);
|
|
expect(configManagerService.getZulipTopics).toHaveBeenCalledWith(streamId);
|
|
});
|
|
|
|
it('should handle invalid stream ID', async () => {
|
|
await expect(controller.getTopics('invalid')).rejects.toThrow(HttpException);
|
|
});
|
|
|
|
it('should handle topic retrieval errors', async () => {
|
|
configManagerService.getZulipTopics.mockRejectedValue(new Error('Topic error'));
|
|
|
|
await expect(controller.getTopics(streamId.toString())).rejects.toThrow(HttpException);
|
|
});
|
|
});
|
|
|
|
describe('mapToStream', () => {
|
|
const mapId = 'whale_port';
|
|
|
|
it('should return stream name for map ID', async () => {
|
|
configManagerService.getStreamByMap.mockResolvedValue('Whale Port');
|
|
|
|
const result = await controller.mapToStream(mapId);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.mapId).toBe(mapId);
|
|
expect(result.data.streamName).toBe('Whale Port');
|
|
expect(result.data.found).toBe(true);
|
|
expect(configManagerService.getStreamByMap).toHaveBeenCalledWith(mapId);
|
|
});
|
|
|
|
it('should handle map not found', async () => {
|
|
configManagerService.getStreamByMap.mockResolvedValue(null);
|
|
|
|
const result = await controller.mapToStream('invalid_map');
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.mapId).toBe('invalid_map');
|
|
expect(result.data.streamName).toBe(null);
|
|
expect(result.data.found).toBe(false);
|
|
});
|
|
|
|
it('should handle map stream retrieval errors', async () => {
|
|
configManagerService.getStreamByMap.mockRejectedValue(new Error('Map error'));
|
|
|
|
await expect(controller.mapToStream(mapId)).rejects.toThrow(HttpException);
|
|
});
|
|
});
|
|
|
|
describe('streamToMap', () => {
|
|
const streamName = 'Whale Port';
|
|
|
|
it('should return map ID for stream name', async () => {
|
|
configManagerService.getMapIdByStream.mockResolvedValue('whale_port');
|
|
|
|
const result = await controller.streamToMap(streamName);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.streamName).toBe(streamName);
|
|
expect(result.data.mapId).toBe('whale_port');
|
|
expect(result.data.found).toBe(true);
|
|
expect(configManagerService.getMapIdByStream).toHaveBeenCalledWith(streamName);
|
|
});
|
|
|
|
it('should handle stream not found', async () => {
|
|
configManagerService.getMapIdByStream.mockResolvedValue(null);
|
|
|
|
const result = await controller.streamToMap('Invalid Stream');
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.streamName).toBe('Invalid Stream');
|
|
expect(result.data.mapId).toBe(null);
|
|
expect(result.data.found).toBe(false);
|
|
});
|
|
|
|
it('should handle stream map retrieval errors', async () => {
|
|
configManagerService.getMapIdByStream.mockRejectedValue(new Error('Stream error'));
|
|
|
|
await expect(controller.streamToMap(streamName)).rejects.toThrow(HttpException);
|
|
});
|
|
});
|
|
|
|
describe('getMaps', () => {
|
|
it('should return all map configurations', async () => {
|
|
configManagerService.getAllMapConfigs.mockResolvedValue(mockConfig.maps);
|
|
|
|
const result = await controller.getMaps();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.maps).toHaveLength(1);
|
|
expect(result.data.count).toBe(1);
|
|
expect(configManagerService.getAllMapConfigs).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle map retrieval errors', async () => {
|
|
configManagerService.getAllMapConfigs.mockRejectedValue(new Error('Maps error'));
|
|
|
|
await expect(controller.getMaps()).rejects.toThrow(HttpException);
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should throw HttpException with INTERNAL_SERVER_ERROR status', async () => {
|
|
configManagerService.getConfig.mockRejectedValue(new Error('Test error'));
|
|
|
|
try {
|
|
await controller.getCurrentConfig();
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(HttpException);
|
|
expect((error as any).getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
|
|
}
|
|
});
|
|
|
|
it('should preserve error messages in HttpException', async () => {
|
|
const errorMessage = 'Specific error message';
|
|
configManagerService.syncConfig.mockRejectedValue(new Error(errorMessage));
|
|
|
|
try {
|
|
await controller.syncConfig();
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(HttpException);
|
|
expect((error as any).getResponse()).toMatchObject({
|
|
success: false,
|
|
error: errorMessage
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}); |