/** * 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; let mockCacheManager: jest.Mocked; // 测试数据常量 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); }); 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(); }); }); });