Files
whale-town-end/src/gateway/zulip/dynamic_config.controller.spec.ts
moyin ed04b8c92d docs(zulip): 完善Zulip业务模块功能文档
范围: src/business/zulip/README.md
- 补充对外提供的接口章节(14个公共方法)
- 添加使用的项目内部依赖说明(7个依赖)
- 完善核心特性描述(5个特性)
- 添加潜在风险评估(4个风险及缓解措施)
- 优化文档结构和内容完整性
2026-01-15 10:53:04 +08:00

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
});
}
});
});
});