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.
115 lines
4.8 KiB
115 lines
4.8 KiB
package com.mydreamjob.auth;
|
|
|
|
import com.mydreamjob.audit.AuditLog;
|
|
import com.mydreamjob.audit.AuditLogRepository;
|
|
import com.mydreamjob.auth.sms.SmsCodeSender;
|
|
import com.mydreamjob.auth.sms.UnavailableSmsCodeSender;
|
|
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.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.boot.test.context.SpringBootTest;
|
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
import org.springframework.context.ApplicationContext;
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
import org.springframework.data.redis.core.ValueOperations;
|
|
import org.springframework.test.context.ActiveProfiles;
|
|
|
|
import java.time.LocalDateTime;
|
|
import java.util.List;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
import static org.mockito.ArgumentMatchers.anyString;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
/**
|
|
* S01 定向整改 —— 补充回归:不可用发送实现下的事务回滚。
|
|
*
|
|
* <p>本测试使用<strong>真实 Spring 上下文 + 真实 H2 + Flyway + 真实
|
|
* {@link SmsVerificationService} 与真实 {@code @Transactional} 代理</strong>,
|
|
* 并在本测试类中把 {@code app.sms.mock.enabled} 覆盖为 {@code false},
|
|
* 因此容器装配的是 {@link UnavailableSmsCodeSender}。</p>
|
|
*
|
|
* <p>测试类本身<strong>不加</strong> {@code @Transactional},以保证
|
|
* {@code sendCode} 的事务独立提交/回滚,从而真实验证“发送失败不残留可用验证码”。</p>
|
|
*/
|
|
@SpringBootTest(properties = {"app.sms.mock.enabled=false"})
|
|
@ActiveProfiles("integration-test")
|
|
class SmsUnavailableSenderRollbackIntegrationTest {
|
|
|
|
private static final String PHONE = "19900001234";
|
|
private static final String PURPOSE = "login";
|
|
|
|
@MockBean
|
|
private RedisTemplate<String, Object> redisTemplate;
|
|
|
|
@Autowired
|
|
private ApplicationContext applicationContext;
|
|
|
|
@Autowired
|
|
private SmsVerificationService smsVerificationService;
|
|
|
|
@Autowired
|
|
private SmsVerificationCodeRepository codeRepository;
|
|
|
|
@Autowired
|
|
private AuditLogRepository auditLogRepository;
|
|
|
|
@BeforeEach
|
|
@SuppressWarnings("unchecked")
|
|
void setUp() {
|
|
ValueOperations<String, Object> ops = mock(ValueOperations.class);
|
|
when(redisTemplate.opsForValue()).thenReturn(ops);
|
|
when(redisTemplate.hasKey(anyString())).thenReturn(false);
|
|
}
|
|
|
|
@Test
|
|
void containerProvidesExactlyOneUnavailableSenderWhenMockDisabled() {
|
|
assertThat(applicationContext.getBeanNamesForType(SmsCodeSender.class))
|
|
.as("mock=false 时必须恰好一个 SmsCodeSender")
|
|
.hasSize(1);
|
|
assertThat(applicationContext.getBean(SmsCodeSender.class))
|
|
.isInstanceOf(UnavailableSmsCodeSender.class);
|
|
}
|
|
|
|
@Test
|
|
void unavailableSenderFailureLeavesNoVerificationCodeRow() {
|
|
// 清理本测试手机号的既有数据,保证断言只反映本次调用
|
|
List<SmsVerificationCode> pre = codeRepository.findByPhoneAndPurposeOrderByCreatedAtDesc(PHONE, PURPOSE);
|
|
if (!pre.isEmpty()) {
|
|
codeRepository.deleteAll(pre);
|
|
}
|
|
long beforeTotal = codeRepository.count();
|
|
LocalDateTime since = LocalDateTime.now().minusMinutes(1);
|
|
long auditBefore = auditLogRepository.findByCreatedAtAfter(since).stream()
|
|
.filter(a -> "SMS_SEND".equals(a.getAction()))
|
|
.count();
|
|
|
|
assertThatThrownBy(() -> smsVerificationService.sendCode(PHONE, PURPOSE, "rollback-test", null, null))
|
|
.isInstanceOf(BusinessException.class)
|
|
.satisfies(e -> assertThat(((BusinessException) e).getErrorCodeInt())
|
|
.isEqualTo(ErrorCode.SERVICE_UNAVAILABLE.getCode()));
|
|
|
|
// 事务已回滚:不得残留该手机号的任何验证码行(不只是数量相等)
|
|
assertThat(codeRepository.findByPhoneAndPurposeOrderByCreatedAtDesc(PHONE, PURPOSE))
|
|
.as("发送失败后不得残留可用验证码")
|
|
.isEmpty();
|
|
assertThat(codeRepository.count()).isEqualTo(beforeTotal);
|
|
|
|
// 失败不得产生“发送成功”审计
|
|
long auditAfter = auditLogRepository.findByCreatedAtAfter(since).stream()
|
|
.filter(a -> "SMS_SEND".equals(a.getAction()))
|
|
.count();
|
|
assertThat(auditAfter).isEqualTo(auditBefore);
|
|
}
|
|
|
|
@Test
|
|
void unavailableSenderFailureDoesNotReturnFalseSuccess() {
|
|
assertThatThrownBy(() -> smsVerificationService.sendCode(PHONE, PURPOSE, null, null, null))
|
|
.isInstanceOf(BusinessException.class)
|
|
.isNotInstanceOf(NumberFormatException.class);
|
|
}
|
|
}
|
|
|