Remove merge-requests files from git tracking
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* Zulip账号关联业务服务测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试ZulipAccountsBusinessService的业务逻辑
|
||||
* - 验证缓存机制和性能监控
|
||||
* - 测试异常处理和错误转换
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-12: 代码规范优化 - 提取测试数据魔法数字为常量,提升代码可读性 (修改者: moyin)
|
||||
* - 2026-01-12: 架构优化 - 从core/zulip_core移动到business/zulip,符合架构分层规范 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 2.1.1
|
||||
* @since 2026-01-12
|
||||
* @lastModified 2026-01-12
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConflictException, NotFoundException } from '@nestjs/common';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { ZulipAccountsBusinessService } from './zulip_accounts_business.service';
|
||||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||||
import { CreateZulipAccountDto, ZulipAccountResponseDto } from '../../../core/db/zulip_accounts/zulip_accounts.dto';
|
||||
|
||||
describe('ZulipAccountsBusinessService', () => {
|
||||
let service: ZulipAccountsBusinessService;
|
||||
let mockRepository: any;
|
||||
let mockLogger: jest.Mocked<AppLoggerService>;
|
||||
let mockCacheManager: jest.Mocked<Cache>;
|
||||
|
||||
// 测试数据常量
|
||||
const TEST_ACCOUNT_ID = BigInt(1);
|
||||
const TEST_GAME_USER_ID = BigInt(12345);
|
||||
const TEST_ZULIP_USER_ID = 67890;
|
||||
|
||||
const mockAccount = {
|
||||
id: TEST_ACCOUNT_ID,
|
||||
gameUserId: TEST_GAME_USER_ID,
|
||||
zulipUserId: TEST_ZULIP_USER_ID,
|
||||
zulipEmail: 'test@example.com',
|
||||
zulipFullName: 'Test User',
|
||||
zulipApiKeyEncrypted: 'encrypted_key',
|
||||
status: 'active',
|
||||
lastVerifiedAt: new Date('2026-01-12T00:00:00Z'),
|
||||
lastSyncedAt: new Date('2026-01-12T00:00:00Z'),
|
||||
errorMessage: null,
|
||||
retryCount: 0,
|
||||
createdAt: new Date('2026-01-12T00:00:00Z'),
|
||||
updatedAt: new Date('2026-01-12T00:00:00Z'),
|
||||
gameUser: null,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockRepository = {
|
||||
create: jest.fn(),
|
||||
findByGameUserId: jest.fn(),
|
||||
getStatusStatistics: jest.fn(),
|
||||
};
|
||||
|
||||
mockLogger = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockCacheManager = {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
del: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ZulipAccountsBusinessService,
|
||||
{
|
||||
provide: 'ZulipAccountsRepository',
|
||||
useValue: mockRepository,
|
||||
},
|
||||
{
|
||||
provide: AppLoggerService,
|
||||
useValue: mockLogger,
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: mockCacheManager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ZulipAccountsBusinessService>(ZulipAccountsBusinessService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
const createDto: CreateZulipAccountDto = {
|
||||
gameUserId: TEST_GAME_USER_ID.toString(),
|
||||
zulipUserId: TEST_ZULIP_USER_ID,
|
||||
zulipEmail: 'test@example.com',
|
||||
zulipFullName: 'Test User',
|
||||
zulipApiKeyEncrypted: 'encrypted_key',
|
||||
status: 'active',
|
||||
};
|
||||
|
||||
it('应该成功创建Zulip账号关联', async () => {
|
||||
mockRepository.create.mockResolvedValue(mockAccount);
|
||||
|
||||
const result = await service.create(createDto);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.gameUserId).toBe(TEST_GAME_USER_ID.toString());
|
||||
expect(result.zulipEmail).toBe('test@example.com');
|
||||
expect(mockRepository.create).toHaveBeenCalledWith({
|
||||
gameUserId: TEST_GAME_USER_ID,
|
||||
zulipUserId: TEST_ZULIP_USER_ID,
|
||||
zulipEmail: 'test@example.com',
|
||||
zulipFullName: 'Test User',
|
||||
zulipApiKeyEncrypted: 'encrypted_key',
|
||||
status: 'active',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该处理重复关联异常', async () => {
|
||||
const error = new Error(`Game user ${TEST_GAME_USER_ID} already has a Zulip account`);
|
||||
mockRepository.create.mockRejectedValue(error);
|
||||
|
||||
await expect(service.create(createDto)).rejects.toThrow(ConflictException);
|
||||
});
|
||||
|
||||
it('应该处理Zulip用户已关联异常', async () => {
|
||||
const error = new Error(`Zulip user ${TEST_ZULIP_USER_ID} is already linked`);
|
||||
mockRepository.create.mockRejectedValue(error);
|
||||
|
||||
await expect(service.create(createDto)).rejects.toThrow(ConflictException);
|
||||
});
|
||||
|
||||
it('应该处理无效的游戏用户ID格式', async () => {
|
||||
const invalidDto = { ...createDto, gameUserId: 'invalid' };
|
||||
|
||||
await expect(service.create(invalidDto)).rejects.toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByGameUserId', () => {
|
||||
it('应该从缓存返回结果', async () => {
|
||||
const cachedResult: ZulipAccountResponseDto = {
|
||||
id: TEST_ACCOUNT_ID.toString(),
|
||||
gameUserId: TEST_GAME_USER_ID.toString(),
|
||||
zulipUserId: TEST_ZULIP_USER_ID,
|
||||
zulipEmail: 'test@example.com',
|
||||
zulipFullName: 'Test User',
|
||||
status: 'active',
|
||||
lastVerifiedAt: '2026-01-12T00:00:00.000Z',
|
||||
lastSyncedAt: '2026-01-12T00:00:00.000Z',
|
||||
errorMessage: null,
|
||||
retryCount: 0,
|
||||
createdAt: '2026-01-12T00:00:00.000Z',
|
||||
updatedAt: '2026-01-12T00:00:00.000Z',
|
||||
gameUser: null,
|
||||
};
|
||||
|
||||
mockCacheManager.get.mockResolvedValue(cachedResult);
|
||||
|
||||
const result = await service.findByGameUserId(TEST_GAME_USER_ID.toString());
|
||||
|
||||
expect(result).toEqual(cachedResult);
|
||||
expect(mockRepository.findByGameUserId).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该从Repository查询并缓存结果', async () => {
|
||||
mockCacheManager.get.mockResolvedValue(null);
|
||||
mockRepository.findByGameUserId.mockResolvedValue(mockAccount);
|
||||
|
||||
const result = await service.findByGameUserId(TEST_GAME_USER_ID.toString());
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.gameUserId).toBe(TEST_GAME_USER_ID.toString());
|
||||
expect(mockRepository.findByGameUserId).toHaveBeenCalledWith(TEST_GAME_USER_ID, false);
|
||||
expect(mockCacheManager.set).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该在未找到时返回null', async () => {
|
||||
mockCacheManager.get.mockResolvedValue(null);
|
||||
mockRepository.findByGameUserId.mockResolvedValue(null);
|
||||
|
||||
const result = await service.findByGameUserId(TEST_GAME_USER_ID.toString());
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('应该处理Repository异常', async () => {
|
||||
mockCacheManager.get.mockResolvedValue(null);
|
||||
mockRepository.findByGameUserId.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
await expect(service.findByGameUserId(TEST_GAME_USER_ID.toString())).rejects.toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatusStatistics', () => {
|
||||
const mockStats = {
|
||||
active: 10,
|
||||
inactive: 5,
|
||||
suspended: 2,
|
||||
error: 1,
|
||||
};
|
||||
|
||||
it('应该从缓存返回统计数据', async () => {
|
||||
const cachedStats = {
|
||||
active: 10,
|
||||
inactive: 5,
|
||||
suspended: 2,
|
||||
error: 1,
|
||||
total: 18,
|
||||
};
|
||||
|
||||
mockCacheManager.get.mockResolvedValue(cachedStats);
|
||||
|
||||
const result = await service.getStatusStatistics();
|
||||
|
||||
expect(result).toEqual(cachedStats);
|
||||
expect(mockRepository.getStatusStatistics).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该从Repository查询并缓存统计数据', async () => {
|
||||
mockCacheManager.get.mockResolvedValue(null);
|
||||
mockRepository.getStatusStatistics.mockResolvedValue(mockStats);
|
||||
|
||||
const result = await service.getStatusStatistics();
|
||||
|
||||
expect(result).toEqual({
|
||||
active: 10,
|
||||
inactive: 5,
|
||||
suspended: 2,
|
||||
error: 1,
|
||||
total: 18,
|
||||
});
|
||||
expect(mockRepository.getStatusStatistics).toHaveBeenCalled();
|
||||
expect(mockCacheManager.set).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该处理缺失的统计字段', async () => {
|
||||
mockCacheManager.get.mockResolvedValue(null);
|
||||
mockRepository.getStatusStatistics.mockResolvedValue({
|
||||
active: 5,
|
||||
// 缺少其他字段
|
||||
});
|
||||
|
||||
const result = await service.getStatusStatistics();
|
||||
|
||||
expect(result).toEqual({
|
||||
active: 5,
|
||||
inactive: 0,
|
||||
suspended: 0,
|
||||
error: 0,
|
||||
total: 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toResponseDto', () => {
|
||||
it('应该正确转换实体为响应DTO', () => {
|
||||
const result = (service as any).toResponseDto(mockAccount);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: TEST_ACCOUNT_ID.toString(),
|
||||
gameUserId: TEST_GAME_USER_ID.toString(),
|
||||
zulipUserId: TEST_ZULIP_USER_ID,
|
||||
zulipEmail: 'test@example.com',
|
||||
zulipFullName: 'Test User',
|
||||
status: 'active',
|
||||
lastVerifiedAt: '2026-01-12T00:00:00.000Z',
|
||||
lastSyncedAt: '2026-01-12T00:00:00.000Z',
|
||||
errorMessage: null,
|
||||
retryCount: 0,
|
||||
createdAt: '2026-01-12T00:00:00.000Z',
|
||||
updatedAt: '2026-01-12T00:00:00.000Z',
|
||||
gameUser: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('应该处理null的可选字段', () => {
|
||||
const accountWithNulls = {
|
||||
...mockAccount,
|
||||
lastVerifiedAt: null,
|
||||
lastSyncedAt: null,
|
||||
errorMessage: null,
|
||||
gameUser: null,
|
||||
};
|
||||
|
||||
const result = (service as any).toResponseDto(accountWithNulls);
|
||||
|
||||
expect(result.lastVerifiedAt).toBeUndefined();
|
||||
expect(result.lastSyncedAt).toBeUndefined();
|
||||
expect(result.errorMessage).toBeNull();
|
||||
expect(result.gameUser).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseGameUserId', () => {
|
||||
it('应该正确解析有效的游戏用户ID', () => {
|
||||
const result = (service as any).parseGameUserId(TEST_GAME_USER_ID.toString());
|
||||
expect(result).toBe(TEST_GAME_USER_ID);
|
||||
});
|
||||
|
||||
it('应该在无效ID时抛出异常', () => {
|
||||
expect(() => (service as any).parseGameUserId('invalid')).toThrow(ConflictException);
|
||||
});
|
||||
|
||||
it('应该处理大数字ID', () => {
|
||||
const largeId = '9007199254740991';
|
||||
const result = (service as any).parseGameUserId(largeId);
|
||||
expect(result).toBe(BigInt(largeId));
|
||||
});
|
||||
});
|
||||
|
||||
describe('缓存管理', () => {
|
||||
it('应该构建正确的缓存键', () => {
|
||||
const key1 = (service as any).buildCacheKey('game_user', '12345', false);
|
||||
const key2 = (service as any).buildCacheKey('game_user', '12345', true);
|
||||
const key3 = (service as any).buildCacheKey('stats');
|
||||
|
||||
expect(key1).toBe('zulip_accounts:game_user:12345');
|
||||
expect(key2).toBe('zulip_accounts:game_user:12345:with_user');
|
||||
expect(key3).toBe('zulip_accounts:stats');
|
||||
});
|
||||
|
||||
it('应该清除相关缓存', async () => {
|
||||
await (service as any).clearRelatedCache(TEST_GAME_USER_ID.toString(), TEST_ZULIP_USER_ID, 'test@example.com');
|
||||
|
||||
expect(mockCacheManager.del).toHaveBeenCalledTimes(7); // stats + game_user*2 + zulip_user*2 + zulip_email*2
|
||||
});
|
||||
|
||||
it('应该处理缓存清除失败', async () => {
|
||||
mockCacheManager.del.mockRejectedValue(new Error('Cache error'));
|
||||
|
||||
// 不应该抛出异常
|
||||
await expect((service as any).clearRelatedCache(TEST_GAME_USER_ID.toString())).resolves.not.toThrow();
|
||||
expect(mockLogger.warn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('应该格式化Error对象', () => {
|
||||
const error = new Error('Test error');
|
||||
const result = (service as any).formatError(error);
|
||||
expect(result).toBe('Test error');
|
||||
});
|
||||
|
||||
it('应该格式化非Error对象', () => {
|
||||
const result = (service as any).formatError('String error');
|
||||
expect(result).toBe('String error');
|
||||
});
|
||||
|
||||
it('应该处理ConflictException', () => {
|
||||
const error = new ConflictException('Conflict');
|
||||
expect(() => (service as any).handleServiceError(error, 'test')).toThrow(ConflictException);
|
||||
});
|
||||
|
||||
it('应该处理NotFoundException', () => {
|
||||
const error = new NotFoundException('Not found');
|
||||
expect(() => (service as any).handleServiceError(error, 'test')).toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('应该将其他异常转换为ConflictException', () => {
|
||||
const error = new Error('Generic error');
|
||||
expect(() => (service as any).handleServiceError(error, 'test')).toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能监控', () => {
|
||||
it('应该创建性能监控器', () => {
|
||||
const monitor = (service as any).createPerformanceMonitor('test', { key: 'value' });
|
||||
|
||||
expect(monitor).toHaveProperty('success');
|
||||
expect(monitor).toHaveProperty('error');
|
||||
expect(typeof monitor.success).toBe('function');
|
||||
expect(typeof monitor.error).toBe('function');
|
||||
});
|
||||
|
||||
it('应该记录成功操作', () => {
|
||||
const monitor = (service as any).createPerformanceMonitor('test');
|
||||
monitor.success({ result: 'ok' });
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
expect.stringContaining('成功'),
|
||||
expect.objectContaining({
|
||||
operation: 'test',
|
||||
duration: expect.any(Number)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('应该记录失败操作', () => {
|
||||
const monitor = (service as any).createPerformanceMonitor('test');
|
||||
const error = new Error('Test error');
|
||||
|
||||
expect(() => monitor.error(error)).toThrow();
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user