forked from datawhale/whale-town-end
test:添加验证码冷却时间清除功能测试
为新增的验证码冷却时间清除功能添加全面的测试用例: 验证服务测试: - 测试成功清除冷却时间 - 测试清除不存在的冷却时间 - 测试Redis操作错误处理 - 测试不同类型和标识符的冷却时间清除 登录核心服务测试: - 测试注册成功后自动清除冷却时间 - 测试密码重置成功后自动清除冷却时间 - 测试验证码登录成功后自动清除冷却时间 - 测试冷却时间清除失败的优雅处理
This commit is contained in:
@@ -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]);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user