forked from baskbuddy/zhangshun
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
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();
|
|
}
|
|
}
|
|
|