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 定向整改 —— 补充回归:不可用发送实现下的事务回滚。
*
*
本测试使用真实 Spring 上下文 + 真实 H2 + Flyway + 真实
* {@link SmsVerificationService} 与真实 {@code @Transactional} 代理,
* 并在本测试类中把 {@code app.sms.mock.enabled} 覆盖为 {@code false},
* 因此容器装配的是 {@link UnavailableSmsCodeSender}。
*
* 测试类本身不加 {@code @Transactional},以保证
* {@code sendCode} 的事务独立提交/回滚,从而真实验证“发送失败不残留可用验证码”。
*/
@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 redisTemplate;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private SmsVerificationService smsVerificationService;
@Autowired
private SmsVerificationCodeRepository codeRepository;
@Autowired
private AuditLogRepository auditLogRepository;
@BeforeEach
@SuppressWarnings("unchecked")
void setUp() {
ValueOperations 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 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);
}
}