Files
whale-town-end/src/business/zulip/services/zulip_accounts_business.service.spec.ts

406 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
});
});
});