/** * 动态配置控制器测试 * * 功能描述: * - 测试动态配置管理的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; 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); 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 }); } }); }); });