test:添加验证码冷却时间清除功能测试

为新增的验证码冷却时间清除功能添加全面的测试用例:

验证服务测试:
- 测试成功清除冷却时间
- 测试清除不存在的冷却时间
- 测试Redis操作错误处理
- 测试不同类型和标识符的冷却时间清除

登录核心服务测试:
- 测试注册成功后自动清除冷却时间
- 测试密码重置成功后自动清除冷却时间
- 测试验证码登录成功后自动清除冷却时间
- 测试冷却时间清除失败的优雅处理
This commit is contained in:
moyin
2025-12-25 20:49:16 +08:00
parent 64370c3206
commit 0192934c66
2 changed files with 238 additions and 63 deletions

View File

@@ -51,6 +51,7 @@ describe('LoginCoreService', () => {
const mockVerificationService = { const mockVerificationService = {
generateCode: jest.fn(), generateCode: jest.fn(),
verifyCode: jest.fn(), verifyCode: jest.fn(),
clearCooldown: jest.fn(),
}; };
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@@ -146,6 +147,69 @@ describe('LoginCoreService', () => {
expect(result.isNewUser).toBe(true); 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 () => { it('should validate password strength', async () => {
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => { jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {
throw new BadRequestException('密码长度至少8位'); throw new BadRequestException('密码长度至少8位');
@@ -231,6 +295,59 @@ describe('LoginCoreService', () => {
expect(result).toEqual(mockUser); 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 () => { it('should throw BadRequestException for invalid verification code', async () => {
verificationService.verifyCode.mockResolvedValue(false); verificationService.verifyCode.mockResolvedValue(false);
@@ -336,82 +453,75 @@ describe('LoginCoreService', () => {
); );
}); });
it('should successfully login with phone verification code', async () => { it('should successfully login with email verification code and clear cooldown', async () => {
const phoneUser = { ...mockUser, phone: '+8613800138000' }; const identifier = 'test@example.com';
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 () => {
const verifiedUser = { ...mockUser, email_verified: true }; const verifiedUser = { ...mockUser, email_verified: true };
usersService.findByEmail.mockResolvedValue(verifiedUser); usersService.findByEmail.mockResolvedValue(verifiedUser);
verificationService.verifyCode.mockResolvedValue(true); verificationService.verifyCode.mockResolvedValue(true);
verificationService.clearCooldown.mockResolvedValue(undefined);
const result = await service.verificationCodeLogin({ const result = await service.verificationCodeLogin({
identifier: 'test@example.com', identifier,
verificationCode: '123456' verificationCode: '123456'
}); });
expect(result.user).toEqual(verifiedUser); expect(result.user).toEqual(verifiedUser);
expect(result.isNewUser).toBe(false); expect(result.isNewUser).toBe(false);
expect(verificationService.verifyCode).toHaveBeenCalledWith( expect(verificationService.clearCooldown).toHaveBeenCalledWith(
'test@example.com', identifier,
VerificationCodeType.EMAIL_VERIFICATION, VerificationCodeType.EMAIL_VERIFICATION
'123456'
); );
}); });
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 () => { it('should successfully login with phone verification code', async () => {
const phoneUser = { ...mockUser, phone: '+8613800138000' }; const phoneUser = { ...mockUser, phone: '+8613800138000' };
usersService.findAll.mockResolvedValue([phoneUser]); usersService.findAll.mockResolvedValue([phoneUser]);

View File

@@ -587,4 +587,69 @@ describe('VerificationService', () => {
expect(code).toMatch(/^\d{6}$/); 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);
});
});
}); });