test(zulip-accounts):完善zulip_accounts模块测试覆盖

范围:src/core/db/zulip_accounts/
- 添加ZulipAccounts实体业务方法测试
- 添加ZulipAccountsModule动态配置测试
- 提升测试覆盖率,确保实体逻辑和模块配置的正确性
- 测试包含状态管理、时间判断、错误处理等核心业务逻辑
This commit is contained in:
moyin
2026-01-12 18:39:32 +08:00
parent 4b349e0cd9
commit 1b4c952666
2 changed files with 795 additions and 0 deletions

View File

@@ -0,0 +1,468 @@
/**
* Zulip账号关联实体测试
*
* 功能描述:
* - 测试实体的业务方法逻辑
* - 验证状态管理和判断方法
* - 测试时间相关的业务逻辑
* - 验证错误处理和重试机制
*
* 测试范围:
* - 状态判断方法isActive, isHealthy, canBeDeleted等
* - 时间相关方法isStale, needsVerification等
* - 状态更新方法activate, suspend, deactivate等
* - 错误处理方法setError, clearError等
*
* 最近修改:
* - 2026-01-12: 代码规范优化 - 创建测试文件,确保实体业务方法的测试覆盖 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-12
* @lastModified 2026-01-12
*/
import { ZulipAccounts } from './zulip_accounts.entity';
import {
DEFAULT_MAX_RETRY_COUNT,
HIGH_RETRY_THRESHOLD,
DEFAULT_MAX_AGE_DAYS,
DEFAULT_VERIFICATION_HOURS,
MILLISECONDS_PER_DAY,
MILLISECONDS_PER_HOUR
} from './zulip_accounts.constants';
describe('ZulipAccounts Entity', () => {
let entity: ZulipAccounts;
beforeEach(() => {
entity = new ZulipAccounts();
entity.id = BigInt(1);
entity.gameUserId = BigInt(123);
entity.zulipUserId = 12345;
entity.zulipEmail = 'test@example.com';
entity.zulipFullName = 'Test User';
entity.zulipApiKeyEncrypted = 'encrypted-key';
entity.status = 'active';
entity.retryCount = 0;
entity.errorMessage = null;
entity.createdAt = new Date();
entity.updatedAt = new Date();
entity.lastVerifiedAt = new Date();
entity.lastSyncedAt = new Date();
});
describe('isActive()', () => {
it('should return true when status is active', () => {
entity.status = 'active';
expect(entity.isActive()).toBe(true);
});
it('should return false when status is not active', () => {
entity.status = 'inactive';
expect(entity.isActive()).toBe(false);
entity.status = 'suspended';
expect(entity.isActive()).toBe(false);
entity.status = 'error';
expect(entity.isActive()).toBe(false);
});
});
describe('isHealthy()', () => {
it('should return true when status is active and retry count is low', () => {
entity.status = 'active';
entity.retryCount = 0;
expect(entity.isHealthy()).toBe(true);
entity.retryCount = DEFAULT_MAX_RETRY_COUNT - 1;
expect(entity.isHealthy()).toBe(true);
});
it('should return false when status is not active', () => {
entity.status = 'inactive';
entity.retryCount = 0;
expect(entity.isHealthy()).toBe(false);
entity.status = 'error';
entity.retryCount = 0;
expect(entity.isHealthy()).toBe(false);
});
it('should return false when retry count exceeds limit', () => {
entity.status = 'active';
entity.retryCount = DEFAULT_MAX_RETRY_COUNT;
expect(entity.isHealthy()).toBe(false);
entity.retryCount = DEFAULT_MAX_RETRY_COUNT + 1;
expect(entity.isHealthy()).toBe(false);
});
});
describe('canBeDeleted()', () => {
it('should return true when status is not active', () => {
entity.status = 'inactive';
entity.retryCount = 0;
expect(entity.canBeDeleted()).toBe(true);
entity.status = 'suspended';
entity.retryCount = 0;
expect(entity.canBeDeleted()).toBe(true);
entity.status = 'error';
entity.retryCount = 0;
expect(entity.canBeDeleted()).toBe(true);
});
it('should return true when retry count exceeds high threshold', () => {
entity.status = 'active';
entity.retryCount = HIGH_RETRY_THRESHOLD + 1;
expect(entity.canBeDeleted()).toBe(true);
});
it('should return false when status is active and retry count is low', () => {
entity.status = 'active';
entity.retryCount = 0;
expect(entity.canBeDeleted()).toBe(false);
entity.retryCount = HIGH_RETRY_THRESHOLD;
expect(entity.canBeDeleted()).toBe(false);
});
});
describe('isStale()', () => {
it('should return false for recently updated entity', () => {
entity.updatedAt = new Date(); // 刚刚更新
expect(entity.isStale()).toBe(false);
});
it('should return true for old entity using default max age', () => {
const oldDate = new Date();
oldDate.setTime(oldDate.getTime() - (DEFAULT_MAX_AGE_DAYS * MILLISECONDS_PER_DAY + 1000));
entity.updatedAt = oldDate;
expect(entity.isStale()).toBe(true);
});
it('should return false for entity within default max age', () => {
const recentDate = new Date();
recentDate.setTime(recentDate.getTime() - (DEFAULT_MAX_AGE_DAYS * MILLISECONDS_PER_DAY - 1000));
entity.updatedAt = recentDate;
expect(entity.isStale()).toBe(false);
});
it('should respect custom max age parameter', () => {
const customMaxAge = 2 * MILLISECONDS_PER_DAY; // 2天
const oldDate = new Date();
oldDate.setTime(oldDate.getTime() - (customMaxAge + 1000));
entity.updatedAt = oldDate;
expect(entity.isStale(customMaxAge)).toBe(true);
});
it('should handle edge case at exact max age boundary', () => {
const exactDate = new Date();
exactDate.setTime(exactDate.getTime() - (DEFAULT_MAX_AGE_DAYS * MILLISECONDS_PER_DAY));
entity.updatedAt = exactDate;
expect(entity.isStale()).toBe(false); // 等于边界值应该返回false
});
});
describe('needsVerification()', () => {
it('should return true when lastVerifiedAt is null', () => {
entity.lastVerifiedAt = null;
expect(entity.needsVerification()).toBe(true);
});
it('should return false for recently verified entity', () => {
entity.lastVerifiedAt = new Date(); // 刚刚验证
expect(entity.needsVerification()).toBe(false);
});
it('should return true for old verification using default max age', () => {
const oldDate = new Date();
oldDate.setTime(oldDate.getTime() - (DEFAULT_VERIFICATION_HOURS * MILLISECONDS_PER_HOUR + 1000));
entity.lastVerifiedAt = oldDate;
expect(entity.needsVerification()).toBe(true);
});
it('should return false for verification within default max age', () => {
const recentDate = new Date();
recentDate.setTime(recentDate.getTime() - (DEFAULT_VERIFICATION_HOURS * MILLISECONDS_PER_HOUR - 1000));
entity.lastVerifiedAt = recentDate;
expect(entity.needsVerification()).toBe(false);
});
it('should respect custom max age parameter', () => {
const customMaxAge = 2 * MILLISECONDS_PER_HOUR; // 2小时
const oldDate = new Date();
oldDate.setTime(oldDate.getTime() - (customMaxAge + 1000));
entity.lastVerifiedAt = oldDate;
expect(entity.needsVerification(customMaxAge)).toBe(true);
});
});
describe('shouldRetry()', () => {
it('should return true when status is error and retry count is below limit', () => {
entity.status = 'error';
entity.retryCount = 0;
expect(entity.shouldRetry()).toBe(true);
entity.retryCount = DEFAULT_MAX_RETRY_COUNT - 1;
expect(entity.shouldRetry()).toBe(true);
});
it('should return false when status is not error', () => {
entity.status = 'active';
entity.retryCount = 0;
expect(entity.shouldRetry()).toBe(false);
entity.status = 'inactive';
entity.retryCount = 0;
expect(entity.shouldRetry()).toBe(false);
});
it('should return false when retry count exceeds limit', () => {
entity.status = 'error';
entity.retryCount = DEFAULT_MAX_RETRY_COUNT;
expect(entity.shouldRetry()).toBe(false);
entity.retryCount = DEFAULT_MAX_RETRY_COUNT + 1;
expect(entity.shouldRetry()).toBe(false);
});
it('should respect custom max retry count parameter', () => {
const customMaxRetry = 2;
entity.status = 'error';
entity.retryCount = 1;
expect(entity.shouldRetry(customMaxRetry)).toBe(true);
entity.retryCount = 2;
expect(entity.shouldRetry(customMaxRetry)).toBe(false);
});
});
describe('updateVerificationTime()', () => {
it('should update lastVerifiedAt and updatedAt to current time', () => {
const beforeUpdate = new Date();
entity.lastVerifiedAt = new Date('2020-01-01');
entity.updatedAt = new Date('2020-01-01');
entity.updateVerificationTime();
expect(entity.lastVerifiedAt.getTime()).toBeGreaterThanOrEqual(beforeUpdate.getTime());
expect(entity.updatedAt.getTime()).toBeGreaterThanOrEqual(beforeUpdate.getTime());
});
});
describe('updateSyncTime()', () => {
it('should update lastSyncedAt and updatedAt to current time', () => {
const beforeUpdate = new Date();
entity.lastSyncedAt = new Date('2020-01-01');
entity.updatedAt = new Date('2020-01-01');
entity.updateSyncTime();
expect(entity.lastSyncedAt.getTime()).toBeGreaterThanOrEqual(beforeUpdate.getTime());
expect(entity.updatedAt.getTime()).toBeGreaterThanOrEqual(beforeUpdate.getTime());
});
});
describe('setError()', () => {
it('should set status to error and update error message and retry count', () => {
const errorMessage = 'Test error message';
entity.status = 'active';
entity.errorMessage = null;
entity.retryCount = 0;
entity.setError(errorMessage);
expect(entity.status).toBe('error');
expect(entity.errorMessage).toBe(errorMessage);
expect(entity.retryCount).toBe(1);
});
it('should increment retry count on subsequent errors', () => {
entity.status = 'error';
entity.retryCount = 2;
entity.setError('Another error');
expect(entity.retryCount).toBe(3);
});
it('should update updatedAt timestamp', () => {
const beforeUpdate = new Date();
entity.updatedAt = new Date('2020-01-01');
entity.setError('Test error');
expect(entity.updatedAt.getTime()).toBeGreaterThanOrEqual(beforeUpdate.getTime());
});
});
describe('clearError()', () => {
it('should clear error status and message when status is error', () => {
entity.status = 'error';
entity.errorMessage = 'Some error';
entity.clearError();
expect(entity.status).toBe('active');
expect(entity.errorMessage).toBeNull();
});
it('should not change status when not in error state', () => {
entity.status = 'inactive';
entity.errorMessage = null;
entity.clearError();
expect(entity.status).toBe('inactive');
expect(entity.errorMessage).toBeNull();
});
it('should update updatedAt timestamp when clearing error', () => {
const beforeUpdate = new Date();
entity.status = 'error';
entity.updatedAt = new Date('2020-01-01');
entity.clearError();
expect(entity.updatedAt.getTime()).toBeGreaterThanOrEqual(beforeUpdate.getTime());
});
});
describe('resetRetryCount()', () => {
it('should reset retry count to zero', () => {
entity.retryCount = 5;
entity.resetRetryCount();
expect(entity.retryCount).toBe(0);
});
it('should update updatedAt timestamp', () => {
const beforeUpdate = new Date();
entity.updatedAt = new Date('2020-01-01');
entity.resetRetryCount();
expect(entity.updatedAt.getTime()).toBeGreaterThanOrEqual(beforeUpdate.getTime());
});
});
describe('activate()', () => {
it('should set status to active and clear error message and retry count', () => {
entity.status = 'error';
entity.errorMessage = 'Some error';
entity.retryCount = 3;
entity.activate();
expect(entity.status).toBe('active');
expect(entity.errorMessage).toBeNull();
expect(entity.retryCount).toBe(0);
});
it('should update updatedAt timestamp', () => {
const beforeUpdate = new Date();
entity.updatedAt = new Date('2020-01-01');
entity.activate();
expect(entity.updatedAt.getTime()).toBeGreaterThanOrEqual(beforeUpdate.getTime());
});
});
describe('suspend()', () => {
it('should set status to suspended', () => {
entity.status = 'active';
entity.suspend();
expect(entity.status).toBe('suspended');
});
it('should set error message when reason is provided', () => {
const reason = 'Suspended for maintenance';
entity.errorMessage = null;
entity.suspend(reason);
expect(entity.errorMessage).toBe(reason);
});
it('should not change error message when no reason is provided', () => {
const existingMessage = 'Existing message';
entity.errorMessage = existingMessage;
entity.suspend();
expect(entity.errorMessage).toBe(existingMessage);
});
it('should update updatedAt timestamp', () => {
const beforeUpdate = new Date();
entity.updatedAt = new Date('2020-01-01');
entity.suspend();
expect(entity.updatedAt.getTime()).toBeGreaterThanOrEqual(beforeUpdate.getTime());
});
});
describe('deactivate()', () => {
it('should set status to inactive', () => {
entity.status = 'active';
entity.deactivate();
expect(entity.status).toBe('inactive');
});
it('should update updatedAt timestamp', () => {
const beforeUpdate = new Date();
entity.updatedAt = new Date('2020-01-01');
entity.deactivate();
expect(entity.updatedAt.getTime()).toBeGreaterThanOrEqual(beforeUpdate.getTime());
});
});
describe('edge cases and boundary conditions', () => {
it('should handle null dates gracefully in time-based methods', () => {
entity.lastVerifiedAt = null;
entity.updatedAt = null as any; // 强制设置为null进行边界测试
expect(() => entity.needsVerification()).not.toThrow();
expect(entity.needsVerification()).toBe(true);
});
it('should handle zero retry count correctly', () => {
entity.retryCount = 0;
entity.status = 'error';
expect(entity.shouldRetry()).toBe(true);
expect(entity.isHealthy()).toBe(false);
});
it('should handle maximum retry count boundary', () => {
entity.retryCount = DEFAULT_MAX_RETRY_COUNT;
entity.status = 'error';
expect(entity.shouldRetry()).toBe(false);
expect(entity.isHealthy()).toBe(false);
});
it('should handle very large retry counts', () => {
entity.retryCount = 999999;
entity.status = 'active';
expect(entity.isHealthy()).toBe(false);
expect(entity.canBeDeleted()).toBe(true);
});
});
});

View File

@@ -0,0 +1,327 @@
/**
* Zulip账号关联数据模块测试
*
* 功能描述:
* - 测试动态模块配置的正确性
* - 验证数据库和内存模式的切换逻辑
* - 测试依赖注入配置的完整性
* - 验证环境自适应功能
*
* 测试范围:
* - forDatabase()方法的模块配置
* - forMemory()方法的模块配置
* - forRoot()方法的自动选择逻辑
* - isDatabaseConfigured()函数的环境检测
*
* 最近修改:
* - 2026-01-12: 代码规范优化 - 创建测试文件,确保模块配置逻辑的测试覆盖 (修改者: moyin)
* - 2026-01-12: 代码规范优化 - 修复测试依赖注入问题,简化集成测试 (修改者: moyin)
*
* @author moyin
* @version 1.0.1
* @since 2026-01-12
* @lastModified 2026-01-12
*/
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CacheModule } from '@nestjs/cache-manager';
import { ZulipAccountsModule } from './zulip_accounts.module';
import { ZulipAccounts } from './zulip_accounts.entity';
import { ZulipAccountsRepository } from './zulip_accounts.repository';
import { ZulipAccountsMemoryRepository } from './zulip_accounts_memory.repository';
import { ZulipAccountsService } from './zulip_accounts.service';
import { ZulipAccountsMemoryService } from './zulip_accounts_memory.service';
import { AppLoggerService } from '../../utils/logger/logger.service';
import { REQUIRED_DB_ENV_VARS } from './zulip_accounts.constants';
describe('ZulipAccountsModule', () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
// 保存原始环境变量
originalEnv = { ...process.env };
});
afterEach(() => {
// 恢复原始环境变量
process.env = originalEnv;
});
describe('forDatabase()', () => {
it('should create database module with correct configuration', () => {
const module = ZulipAccountsModule.forDatabase();
expect(module).toBeDefined();
expect(module.module).toBe(ZulipAccountsModule);
expect(module.imports).toHaveLength(2);
expect(module.providers).toHaveLength(3);
expect(module.exports).toHaveLength(4);
});
it('should include TypeORM configuration for ZulipAccounts entity', () => {
const module = ZulipAccountsModule.forDatabase();
// 检查是否包含TypeORM模块配置
const typeOrmModule = module.imports?.find(imp =>
imp && typeof imp === 'object' && 'module' in imp
);
expect(typeOrmModule).toBeDefined();
});
it('should include CacheModule with correct configuration', () => {
const module = ZulipAccountsModule.forDatabase();
// 检查是否包含缓存模块
const cacheModule = module.imports?.find(imp =>
imp && typeof imp === 'object' && 'module' in imp
);
expect(cacheModule).toBeDefined();
});
it('should provide ZulipAccountsRepository', () => {
const module = ZulipAccountsModule.forDatabase();
expect(module.providers).toContain(ZulipAccountsRepository);
});
it('should provide AppLoggerService', () => {
const module = ZulipAccountsModule.forDatabase();
expect(module.providers).toContain(AppLoggerService);
});
it('should provide ZulipAccountsService with correct token', () => {
const module = ZulipAccountsModule.forDatabase();
const serviceProvider = module.providers?.find(provider =>
typeof provider === 'object' &&
provider !== null &&
'provide' in provider &&
provider.provide === 'ZulipAccountsService'
);
expect(serviceProvider).toBeDefined();
expect((serviceProvider as any).useClass).toBe(ZulipAccountsService);
});
it('should export all required services', () => {
const module = ZulipAccountsModule.forDatabase();
expect(module.exports).toContain(ZulipAccountsRepository);
expect(module.exports).toContain('ZulipAccountsService');
expect(module.exports).toContain(TypeOrmModule);
expect(module.exports).toContain(AppLoggerService);
});
});
describe('forMemory()', () => {
it('should create memory module with correct configuration', () => {
const module = ZulipAccountsModule.forMemory();
expect(module).toBeDefined();
expect(module.module).toBe(ZulipAccountsModule);
expect(module.imports).toHaveLength(1);
expect(module.providers).toHaveLength(3);
expect(module.exports).toHaveLength(3);
});
it('should include CacheModule with memory-optimized configuration', () => {
const module = ZulipAccountsModule.forMemory();
// 检查是否包含缓存模块
expect(module.imports).toHaveLength(1);
});
it('should provide AppLoggerService', () => {
const module = ZulipAccountsModule.forMemory();
expect(module.providers).toContain(AppLoggerService);
});
it('should provide ZulipAccountsRepository with memory implementation', () => {
const module = ZulipAccountsModule.forMemory();
const repositoryProvider = module.providers?.find(provider =>
typeof provider === 'object' &&
provider !== null &&
'provide' in provider &&
provider.provide === 'ZulipAccountsRepository'
);
expect(repositoryProvider).toBeDefined();
expect((repositoryProvider as any).useClass).toBe(ZulipAccountsMemoryRepository);
});
it('should provide ZulipAccountsService with memory implementation', () => {
const module = ZulipAccountsModule.forMemory();
const serviceProvider = module.providers?.find(provider =>
typeof provider === 'object' &&
provider !== null &&
'provide' in provider &&
provider.provide === 'ZulipAccountsService'
);
expect(serviceProvider).toBeDefined();
expect((serviceProvider as any).useClass).toBe(ZulipAccountsMemoryService);
});
it('should export memory-specific services', () => {
const module = ZulipAccountsModule.forMemory();
expect(module.exports).toContain('ZulipAccountsRepository');
expect(module.exports).toContain('ZulipAccountsService');
expect(module.exports).toContain(AppLoggerService);
expect(module.exports).not.toContain(TypeOrmModule);
});
});
describe('forRoot()', () => {
it('should return database module when all required env vars are set', () => {
// 设置所有必需的环境变量
REQUIRED_DB_ENV_VARS.forEach(varName => {
process.env[varName] = 'test_value';
});
const module = ZulipAccountsModule.forRoot();
// 应该返回数据库模式的配置
expect(module.imports).toHaveLength(2); // TypeORM + Cache
expect(module.providers).toHaveLength(3);
expect(module.exports).toContain(TypeOrmModule);
});
it('should return memory module when some required env vars are missing', () => {
// 清除所有环境变量
REQUIRED_DB_ENV_VARS.forEach(varName => {
delete process.env[varName];
});
const module = ZulipAccountsModule.forRoot();
// 应该返回内存模式的配置
expect(module.imports).toHaveLength(1); // 只有Cache
expect(module.providers).toHaveLength(3);
expect(module.exports).not.toContain(TypeOrmModule);
});
it('should return memory module when env vars are empty strings', () => {
// 设置空字符串环境变量
REQUIRED_DB_ENV_VARS.forEach(varName => {
process.env[varName] = '';
});
const module = ZulipAccountsModule.forRoot();
// 应该返回内存模式的配置
expect(module.imports).toHaveLength(1);
expect(module.exports).not.toContain(TypeOrmModule);
});
it('should return database module when all env vars have valid values', () => {
// 设置有效的环境变量值
process.env.DB_HOST = 'localhost';
process.env.DB_PORT = '3306';
process.env.DB_USERNAME = 'user';
process.env.DB_PASSWORD = 'password';
process.env.DB_DATABASE = 'testdb';
const module = ZulipAccountsModule.forRoot();
// 应该返回数据库模式的配置
expect(module.imports).toHaveLength(2);
expect(module.exports).toContain(TypeOrmModule);
});
});
describe('isDatabaseConfigured() function behavior', () => {
it('should detect complete database configuration', () => {
// 设置完整的数据库配置
REQUIRED_DB_ENV_VARS.forEach(varName => {
process.env[varName] = 'valid_value';
});
const module = ZulipAccountsModule.forRoot();
// 验证返回的是数据库模式
expect(module.exports).toContain(TypeOrmModule);
});
it('should detect incomplete database configuration', () => {
// 只设置部分环境变量
if (REQUIRED_DB_ENV_VARS.length > 1) {
process.env[REQUIRED_DB_ENV_VARS[0]] = 'value1';
// 删除其他变量确保不完整
for (let i = 1; i < REQUIRED_DB_ENV_VARS.length; i++) {
delete process.env[REQUIRED_DB_ENV_VARS[i]];
}
}
const module = ZulipAccountsModule.forRoot();
// 验证返回的是内存模式
expect(module.exports).not.toContain(TypeOrmModule);
});
it('should handle undefined environment variables', () => {
// 确保所有必需变量都未定义
REQUIRED_DB_ENV_VARS.forEach(varName => {
delete process.env[varName];
});
const module = ZulipAccountsModule.forRoot();
// 验证返回的是内存模式
expect(module.exports).not.toContain(TypeOrmModule);
});
});
describe('module integration', () => {
it('should create module configuration without errors', () => {
expect(() => ZulipAccountsModule.forDatabase()).not.toThrow();
expect(() => ZulipAccountsModule.forMemory()).not.toThrow();
expect(() => ZulipAccountsModule.forRoot()).not.toThrow();
});
it('should have different configurations for database and memory modes', () => {
const databaseModule = ZulipAccountsModule.forDatabase();
const memoryModule = ZulipAccountsModule.forMemory();
expect(databaseModule.imports?.length).toBeGreaterThan(memoryModule.imports?.length || 0);
expect(databaseModule.exports).toContain(TypeOrmModule);
expect(memoryModule.exports).not.toContain(TypeOrmModule);
});
it('should provide consistent service interfaces', () => {
const databaseModule = ZulipAccountsModule.forDatabase();
const memoryModule = ZulipAccountsModule.forMemory();
expect(databaseModule.exports).toContain('ZulipAccountsService');
expect(memoryModule.exports).toContain('ZulipAccountsService');
expect(databaseModule.exports).toContain(AppLoggerService);
expect(memoryModule.exports).toContain(AppLoggerService);
});
});
describe('error handling', () => {
it('should handle missing REQUIRED_DB_ENV_VARS constant gracefully', () => {
// 这个测试确保即使常量有问题,模块仍能工作
expect(() => {
ZulipAccountsModule.forRoot();
}).not.toThrow();
});
it('should handle null/undefined environment values', () => {
// 设置一些环境变量为null通过删除后重新设置
REQUIRED_DB_ENV_VARS.forEach(varName => {
delete process.env[varName];
(process.env as any)[varName] = null;
});
expect(() => {
ZulipAccountsModule.forRoot();
}).not.toThrow();
});
});
});