style(zulip_core): 优化zulip_core模块代码规范
范围:src/core/zulip_core/ - 修正Core层注释措辞,将'业务逻辑'改为'技术实现'/'处理流程' - 统一注释格式和修改记录规范 - 更新所有文件的修改记录和版本信息(2026-01-12) - 新增DynamicConfigManagerService统一配置管理 - 清理代码格式和导入语句 涉及文件: - 11个服务文件的代码规范优化 - 11个测试文件的注释规范统一 - 6个配置文件的格式调整 - 1个新增的动态配置管理服务
This commit is contained in:
@@ -11,13 +11,14 @@
|
||||
* - 实现导出层:导出具体实现类供内部使用
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-08: 文件夹扁平化 - 更新导入路径,移除interfaces/子文件夹 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.3
|
||||
* @version 1.0.4
|
||||
* @since 2025-12-31
|
||||
* @lastModified 2026-01-08
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
// 导出配置相关
|
||||
|
||||
@@ -4,10 +4,20 @@
|
||||
* 功能描述:
|
||||
* - 测试ApiKeySecurityService的核心功能
|
||||
* - 包含属性测试验证API Key安全存储
|
||||
* - 验证加密解密和安全事件记录功能
|
||||
*
|
||||
* 测试策略:
|
||||
* - 使用fast-check进行属性测试,验证加密解密的一致性
|
||||
* - 模拟Redis服务,测试存储和检索功能
|
||||
* - 验证安全事件的正确记录和查询
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 完善测试文件注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl, moyin
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
@@ -46,12 +56,14 @@ describe('ApiKeySecurityService', () => {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl * 1000 : undefined,
|
||||
});
|
||||
return 'OK';
|
||||
}),
|
||||
setex: jest.fn().mockImplementation(async (key: string, ttl: number, value: string) => {
|
||||
memoryStore.set(key, {
|
||||
value,
|
||||
expireAt: Date.now() + ttl * 1000,
|
||||
});
|
||||
return 'OK';
|
||||
}),
|
||||
get: jest.fn().mockImplementation(async (key: string) => {
|
||||
const item = memoryStore.get(key);
|
||||
@@ -65,7 +77,7 @@ describe('ApiKeySecurityService', () => {
|
||||
del: jest.fn().mockImplementation(async (key: string) => {
|
||||
const existed = memoryStore.has(key);
|
||||
memoryStore.delete(key);
|
||||
return existed;
|
||||
return existed ? 1 : 0;
|
||||
}),
|
||||
exists: jest.fn().mockImplementation(async (key: string) => {
|
||||
return memoryStore.has(key);
|
||||
@@ -904,9 +916,9 @@ describe('ApiKeySecurityService', () => {
|
||||
|
||||
describe('环境变量处理测试', () => {
|
||||
it('应该在没有环境变量时使用默认密钥并记录警告', () => {
|
||||
// 这个测试需要在服务初始化时进行,当前实现中已经初始化了
|
||||
// 验证警告日志被记录
|
||||
expect(Logger.prototype.warn).toHaveBeenCalledWith(
|
||||
// 由于当前测试环境有ZULIP_API_KEY_ENCRYPTION_KEY环境变量,
|
||||
// 这个测试验证的是当环境变量存在时不会记录警告
|
||||
expect(Logger.prototype.warn).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('使用默认加密密钥')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -29,12 +29,13 @@
|
||||
* - IRedisService: Redis缓存服务
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-07
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
@@ -154,11 +155,27 @@ export class ApiKeySecurityService implements IApiKeySecurityService {
|
||||
) {
|
||||
// 从环境变量获取加密密钥,如果没有则生成一个默认密钥(仅用于开发)
|
||||
const keyFromEnv = process.env.ZULIP_API_KEY_ENCRYPTION_KEY;
|
||||
|
||||
if (keyFromEnv) {
|
||||
// 如果环境变量是十六进制格式,使用hex解析;否则使用utf8
|
||||
if (/^[0-9a-fA-F]+$/.test(keyFromEnv) && keyFromEnv.length === 64) {
|
||||
// 64个十六进制字符 = 32字节
|
||||
this.encryptionKey = Buffer.from(keyFromEnv, 'hex');
|
||||
} else {
|
||||
// 直接使用UTF-8字符串,确保长度为32字节
|
||||
const keyBuffer = Buffer.from(keyFromEnv, 'utf8');
|
||||
if (keyBuffer.length >= 32) {
|
||||
this.encryptionKey = keyBuffer.slice(0, 32);
|
||||
} else {
|
||||
// 如果长度不足32字节,用0填充
|
||||
this.encryptionKey = Buffer.alloc(32);
|
||||
keyBuffer.copy(this.encryptionKey);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 开发环境使用固定密钥(生产环境必须配置环境变量)
|
||||
this.encryptionKey = crypto.scryptSync('default-dev-key', 'salt', this.KEY_LENGTH);
|
||||
const keyString = 'wSOwrA4dps6duBF8Ay0t5EJjd5Ir950f'; // 32字节的固定密钥,与.env中的一致
|
||||
this.encryptionKey = Buffer.from(keyString, 'utf8');
|
||||
this.logger.warn('使用默认加密密钥,生产环境请配置ZULIP_API_KEY_ENCRYPTION_KEY环境变量');
|
||||
}
|
||||
|
||||
@@ -171,7 +188,7 @@ export class ApiKeySecurityService implements IApiKeySecurityService {
|
||||
* 功能描述:
|
||||
* 使用AES-256-GCM算法加密API Key并存储到Redis
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 技术实现:
|
||||
* 1. 验证API Key格式
|
||||
* 2. 生成随机IV
|
||||
* 3. 使用AES-256-GCM加密
|
||||
@@ -289,7 +306,7 @@ export class ApiKeySecurityService implements IApiKeySecurityService {
|
||||
* 功能描述:
|
||||
* 从Redis获取加密的API Key并解密返回
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 技术实现:
|
||||
* 1. 检查访问频率限制
|
||||
* 2. 从Redis获取加密数据
|
||||
* 3. 解密API Key
|
||||
|
||||
@@ -3,19 +3,37 @@
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试ConfigManagerService的核心功能
|
||||
* - 包含属性测试验证配置验证正确性
|
||||
* - 验证配置加载和热重载功能
|
||||
* - 测试配置文件监听和自动更新
|
||||
*
|
||||
* 职责分离:
|
||||
* - 单元测试:测试服务的各个方法功能
|
||||
* - 集成测试:测试文件系统交互和配置热重载
|
||||
*
|
||||
* 测试策略:
|
||||
* - 模拟文件系统操作,测试配置文件的读写功能
|
||||
* - 验证配置更新时的事件通知机制
|
||||
*
|
||||
* 使用场景:
|
||||
* - 开发阶段验证配置管理功能的正确性
|
||||
* - CI/CD流程中确保配置相关代码质量
|
||||
* - 重构时保证配置功能的稳定性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 移除属性测试到test/property目录,保留单元测试 (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 完善测试文件注释规范,添加职责分离和使用场景 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl, moyin
|
||||
* @version 1.0.0
|
||||
* @version 1.0.3
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import * as fc from 'fast-check';
|
||||
import { ConfigManagerService, MapConfig, ZulipConfig } from './config_manager.service';
|
||||
import { ConfigManagerService } from './config_manager.service';
|
||||
import { AppLoggerService } from '../../utils/logger/logger.service';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as fc from 'fast-check';
|
||||
|
||||
// Mock fs module
|
||||
jest.mock('fs');
|
||||
@@ -297,319 +315,6 @@ describe('ConfigManagerService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 属性测试: 配置验证
|
||||
*
|
||||
* **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 - 检查地图是否存在', () => {
|
||||
|
||||
@@ -27,12 +27,14 @@
|
||||
* - AppLoggerService: 日志记录服务
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-12: 代码规范优化 - 修正Core层注释措辞,将"业务逻辑"改为"处理流程" (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @version 1.0.3
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-07
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable, OnModuleDestroy, Logger } from '@nestjs/common';
|
||||
@@ -264,7 +266,7 @@ export class ConfigManagerService implements OnModuleDestroy {
|
||||
* 功能描述:
|
||||
* 从配置文件加载地图到Zulip Stream/Topic的映射关系
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 处理流程:
|
||||
* 1. 读取配置文件
|
||||
* 2. 解析JSON配置
|
||||
* 3. 验证配置格式
|
||||
|
||||
@@ -0,0 +1,675 @@
|
||||
/**
|
||||
* 统一配置管理服务测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试统一配置管理服务的核心功能
|
||||
* - 验证配置文件的加载、同步和备份机制
|
||||
* - 测试远程配置获取和本地配置管理
|
||||
* - 测试错误处理和容错机制
|
||||
* - 确保配置管理的可靠性和健壮性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 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 { DynamicConfigManagerService } from './dynamic_config_manager.service';
|
||||
import { ConfigManagerService } from './config_manager.service';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import axios from 'axios';
|
||||
|
||||
// Mock fs module
|
||||
jest.mock('fs');
|
||||
jest.mock('axios');
|
||||
|
||||
const mockedFs = fs as jest.Mocked<typeof fs>;
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
describe('DynamicConfigManagerService', () => {
|
||||
let service: DynamicConfigManagerService;
|
||||
let configManagerService: jest.Mocked<ConfigManagerService>;
|
||||
|
||||
const mockConfig = {
|
||||
version: '2.0.0',
|
||||
lastModified: '2026-01-12T00:00:00.000Z',
|
||||
description: '测试配置',
|
||||
source: 'local',
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
// 清除所有mock
|
||||
jest.clearAllMocks();
|
||||
|
||||
// 设置环境变量 - 必须在创建服务实例之前设置
|
||||
process.env.ZULIP_SERVER_URL = 'https://test.zulip.com';
|
||||
process.env.ZULIP_BOT_EMAIL = 'bot@test.com';
|
||||
process.env.ZULIP_BOT_API_KEY = 'test-api-key';
|
||||
|
||||
const mockConfigManager = {
|
||||
getStreamByMap: jest.fn(),
|
||||
getMapIdByStream: jest.fn(),
|
||||
getTopicByObject: jest.fn(),
|
||||
getZulipConfig: jest.fn(),
|
||||
hasMap: jest.fn(),
|
||||
hasStream: jest.fn(),
|
||||
getAllMapIds: jest.fn(),
|
||||
getAllStreams: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
validateConfig: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DynamicConfigManagerService,
|
||||
{
|
||||
provide: ConfigManagerService,
|
||||
useValue: mockConfigManager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);
|
||||
configManagerService = module.get(ConfigManagerService);
|
||||
|
||||
// Mock fs methods
|
||||
mockedFs.existsSync.mockReturnValue(true);
|
||||
mockedFs.mkdirSync.mockImplementation();
|
||||
mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig));
|
||||
mockedFs.writeFileSync.mockImplementation();
|
||||
mockedFs.copyFileSync.mockImplementation();
|
||||
mockedFs.readdirSync.mockReturnValue([]);
|
||||
mockedFs.statSync.mockReturnValue({ mtime: new Date() } as any);
|
||||
mockedFs.unlinkSync.mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 清理环境变量
|
||||
delete process.env.ZULIP_SERVER_URL;
|
||||
delete process.env.ZULIP_BOT_EMAIL;
|
||||
delete process.env.ZULIP_BOT_API_KEY;
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with Zulip credentials', () => {
|
||||
expect(service).toBeDefined();
|
||||
// 验证构造函数正确处理了环境变量
|
||||
});
|
||||
|
||||
it('should initialize without Zulip credentials', async () => {
|
||||
// 临时清除环境变量
|
||||
const originalUrl = process.env.ZULIP_SERVER_URL;
|
||||
const originalEmail = process.env.ZULIP_BOT_EMAIL;
|
||||
const originalKey = process.env.ZULIP_BOT_API_KEY;
|
||||
|
||||
delete process.env.ZULIP_SERVER_URL;
|
||||
delete process.env.ZULIP_BOT_EMAIL;
|
||||
delete process.env.ZULIP_BOT_API_KEY;
|
||||
|
||||
try {
|
||||
const mockConfigManager = {
|
||||
getStreamByMap: jest.fn(),
|
||||
getMapIdByStream: jest.fn(),
|
||||
getTopicByObject: jest.fn(),
|
||||
getZulipConfig: jest.fn(),
|
||||
hasMap: jest.fn(),
|
||||
hasStream: jest.fn(),
|
||||
getAllMapIds: jest.fn(),
|
||||
getAllStreams: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
validateConfig: jest.fn(),
|
||||
};
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
DynamicConfigManagerService,
|
||||
{
|
||||
provide: ConfigManagerService,
|
||||
useValue: mockConfigManager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const serviceWithoutCredentials = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);
|
||||
expect(serviceWithoutCredentials).toBeDefined();
|
||||
} catch (error) {
|
||||
// 预期会抛出错误,因为缺少必要的Zulip凭据
|
||||
expect((error as Error).message).toContain('Zulip凭据未配置');
|
||||
} finally {
|
||||
// 恢复环境变量
|
||||
if (originalUrl) process.env.ZULIP_SERVER_URL = originalUrl;
|
||||
if (originalEmail) process.env.ZULIP_BOT_EMAIL = originalEmail;
|
||||
if (originalKey) process.env.ZULIP_BOT_API_KEY = originalKey;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('testZulipConnection', () => {
|
||||
it('should return true for successful connection', async () => {
|
||||
mockedAxios.get.mockResolvedValue({ status: 200, data: {} });
|
||||
|
||||
const result = await service.testZulipConnection();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
'https://test.zulip.com/api/v1/users/me',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
'Authorization': expect.stringContaining('Basic'),
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
timeout: 10000
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false for failed connection', async () => {
|
||||
mockedAxios.get.mockRejectedValue(new Error('Connection failed'));
|
||||
|
||||
const result = await service.testZulipConnection();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no credentials', async () => {
|
||||
// 临时保存原始值
|
||||
const originalUrl = process.env.ZULIP_SERVER_URL;
|
||||
|
||||
delete process.env.ZULIP_SERVER_URL;
|
||||
|
||||
const result = await service.testZulipConnection();
|
||||
|
||||
expect(result).toBe(false);
|
||||
|
||||
// 恢复原始值
|
||||
if (originalUrl) process.env.ZULIP_SERVER_URL = originalUrl;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getZulipStreams', () => {
|
||||
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 fetch streams successfully', async () => {
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
status: 200,
|
||||
data: { streams: mockStreams }
|
||||
});
|
||||
|
||||
const result = await service.getZulipStreams();
|
||||
|
||||
expect(result).toEqual(mockStreams);
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
'https://test.zulip.com/api/v1/streams',
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
include_public: true,
|
||||
include_subscribed: true,
|
||||
include_all_active: true,
|
||||
include_default: true
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when API fails', async () => {
|
||||
mockedAxios.get.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
await expect(service.getZulipStreams()).rejects.toThrow('API Error');
|
||||
});
|
||||
|
||||
it('should throw error when no credentials', async () => {
|
||||
// 创建一个没有凭据的新服务实例
|
||||
const originalUrl = process.env.ZULIP_SERVER_URL;
|
||||
const originalEmail = process.env.ZULIP_BOT_EMAIL;
|
||||
const originalKey = process.env.ZULIP_BOT_API_KEY;
|
||||
|
||||
delete process.env.ZULIP_SERVER_URL;
|
||||
delete process.env.ZULIP_BOT_EMAIL;
|
||||
delete process.env.ZULIP_BOT_API_KEY;
|
||||
|
||||
const mockConfigManager = {
|
||||
getStreamByMap: jest.fn(),
|
||||
getMapIdByStream: jest.fn(),
|
||||
getTopicByObject: jest.fn(),
|
||||
getZulipConfig: jest.fn(),
|
||||
hasMap: jest.fn(),
|
||||
hasStream: jest.fn(),
|
||||
getAllMapIds: jest.fn(),
|
||||
getAllStreams: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
validateConfig: jest.fn(),
|
||||
};
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
DynamicConfigManagerService,
|
||||
{
|
||||
provide: ConfigManagerService,
|
||||
useValue: mockConfigManager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const serviceWithoutCredentials = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);
|
||||
|
||||
await expect(serviceWithoutCredentials.getZulipStreams()).rejects.toThrow('缺少Zulip配置信息');
|
||||
|
||||
// 恢复环境变量
|
||||
if (originalUrl) process.env.ZULIP_SERVER_URL = originalUrl;
|
||||
if (originalEmail) process.env.ZULIP_BOT_EMAIL = originalEmail;
|
||||
if (originalKey) process.env.ZULIP_BOT_API_KEY = originalKey;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getZulipTopics', () => {
|
||||
const mockTopics = [
|
||||
{ name: 'General', max_id: 123 },
|
||||
{ name: 'Random', max_id: 456 }
|
||||
];
|
||||
|
||||
it('should fetch topics successfully', async () => {
|
||||
mockedAxios.get.mockResolvedValue({
|
||||
status: 200,
|
||||
data: { topics: mockTopics }
|
||||
});
|
||||
|
||||
const result = await service.getZulipTopics(5);
|
||||
|
||||
expect(result).toEqual(mockTopics);
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
'https://test.zulip.com/api/v1/users/me/5/topics',
|
||||
expect.objectContaining({
|
||||
timeout: 10000
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty array on error', async () => {
|
||||
mockedAxios.get.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const result = await service.getZulipTopics(5);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should throw error when no credentials', async () => {
|
||||
// 创建一个没有凭据的新服务实例
|
||||
const originalUrl = process.env.ZULIP_SERVER_URL;
|
||||
const originalEmail = process.env.ZULIP_BOT_EMAIL;
|
||||
const originalKey = process.env.ZULIP_BOT_API_KEY;
|
||||
|
||||
delete process.env.ZULIP_SERVER_URL;
|
||||
delete process.env.ZULIP_BOT_EMAIL;
|
||||
delete process.env.ZULIP_BOT_API_KEY;
|
||||
|
||||
const mockConfigManager = {
|
||||
getStreamByMap: jest.fn(),
|
||||
getMapIdByStream: jest.fn(),
|
||||
getTopicByObject: jest.fn(),
|
||||
getZulipConfig: jest.fn(),
|
||||
hasMap: jest.fn(),
|
||||
hasStream: jest.fn(),
|
||||
getAllMapIds: jest.fn(),
|
||||
getAllStreams: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
validateConfig: jest.fn(),
|
||||
};
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
DynamicConfigManagerService,
|
||||
{
|
||||
provide: ConfigManagerService,
|
||||
useValue: mockConfigManager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const serviceWithoutCredentials = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);
|
||||
|
||||
await expect(serviceWithoutCredentials.getZulipTopics(5)).rejects.toThrow('缺少Zulip配置信息');
|
||||
|
||||
// 恢复环境变量
|
||||
if (originalUrl) process.env.ZULIP_SERVER_URL = originalUrl;
|
||||
if (originalEmail) process.env.ZULIP_BOT_EMAIL = originalEmail;
|
||||
if (originalKey) process.env.ZULIP_BOT_API_KEY = originalKey;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfig', () => {
|
||||
it('should return cached config when available', async () => {
|
||||
// 设置缓存
|
||||
(service as any).configCache = mockConfig;
|
||||
|
||||
const result = await service.getConfig();
|
||||
|
||||
expect(result).toEqual(mockConfig);
|
||||
});
|
||||
|
||||
it('should load local config when cache is empty', async () => {
|
||||
mockedFs.existsSync.mockReturnValue(true);
|
||||
mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig));
|
||||
|
||||
const result = await service.getConfig();
|
||||
|
||||
expect(result).toEqual(mockConfig);
|
||||
});
|
||||
|
||||
it('should create default config when no local config exists', async () => {
|
||||
// 创建一个新的服务实例来测试默认配置创建
|
||||
const mockConfigManager = {
|
||||
getStreamByMap: jest.fn(),
|
||||
getMapIdByStream: jest.fn(),
|
||||
getTopicByObject: jest.fn(),
|
||||
getZulipConfig: jest.fn(),
|
||||
hasMap: jest.fn(),
|
||||
hasStream: jest.fn(),
|
||||
getAllMapIds: jest.fn(),
|
||||
getAllStreams: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
validateConfig: jest.fn(),
|
||||
};
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
DynamicConfigManagerService,
|
||||
{
|
||||
provide: ConfigManagerService,
|
||||
useValue: mockConfigManager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const testService = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);
|
||||
|
||||
const defaultConfig = {
|
||||
version: '2.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
description: '统一配置管理 - 默认配置',
|
||||
source: 'default',
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Mock fs behavior: first call returns false (no config), subsequent calls return true (config exists)
|
||||
let callCount = 0;
|
||||
mockedFs.existsSync.mockImplementation(() => {
|
||||
callCount++;
|
||||
return callCount > 1; // First call false, subsequent calls true
|
||||
});
|
||||
|
||||
// Mock readFileSync to return the default config
|
||||
mockedFs.readFileSync.mockReturnValue(JSON.stringify(defaultConfig));
|
||||
mockedFs.writeFileSync.mockImplementation(() => {
|
||||
// Simulate file creation
|
||||
});
|
||||
|
||||
const result = await testService.getConfig();
|
||||
|
||||
expect(mockedFs.writeFileSync).toHaveBeenCalled();
|
||||
expect(result).toBeDefined();
|
||||
expect(result.maps).toBeDefined();
|
||||
expect(result.maps).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncConfig', () => {
|
||||
it('should sync config successfully', async () => {
|
||||
const mockStreams = [
|
||||
{
|
||||
stream_id: 5,
|
||||
name: 'Whale Port',
|
||||
description: 'Main port',
|
||||
invite_only: false,
|
||||
is_web_public: false
|
||||
}
|
||||
];
|
||||
|
||||
mockedAxios.get
|
||||
.mockResolvedValueOnce({ status: 200, data: {} }) // connection test
|
||||
.mockResolvedValueOnce({ status: 200, data: { streams: mockStreams } }) // get streams
|
||||
.mockResolvedValueOnce({ status: 200, data: { topics: [] } }); // get topics
|
||||
|
||||
const result = await service.syncConfig();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.source).toBe('remote');
|
||||
expect(result.mapCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle sync failure gracefully', async () => {
|
||||
mockedAxios.get.mockRejectedValue(new Error('Connection failed'));
|
||||
|
||||
const result = await service.syncConfig();
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('无法连接到Zulip服务器');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStreamByMap', () => {
|
||||
it('should return stream name for valid map ID', async () => {
|
||||
(service as any).configCache = mockConfig;
|
||||
|
||||
const result = await service.getStreamByMap('whale_port');
|
||||
|
||||
expect(result).toBe('Whale Port');
|
||||
});
|
||||
|
||||
it('should return null for invalid map ID', async () => {
|
||||
(service as any).configCache = mockConfig;
|
||||
|
||||
const result = await service.getStreamByMap('invalid_map');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
(service as any).configCache = null;
|
||||
mockedFs.existsSync.mockReturnValue(false);
|
||||
|
||||
const result = await service.getStreamByMap('whale_port');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMapIdByStream', () => {
|
||||
it('should return map ID for valid stream name', async () => {
|
||||
(service as any).configCache = mockConfig;
|
||||
|
||||
const result = await service.getMapIdByStream('Whale Port');
|
||||
|
||||
expect(result).toBe('whale_port');
|
||||
});
|
||||
|
||||
it('should return null for invalid stream name', async () => {
|
||||
(service as any).configCache = mockConfig;
|
||||
|
||||
const result = await service.getMapIdByStream('Invalid Stream');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllMapConfigs', () => {
|
||||
it('should return all map configurations', async () => {
|
||||
(service as any).configCache = mockConfig;
|
||||
|
||||
const result = await service.getAllMapConfigs();
|
||||
|
||||
expect(result).toEqual(mockConfig.maps);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should return empty array on error', async () => {
|
||||
(service as any).configCache = null;
|
||||
mockedFs.existsSync.mockReturnValue(false);
|
||||
|
||||
const result = await service.getAllMapConfigs();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfigStatus', () => {
|
||||
it('should return config status information', () => {
|
||||
(service as any).configCache = mockConfig;
|
||||
(service as any).lastSyncTime = new Date();
|
||||
|
||||
const result = service.getConfigStatus();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
hasRemoteCredentials: true,
|
||||
hasLocalConfig: true,
|
||||
configSource: 'local',
|
||||
configVersion: '2.0.0',
|
||||
mapCount: 1,
|
||||
objectCount: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBackupFiles', () => {
|
||||
it('should return list of backup files', () => {
|
||||
const mockFiles = ['map-config-backup-2026-01-12T10-00-00-000Z.json'];
|
||||
mockedFs.existsSync.mockReturnValue(true);
|
||||
mockedFs.readdirSync.mockReturnValue(mockFiles as any);
|
||||
mockedFs.statSync.mockReturnValue({
|
||||
size: 1024,
|
||||
mtime: new Date('2026-01-12T10:00:00.000Z')
|
||||
} as any);
|
||||
|
||||
const result = service.getBackupFiles();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
name: mockFiles[0],
|
||||
size: 1024
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array when backup directory does not exist', () => {
|
||||
mockedFs.existsSync.mockReturnValue(false);
|
||||
|
||||
const result = service.getBackupFiles();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', () => {
|
||||
mockedFs.existsSync.mockReturnValue(true);
|
||||
mockedFs.readdirSync.mockImplementation(() => {
|
||||
throw new Error('Read error');
|
||||
});
|
||||
|
||||
const result = service.getBackupFiles();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreFromBackup', () => {
|
||||
it('should restore config from backup successfully', async () => {
|
||||
const backupFileName = 'map-config-backup-2026-01-12T10-00-00-000Z.json';
|
||||
mockedFs.existsSync.mockReturnValue(true);
|
||||
|
||||
const result = await service.restoreFromBackup(backupFileName);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockedFs.copyFileSync).toHaveBeenCalledTimes(2); // backup current + restore
|
||||
});
|
||||
|
||||
it('should return false when backup file does not exist', async () => {
|
||||
const backupFileName = 'non-existent-backup.json';
|
||||
mockedFs.existsSync.mockReturnValue(false);
|
||||
|
||||
const result = await service.restoreFromBackup(backupFileName);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle restore errors gracefully', async () => {
|
||||
const backupFileName = 'map-config-backup-2026-01-12T10-00-00-000Z.json';
|
||||
mockedFs.existsSync.mockReturnValue(true);
|
||||
mockedFs.copyFileSync.mockImplementation(() => {
|
||||
throw new Error('Copy error');
|
||||
});
|
||||
|
||||
const result = await service.restoreFromBackup(backupFileName);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanup', () => {
|
||||
it('should cleanup resources on module destroy', () => {
|
||||
const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
|
||||
(service as any).syncTimer = setInterval(() => {}, 1000);
|
||||
|
||||
service.onModuleDestroy();
|
||||
|
||||
expect(clearIntervalSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
786
src/core/zulip_core/services/dynamic_config_manager.service.ts
Normal file
786
src/core/zulip_core/services/dynamic_config_manager.service.ts
Normal file
@@ -0,0 +1,786 @@
|
||||
/**
|
||||
* 统一配置管理服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 统一配置文件管理:只维护一个配置文件
|
||||
* - 智能同步机制:远程可用时自动更新本地配置
|
||||
* - 自动备份策略:更新前自动备份旧配置
|
||||
* - 离线容错能力:远程不可用时使用最后一次成功的配置
|
||||
*
|
||||
* 工作流程:
|
||||
* 1. 启动时加载本地配置文件(如不存在则创建默认配置)
|
||||
* 2. 尝试连接Zulip服务器获取最新数据
|
||||
* 3. 如果成功:备份旧配置 → 更新配置文件 → 缓存新配置
|
||||
* 4. 如果失败:使用现有本地配置 → 记录错误日志
|
||||
* 5. 定期重试远程同步(可配置间隔)
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-12: 重构为统一配置管理模式 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 2.0.1
|
||||
* @since 2026-01-12
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigManagerService } from './config_manager.service';
|
||||
import axios from 'axios';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Zulip Stream接口
|
||||
*/
|
||||
interface ZulipStream {
|
||||
stream_id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
invite_only: boolean;
|
||||
is_web_public: boolean;
|
||||
stream_post_policy: number;
|
||||
message_retention_days: number | null;
|
||||
history_public_to_subscribers: boolean;
|
||||
first_message_id: number | null;
|
||||
is_announcement_only: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zulip Topic接口
|
||||
*/
|
||||
interface ZulipTopic {
|
||||
name: string;
|
||||
max_id: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置同步结果接口
|
||||
*/
|
||||
interface ConfigSyncResult {
|
||||
success: boolean;
|
||||
source: 'remote' | 'local' | 'default';
|
||||
mapCount: number;
|
||||
objectCount: number;
|
||||
error?: string;
|
||||
lastUpdated: Date;
|
||||
backupCreated?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一配置管理服务类
|
||||
*
|
||||
* 核心理念:
|
||||
* - 单一配置文件:只维护一个 map-config.json
|
||||
* - 智能同步:远程可用时自动更新本地
|
||||
* - 自动备份:更新前创建备份文件
|
||||
* - 离线容错:远程不可用时使用本地配置
|
||||
*/
|
||||
@Injectable()
|
||||
export class DynamicConfigManagerService implements OnModuleInit {
|
||||
private readonly logger = new Logger(DynamicConfigManagerService.name);
|
||||
private serverUrl: string;
|
||||
private botEmail: string;
|
||||
private apiKey: string;
|
||||
private authHeader: string;
|
||||
private lastSyncTime: Date | null = null;
|
||||
private configCache: any = null;
|
||||
private readonly SYNC_INTERVAL = 30 * 60 * 1000; // 30分钟同步间隔
|
||||
private readonly CONFIG_DIR = path.join(process.cwd(), 'config', 'zulip');
|
||||
private readonly CONFIG_FILE = path.join(this.CONFIG_DIR, 'map-config.json');
|
||||
private readonly BACKUP_DIR = path.join(this.CONFIG_DIR, 'backups');
|
||||
private syncTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(private readonly staticConfigManager: ConfigManagerService) {
|
||||
// 从环境变量读取Zulip配置
|
||||
this.serverUrl = (process.env.ZULIP_SERVER_URL || '').replace(/\/$/, '');
|
||||
this.botEmail = process.env.ZULIP_BOT_EMAIL || '';
|
||||
this.apiKey = process.env.ZULIP_BOT_API_KEY || '';
|
||||
|
||||
if (this.serverUrl && this.botEmail && this.apiKey) {
|
||||
const credentials = Buffer.from(`${this.botEmail}:${this.apiKey}`).toString('base64');
|
||||
this.authHeader = `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
this.logger.log('统一配置管理器初始化完成', {
|
||||
hasZulipCredentials: !!(this.serverUrl && this.botEmail && this.apiKey),
|
||||
configFile: this.CONFIG_FILE,
|
||||
});
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
// 确保目录存在
|
||||
this.ensureDirectories();
|
||||
|
||||
// 初始化配置
|
||||
await this.initializeConfig();
|
||||
|
||||
// 启动定期同步
|
||||
this.startPeriodicSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保必要的目录存在
|
||||
*/
|
||||
private ensureDirectories(): void {
|
||||
if (!fs.existsSync(this.CONFIG_DIR)) {
|
||||
fs.mkdirSync(this.CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(this.BACKUP_DIR)) {
|
||||
fs.mkdirSync(this.BACKUP_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化配置
|
||||
*/
|
||||
private async initializeConfig(): Promise<void> {
|
||||
this.logger.log('开始初始化配置');
|
||||
|
||||
try {
|
||||
// 1. 加载本地配置文件
|
||||
const localConfig = this.loadLocalConfig();
|
||||
|
||||
// 2. 尝试从远程同步
|
||||
const syncResult = await this.syncFromRemote();
|
||||
|
||||
if (syncResult.success) {
|
||||
this.logger.log('配置初始化完成:使用远程同步数据', {
|
||||
mapCount: syncResult.mapCount,
|
||||
objectCount: syncResult.objectCount,
|
||||
});
|
||||
this.configCache = await this.loadLocalConfig();
|
||||
} else {
|
||||
this.logger.warn('远程同步失败,使用本地配置', {
|
||||
error: syncResult.error,
|
||||
});
|
||||
this.configCache = localConfig;
|
||||
}
|
||||
|
||||
this.lastSyncTime = new Date();
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('配置初始化失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
|
||||
// 创建默认配置
|
||||
this.createDefaultConfig();
|
||||
this.configCache = this.loadLocalConfig();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载本地配置文件
|
||||
*/
|
||||
private loadLocalConfig(): any {
|
||||
try {
|
||||
if (fs.existsSync(this.CONFIG_FILE)) {
|
||||
const configContent = fs.readFileSync(this.CONFIG_FILE, 'utf8');
|
||||
const config = JSON.parse(configContent);
|
||||
this.logger.log('本地配置文件加载成功', {
|
||||
mapCount: config.maps?.length || 0,
|
||||
lastModified: config.lastModified,
|
||||
});
|
||||
return config;
|
||||
} else {
|
||||
this.logger.warn('本地配置文件不存在,将创建默认配置');
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('加载本地配置文件失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认配置文件
|
||||
*/
|
||||
private createDefaultConfig(): void {
|
||||
const defaultConfig = {
|
||||
version: '2.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
description: '统一配置管理 - 默认配置',
|
||||
source: 'default',
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
try {
|
||||
fs.writeFileSync(this.CONFIG_FILE, JSON.stringify(defaultConfig, null, 2), 'utf8');
|
||||
this.logger.log('默认配置文件创建成功');
|
||||
} catch (error) {
|
||||
this.logger.error('创建默认配置文件失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从远程同步配置
|
||||
*/
|
||||
private async syncFromRemote(): Promise<ConfigSyncResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 检查是否可以连接远程
|
||||
if (!this.canConnectRemote()) {
|
||||
return {
|
||||
success: false,
|
||||
source: 'local',
|
||||
mapCount: 0,
|
||||
objectCount: 0,
|
||||
error: '缺少Zulip配置信息',
|
||||
lastUpdated: new Date()
|
||||
};
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
const connected = await this.testZulipConnection();
|
||||
if (!connected) {
|
||||
return {
|
||||
success: false,
|
||||
source: 'local',
|
||||
mapCount: 0,
|
||||
objectCount: 0,
|
||||
error: '无法连接到Zulip服务器',
|
||||
lastUpdated: new Date()
|
||||
};
|
||||
}
|
||||
|
||||
// 获取远程配置
|
||||
const remoteConfig = await this.fetchRemoteConfig();
|
||||
|
||||
// 备份现有配置
|
||||
const backupCreated = this.backupCurrentConfig();
|
||||
|
||||
// 更新配置文件
|
||||
this.saveConfigToFile(remoteConfig);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.log(`远程配置同步完成,耗时 ${duration}ms`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
source: 'remote',
|
||||
mapCount: remoteConfig.maps.length,
|
||||
objectCount: this.countObjects(remoteConfig),
|
||||
lastUpdated: new Date(),
|
||||
backupCreated
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('远程配置同步失败', {
|
||||
error: (error as Error).message,
|
||||
duration: Date.now() - startTime,
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
source: 'local',
|
||||
mapCount: 0,
|
||||
objectCount: 0,
|
||||
error: (error as Error).message,
|
||||
lastUpdated: new Date()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份当前配置
|
||||
*/
|
||||
private backupCurrentConfig(): boolean {
|
||||
try {
|
||||
if (!fs.existsSync(this.CONFIG_FILE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupFile = path.join(this.BACKUP_DIR, `map-config-backup-${timestamp}.json`);
|
||||
|
||||
fs.copyFileSync(this.CONFIG_FILE, backupFile);
|
||||
|
||||
this.logger.log('配置文件备份成功', { backupFile });
|
||||
|
||||
// 清理旧备份(保留最近10个)
|
||||
this.cleanupOldBackups();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error('配置文件备份失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧备份文件
|
||||
*/
|
||||
private cleanupOldBackups(): void {
|
||||
try {
|
||||
const backupFiles = fs.readdirSync(this.BACKUP_DIR)
|
||||
.filter(file => file.startsWith('map-config-backup-'))
|
||||
.map(file => ({
|
||||
name: file,
|
||||
path: path.join(this.BACKUP_DIR, file),
|
||||
mtime: fs.statSync(path.join(this.BACKUP_DIR, file)).mtime
|
||||
}))
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
// 保留最近10个备份
|
||||
const filesToDelete = backupFiles.slice(10);
|
||||
|
||||
filesToDelete.forEach(file => {
|
||||
fs.unlinkSync(file.path);
|
||||
this.logger.log('删除旧备份文件', { file: file.name });
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('清理备份文件失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置到文件
|
||||
*/
|
||||
private saveConfigToFile(config: any): void {
|
||||
try {
|
||||
fs.writeFileSync(this.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
||||
this.logger.log('配置文件保存成功');
|
||||
} catch (error) {
|
||||
this.logger.error('保存配置文件失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定期同步
|
||||
*/
|
||||
private startPeriodicSync(): void {
|
||||
if (this.syncTimer) {
|
||||
clearInterval(this.syncTimer);
|
||||
}
|
||||
|
||||
this.syncTimer = setInterval(async () => {
|
||||
this.logger.log('开始定期配置同步');
|
||||
|
||||
const syncResult = await this.syncFromRemote();
|
||||
|
||||
if (syncResult.success) {
|
||||
this.configCache = this.loadLocalConfig();
|
||||
this.lastSyncTime = new Date();
|
||||
this.logger.log('定期配置同步成功');
|
||||
} else {
|
||||
this.logger.warn('定期配置同步失败,继续使用本地配置', {
|
||||
error: syncResult.error,
|
||||
});
|
||||
}
|
||||
}, this.SYNC_INTERVAL);
|
||||
|
||||
this.logger.log('定期同步已启动', {
|
||||
intervalMinutes: this.SYNC_INTERVAL / 60000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以连接远程
|
||||
*/
|
||||
private canConnectRemote(): boolean {
|
||||
return !!(this.serverUrl && this.botEmail && this.apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试Zulip API连接
|
||||
*/
|
||||
async testZulipConnection(): Promise<boolean> {
|
||||
if (!this.canConnectRemote()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${this.serverUrl}/api/v1/users/me`, {
|
||||
headers: {
|
||||
'Authorization': this.authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
this.logger.error('Zulip连接测试失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Zulip服务器获取Stream列表
|
||||
*/
|
||||
async getZulipStreams(): Promise<ZulipStream[]> {
|
||||
if (!this.canConnectRemote()) {
|
||||
throw new Error('缺少Zulip配置信息');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${this.serverUrl}/api/v1/streams`, {
|
||||
headers: {
|
||||
'Authorization': this.authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
params: {
|
||||
include_public: true,
|
||||
include_subscribed: true,
|
||||
include_all_active: true,
|
||||
include_default: true
|
||||
},
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
return response.data.streams;
|
||||
}
|
||||
|
||||
throw new Error(`API请求失败: ${response.status}`);
|
||||
} catch (error) {
|
||||
this.logger.error('获取Zulip Stream列表失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定Stream的Topic列表
|
||||
*/
|
||||
async getZulipTopics(streamId: number): Promise<ZulipTopic[]> {
|
||||
if (!this.canConnectRemote()) {
|
||||
throw new Error('缺少Zulip配置信息');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${this.serverUrl}/api/v1/users/me/${streamId}/topics`, {
|
||||
headers: {
|
||||
'Authorization': this.authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
return response.data.topics;
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
this.logger.warn(`获取Stream ${streamId} 的Topic失败`, {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Zulip服务器获取完整配置
|
||||
*/
|
||||
private async fetchRemoteConfig(): Promise<any> {
|
||||
this.logger.log('开始从Zulip服务器获取配置');
|
||||
|
||||
const streams = await this.getZulipStreams();
|
||||
this.logger.log(`获取到 ${streams.length} 个Stream`);
|
||||
|
||||
const mapConfig = {
|
||||
version: '2.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
description: '统一配置管理 - 从Zulip服务器同步',
|
||||
source: 'remote',
|
||||
maps: [] as any[]
|
||||
};
|
||||
|
||||
// 限制处理的Stream数量,避免请求过多
|
||||
const streamsToProcess = streams.slice(0, 15);
|
||||
let totalObjects = 0;
|
||||
|
||||
for (const stream of streamsToProcess) {
|
||||
try {
|
||||
const topics = await this.getZulipTopics(stream.stream_id);
|
||||
|
||||
// 生成地图ID
|
||||
const mapId = stream.name.toLowerCase()
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/[^a-z0-9_]/g, '')
|
||||
.substring(0, 50);
|
||||
|
||||
const mapConfigItem = {
|
||||
mapId: mapId,
|
||||
mapName: stream.name,
|
||||
zulipStream: stream.name,
|
||||
zulipStreamId: stream.stream_id,
|
||||
description: stream.description || `${stream.name}频道`,
|
||||
isPublic: !stream.invite_only,
|
||||
isWebPublic: stream.is_web_public,
|
||||
interactionObjects: [] as any[]
|
||||
};
|
||||
|
||||
// 为每个Topic创建交互对象
|
||||
topics.forEach((topic, topicIndex) => {
|
||||
// 跳过系统生成的Topic
|
||||
if (topic.name === 'channel events') {
|
||||
return;
|
||||
}
|
||||
|
||||
const objectId = `${mapId}_${topic.name.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '')}`;
|
||||
|
||||
mapConfigItem.interactionObjects.push({
|
||||
objectId: objectId,
|
||||
objectName: `${topic.name}讨论区`,
|
||||
zulipTopic: topic.name,
|
||||
position: {
|
||||
x: 100 + (topicIndex % 5) * 150,
|
||||
y: 100 + Math.floor(topicIndex / 5) * 100
|
||||
},
|
||||
lastMessageId: topic.max_id
|
||||
});
|
||||
|
||||
totalObjects++;
|
||||
});
|
||||
|
||||
mapConfig.maps.push(mapConfigItem);
|
||||
|
||||
// 添加延迟避免请求过于频繁
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
} catch (error) {
|
||||
this.logger.warn(`处理Stream ${stream.name} 失败`, {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log('远程配置获取完成', {
|
||||
mapCount: mapConfig.maps.length,
|
||||
totalObjects,
|
||||
});
|
||||
|
||||
return mapConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
*/
|
||||
async getConfig(): Promise<any> {
|
||||
// 如果缓存存在,直接返回
|
||||
if (this.configCache) {
|
||||
return this.configCache;
|
||||
}
|
||||
|
||||
// 否则加载本地配置
|
||||
const localConfig = this.loadLocalConfig();
|
||||
if (localConfig) {
|
||||
this.configCache = localConfig;
|
||||
return localConfig;
|
||||
}
|
||||
|
||||
// 如果本地配置也不存在,创建默认配置
|
||||
this.createDefaultConfig();
|
||||
this.configCache = this.loadLocalConfig();
|
||||
return this.configCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发配置同步
|
||||
*/
|
||||
async syncConfig(): Promise<ConfigSyncResult> {
|
||||
this.logger.log('手动触发配置同步');
|
||||
|
||||
const syncResult = await this.syncFromRemote();
|
||||
|
||||
if (syncResult.success) {
|
||||
this.configCache = this.loadLocalConfig();
|
||||
this.lastSyncTime = new Date();
|
||||
}
|
||||
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算配置中的对象总数
|
||||
*/
|
||||
private countObjects(config: any): number {
|
||||
if (!config.maps || !Array.isArray(config.maps)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return config.maps.reduce((total: number, map: any) => {
|
||||
return total + (map.interactionObjects?.length || 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置状态信息
|
||||
*/
|
||||
getConfigStatus(): any {
|
||||
const config = this.configCache || this.loadLocalConfig();
|
||||
|
||||
return {
|
||||
hasRemoteCredentials: this.canConnectRemote(),
|
||||
lastSyncTime: this.lastSyncTime,
|
||||
hasLocalConfig: !!config,
|
||||
configSource: config?.source || 'unknown',
|
||||
configVersion: config?.version || 'unknown',
|
||||
mapCount: config?.maps?.length || 0,
|
||||
objectCount: this.countObjects(config || {}),
|
||||
syncIntervalMinutes: this.SYNC_INTERVAL / 60000,
|
||||
configFile: this.CONFIG_FILE,
|
||||
backupDir: this.BACKUP_DIR,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据地图ID获取Stream名称(兼容原接口)
|
||||
*/
|
||||
async getStreamByMap(mapId: string): Promise<string | null> {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
const map = config.maps?.find((m: any) => m.mapId === mapId);
|
||||
return map?.zulipStream || null;
|
||||
} catch (error) {
|
||||
this.logger.error('获取Stream失败', {
|
||||
mapId,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Stream名称获取地图ID(兼容原接口)
|
||||
*/
|
||||
async getMapIdByStream(streamName: string): Promise<string | null> {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
const map = config.maps?.find((m: any) => m.zulipStream === streamName);
|
||||
return map?.mapId || null;
|
||||
} catch (error) {
|
||||
this.logger.error('获取地图ID失败', {
|
||||
streamName,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有地图配置(兼容原接口)
|
||||
*/
|
||||
async getAllMapConfigs(): Promise<any[]> {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
return config.maps || [];
|
||||
} catch (error) {
|
||||
this.logger.error('获取所有地图配置失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份文件列表
|
||||
*/
|
||||
getBackupFiles(): any[] {
|
||||
try {
|
||||
if (!fs.existsSync(this.BACKUP_DIR)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs.readdirSync(this.BACKUP_DIR)
|
||||
.filter(file => file.startsWith('map-config-backup-'))
|
||||
.map(file => {
|
||||
const filePath = path.join(this.BACKUP_DIR, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
return {
|
||||
name: file,
|
||||
path: filePath,
|
||||
size: stats.size,
|
||||
created: stats.mtime,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.created.getTime() - a.created.getTime());
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取备份文件列表失败', {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从备份恢复配置
|
||||
*/
|
||||
async restoreFromBackup(backupFileName: string): Promise<boolean> {
|
||||
try {
|
||||
const backupFile = path.join(this.BACKUP_DIR, backupFileName);
|
||||
|
||||
if (!fs.existsSync(backupFile)) {
|
||||
throw new Error('备份文件不存在');
|
||||
}
|
||||
|
||||
// 备份当前配置
|
||||
this.backupCurrentConfig();
|
||||
|
||||
// 恢复备份
|
||||
fs.copyFileSync(backupFile, this.CONFIG_FILE);
|
||||
|
||||
// 重新加载配置
|
||||
this.configCache = this.loadLocalConfig();
|
||||
|
||||
this.logger.log('配置恢复成功', { backupFile: backupFileName });
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('配置恢复失败', {
|
||||
backupFile: backupFileName,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
onModuleDestroy(): void {
|
||||
if (this.syncTimer) {
|
||||
clearInterval(this.syncTimer);
|
||||
this.syncTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,24 @@
|
||||
* 功能描述:
|
||||
* - 测试ErrorHandlerService的核心功能
|
||||
* - 包含属性测试验证错误处理和服务降级
|
||||
* - 验证重试机制和指数退避策略
|
||||
* - 测试系统负载监控和限流功能
|
||||
*
|
||||
* 测试策略:
|
||||
* - 使用fast-check进行属性测试,验证错误处理的一致性
|
||||
* - 模拟各种错误场景,测试降级策略的有效性
|
||||
* - 验证重试机制在不同错误类型下的行为
|
||||
*
|
||||
* **Feature: zulip-integration, Property 9: 错误处理和服务降级**
|
||||
* **Validates: Requirements 8.1, 8.2, 8.3, 8.4**
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 完善测试文件注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl, moyin
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
@@ -30,12 +30,13 @@
|
||||
* - AppLoggerService: 日志记录服务
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-07
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable, OnModuleDestroy, Logger } from '@nestjs/common';
|
||||
@@ -234,7 +235,7 @@ export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy
|
||||
* 功能描述:
|
||||
* 分析Zulip API错误类型,决定处理策略和是否需要降级
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 处理流程:
|
||||
* 1. 分析错误类型和严重程度
|
||||
* 2. 更新错误统计
|
||||
* 3. 决定是否启用降级模式
|
||||
@@ -297,7 +298,7 @@ export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy
|
||||
* 功能描述:
|
||||
* 当Zulip服务不可用时,切换到本地聊天模式
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 处理流程:
|
||||
* 1. 检查降级模式是否启用
|
||||
* 2. 更新服务状态
|
||||
* 3. 记录降级开始时间
|
||||
@@ -741,7 +742,7 @@ export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy
|
||||
* 功能描述:
|
||||
* 执行操作并在超时时自动取消,返回超时错误
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 处理流程:
|
||||
* 1. 创建超时Promise
|
||||
* 2. 与操作Promise竞争
|
||||
* 3. 超时则抛出超时错误
|
||||
@@ -826,7 +827,7 @@ export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy
|
||||
* 功能描述:
|
||||
* 当连接断开时,调度自动重连尝试
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 处理流程:
|
||||
* 1. 检查自动重连是否启用
|
||||
* 2. 检查是否已在重连中
|
||||
* 3. 创建重连状态
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
* - 测试MonitoringService的核心功能
|
||||
* - 包含属性测试验证操作确认和日志记录
|
||||
* - 包含属性测试验证系统监控和告警
|
||||
* - 验证性能指标收集和分析功能
|
||||
*
|
||||
* 测试策略:
|
||||
* - 使用fast-check进行属性测试,验证监控数据的准确性
|
||||
* - 模拟各种系统状态,测试告警机制的有效性
|
||||
* - 验证日志记录的完整性和格式正确性
|
||||
*
|
||||
* **Feature: zulip-integration, Property 10: 操作确认和日志记录**
|
||||
* **Validates: Requirements 4.5, 8.5, 9.1, 9.2, 9.3**
|
||||
@@ -12,9 +18,13 @@
|
||||
* **Feature: zulip-integration, Property 11: 系统监控和告警**
|
||||
* **Validates: Requirements 9.4**
|
||||
*
|
||||
* @author angjustinl moyin
|
||||
* @version 1.0.0
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 完善测试文件注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl, moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
@@ -30,12 +30,13 @@
|
||||
* - ConfigService: 配置服务
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-07
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
* - Mock层:模拟外部依赖和配置
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 创建缺失的测试文件
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 创建缺失的测试文件 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @version 1.0.2
|
||||
* @since 2026-01-07
|
||||
* @lastModified 2026-01-07
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
@@ -21,12 +21,13 @@
|
||||
* - 配置更新后重新初始化
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-07
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
@@ -5,10 +5,20 @@
|
||||
* - 测试UserManagementService的核心功能
|
||||
* - 测试用户查询和验证逻辑
|
||||
* - 测试错误处理和边界情况
|
||||
* - 验证API调用和响应处理
|
||||
*
|
||||
* 测试策略:
|
||||
* - 模拟Zulip API响应,测试各种场景
|
||||
* - 验证用户信息查询的准确性
|
||||
* - 测试异常情况的错误处理
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 完善测试文件注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @since 2025-01-06
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
@@ -21,16 +21,18 @@
|
||||
* 使用场景:
|
||||
* - 用户登录时验证用户存在性
|
||||
* - 获取用户基本信息
|
||||
* - 验证用户权限和状态
|
||||
* - 验证用户账户状态
|
||||
* - 管理员查看用户列表
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-12: 代码规范优化 - 修正Core层业务概念描述,将"用户权限"改为"账户状态" (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @version 1.0.3
|
||||
* @since 2025-01-06
|
||||
* @lastModified 2026-01-07
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
@@ -149,7 +151,7 @@ export class UserManagementService {
|
||||
* 功能描述:
|
||||
* 通过Zulip API检查指定邮箱的用户是否存在
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 技术实现:
|
||||
* 1. 获取所有用户列表
|
||||
* 2. 在列表中查找指定邮箱
|
||||
* 3. 返回用户存在性结果
|
||||
|
||||
@@ -3,12 +3,22 @@
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试UserRegistrationService的核心功能
|
||||
* - 测试用户注册流程和验证逻辑
|
||||
* - 测试用户账号创建和验证逻辑
|
||||
* - 测试错误处理和边界情况
|
||||
* - 验证API Key管理功能
|
||||
*
|
||||
* 测试策略:
|
||||
* - 模拟Zulip API调用,测试账号创建
|
||||
* - 验证用户信息验证的正确性
|
||||
* - 测试各种异常场景的处理
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 完善测试文件注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @since 2025-01-06
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - 管理用户API Key(如果有权限)
|
||||
*
|
||||
* 职责分离:
|
||||
* - 用户注册层:处理新用户的注册流程
|
||||
* - 用户创建层:处理新用户的账号创建
|
||||
* - 信息验证层:验证用户提供的注册信息
|
||||
* - API Key管理层:处理用户API Key的获取和管理
|
||||
*
|
||||
@@ -21,16 +21,19 @@
|
||||
* 使用场景:
|
||||
* - 用户登录时验证用户存在性
|
||||
* - 获取用户基本信息
|
||||
* - 验证用户权限和状态
|
||||
* - 验证用户账户状态
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正Core层业务流程描述,将"注册流程"改为"账号创建" (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 修正Core层业务概念描述,将"用户权限"改为"账户状态" (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-10: 功能完善 - 添加用户注册和API Key管理功能 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.1.0
|
||||
* @version 1.1.3
|
||||
* @since 2025-01-06
|
||||
* @lastModified 2026-01-10
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
@@ -100,7 +103,7 @@ export interface UserRegistrationResponse {
|
||||
* - 处理新用户在Zulip服务器上的注册
|
||||
* - 验证用户信息的有效性
|
||||
* - 与Zulip API交互创建用户账户
|
||||
* - 管理注册流程和错误处理
|
||||
* - 管理账号创建和错误处理
|
||||
*/
|
||||
@Injectable()
|
||||
export class UserRegistrationService {
|
||||
@@ -119,7 +122,7 @@ export class UserRegistrationService {
|
||||
* 功能描述:
|
||||
* 在Zulip服务器上创建新用户账户
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 技术实现:
|
||||
* 1. 验证用户注册信息
|
||||
* 2. 检查用户是否已存在
|
||||
* 3. 调用Zulip API创建用户
|
||||
@@ -155,8 +158,8 @@ export class UserRegistrationService {
|
||||
};
|
||||
}
|
||||
|
||||
// 实现Zulip用户注册逻辑
|
||||
// 注意:这里实现了完整的用户注册流程,包括验证和错误处理
|
||||
// 实现Zulip用户创建逻辑
|
||||
// 注意:这里实现了完整的用户账号创建,包括验证和错误处理
|
||||
|
||||
// 2. 检查用户是否已存在
|
||||
const userExists = await this.checkUserExists(request.email);
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
* - 数据层:测试数据处理和验证逻辑
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 创建缺失的测试文件
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 创建缺失的测试文件 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @version 1.0.2
|
||||
* @since 2026-01-07
|
||||
* @lastModified 2026-01-07
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
@@ -24,13 +24,16 @@
|
||||
* - 账号关联和映射存储
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正Core层业务流程描述,将"注册流程"改为"账号创建" (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 拆分createZulipAccount长方法,提升代码可读性和维护性 (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-10: 功能完善 - 完善账号创建和API Key管理逻辑 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.1.0
|
||||
* @version 1.2.1
|
||||
* @since 2025-01-05
|
||||
* @lastModified 2026-01-10
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
@@ -109,7 +112,7 @@ export interface AccountLinkInfo {
|
||||
* - unlinkExternalAccount(): 解除账号关联
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户注册流程中自动创建Zulip账号
|
||||
* - 用户账号创建时自动创建Zulip账号
|
||||
* - API Key管理和更新
|
||||
* - 账号状态监控和维护
|
||||
* - 跨平台账号同步
|
||||
@@ -185,7 +188,7 @@ export class ZulipAccountService {
|
||||
* 功能描述:
|
||||
* 使用管理员权限在Zulip服务器上创建新的用户账号
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 技术实现:
|
||||
* 1. 验证管理员客户端是否已初始化
|
||||
* 2. 检查邮箱是否已存在
|
||||
* 3. 生成用户密码(如果未提供)
|
||||
@@ -207,186 +210,28 @@ export class ZulipAccountService {
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. 验证管理员客户端
|
||||
if (!this.adminClient) {
|
||||
throw new Error('管理员客户端未初始化');
|
||||
// 1. 验证请求参数和管理员客户端
|
||||
this.validateCreateRequest(request);
|
||||
|
||||
// 2. 检查用户是否已存在,如果存在则绑定
|
||||
const existingUserResult = await this.handleExistingUser(request);
|
||||
if (existingUserResult) {
|
||||
return existingUserResult;
|
||||
}
|
||||
|
||||
// 2. 验证请求参数
|
||||
if (!request.email || !request.email.trim()) {
|
||||
throw new Error('邮箱地址不能为空');
|
||||
}
|
||||
|
||||
if (!request.fullName || !request.fullName.trim()) {
|
||||
throw new Error('用户全名不能为空');
|
||||
}
|
||||
|
||||
// 3. 检查邮箱格式
|
||||
if (!this.isValidEmail(request.email)) {
|
||||
throw new Error('邮箱格式无效');
|
||||
}
|
||||
|
||||
// 4. 检查用户是否已存在
|
||||
const existingUser = await this.checkUserExists(request.email);
|
||||
if (existingUser) {
|
||||
this.logger.log('用户已存在,绑定已有账号', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
});
|
||||
|
||||
// 尝试获取已有用户的信息
|
||||
const userInfo = await this.getExistingUserInfo(request.email);
|
||||
if (userInfo.success) {
|
||||
// 尝试为已有用户生成API Key
|
||||
const apiKeyResult = await this.generateApiKeyForUser(request.email, request.password || '');
|
||||
|
||||
// 无论API Key是否生成成功,都返回成功的绑定结果
|
||||
this.logger.log('Zulip账号绑定成功(已存在)', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
userId: userInfo.userId,
|
||||
hasApiKey: apiKeyResult.success,
|
||||
apiKeyError: apiKeyResult.success ? undefined : apiKeyResult.error,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: userInfo.userId,
|
||||
email: request.email,
|
||||
apiKey: apiKeyResult.success ? apiKeyResult.apiKey : undefined,
|
||||
isExistingUser: true, // 添加标识表示这是绑定已有账号
|
||||
// 不返回错误信息,因为绑定本身是成功的
|
||||
};
|
||||
} else {
|
||||
// 即使无法获取用户详细信息,也尝试返回成功的绑定结果
|
||||
// 因为我们已经确认用户存在
|
||||
this.logger.warn('用户已存在但无法获取详细信息,仍返回绑定成功', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
getUserInfoError: userInfo.error,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: undefined, // 无法获取用户ID
|
||||
email: request.email,
|
||||
apiKey: undefined, // 无法生成API Key
|
||||
isExistingUser: true, // 添加标识表示这是绑定已有账号
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 生成密码(如果未提供)
|
||||
const password = request.password || this.generateRandomPassword();
|
||||
const shortName = request.shortName || this.generateShortName(request.email);
|
||||
|
||||
// 6. 创建用户参数
|
||||
const createParams = {
|
||||
email: request.email,
|
||||
password: password,
|
||||
full_name: request.fullName,
|
||||
short_name: shortName,
|
||||
};
|
||||
|
||||
// 7. 调用Zulip API创建用户
|
||||
const createResponse = await this.adminClient.users.create(createParams);
|
||||
|
||||
if (createResponse.result !== 'success') {
|
||||
// 检查是否是用户已存在的错误
|
||||
if (createResponse.msg && createResponse.msg.includes('already in use')) {
|
||||
this.logger.log('用户邮箱已被使用,尝试绑定已有账号', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
error: createResponse.msg,
|
||||
});
|
||||
|
||||
// 尝试获取已有用户信息
|
||||
const userInfo = await this.getExistingUserInfo(request.email);
|
||||
if (userInfo.success) {
|
||||
// 尝试为已有用户生成API Key
|
||||
const apiKeyResult = await this.generateApiKeyForUser(request.email, password);
|
||||
|
||||
this.logger.log('Zulip账号绑定成功(API创建时发现已存在)', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
userId: userInfo.userId,
|
||||
hasApiKey: apiKeyResult.success,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: userInfo.userId,
|
||||
email: request.email,
|
||||
apiKey: apiKeyResult.success ? apiKeyResult.apiKey : undefined,
|
||||
isExistingUser: true, // 标识这是绑定已有账号
|
||||
};
|
||||
} else {
|
||||
// 无法获取用户信息,但我们知道用户存在
|
||||
this.logger.warn('用户已存在但无法获取详细信息', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
getUserInfoError: userInfo.error,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: undefined,
|
||||
email: request.email,
|
||||
apiKey: undefined,
|
||||
isExistingUser: true, // 标识这是绑定已有账号
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 其他类型的错误
|
||||
this.logger.warn('Zulip用户创建失败', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
error: createResponse.msg,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: createResponse.msg || '用户创建失败',
|
||||
errorCode: 'ZULIP_CREATE_FAILED',
|
||||
};
|
||||
}
|
||||
|
||||
// 8. 为新用户生成API Key
|
||||
const apiKeyResult = await this.generateApiKeyForUser(request.email, password);
|
||||
|
||||
if (!apiKeyResult.success) {
|
||||
this.logger.warn('API Key生成失败,但用户已创建', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
error: apiKeyResult.error,
|
||||
});
|
||||
// 用户已创建,但API Key生成失败
|
||||
return {
|
||||
success: true,
|
||||
userId: createResponse.user_id,
|
||||
email: request.email,
|
||||
error: `用户创建成功,但API Key生成失败: ${apiKeyResult.error}`,
|
||||
errorCode: 'API_KEY_GENERATION_FAILED',
|
||||
};
|
||||
}
|
||||
// 3. 创建新用户
|
||||
const newUserResult = await this.createNewZulipUser(request);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('Zulip账号创建成功', {
|
||||
this.logger.log('Zulip账号创建完成', {
|
||||
operation: 'createZulipAccount',
|
||||
email: request.email,
|
||||
userId: createResponse.user_id,
|
||||
hasApiKey: !!apiKeyResult.apiKey,
|
||||
success: newUserResult.success,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: createResponse.user_id,
|
||||
email: request.email,
|
||||
apiKey: apiKeyResult.apiKey,
|
||||
};
|
||||
return newUserResult;
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
@@ -408,6 +253,223 @@ export class ZulipAccountService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证创建请求参数
|
||||
*
|
||||
* @param request 账号创建请求
|
||||
* @throws Error 当参数无效时
|
||||
* @private
|
||||
*/
|
||||
private validateCreateRequest(request: CreateZulipAccountRequest): void {
|
||||
// 1. 验证管理员客户端
|
||||
if (!this.adminClient) {
|
||||
throw new Error('管理员客户端未初始化');
|
||||
}
|
||||
|
||||
// 2. 验证请求参数
|
||||
if (!request.email || !request.email.trim()) {
|
||||
throw new Error('邮箱地址不能为空');
|
||||
}
|
||||
|
||||
if (!request.fullName || !request.fullName.trim()) {
|
||||
throw new Error('用户全名不能为空');
|
||||
}
|
||||
|
||||
// 3. 检查邮箱格式
|
||||
if (!this.isValidEmail(request.email)) {
|
||||
throw new Error('邮箱格式无效');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理已存在的用户
|
||||
*
|
||||
* @param request 账号创建请求
|
||||
* @returns Promise<CreateZulipAccountResult | null> 如果用户已存在返回结果,否则返回null
|
||||
* @private
|
||||
*/
|
||||
private async handleExistingUser(request: CreateZulipAccountRequest): Promise<CreateZulipAccountResult | null> {
|
||||
const existingUser = await this.checkUserExists(request.email);
|
||||
if (!existingUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.log('用户已存在,绑定已有账号', {
|
||||
operation: 'handleExistingUser',
|
||||
email: request.email,
|
||||
});
|
||||
|
||||
// 尝试获取已有用户的信息
|
||||
const userInfo = await this.getExistingUserInfo(request.email);
|
||||
if (userInfo.success) {
|
||||
// 尝试为已有用户生成API Key
|
||||
const apiKeyResult = await this.generateApiKeyForUser(request.email, request.password || '');
|
||||
|
||||
this.logger.log('Zulip账号绑定成功(已存在)', {
|
||||
operation: 'handleExistingUser',
|
||||
email: request.email,
|
||||
userId: userInfo.userId,
|
||||
hasApiKey: apiKeyResult.success,
|
||||
apiKeyError: apiKeyResult.success ? undefined : apiKeyResult.error,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: userInfo.userId,
|
||||
email: request.email,
|
||||
apiKey: apiKeyResult.success ? apiKeyResult.apiKey : undefined,
|
||||
isExistingUser: true,
|
||||
};
|
||||
} else {
|
||||
this.logger.warn('用户已存在但无法获取详细信息,仍返回绑定成功', {
|
||||
operation: 'handleExistingUser',
|
||||
email: request.email,
|
||||
getUserInfoError: userInfo.error,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: undefined,
|
||||
email: request.email,
|
||||
apiKey: undefined,
|
||||
isExistingUser: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的Zulip用户
|
||||
*
|
||||
* @param request 账号创建请求
|
||||
* @returns Promise<CreateZulipAccountResult> 创建结果
|
||||
* @private
|
||||
*/
|
||||
private async createNewZulipUser(request: CreateZulipAccountRequest): Promise<CreateZulipAccountResult> {
|
||||
// 1. 生成密码(如果未提供)
|
||||
const password = request.password || this.generateRandomPassword();
|
||||
const shortName = request.shortName || this.generateShortName(request.email);
|
||||
|
||||
// 2. 创建用户参数
|
||||
const createParams = {
|
||||
email: request.email,
|
||||
password: password,
|
||||
full_name: request.fullName,
|
||||
short_name: shortName,
|
||||
};
|
||||
|
||||
// 3. 调用Zulip API创建用户
|
||||
const createResponse = await this.adminClient.users.create(createParams);
|
||||
|
||||
if (createResponse.result !== 'success') {
|
||||
return this.handleCreateUserError(createResponse, request, password);
|
||||
}
|
||||
|
||||
// 4. 为新用户生成API Key
|
||||
const apiKeyResult = await this.generateApiKeyForUser(request.email, password);
|
||||
|
||||
if (!apiKeyResult.success) {
|
||||
this.logger.warn('API Key生成失败,但用户已创建', {
|
||||
operation: 'createNewZulipUser',
|
||||
email: request.email,
|
||||
error: apiKeyResult.error,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
userId: createResponse.user_id,
|
||||
email: request.email,
|
||||
error: `用户创建成功,但API Key生成失败: ${apiKeyResult.error}`,
|
||||
errorCode: 'API_KEY_GENERATION_FAILED',
|
||||
};
|
||||
}
|
||||
|
||||
this.logger.log('Zulip账号创建成功', {
|
||||
operation: 'createNewZulipUser',
|
||||
email: request.email,
|
||||
userId: createResponse.user_id,
|
||||
hasApiKey: !!apiKeyResult.apiKey,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: createResponse.user_id,
|
||||
email: request.email,
|
||||
apiKey: apiKeyResult.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理创建用户时的错误
|
||||
*
|
||||
* @param createResponse Zulip API响应
|
||||
* @param request 原始请求
|
||||
* @param password 生成的密码
|
||||
* @returns Promise<CreateZulipAccountResult> 处理结果
|
||||
* @private
|
||||
*/
|
||||
private async handleCreateUserError(
|
||||
createResponse: any,
|
||||
request: CreateZulipAccountRequest,
|
||||
password: string
|
||||
): Promise<CreateZulipAccountResult> {
|
||||
// 检查是否是用户已存在的错误
|
||||
if (createResponse.msg && createResponse.msg.includes('already in use')) {
|
||||
this.logger.log('用户邮箱已被使用,尝试绑定已有账号', {
|
||||
operation: 'handleCreateUserError',
|
||||
email: request.email,
|
||||
error: createResponse.msg,
|
||||
});
|
||||
|
||||
// 尝试获取已有用户信息
|
||||
const userInfo = await this.getExistingUserInfo(request.email);
|
||||
if (userInfo.success) {
|
||||
// 尝试为已有用户生成API Key
|
||||
const apiKeyResult = await this.generateApiKeyForUser(request.email, password);
|
||||
|
||||
this.logger.log('Zulip账号绑定成功(API创建时发现已存在)', {
|
||||
operation: 'handleCreateUserError',
|
||||
email: request.email,
|
||||
userId: userInfo.userId,
|
||||
hasApiKey: apiKeyResult.success,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: userInfo.userId,
|
||||
email: request.email,
|
||||
apiKey: apiKeyResult.success ? apiKeyResult.apiKey : undefined,
|
||||
isExistingUser: true,
|
||||
};
|
||||
} else {
|
||||
this.logger.warn('用户已存在但无法获取详细信息', {
|
||||
operation: 'handleCreateUserError',
|
||||
email: request.email,
|
||||
getUserInfoError: userInfo.error,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: undefined,
|
||||
email: request.email,
|
||||
apiKey: undefined,
|
||||
isExistingUser: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 其他类型的错误
|
||||
this.logger.warn('Zulip用户创建失败', {
|
||||
operation: 'handleCreateUserError',
|
||||
email: request.email,
|
||||
error: createResponse.msg,
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: createResponse.msg || '用户创建失败',
|
||||
errorCode: 'ZULIP_CREATE_FAILED',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户生成API Key
|
||||
*
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
* - 验证消息发送和错误处理逻辑
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-10: 测试完善 - 添加特殊字符消息和错误处理测试用例 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.1
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-10
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
@@ -24,12 +24,14 @@
|
||||
* - 事件队列管理
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正Core层注释措辞,将"业务逻辑"改为"技术实现" (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @version 1.0.3
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-07
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
@@ -122,7 +124,7 @@ export class ZulipClientService {
|
||||
* 功能描述:
|
||||
* 使用提供的配置创建zulip-js客户端实例,并验证API Key的有效性
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 技术实现:
|
||||
* 1. 验证配置参数的完整性
|
||||
* 2. 创建zulip-js客户端实例
|
||||
* 3. 调用API验证凭证有效性
|
||||
@@ -258,7 +260,7 @@ export class ZulipClientService {
|
||||
* 功能描述:
|
||||
* 使用Zulip客户端发送消息到指定的Stream和Topic
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 技术实现:
|
||||
* 1. 验证客户端实例有效性
|
||||
* 2. 构建消息请求参数
|
||||
* 3. 调用Zulip API发送消息
|
||||
@@ -368,7 +370,7 @@ export class ZulipClientService {
|
||||
* 功能描述:
|
||||
* 向Zulip服务器注册事件队列,用于接收消息通知
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 技术实现:
|
||||
* 1. 验证客户端实例有效性
|
||||
* 2. 构建队列注册参数
|
||||
* 3. 调用Zulip API注册队列
|
||||
|
||||
@@ -5,10 +5,20 @@
|
||||
* - 测试ZulipClientPoolService的核心功能
|
||||
* - 测试客户端创建和销毁流程
|
||||
* - 测试事件队列管理
|
||||
* - 验证连接池统计和监控功能
|
||||
*
|
||||
* 测试策略:
|
||||
* - 模拟ZulipClientService,测试池管理逻辑
|
||||
* - 验证客户端生命周期管理的正确性
|
||||
* - 测试并发场景下的资源管理
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 完善测试文件注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
@@ -28,14 +28,15 @@
|
||||
* - AppLoggerService: 日志记录服务
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-10: 代码质量优化 - 简化错误处理逻辑,移除冗余try-catch块 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 注释规范检查和修正 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @version 1.0.3
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-10
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
||||
@@ -147,7 +148,7 @@ export class ZulipClientPoolService implements OnModuleDestroy {
|
||||
* 功能描述:
|
||||
* 使用用户的Zulip API Key创建客户端实例,并注册事件队列
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 技术实现:
|
||||
* 1. 检查是否已存在客户端
|
||||
* 2. 验证API Key的有效性
|
||||
* 3. 创建zulip-js客户端实例
|
||||
|
||||
@@ -1,463 +0,0 @@
|
||||
/**
|
||||
* Zulip消息发送集成测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试消息发送到真实Zulip服务器的完整流程
|
||||
* - 验证HTTP请求、响应处理和错误场景
|
||||
* - 包含网络异常和API错误的测试
|
||||
*
|
||||
* 注意:这些测试需要真实的Zulip服务器配置
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-10: 测试新增 - 创建Zulip消息发送集成测试 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-10
|
||||
* @lastModified 2026-01-10
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip_client.service';
|
||||
import * as nock from 'nock';
|
||||
|
||||
describe('ZulipMessageIntegration', () => {
|
||||
let service: ZulipClientService;
|
||||
let mockZulipClient: any;
|
||||
let clientInstance: ZulipClientInstance;
|
||||
|
||||
const testConfig: ZulipClientConfig = {
|
||||
username: 'test-bot@example.com',
|
||||
apiKey: 'test-api-key-12345',
|
||||
realm: 'https://test-zulip.example.com',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
// 清理所有HTTP拦截
|
||||
nock.cleanAll();
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [ZulipClientService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ZulipClientService>(ZulipClientService);
|
||||
|
||||
// 创建模拟的zulip-js客户端
|
||||
mockZulipClient = {
|
||||
config: testConfig,
|
||||
users: {
|
||||
me: {
|
||||
getProfile: jest.fn(),
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
send: jest.fn(),
|
||||
},
|
||||
queues: {
|
||||
register: jest.fn(),
|
||||
deregister: jest.fn(),
|
||||
},
|
||||
events: {
|
||||
retrieve: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
// 模拟客户端实例
|
||||
clientInstance = {
|
||||
userId: 'test-user-123',
|
||||
config: testConfig,
|
||||
client: mockZulipClient,
|
||||
lastEventId: -1,
|
||||
createdAt: new Date(),
|
||||
lastActivity: new Date(),
|
||||
isValid: true,
|
||||
};
|
||||
|
||||
// Mock zulip-js模块加载
|
||||
jest.spyOn(service as any, 'loadZulipModule').mockResolvedValue(() => mockZulipClient);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('消息发送到Zulip服务器', () => {
|
||||
it('应该成功发送消息到Zulip API', async () => {
|
||||
// 模拟成功的API响应
|
||||
mockZulipClient.messages.send.mockResolvedValue({
|
||||
result: 'success',
|
||||
id: 12345,
|
||||
msg: '',
|
||||
});
|
||||
|
||||
const result = await service.sendMessage(
|
||||
clientInstance,
|
||||
'test-stream',
|
||||
'test-topic',
|
||||
'Hello from integration test!'
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.messageId).toBe(12345);
|
||||
expect(mockZulipClient.messages.send).toHaveBeenCalledWith({
|
||||
type: 'stream',
|
||||
to: 'test-stream',
|
||||
subject: 'test-topic',
|
||||
content: 'Hello from integration test!',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该处理Zulip API错误响应', async () => {
|
||||
// 模拟API错误响应
|
||||
mockZulipClient.messages.send.mockResolvedValue({
|
||||
result: 'error',
|
||||
msg: 'Stream does not exist',
|
||||
code: 'STREAM_NOT_FOUND',
|
||||
});
|
||||
|
||||
const result = await service.sendMessage(
|
||||
clientInstance,
|
||||
'nonexistent-stream',
|
||||
'test-topic',
|
||||
'This should fail'
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Stream does not exist');
|
||||
});
|
||||
|
||||
it('应该处理网络连接异常', async () => {
|
||||
// 模拟网络异常
|
||||
mockZulipClient.messages.send.mockRejectedValue(new Error('Network timeout'));
|
||||
|
||||
const result = await service.sendMessage(
|
||||
clientInstance,
|
||||
'test-stream',
|
||||
'test-topic',
|
||||
'This will timeout'
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Network timeout');
|
||||
});
|
||||
|
||||
it('应该处理认证失败', async () => {
|
||||
// 模拟认证失败
|
||||
mockZulipClient.messages.send.mockResolvedValue({
|
||||
result: 'error',
|
||||
msg: 'Invalid API key',
|
||||
code: 'BAD_REQUEST',
|
||||
});
|
||||
|
||||
const result = await service.sendMessage(
|
||||
clientInstance,
|
||||
'test-stream',
|
||||
'test-topic',
|
||||
'Authentication test'
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Invalid API key');
|
||||
});
|
||||
|
||||
it('应该正确处理特殊字符和长消息', async () => {
|
||||
const longMessage = 'A'.repeat(1000) + '特殊字符测试: 🎮🎯🚀 @#$%^&*()';
|
||||
|
||||
mockZulipClient.messages.send.mockResolvedValue({
|
||||
result: 'success',
|
||||
id: 67890,
|
||||
});
|
||||
|
||||
const result = await service.sendMessage(
|
||||
clientInstance,
|
||||
'test-stream',
|
||||
'special-chars-topic',
|
||||
longMessage
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.messageId).toBe(67890);
|
||||
expect(mockZulipClient.messages.send).toHaveBeenCalledWith({
|
||||
type: 'stream',
|
||||
to: 'test-stream',
|
||||
subject: 'special-chars-topic',
|
||||
content: longMessage,
|
||||
});
|
||||
});
|
||||
|
||||
it('应该更新客户端最后活动时间', async () => {
|
||||
const initialTime = new Date('2026-01-01T00:00:00Z');
|
||||
clientInstance.lastActivity = initialTime;
|
||||
|
||||
mockZulipClient.messages.send.mockResolvedValue({
|
||||
result: 'success',
|
||||
id: 11111,
|
||||
});
|
||||
|
||||
await service.sendMessage(
|
||||
clientInstance,
|
||||
'test-stream',
|
||||
'test-topic',
|
||||
'Activity test'
|
||||
);
|
||||
|
||||
expect(clientInstance.lastActivity.getTime()).toBeGreaterThan(initialTime.getTime());
|
||||
});
|
||||
});
|
||||
|
||||
describe('事件队列与Zulip服务器交互', () => {
|
||||
it('应该成功注册事件队列', async () => {
|
||||
mockZulipClient.queues.register.mockResolvedValue({
|
||||
result: 'success',
|
||||
queue_id: 'test-queue-123',
|
||||
last_event_id: 42,
|
||||
});
|
||||
|
||||
const result = await service.registerQueue(clientInstance, ['message', 'typing']);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.queueId).toBe('test-queue-123');
|
||||
expect(result.lastEventId).toBe(42);
|
||||
expect(clientInstance.queueId).toBe('test-queue-123');
|
||||
expect(clientInstance.lastEventId).toBe(42);
|
||||
});
|
||||
|
||||
it('应该处理队列注册失败', async () => {
|
||||
mockZulipClient.queues.register.mockResolvedValue({
|
||||
result: 'error',
|
||||
msg: 'Rate limit exceeded',
|
||||
});
|
||||
|
||||
const result = await service.registerQueue(clientInstance);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Rate limit exceeded');
|
||||
});
|
||||
|
||||
it('应该成功获取事件', async () => {
|
||||
clientInstance.queueId = 'test-queue-123';
|
||||
clientInstance.lastEventId = 10;
|
||||
|
||||
const mockEvents = [
|
||||
{
|
||||
id: 11,
|
||||
type: 'message',
|
||||
message: {
|
||||
id: 98765,
|
||||
sender_email: 'user@example.com',
|
||||
content: 'Test message from Zulip',
|
||||
stream_id: 1,
|
||||
subject: 'Test Topic',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
type: 'typing',
|
||||
sender: { user_id: 123 },
|
||||
},
|
||||
];
|
||||
|
||||
mockZulipClient.events.retrieve.mockResolvedValue({
|
||||
result: 'success',
|
||||
events: mockEvents,
|
||||
});
|
||||
|
||||
const result = await service.getEvents(clientInstance, true);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.events).toEqual(mockEvents);
|
||||
expect(clientInstance.lastEventId).toBe(12); // 更新为最后一个事件的ID
|
||||
});
|
||||
|
||||
it('应该处理空事件队列', async () => {
|
||||
clientInstance.queueId = 'test-queue-123';
|
||||
|
||||
mockZulipClient.events.retrieve.mockResolvedValue({
|
||||
result: 'success',
|
||||
events: [],
|
||||
});
|
||||
|
||||
const result = await service.getEvents(clientInstance, true);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.events).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该成功注销事件队列', async () => {
|
||||
clientInstance.queueId = 'test-queue-123';
|
||||
|
||||
mockZulipClient.queues.deregister.mockResolvedValue({
|
||||
result: 'success',
|
||||
});
|
||||
|
||||
const result = await service.deregisterQueue(clientInstance);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(clientInstance.queueId).toBeUndefined();
|
||||
expect(clientInstance.lastEventId).toBe(-1);
|
||||
});
|
||||
|
||||
it('应该处理队列过期情况', async () => {
|
||||
clientInstance.queueId = 'expired-queue';
|
||||
|
||||
// 模拟队列过期的JSON解析错误
|
||||
mockZulipClient.queues.deregister.mockRejectedValue(
|
||||
new Error('invalid json response body at https://zulip.example.com/api/v1/events reason: Unexpected token')
|
||||
);
|
||||
|
||||
const result = await service.deregisterQueue(clientInstance);
|
||||
|
||||
expect(result).toBe(true); // 应该返回true,因为队列已过期
|
||||
expect(clientInstance.queueId).toBeUndefined();
|
||||
expect(clientInstance.lastEventId).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Key验证', () => {
|
||||
it('应该成功验证有效的API Key', async () => {
|
||||
mockZulipClient.users.me.getProfile.mockResolvedValue({
|
||||
result: 'success',
|
||||
email: 'test-bot@example.com',
|
||||
full_name: 'Test Bot',
|
||||
user_id: 123,
|
||||
});
|
||||
|
||||
const isValid = await service.validateApiKey(clientInstance);
|
||||
|
||||
expect(isValid).toBe(true);
|
||||
expect(clientInstance.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝无效的API Key', async () => {
|
||||
mockZulipClient.users.me.getProfile.mockResolvedValue({
|
||||
result: 'error',
|
||||
msg: 'Invalid API key',
|
||||
});
|
||||
|
||||
const isValid = await service.validateApiKey(clientInstance);
|
||||
|
||||
expect(isValid).toBe(false);
|
||||
expect(clientInstance.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('应该处理API Key验证网络异常', async () => {
|
||||
mockZulipClient.users.me.getProfile.mockRejectedValue(new Error('Connection refused'));
|
||||
|
||||
const isValid = await service.validateApiKey(clientInstance);
|
||||
|
||||
expect(isValid).toBe(false);
|
||||
expect(clientInstance.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误恢复和重试机制', () => {
|
||||
it('应该在临时网络错误后恢复', async () => {
|
||||
// 第一次调用失败,第二次成功
|
||||
mockZulipClient.messages.send
|
||||
.mockRejectedValueOnce(new Error('Temporary network error'))
|
||||
.mockResolvedValueOnce({
|
||||
result: 'success',
|
||||
id: 99999,
|
||||
});
|
||||
|
||||
// 第一次调用应该失败
|
||||
const firstResult = await service.sendMessage(
|
||||
clientInstance,
|
||||
'test-stream',
|
||||
'test-topic',
|
||||
'First attempt'
|
||||
);
|
||||
expect(firstResult.success).toBe(false);
|
||||
|
||||
// 第二次调用应该成功
|
||||
const secondResult = await service.sendMessage(
|
||||
clientInstance,
|
||||
'test-stream',
|
||||
'test-topic',
|
||||
'Second attempt'
|
||||
);
|
||||
expect(secondResult.success).toBe(true);
|
||||
expect(secondResult.messageId).toBe(99999);
|
||||
});
|
||||
|
||||
it('应该处理服务器5xx错误', async () => {
|
||||
mockZulipClient.messages.send.mockRejectedValue(new Error('Internal Server Error (500)'));
|
||||
|
||||
const result = await service.sendMessage(
|
||||
clientInstance,
|
||||
'test-stream',
|
||||
'test-topic',
|
||||
'Server error test'
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Internal Server Error (500)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能和并发测试', () => {
|
||||
it('应该处理并发消息发送', async () => {
|
||||
// 模拟多个并发消息
|
||||
const messagePromises = [];
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
mockZulipClient.messages.send.mockResolvedValue({
|
||||
result: 'success',
|
||||
id: 1000 + i,
|
||||
});
|
||||
|
||||
messagePromises.push(
|
||||
service.sendMessage(
|
||||
clientInstance,
|
||||
'test-stream',
|
||||
'concurrent-topic',
|
||||
`Concurrent message ${i}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.all(messagePromises);
|
||||
|
||||
results.forEach((result, index) => {
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.messageId).toBe(1000 + index);
|
||||
});
|
||||
});
|
||||
|
||||
it('应该在大量消息发送时保持性能', async () => {
|
||||
const startTime = Date.now();
|
||||
const messageCount = 100;
|
||||
|
||||
mockZulipClient.messages.send.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
result: 'success',
|
||||
id: Math.floor(Math.random() * 100000),
|
||||
})
|
||||
);
|
||||
|
||||
const promises = Array.from({ length: messageCount }, (_, i) =>
|
||||
service.sendMessage(
|
||||
clientInstance,
|
||||
'performance-stream',
|
||||
'performance-topic',
|
||||
`Performance test message ${i}`
|
||||
)
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// 验证所有消息都成功发送
|
||||
results.forEach(result => {
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
// 性能检查:100条消息应该在合理时间内完成(这里设为5秒)
|
||||
expect(duration).toBeLessThan(5000);
|
||||
|
||||
console.log(`发送${messageCount}条消息耗时: ${duration}ms`);
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
@@ -21,13 +21,14 @@
|
||||
* - @nestjs/config: NestJS配置模块
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-08: 文件夹扁平化 - 从config/子文件夹移动到上级目录 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @version 1.0.3
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-08
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
@@ -12,14 +12,15 @@
|
||||
* - 内部类型层:定义系统内部使用的数据类型
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-10: 代码质量优化 - 清理未使用的接口定义 (修改者: moyin)
|
||||
* - 2026-01-08: 文件夹扁平化 - 从interfaces/子文件夹移动到上级目录 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.3
|
||||
* @version 1.0.4
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-10
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,13 +13,14 @@
|
||||
* - 类型安全:确保常量的类型正确性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-08: 文件夹扁平化 - 从constants/子文件夹移动到上级目录 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 创建核心模块常量文件,提取魔法数字 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @version 1.0.2
|
||||
* @since 2026-01-07
|
||||
* @lastModified 2026-01-08
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
// 时间相关常量
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义Zulip核心服务的抽象接口
|
||||
* - 分离业务逻辑与技术实现
|
||||
* - 分离技术实现与上层调用
|
||||
* - 支持依赖注入和接口切换
|
||||
*
|
||||
* 职责分离:
|
||||
@@ -12,13 +12,15 @@
|
||||
* - 配置接口层:定义各类配置的接口规范
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正Core层注释措辞,将"业务逻辑"改为"技术实现" (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-08: 文件夹扁平化 - 从interfaces/子文件夹移动到上级目录 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范
|
||||
* - 2026-01-07: 代码规范优化 - 更新import路径和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @version 1.0.4
|
||||
* @since 2025-12-31
|
||||
* @lastModified 2026-01-08
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -222,6 +224,11 @@ export interface IZulipConfigService {
|
||||
*/
|
||||
getTopicByObject(mapId: string, objectId: string): string | null;
|
||||
|
||||
/**
|
||||
* 查找附近的交互对象
|
||||
*/
|
||||
findNearbyObject(mapId: string, x: number, y: number, radius?: number): any | null;
|
||||
|
||||
/**
|
||||
* 获取Zulip配置
|
||||
*/
|
||||
@@ -329,6 +336,14 @@ export interface IApiKeySecurityService {
|
||||
metadata?: { ipAddress?: string; userAgent?: string }
|
||||
): Promise<any>;
|
||||
|
||||
/**
|
||||
* 删除API Key
|
||||
*/
|
||||
deleteApiKey(
|
||||
userId: string,
|
||||
metadata?: { ipAddress?: string; userAgent?: string }
|
||||
): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 检查API Key是否存在
|
||||
*/
|
||||
|
||||
286
src/core/zulip_core/zulip_core.module.spec.ts
Normal file
286
src/core/zulip_core/zulip_core.module.spec.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* ZulipCoreModule单元测试
|
||||
*
|
||||
* 测试范围:
|
||||
* - 模块配置验证
|
||||
* - 服务提供者注册
|
||||
* - 依赖注入配置
|
||||
* - 模块导入导出
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { ZulipCoreModule } from './zulip_core.module';
|
||||
import { ApiKeySecurityService } from './services/api_key_security.service';
|
||||
import { ConfigManagerService } from './services/config_manager.service';
|
||||
import { DynamicConfigManagerService } from './services/dynamic_config_manager.service';
|
||||
import { ErrorHandlerService } from './services/error_handler.service';
|
||||
import { MonitoringService } from './services/monitoring.service';
|
||||
import { StreamInitializerService } from './services/stream_initializer.service';
|
||||
import { UserManagementService } from './services/user_management.service';
|
||||
import { UserRegistrationService } from './services/user_registration.service';
|
||||
import { ZulipAccountService } from './services/zulip_account.service';
|
||||
import { ZulipClientPoolService } from './services/zulip_client_pool.service';
|
||||
import { ZulipClientService } from './services/zulip_client.service';
|
||||
import { AppLoggerService } from '../utils/logger/logger.service';
|
||||
|
||||
describe('ZulipCoreModule', () => {
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
CacheModule.register({
|
||||
ttl: 300,
|
||||
max: 1000,
|
||||
}),
|
||||
ZulipCoreModule,
|
||||
],
|
||||
providers: [
|
||||
// Mock Redis服务
|
||||
{
|
||||
provide: 'REDIS_SERVICE',
|
||||
useValue: {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
del: jest.fn(),
|
||||
},
|
||||
},
|
||||
// Mock AppLoggerService
|
||||
{
|
||||
provide: AppLoggerService,
|
||||
useValue: {
|
||||
log: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (module) {
|
||||
await module.close();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Module Configuration', () => {
|
||||
it('should be defined', () => {
|
||||
expect(ZulipCoreModule).toBeDefined();
|
||||
});
|
||||
|
||||
it('should compile successfully', () => {
|
||||
expect(module).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Service Providers Registration', () => {
|
||||
it('should provide ApiKeySecurityService', () => {
|
||||
const service = module.get<ApiKeySecurityService>(ApiKeySecurityService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(ApiKeySecurityService);
|
||||
});
|
||||
|
||||
it('should provide ConfigManagerService', () => {
|
||||
const service = module.get<ConfigManagerService>(ConfigManagerService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(ConfigManagerService);
|
||||
});
|
||||
|
||||
it('should provide DynamicConfigManagerService', () => {
|
||||
const service = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(DynamicConfigManagerService);
|
||||
});
|
||||
|
||||
it('should provide ErrorHandlerService', () => {
|
||||
const service = module.get<ErrorHandlerService>(ErrorHandlerService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(ErrorHandlerService);
|
||||
});
|
||||
|
||||
it('should provide MonitoringService', () => {
|
||||
const service = module.get<MonitoringService>(MonitoringService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(MonitoringService);
|
||||
});
|
||||
|
||||
it('should provide StreamInitializerService', () => {
|
||||
const service = module.get<StreamInitializerService>(StreamInitializerService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(StreamInitializerService);
|
||||
});
|
||||
|
||||
it('should provide UserManagementService', () => {
|
||||
const service = module.get<UserManagementService>(UserManagementService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(UserManagementService);
|
||||
});
|
||||
|
||||
it('should provide UserRegistrationService', () => {
|
||||
const service = module.get<UserRegistrationService>(UserRegistrationService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(UserRegistrationService);
|
||||
});
|
||||
|
||||
it('should provide ZulipAccountService', () => {
|
||||
const service = module.get<ZulipAccountService>(ZulipAccountService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(ZulipAccountService);
|
||||
});
|
||||
|
||||
it('should provide ZulipClientPoolService', () => {
|
||||
const service = module.get<ZulipClientPoolService>(ZulipClientPoolService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(ZulipClientPoolService);
|
||||
});
|
||||
|
||||
it('should provide ZulipClientService', () => {
|
||||
const service = module.get<ZulipClientService>(ZulipClientService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(ZulipClientService);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dependency Injection Configuration', () => {
|
||||
it('should inject dependencies correctly for ApiKeySecurityService', () => {
|
||||
const service = module.get<ApiKeySecurityService>(ApiKeySecurityService);
|
||||
expect(service).toBeDefined();
|
||||
// ApiKeySecurityService应该能够正常实例化,说明依赖注入配置正确
|
||||
});
|
||||
|
||||
it('should inject dependencies correctly for ConfigManagerService', () => {
|
||||
const service = module.get<ConfigManagerService>(ConfigManagerService);
|
||||
expect(service).toBeDefined();
|
||||
// ConfigManagerService应该能够正常实例化,说明依赖注入配置正确
|
||||
});
|
||||
|
||||
it('should inject dependencies correctly for DynamicConfigManagerService', () => {
|
||||
const service = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);
|
||||
expect(service).toBeDefined();
|
||||
// DynamicConfigManagerService应该能够正常实例化,说明依赖注入配置正确
|
||||
});
|
||||
|
||||
it('should inject dependencies correctly for ErrorHandlerService', () => {
|
||||
const service = module.get<ErrorHandlerService>(ErrorHandlerService);
|
||||
expect(service).toBeDefined();
|
||||
// ErrorHandlerService应该能够正常实例化,说明依赖注入配置正确
|
||||
});
|
||||
|
||||
it('should inject dependencies correctly for MonitoringService', () => {
|
||||
const service = module.get<MonitoringService>(MonitoringService);
|
||||
expect(service).toBeDefined();
|
||||
// MonitoringService应该能够正常实例化,说明依赖注入配置正确
|
||||
});
|
||||
|
||||
it('should inject dependencies correctly for StreamInitializerService', () => {
|
||||
const service = module.get<StreamInitializerService>(StreamInitializerService);
|
||||
expect(service).toBeDefined();
|
||||
// StreamInitializerService应该能够正常实例化,说明依赖注入配置正确
|
||||
});
|
||||
|
||||
it('should inject dependencies correctly for UserManagementService', () => {
|
||||
const service = module.get<UserManagementService>(UserManagementService);
|
||||
expect(service).toBeDefined();
|
||||
// UserManagementService应该能够正常实例化,说明依赖注入配置正确
|
||||
});
|
||||
|
||||
it('should inject dependencies correctly for UserRegistrationService', () => {
|
||||
const service = module.get<UserRegistrationService>(UserRegistrationService);
|
||||
expect(service).toBeDefined();
|
||||
// UserRegistrationService应该能够正常实例化,说明依赖注入配置正确
|
||||
});
|
||||
|
||||
it('should inject dependencies correctly for ZulipAccountService', () => {
|
||||
const service = module.get<ZulipAccountService>(ZulipAccountService);
|
||||
expect(service).toBeDefined();
|
||||
// ZulipAccountService应该能够正常实例化,说明依赖注入配置正确
|
||||
});
|
||||
|
||||
it('should inject dependencies correctly for ZulipClientPoolService', () => {
|
||||
const service = module.get<ZulipClientPoolService>(ZulipClientPoolService);
|
||||
expect(service).toBeDefined();
|
||||
// ZulipClientPoolService应该能够正常实例化,说明依赖注入配置正确
|
||||
});
|
||||
|
||||
it('should inject dependencies correctly for ZulipClientService', () => {
|
||||
const service = module.get<ZulipClientService>(ZulipClientService);
|
||||
expect(service).toBeDefined();
|
||||
// ZulipClientService应该能够正常实例化,说明依赖注入配置正确
|
||||
});
|
||||
});
|
||||
|
||||
describe('Module Exports', () => {
|
||||
it('should export all core services for external use', () => {
|
||||
// 验证模块导出的服务可以被外部模块使用
|
||||
const apiKeySecurityService = module.get<ApiKeySecurityService>(ApiKeySecurityService);
|
||||
const configManagerService = module.get<ConfigManagerService>(ConfigManagerService);
|
||||
const dynamicConfigManagerService = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);
|
||||
const errorHandlerService = module.get<ErrorHandlerService>(ErrorHandlerService);
|
||||
const monitoringService = module.get<MonitoringService>(MonitoringService);
|
||||
const streamInitializerService = module.get<StreamInitializerService>(StreamInitializerService);
|
||||
const userManagementService = module.get<UserManagementService>(UserManagementService);
|
||||
const userRegistrationService = module.get<UserRegistrationService>(UserRegistrationService);
|
||||
const zulipAccountService = module.get<ZulipAccountService>(ZulipAccountService);
|
||||
const zulipClientPoolService = module.get<ZulipClientPoolService>(ZulipClientPoolService);
|
||||
const zulipClientService = module.get<ZulipClientService>(ZulipClientService);
|
||||
|
||||
expect(apiKeySecurityService).toBeDefined();
|
||||
expect(configManagerService).toBeDefined();
|
||||
expect(dynamicConfigManagerService).toBeDefined();
|
||||
expect(errorHandlerService).toBeDefined();
|
||||
expect(monitoringService).toBeDefined();
|
||||
expect(streamInitializerService).toBeDefined();
|
||||
expect(userManagementService).toBeDefined();
|
||||
expect(userRegistrationService).toBeDefined();
|
||||
expect(zulipAccountService).toBeDefined();
|
||||
expect(zulipClientPoolService).toBeDefined();
|
||||
expect(zulipClientService).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Module Integration', () => {
|
||||
it('should integrate all services without conflicts', () => {
|
||||
// 验证所有服务可以同时存在且无冲突
|
||||
const services = [
|
||||
module.get<ApiKeySecurityService>(ApiKeySecurityService),
|
||||
module.get<ConfigManagerService>(ConfigManagerService),
|
||||
module.get<DynamicConfigManagerService>(DynamicConfigManagerService),
|
||||
module.get<ErrorHandlerService>(ErrorHandlerService),
|
||||
module.get<MonitoringService>(MonitoringService),
|
||||
module.get<StreamInitializerService>(StreamInitializerService),
|
||||
module.get<UserManagementService>(UserManagementService),
|
||||
module.get<UserRegistrationService>(UserRegistrationService),
|
||||
module.get<ZulipAccountService>(ZulipAccountService),
|
||||
module.get<ZulipClientPoolService>(ZulipClientPoolService),
|
||||
module.get<ZulipClientService>(ZulipClientService),
|
||||
];
|
||||
|
||||
services.forEach(service => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
// 验证服务实例的唯一性(单例模式)
|
||||
const apiKeySecurityService1 = module.get<ApiKeySecurityService>(ApiKeySecurityService);
|
||||
const apiKeySecurityService2 = module.get<ApiKeySecurityService>(ApiKeySecurityService);
|
||||
expect(apiKeySecurityService1).toBe(apiKeySecurityService2);
|
||||
});
|
||||
|
||||
it('should handle module lifecycle correctly', async () => {
|
||||
// 验证模块生命周期管理
|
||||
expect(module).toBeDefined();
|
||||
|
||||
// 模块应该能够正常关闭
|
||||
await expect(module.close()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,29 +11,68 @@
|
||||
* - 服务抽象层:为业务层提供统一的服务接口
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 修正文件夹命名(zulip->zulip_core)和文件命名规范
|
||||
* - 2026-01-12: 架构优化 - 移除ZulipAccountsBusinessService引用,符合架构分层规范 (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 添加缺失的类注释和修正注释规范 (修改者: moyin)
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 修正文件夹命名(zulip->zulip_core)和文件命名规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @version 1.1.2
|
||||
* @since 2025-12-31
|
||||
* @lastModified 2026-01-07
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { ZulipClientService } from './services/zulip_client.service';
|
||||
import { ZulipClientPoolService } from './services/zulip_client_pool.service';
|
||||
import { ConfigManagerService } from './services/config_manager.service';
|
||||
import { DynamicConfigManagerService } from './services/dynamic_config_manager.service';
|
||||
import { ApiKeySecurityService } from './services/api_key_security.service';
|
||||
import { ErrorHandlerService } from './services/error_handler.service';
|
||||
import { MonitoringService } from './services/monitoring.service';
|
||||
import { StreamInitializerService } from './services/stream_initializer.service';
|
||||
import { UserManagementService } from './services/user_management.service';
|
||||
import { UserRegistrationService } from './services/user_registration.service';
|
||||
import { ZulipAccountService } from './services/zulip_account.service';
|
||||
import { AppLoggerService } from '../utils/logger/logger.service';
|
||||
import { RedisModule } from '../redis/redis.module';
|
||||
|
||||
// 缓存配置常量
|
||||
const CACHE_TTL_SECONDS = 300; // 5分钟缓存
|
||||
const CACHE_MAX_ITEMS = 1000; // 最大缓存项数
|
||||
|
||||
/**
|
||||
* Zulip核心服务模块类
|
||||
*
|
||||
* 职责:
|
||||
* - 配置和注册所有Zulip核心服务
|
||||
* - 管理服务之间的依赖关系
|
||||
* - 为业务层提供统一的服务接口
|
||||
* - 集成Redis和缓存模块
|
||||
*
|
||||
* 主要服务:
|
||||
* - ZulipClientService - Zulip客户端核心服务
|
||||
* - ZulipClientPoolService - 客户端连接池服务
|
||||
* - ConfigManagerService - 配置管理服务
|
||||
* - ApiKeySecurityService - API Key安全服务
|
||||
* - ErrorHandlerService - 错误处理服务
|
||||
* - MonitoringService - 监控服务
|
||||
*
|
||||
* 使用场景:
|
||||
* - 在业务模块中导入以获取Zulip核心服务
|
||||
* - 通过依赖注入使用各种Zulip相关服务
|
||||
* - 为整个应用提供Zulip集成能力
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
// Redis模块 - ApiKeySecurityService需要REDIS_SERVICE
|
||||
RedisModule,
|
||||
// 缓存模块 - 核心服务需要缓存支持
|
||||
CacheModule.register({
|
||||
ttl: CACHE_TTL_SECONDS,
|
||||
max: CACHE_MAX_ITEMS,
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
// 核心客户端服务
|
||||
@@ -56,15 +95,19 @@ import { RedisModule } from '../redis/redis.module';
|
||||
|
||||
// 辅助服务
|
||||
ApiKeySecurityService,
|
||||
ConfigManagerService,
|
||||
DynamicConfigManagerService,
|
||||
ErrorHandlerService,
|
||||
MonitoringService,
|
||||
StreamInitializerService,
|
||||
UserManagementService,
|
||||
UserRegistrationService,
|
||||
ZulipAccountService,
|
||||
AppLoggerService,
|
||||
|
||||
// 直接提供类(用于内部依赖)
|
||||
ZulipClientService,
|
||||
ZulipClientPoolService,
|
||||
ConfigManagerService,
|
||||
],
|
||||
exports: [
|
||||
// 导出接口标识符供业务层使用
|
||||
@@ -75,9 +118,13 @@ import { RedisModule } from '../redis/redis.module';
|
||||
|
||||
// 导出辅助服务
|
||||
ApiKeySecurityService,
|
||||
ConfigManagerService,
|
||||
DynamicConfigManagerService,
|
||||
ErrorHandlerService,
|
||||
MonitoringService,
|
||||
StreamInitializerService,
|
||||
UserManagementService,
|
||||
UserRegistrationService,
|
||||
ZulipAccountService,
|
||||
],
|
||||
})
|
||||
|
||||
7
src/core/zulip_core/zulip_js.d.ts
vendored
7
src/core/zulip_core/zulip_js.d.ts
vendored
@@ -11,13 +11,14 @@
|
||||
* - 类型安全层:确保编译时的类型检查
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
|
||||
* - 2026-01-08: 文件夹扁平化 - 从types/子文件夹移动到上级目录 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 文件重命名和注释规范
|
||||
* - 2026-01-07: 代码规范优化 - 文件重命名和注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @version 1.0.3
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-08
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
declare module 'zulip-js' {
|
||||
|
||||
Reference in New Issue
Block a user