406 lines
13 KiB
TypeScript
406 lines
13 KiB
TypeScript
/**
|
||
* 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();
|
||
});
|
||
});
|
||
}); |