轻松聘AI开发
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

92 lines
3.9 KiB

package com.mydreamjob.auth;
import com.mydreamjob.auth.sms.SmsCodeSender;
import com.mydreamjob.audit.AuditService;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.time.LocalDateTime;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class SmsVerificationAtomicConsumptionTest {
@Mock SmsVerificationCodeRepository codes;
@Mock RedisTemplate<String, Object> redis;
@Mock ValueOperations<String, Object> values;
@Mock AuditService audit;
@Mock SmsCodeSender sender;
@InjectMocks SmsVerificationService service;
@BeforeEach
void noLockout() {
lenient().when(redis.hasKey(anyString())).thenReturn(false);
lenient().when(redis.opsForValue()).thenReturn(values);
}
@Test
void successfulCodeUsesConditionalUpdateAndNeverEntitySave() {
SmsVerificationCode code = code(false, 0, 5);
when(codes.streamLatestUnusedByPhoneAndPurpose("19900000201", "company-register"))
.thenReturn(Stream.of(code));
when(codes.consumeIfValid(eq(7L), any())).thenReturn(1);
code.setId(7L);
assertTrue(service.verifyCode("19900000201", "123456", "company-register"));
verify(codes).consumeIfValid(eq(7L), any());
verify(codes, never()).save(any(SmsVerificationCode.class));
}
@Test
void zeroAffectedRowsIsAConsumedOrInvalidCode() {
SmsVerificationCode code = code(false, 0, 5);
code.setId(8L);
when(codes.streamLatestUnusedByPhoneAndPurpose("19900000202", "company-register"))
.thenReturn(Stream.of(code));
when(codes.consumeIfValid(eq(8L), any())).thenReturn(0);
assertThatThrownBy(() -> service.verifyCode("19900000202", "123456", "company-register"))
.isInstanceOf(BusinessException.class)
.extracting("errorCodeInt").isEqualTo(ErrorCode.SMS_CODE_INVALID.getCode());
verify(codes, never()).save(any(SmsVerificationCode.class));
}
@Test
void unsupportedPurposeIsRejectedBeforeAnyReadOrUpdate() {
assertThatThrownBy(() -> service.verifyCode("19900000203", "123456", "arbitrary"))
.isInstanceOf(BusinessException.class)
.extracting("errorCodeInt").isEqualTo(ErrorCode.BAD_REQUEST.getCode());
verifyNoInteractions(codes);
}
@Test
void attemptsAtLimitAreNotConsumedAgain() {
SmsVerificationCode code = code(false, 5, 5);
code.setId(9L);
when(codes.streamLatestUnusedByPhoneAndPurpose("19900000204", "company-register"))
.thenReturn(Stream.of(code));
assertThatThrownBy(() -> service.verifyCode("19900000204", "123456", "company-register"))
.isInstanceOf(BusinessException.class)
.extracting("errorCodeInt").isEqualTo(ErrorCode.SMS_CODE_ATTEMPTS_EXCEEDED.getCode());
verify(codes, never()).consumeIfValid(anyLong(), any());
}
private SmsVerificationCode code(boolean used, int attempts, int maxAttempts) {
return SmsVerificationCode.builder().id(7L).phone("19900000201").code("123456")
.purpose("company-register").used(used).attempts(attempts).maxAttempts(maxAttempts)
.createdAt(LocalDateTime.now()).expiredAt(LocalDateTime.now().plusMinutes(5)).build();
}
}