package com.tcm.training.admin.app.auth.service; import com.tcm.training.admin.app.auth.config.AppSmsAuthProperties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * APP 短信验证码场景化调试配置测试。 */ @ExtendWith(MockitoExtension.class) class AppSmsCodeServiceTest { @Mock private StringRedisTemplate stringRedisTemplate; @Mock private ValueOperations valueOperations; @Mock private AppSmsSender appSmsSender; private AppSmsAuthProperties appSmsAuthProperties; private AppSmsCodeService appSmsCodeService; /** * 每个用例前重新构造短信验证码服务。 */ @BeforeEach void setUp() { appSmsAuthProperties = new AppSmsAuthProperties(); appSmsAuthProperties.getLogin().setEnabled(true); appSmsAuthProperties.getLogin().setCode("666666"); appSmsAuthProperties.getRegister().setEnabled(false); appSmsAuthProperties.getRegister().setCode(""); appSmsAuthProperties.getPasswordReset().setEnabled(false); appSmsAuthProperties.getPasswordReset().setCode(""); appSmsAuthProperties.getPhoneChange().setEnabled(false); appSmsAuthProperties.getPhoneChange().setCode(""); appSmsCodeService = new AppSmsCodeService( stringRedisTemplate, appSmsSender, appSmsAuthProperties ); } /** * 仅开启登录调试验证码时,666666 应允许短信登录直接通过。 */ @Test void validateLoginCodeShouldAllowConfiguredDebugCode() { assertDoesNotThrow(() -> appSmsCodeService.validateLoginCode("13800138000", "666666")); verify(stringRedisTemplate, never()).opsForValue(); } /** * 未开启注册调试验证码时,666666 不应绕过真实验证码校验。 */ @Test void validateRegisterCodeShouldRejectDebugCodeWhenSceneDisabled() { when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations); when(valueOperations.get(anyString())).thenReturn(null); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> appSmsCodeService.validateRegisterCode("13800138000", "666666")); assertEquals("验证码已失效,请重新获取", exception.getMessage()); } /** * 验证码校验成功后不应删除缓存,保证 5 分钟有效期内可重复提交。 */ @Test void validateLoginCodeShouldKeepCachedCodeAfterSuccess() { when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations); when(valueOperations.get("app:sms:login:13800138000")).thenReturn("123456"); assertDoesNotThrow(() -> appSmsCodeService.validateLoginCode("13800138000", "123456")); verify(stringRedisTemplate, never()).delete(anyString()); } /** * 同一验证码在缓存 TTL 内多次提交都应校验通过。 */ @Test void validateLoginCodeShouldAllowRepeatedSubmissionsWithinTtl() { when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations); when(valueOperations.get("app:sms:login:13800138000")).thenReturn("123456"); assertDoesNotThrow(() -> appSmsCodeService.validateLoginCode("13800138000", "123456")); assertDoesNotThrow(() -> appSmsCodeService.validateLoginCode("13800138000", "123456")); verify(stringRedisTemplate, never()).delete(anyString()); } /** * 缓存中存在验证码但输入不一致时仍应提示验证码错误。 */ @Test void validateLoginCodeShouldRejectMismatchedCode() { when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations); when(valueOperations.get("app:sms:login:13800138000")).thenReturn("123456"); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> appSmsCodeService.validateLoginCode("13800138000", "654321")); assertEquals("验证码错误", exception.getMessage()); verify(stringRedisTemplate, never()).delete(anyString()); } /** * 短信发送成功后才应写入验证码缓存。 */ @Test void createLoginCodeShouldCacheCodeAfterSendSucceeds() { when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations); appSmsCodeService.createLoginCode("13800138000"); verify(appSmsSender).sendLoginCode(eq("13800138000"), anyString()); verify(valueOperations).set(anyString(), anyString(), anyLong(), eq(java.util.concurrent.TimeUnit.SECONDS)); } /** * 短信发送失败时不应写入验证码缓存。 */ @Test void createLoginCodeShouldNotCacheCodeWhenSendFails() { org.mockito.Mockito.doThrow(new IllegalStateException("短信发送失败,请稍后重试")) .when(appSmsSender).sendLoginCode(eq("13800138000"), anyString()); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> appSmsCodeService.createLoginCode("13800138000")); assertEquals("短信发送失败,请稍后重试", exception.getMessage()); verify(valueOperations, never()).set(anyString(), anyString(), anyLong(), eq(java.util.concurrent.TimeUnit.SECONDS)); } }