/** * 登录服务Zulip集成测试 * * 功能描述: * - 测试用户注册时的Zulip账号创建/绑定逻辑 * - 测试用户登录时的Zulip集成处理 * - 验证API Key的获取和存储机制 * - 测试各种异常情况的处理 * * 测试场景: * - 注册时Zulip中没有用户:创建新账号 * - 注册时Zulip中已有用户:绑定已有账号 * - 登录时没有Zulip关联:尝试创建/绑定 * - 登录时已有Zulip关联:刷新API Key * - 各种错误情况的处理和回滚 * * @author moyin * @version 1.0.0 * @since 2026-01-10 * @lastModified 2026-01-10 */ import { Test, TestingModule } from '@nestjs/testing'; import { Logger } from '@nestjs/common'; import { LoginService } from './login.service'; import { LoginCoreService } from '../../core/login_core/login_core.service'; import { ZulipAccountService } from '../../core/zulip_core/services/zulip_account.service'; import { ZulipAccountsService } from '../../core/db/zulip_accounts/zulip_accounts.service'; import { ApiKeySecurityService } from '../../core/zulip_core/services/api_key_security.service'; import { Users } from '../../core/db/users/users.entity'; import { ZulipAccountResponseDto } from '../../core/db/zulip_accounts/zulip_accounts.dto'; describe('LoginService - Zulip Integration', () => { let service: LoginService; let loginCoreService: jest.Mocked; let zulipAccountService: jest.Mocked; let zulipAccountsService: jest.Mocked; let apiKeySecurityService: jest.Mocked; const mockUser: Users = { id: BigInt(12345), username: 'testuser', nickname: '测试用户', email: 'test@example.com', email_verified: false, phone: null, password_hash: 'hashedpassword', github_id: null, avatar_url: null, role: 1, status: 'active', created_at: new Date(), updated_at: new Date(), } as Users; beforeEach(async () => { const mockLoginCoreService = { register: jest.fn(), login: jest.fn(), generateTokenPair: jest.fn(), }; const mockZulipAccountService = { createZulipAccount: jest.fn(), initializeAdminClient: jest.fn(), }; const mockZulipAccountsService = { findByGameUserId: jest.fn(), create: jest.fn(), updateByGameUserId: jest.fn(), }; const mockApiKeySecurityService = { storeApiKey: jest.fn(), getApiKey: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ LoginService, { provide: LoginCoreService, useValue: mockLoginCoreService, }, { provide: ZulipAccountService, useValue: mockZulipAccountService, }, { provide: 'ZulipAccountsService', useValue: mockZulipAccountsService, }, { provide: ApiKeySecurityService, useValue: mockApiKeySecurityService, }, ], }).compile(); service = module.get(LoginService); loginCoreService = module.get(LoginCoreService); zulipAccountService = module.get(ZulipAccountService); zulipAccountsService = module.get('ZulipAccountsService'); apiKeySecurityService = module.get(ApiKeySecurityService); // 模拟Logger以避免日志输出 jest.spyOn(Logger.prototype, 'log').mockImplementation(); jest.spyOn(Logger.prototype, 'error').mockImplementation(); jest.spyOn(Logger.prototype, 'warn').mockImplementation(); jest.spyOn(Logger.prototype, 'debug').mockImplementation(); }); describe('用户注册时的Zulip集成', () => { it('应该在Zulip中不存在用户时创建新账号', async () => { // 准备测试数据 const registerRequest = { username: 'testuser', password: 'password123', nickname: '测试用户', email: 'test@example.com', }; const mockAuthResult = { user: mockUser, isNewUser: true, }; const mockTokenPair = { access_token: 'access_token', refresh_token: 'refresh_token', expires_in: 3600, token_type: 'Bearer', }; const mockZulipCreateResult = { success: true, userId: 67890, email: 'test@example.com', apiKey: 'test_api_key_12345678901234567890', isExistingUser: false, }; // 设置模拟返回值 loginCoreService.register.mockResolvedValue(mockAuthResult); loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair); zulipAccountsService.findByGameUserId.mockResolvedValue(null); zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipCreateResult); apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' }); zulipAccountsService.create.mockResolvedValue({} as any); // 模拟私有方法 const checkZulipUserExistsSpy = jest.spyOn(service as any, 'checkZulipUserExists') .mockResolvedValue({ exists: false }); jest.spyOn(service as any, 'initializeZulipAdminClient') .mockResolvedValue(true); // 执行测试 const result = await service.register(registerRequest); // 验证结果 expect(result.success).toBe(true); expect(result.data?.is_new_user).toBe(true); expect(result.data?.message).toContain('Zulip'); // 验证调用 expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest); expect(zulipAccountsService.findByGameUserId).toHaveBeenCalledWith('12345'); expect(checkZulipUserExistsSpy).toHaveBeenCalledWith('test@example.com'); expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({ email: 'test@example.com', fullName: '测试用户', password: 'password123', }); expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith('12345', 'test_api_key_12345678901234567890'); expect(zulipAccountsService.create).toHaveBeenCalledWith({ gameUserId: '12345', zulipUserId: 67890, zulipEmail: 'test@example.com', zulipFullName: '测试用户', zulipApiKeyEncrypted: 'stored_in_redis', status: 'active', lastVerifiedAt: expect.any(Date), }); }); it('应该在Zulip中已存在用户时绑定账号', async () => { // 准备测试数据 const registerRequest = { username: 'testuser', password: 'password123', nickname: '测试用户', email: 'test@example.com', }; const mockAuthResult = { user: mockUser, isNewUser: true, }; const mockTokenPair = { access_token: 'access_token', refresh_token: 'refresh_token', expires_in: 3600, token_type: 'Bearer', }; // 设置模拟返回值 loginCoreService.register.mockResolvedValue(mockAuthResult); loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair); zulipAccountsService.findByGameUserId.mockResolvedValue(null); apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' }); zulipAccountsService.create.mockResolvedValue({} as any); // 模拟私有方法 jest.spyOn(service as any, 'checkZulipUserExists') .mockResolvedValue({ exists: true, userId: 67890 }); const refreshZulipApiKeySpy = jest.spyOn(service as any, 'refreshZulipApiKey') .mockResolvedValue({ success: true, apiKey: 'existing_api_key_12345678901234567890' }); jest.spyOn(service as any, 'initializeZulipAdminClient') .mockResolvedValue(true); // 执行测试 const result = await service.register(registerRequest); // 验证结果 expect(result.success).toBe(true); expect(result.data?.message).toContain('绑定'); // 验证调用 expect(refreshZulipApiKeySpy).toHaveBeenCalledWith('test@example.com', 'password123'); expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith('12345', 'existing_api_key_12345678901234567890'); expect(zulipAccountsService.create).toHaveBeenCalledWith({ gameUserId: '12345', zulipUserId: 67890, zulipEmail: 'test@example.com', zulipFullName: '测试用户', zulipApiKeyEncrypted: 'stored_in_redis', status: 'active', lastVerifiedAt: expect.any(Date), }); }); }); describe('用户登录时的Zulip集成', () => { it('应该在用户没有Zulip关联时尝试创建/绑定', async () => { // 准备测试数据 const loginRequest = { identifier: 'testuser', password: 'password123', }; const mockAuthResult = { user: mockUser, isNewUser: false, }; const mockTokenPair = { access_token: 'access_token', refresh_token: 'refresh_token', expires_in: 3600, token_type: 'Bearer', }; const mockZulipCreateResult = { success: true, userId: 67890, email: 'test@example.com', apiKey: 'new_api_key_12345678901234567890', isExistingUser: false, }; // 设置模拟返回值 loginCoreService.login.mockResolvedValue(mockAuthResult); loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair); zulipAccountsService.findByGameUserId.mockResolvedValue(null); zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipCreateResult); apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' }); zulipAccountsService.create.mockResolvedValue({} as any); // 模拟私有方法 jest.spyOn(service as any, 'checkZulipUserExists') .mockResolvedValue({ exists: false }); jest.spyOn(service as any, 'initializeZulipAdminClient') .mockResolvedValue(true); // 执行测试 const result = await service.login(loginRequest); // 验证结果 expect(result.success).toBe(true); expect(result.data?.is_new_user).toBe(false); // 验证调用 expect(loginCoreService.login).toHaveBeenCalledWith(loginRequest); expect(zulipAccountsService.findByGameUserId).toHaveBeenCalledWith('12345'); expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({ email: 'test@example.com', fullName: '测试用户', password: 'password123', }); }); it('应该在用户已有Zulip关联时刷新API Key', async () => { // 准备测试数据 const loginRequest = { identifier: 'testuser', password: 'password123', }; const mockAuthResult = { user: mockUser, isNewUser: false, }; const mockTokenPair = { access_token: 'access_token', refresh_token: 'refresh_token', expires_in: 3600, token_type: 'Bearer', }; const mockExistingAccount: ZulipAccountResponseDto = { id: '1', gameUserId: '12345', zulipUserId: 67890, zulipEmail: 'test@example.com', zulipFullName: '测试用户', status: 'active' as const, lastVerifiedAt: new Date().toISOString(), retryCount: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; // 设置模拟返回值 loginCoreService.login.mockResolvedValue(mockAuthResult); loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair); zulipAccountsService.findByGameUserId.mockResolvedValue(mockExistingAccount); apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' }); zulipAccountsService.updateByGameUserId.mockResolvedValue({} as any); // 模拟私有方法 const refreshZulipApiKeySpy = jest.spyOn(service as any, 'refreshZulipApiKey') .mockResolvedValue({ success: true, apiKey: 'refreshed_api_key_12345678901234567890' }); // 执行测试 const result = await service.login(loginRequest); // 验证结果 expect(result.success).toBe(true); // 验证调用 expect(zulipAccountsService.findByGameUserId).toHaveBeenCalledWith('12345'); expect(refreshZulipApiKeySpy).toHaveBeenCalledWith('test@example.com', 'password123'); expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith('12345', 'refreshed_api_key_12345678901234567890'); expect(zulipAccountsService.updateByGameUserId).toHaveBeenCalledWith('12345', { lastVerifiedAt: expect.any(Date), status: 'active', errorMessage: null, }); }); }); describe('错误处理', () => { it('应该在Zulip创建失败时回滚用户注册', async () => { // 准备测试数据 const registerRequest = { username: 'testuser', password: 'password123', nickname: '测试用户', email: 'test@example.com', }; const mockAuthResult = { user: mockUser, isNewUser: true, }; // 设置模拟返回值 loginCoreService.register.mockResolvedValue(mockAuthResult); loginCoreService.deleteUser = jest.fn().mockResolvedValue(true); zulipAccountsService.findByGameUserId.mockResolvedValue(null); // 模拟Zulip创建失败 jest.spyOn(service as any, 'checkZulipUserExists') .mockResolvedValue({ exists: false }); jest.spyOn(service as any, 'initializeZulipAdminClient') .mockResolvedValue(true); zulipAccountService.createZulipAccount.mockResolvedValue({ success: false, error: 'Zulip服务器错误', }); // 执行测试 const result = await service.register(registerRequest); // 验证结果 expect(result.success).toBe(false); expect(result.message).toContain('Zulip账号创建失败'); // 验证回滚调用 expect(loginCoreService.deleteUser).toHaveBeenCalledWith(mockUser.id); }); it('应该在登录时Zulip集成失败但不影响登录', async () => { // 准备测试数据 const loginRequest = { identifier: 'testuser', password: 'password123', }; const mockAuthResult = { user: mockUser, isNewUser: false, }; const mockTokenPair = { access_token: 'access_token', refresh_token: 'refresh_token', expires_in: 3600, token_type: 'Bearer', }; // 设置模拟返回值 loginCoreService.login.mockResolvedValue(mockAuthResult); loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair); zulipAccountsService.findByGameUserId.mockResolvedValue(null); // 模拟Zulip集成失败 jest.spyOn(service as any, 'initializeZulipAdminClient') .mockRejectedValue(new Error('Zulip服务器不可用')); // 执行测试 const result = await service.login(loginRequest); // 验证结果 - 登录应该成功,即使Zulip集成失败 expect(result.success).toBe(true); expect(result.data?.access_token).toBe('access_token'); }); }); });