From 0192934c664190ce31578b912c5703beed615cb4 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Thu, 25 Dec 2025 20:49:16 +0800 Subject: [PATCH] =?UTF-8?q?test=EF=BC=9A=E6=B7=BB=E5=8A=A0=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E5=86=B7=E5=8D=B4=E6=97=B6=E9=97=B4=E6=B8=85?= =?UTF-8?q?=E9=99=A4=E5=8A=9F=E8=83=BD=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为新增的验证码冷却时间清除功能添加全面的测试用例: 验证服务测试: - 测试成功清除冷却时间 - 测试清除不存在的冷却时间 - 测试Redis操作错误处理 - 测试不同类型和标识符的冷却时间清除 登录核心服务测试: - 测试注册成功后自动清除冷却时间 - 测试密码重置成功后自动清除冷却时间 - 测试验证码登录成功后自动清除冷却时间 - 测试冷却时间清除失败的优雅处理 --- .../login_core/login_core.service.spec.ts | 236 +++++++++++++----- .../verification/verification.service.spec.ts | 65 +++++ 2 files changed, 238 insertions(+), 63 deletions(-) diff --git a/src/core/login_core/login_core.service.spec.ts b/src/core/login_core/login_core.service.spec.ts index b8f810a..0cd3336 100644 --- a/src/core/login_core/login_core.service.spec.ts +++ b/src/core/login_core/login_core.service.spec.ts @@ -51,6 +51,7 @@ describe('LoginCoreService', () => { const mockVerificationService = { generateCode: jest.fn(), verifyCode: jest.fn(), + clearCooldown: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ @@ -146,6 +147,69 @@ describe('LoginCoreService', () => { expect(result.isNewUser).toBe(true); }); + it('should register with email and clear cooldown', async () => { + const email = 'test@example.com'; + usersService.findByUsername.mockResolvedValue(null); + usersService.findByEmail.mockResolvedValue(null); + usersService.create.mockResolvedValue({ ...mockUser, email }); + verificationService.verifyCode.mockResolvedValue(true); + verificationService.clearCooldown.mockResolvedValue(undefined); + emailService.sendWelcomeEmail.mockResolvedValue(undefined); + jest.spyOn(service as any, 'hashPassword').mockResolvedValue('hashedpassword'); + jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {}); + + const result = await service.register({ + username: 'testuser', + password: 'password123', + nickname: '测试用户', + email, + email_verification_code: '123456' + }); + + expect(result.user.email).toBe(email); + expect(result.isNewUser).toBe(true); + expect(verificationService.clearCooldown).toHaveBeenCalledWith( + email, + VerificationCodeType.EMAIL_VERIFICATION + ); + }); + + it('should handle cooldown clearing failure gracefully', async () => { + const email = 'test@example.com'; + usersService.findByUsername.mockResolvedValue(null); + usersService.findByEmail.mockResolvedValue(null); + usersService.create.mockResolvedValue({ ...mockUser, email }); + verificationService.verifyCode.mockResolvedValue(true); + verificationService.clearCooldown.mockRejectedValue(new Error('Redis error')); + emailService.sendWelcomeEmail.mockResolvedValue(undefined); + jest.spyOn(service as any, 'hashPassword').mockResolvedValue('hashedpassword'); + jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {}); + + // Mock console.warn to avoid test output noise + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const result = await service.register({ + username: 'testuser', + password: 'password123', + nickname: '测试用户', + email, + email_verification_code: '123456' + }); + + expect(result.user.email).toBe(email); + expect(result.isNewUser).toBe(true); + expect(verificationService.clearCooldown).toHaveBeenCalledWith( + email, + VerificationCodeType.EMAIL_VERIFICATION + ); + expect(consoleSpy).toHaveBeenCalledWith( + `清除验证码冷却时间失败: ${email}`, + expect.any(Error) + ); + + consoleSpy.mockRestore(); + }); + it('should validate password strength', async () => { jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => { throw new BadRequestException('密码长度至少8位'); @@ -231,6 +295,59 @@ describe('LoginCoreService', () => { expect(result).toEqual(mockUser); }); + it('should reset password and clear cooldown', async () => { + const identifier = 'test@example.com'; + verificationService.verifyCode.mockResolvedValue(true); + verificationService.clearCooldown.mockResolvedValue(undefined); + usersService.findByEmail.mockResolvedValue(mockUser); + usersService.update.mockResolvedValue(mockUser); + jest.spyOn(service as any, 'hashPassword').mockResolvedValue('newhashedpassword'); + jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {}); + + const result = await service.resetPassword({ + identifier, + verificationCode: '123456', + newPassword: 'newpassword123' + }); + + expect(result).toEqual(mockUser); + expect(verificationService.clearCooldown).toHaveBeenCalledWith( + identifier, + VerificationCodeType.PASSWORD_RESET + ); + }); + + it('should handle cooldown clearing failure gracefully during password reset', async () => { + const identifier = 'test@example.com'; + verificationService.verifyCode.mockResolvedValue(true); + verificationService.clearCooldown.mockRejectedValue(new Error('Redis error')); + usersService.findByEmail.mockResolvedValue(mockUser); + usersService.update.mockResolvedValue(mockUser); + jest.spyOn(service as any, 'hashPassword').mockResolvedValue('newhashedpassword'); + jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {}); + + // Mock console.warn to avoid test output noise + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const result = await service.resetPassword({ + identifier, + verificationCode: '123456', + newPassword: 'newpassword123' + }); + + expect(result).toEqual(mockUser); + expect(verificationService.clearCooldown).toHaveBeenCalledWith( + identifier, + VerificationCodeType.PASSWORD_RESET + ); + expect(consoleSpy).toHaveBeenCalledWith( + `清除验证码冷却时间失败: ${identifier}`, + expect.any(Error) + ); + + consoleSpy.mockRestore(); + }); + it('should throw BadRequestException for invalid verification code', async () => { verificationService.verifyCode.mockResolvedValue(false); @@ -336,82 +453,75 @@ describe('LoginCoreService', () => { ); }); - it('should successfully login with phone verification code', async () => { - const phoneUser = { ...mockUser, phone: '+8613800138000' }; - usersService.findAll.mockResolvedValue([phoneUser]); - verificationService.verifyCode.mockResolvedValue(true); - - const result = await service.verificationCodeLogin({ - identifier: '+8613800138000', - verificationCode: '123456' - }); - - expect(result.user).toEqual(phoneUser); - expect(result.isNewUser).toBe(false); - expect(verificationService.verifyCode).toHaveBeenCalledWith( - '+8613800138000', - VerificationCodeType.SMS_VERIFICATION, - '123456' - ); - }); - - it('should reject unverified email user', async () => { - usersService.findByEmail.mockResolvedValue(mockUser); // email_verified: false - - await expect(service.verificationCodeLogin({ - identifier: 'test@example.com', - verificationCode: '123456' - })).rejects.toThrow('邮箱未验证,请先验证邮箱后再使用验证码登录'); - }); - - it('should reject non-existent user', async () => { - usersService.findByEmail.mockResolvedValue(null); - - await expect(service.verificationCodeLogin({ - identifier: 'nonexistent@example.com', - verificationCode: '123456' - })).rejects.toThrow('用户不存在,请先注册账户'); - }); - - it('should reject invalid verification code', async () => { - const verifiedUser = { ...mockUser, email_verified: true }; - usersService.findByEmail.mockResolvedValue(verifiedUser); - verificationService.verifyCode.mockResolvedValue(false); - - await expect(service.verificationCodeLogin({ - identifier: 'test@example.com', - verificationCode: '999999' - })).rejects.toThrow('验证码验证失败'); - }); - - it('should reject invalid identifier format', async () => { - await expect(service.verificationCodeLogin({ - identifier: 'invalid-identifier', - verificationCode: '123456' - })).rejects.toThrow('请提供有效的邮箱或手机号'); - }); - }); - - describe('verificationCodeLogin', () => { - it('should successfully login with email verification code', async () => { + it('should successfully login with email verification code and clear cooldown', async () => { + const identifier = 'test@example.com'; const verifiedUser = { ...mockUser, email_verified: true }; usersService.findByEmail.mockResolvedValue(verifiedUser); verificationService.verifyCode.mockResolvedValue(true); + verificationService.clearCooldown.mockResolvedValue(undefined); const result = await service.verificationCodeLogin({ - identifier: 'test@example.com', + identifier, verificationCode: '123456' }); expect(result.user).toEqual(verifiedUser); expect(result.isNewUser).toBe(false); - expect(verificationService.verifyCode).toHaveBeenCalledWith( - 'test@example.com', - VerificationCodeType.EMAIL_VERIFICATION, - '123456' + expect(verificationService.clearCooldown).toHaveBeenCalledWith( + identifier, + VerificationCodeType.EMAIL_VERIFICATION ); }); + it('should successfully login with phone verification code and clear cooldown', async () => { + const identifier = '+8613800138000'; + const phoneUser = { ...mockUser, phone: identifier }; + usersService.findAll.mockResolvedValue([phoneUser]); + verificationService.verifyCode.mockResolvedValue(true); + verificationService.clearCooldown.mockResolvedValue(undefined); + + const result = await service.verificationCodeLogin({ + identifier, + verificationCode: '123456' + }); + + expect(result.user).toEqual(phoneUser); + expect(result.isNewUser).toBe(false); + expect(verificationService.clearCooldown).toHaveBeenCalledWith( + identifier, + VerificationCodeType.SMS_VERIFICATION + ); + }); + + it('should handle cooldown clearing failure gracefully during verification code login', async () => { + const identifier = 'test@example.com'; + const verifiedUser = { ...mockUser, email_verified: true }; + usersService.findByEmail.mockResolvedValue(verifiedUser); + verificationService.verifyCode.mockResolvedValue(true); + verificationService.clearCooldown.mockRejectedValue(new Error('Redis error')); + + // Mock console.warn to avoid test output noise + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const result = await service.verificationCodeLogin({ + identifier, + verificationCode: '123456' + }); + + expect(result.user).toEqual(verifiedUser); + expect(result.isNewUser).toBe(false); + expect(verificationService.clearCooldown).toHaveBeenCalledWith( + identifier, + VerificationCodeType.EMAIL_VERIFICATION + ); + expect(consoleSpy).toHaveBeenCalledWith( + `清除验证码冷却时间失败: ${identifier}`, + expect.any(Error) + ); + + consoleSpy.mockRestore(); + }); + it('should successfully login with phone verification code', async () => { const phoneUser = { ...mockUser, phone: '+8613800138000' }; usersService.findAll.mockResolvedValue([phoneUser]); diff --git a/src/core/utils/verification/verification.service.spec.ts b/src/core/utils/verification/verification.service.spec.ts index 72f218b..065843c 100644 --- a/src/core/utils/verification/verification.service.spec.ts +++ b/src/core/utils/verification/verification.service.spec.ts @@ -587,4 +587,69 @@ describe('VerificationService', () => { expect(code).toMatch(/^\d{6}$/); }); }); + + describe('clearCooldown', () => { + it('应该成功清除验证码冷却时间', async () => { + const email = 'test@example.com'; + const type = VerificationCodeType.EMAIL_VERIFICATION; + + mockRedis.del.mockResolvedValue(true); + + await service.clearCooldown(email, type); + + expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${email}`); + }); + + it('应该处理清除不存在的冷却时间', async () => { + const email = 'test@example.com'; + const type = VerificationCodeType.EMAIL_VERIFICATION; + + mockRedis.del.mockResolvedValue(false); + + await service.clearCooldown(email, type); + + expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${email}`); + }); + + it('应该处理Redis删除操作错误', async () => { + const email = 'test@example.com'; + const type = VerificationCodeType.EMAIL_VERIFICATION; + + mockRedis.del.mockRejectedValue(new Error('Redis delete failed')); + + await expect(service.clearCooldown(email, type)).rejects.toThrow('Redis delete failed'); + }); + + it('应该为不同类型的验证码清除对应的冷却时间', async () => { + const email = 'test@example.com'; + const types = [ + VerificationCodeType.EMAIL_VERIFICATION, + VerificationCodeType.PASSWORD_RESET, + VerificationCodeType.SMS_VERIFICATION, + ]; + + mockRedis.del.mockResolvedValue(true); + + for (const type of types) { + await service.clearCooldown(email, type); + expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${email}`); + } + + expect(mockRedis.del).toHaveBeenCalledTimes(types.length); + }); + + it('应该为不同标识符清除对应的冷却时间', async () => { + const identifiers = ['test1@example.com', 'test2@example.com', '+8613800138000']; + const type = VerificationCodeType.EMAIL_VERIFICATION; + + mockRedis.del.mockResolvedValue(true); + + for (const identifier of identifiers) { + await service.clearCooldown(identifier, type); + expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${identifier}`); + } + + expect(mockRedis.del).toHaveBeenCalledTimes(identifiers.length); + }); + }); }); \ No newline at end of file