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