package com.tcm.training.admin.app.auth.service;
|
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.tcm.training.admin.app.auth.dto.AppPasswordLoginRequest;
|
import com.tcm.training.admin.app.auth.dto.AppPasswordResetRequest;
|
import com.tcm.training.admin.app.auth.dto.AppPasswordResetVerifyRequest;
|
import com.tcm.training.admin.app.auth.dto.AppSmsCodeSendRequest;
|
import com.tcm.training.admin.app.auth.vo.AppLoginResponse;
|
import com.tcm.training.admin.app.auth.vo.AppPasswordResetVerifyResponse;
|
import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
|
import com.tcm.training.admin.member.entity.AppUser;
|
import com.tcm.training.admin.member.mapper.UserMapper;
|
import cn.dev33.satoken.stp.SaTokenInfo;
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.mockito.ArgumentCaptor;
|
import org.mockito.Captor;
|
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 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
import java.util.concurrent.TimeUnit;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.when;
|
|
/**
|
* APP 认证服务测试。
|
*/
|
@ExtendWith(MockitoExtension.class)
|
class AppAuthServiceTest {
|
|
@BeforeAll
|
static void initLambdaCache() {
|
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
TableInfoHelper.initTableInfo(assistant, AppUser.class);
|
}
|
|
@Mock
|
private UserMapper userMapper;
|
|
@Mock
|
private AppSmsCodeService appSmsCodeService;
|
|
@Mock
|
private BCryptPasswordEncoder passwordEncoder;
|
|
@Mock
|
private AppAuthSessionService appAuthSessionService;
|
|
@Mock
|
private StringRedisTemplate stringRedisTemplate;
|
|
@Mock
|
private ValueOperations<String, String> valueOperations;
|
|
private AppAuthService appAuthService;
|
|
@Captor
|
private ArgumentCaptor<AppUser> appUserCaptor;
|
|
/**
|
* 每个用例前重新构造被测认证服务。
|
*/
|
@BeforeEach
|
void setUp() {
|
appAuthService = new AppAuthService(
|
userMapper,
|
appSmsCodeService,
|
passwordEncoder,
|
appAuthSessionService,
|
stringRedisTemplate
|
);
|
}
|
|
/**
|
* 密码登录成功时应返回统一登录响应并建立 APP 登录态。
|
*/
|
@Test
|
void passwordLoginShouldReturnTokenWhenPasswordMatches() {
|
AppPasswordLoginRequest request = new AppPasswordLoginRequest();
|
request.setPhone("13800138000");
|
request.setPassword("abc123!!");
|
|
AppUser user = buildEnabledUser("13800138000", "ENCODED");
|
SaTokenInfo tokenInfo = new SaTokenInfo();
|
tokenInfo.setTokenValue("token-value");
|
tokenInfo.setTokenTimeout(7200L);
|
|
when(userMapper.selectOne(any())).thenReturn(user);
|
when(passwordEncoder.matches("abc123!!", "ENCODED")).thenReturn(true);
|
when(appAuthSessionService.getTokenInfo()).thenReturn(tokenInfo);
|
|
AppLoginResponse response = appAuthService.passwordLogin(request);
|
|
verify(appAuthSessionService).login(1L);
|
assertEquals("token-value", response.getToken());
|
assertEquals("Bearer", response.getTokenType());
|
assertEquals(7200L, response.getExpiresIn());
|
assertEquals(Long.valueOf(1L), response.getUser().getId());
|
assertEquals("13800138000", response.getUser().getPhone());
|
}
|
|
/**
|
* 密码错误时应返回统一错误文案。
|
*/
|
@Test
|
void passwordLoginShouldFailWhenPasswordMismatch() {
|
AppPasswordLoginRequest request = new AppPasswordLoginRequest();
|
request.setPhone("13800138000");
|
request.setPassword("bad-password");
|
|
AppUser user = buildEnabledUser("13800138000", "ENCODED");
|
when(userMapper.selectOne(any())).thenReturn(user);
|
when(passwordEncoder.matches("bad-password", "ENCODED")).thenReturn(false);
|
|
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
() -> appAuthService.passwordLogin(request));
|
|
assertEquals("账号或密码错误", exception.getMessage());
|
}
|
|
/**
|
* 忘记密码发码仅允许已启用用户发送。
|
*/
|
@Test
|
void sendPasswordResetCodeShouldReturnExpireSeconds() {
|
AppSmsCodeSendRequest request = new AppSmsCodeSendRequest();
|
request.setPhone("13800138001");
|
|
when(userMapper.selectOne(any())).thenReturn(buildEnabledUser("13800138001", "ENCODED"));
|
when(appSmsCodeService.createPasswordResetCode("13800138001")).thenReturn(new AppSmsCodeSendResponse(300));
|
|
AppSmsCodeSendResponse response = appAuthService.sendPasswordResetCode(request);
|
|
assertEquals(300, response.getExpireInSeconds());
|
}
|
|
/**
|
* 忘记密码验证码校验成功后应签发一次性重置凭证并写入缓存。
|
*/
|
@Test
|
void verifyPasswordResetCodeShouldCacheTicket() {
|
AppPasswordResetVerifyRequest request = new AppPasswordResetVerifyRequest();
|
request.setPhone("13800138002");
|
request.setCode("666666");
|
|
when(userMapper.selectOne(any())).thenReturn(buildEnabledUser("13800138002", "ENCODED"));
|
when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
|
doNothing().when(appSmsCodeService).validatePasswordResetCode("13800138002", "666666");
|
|
AppPasswordResetVerifyResponse response = appAuthService.verifyPasswordResetCode(request);
|
|
assertNotNull(response.getResetTicket());
|
verify(valueOperations).set(anyString(), anyString(), anyLong(), any(TimeUnit.class));
|
}
|
|
/**
|
* 使用有效重置凭证时应更新数据库中的密码并销毁凭证。
|
*/
|
@Test
|
void resetPasswordShouldUpdatePasswordAndDeleteTicket() {
|
AppPasswordResetRequest request = new AppPasswordResetRequest();
|
request.setResetTicket("ticket-1");
|
request.setPassword("abc123!!");
|
request.setConfirmPassword("abc123!!");
|
|
AppUser user = buildEnabledUser("13800138003", "OLD");
|
|
when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
|
when(valueOperations.get(anyString())).thenReturn("13800138003");
|
when(userMapper.selectOne(any())).thenReturn(user);
|
when(passwordEncoder.encode("abc123!!")).thenReturn("NEW-ENCODED");
|
|
appAuthService.resetPassword(request);
|
|
verify(userMapper).updateById(appUserCaptor.capture());
|
verify(stringRedisTemplate).delete(anyString());
|
assertEquals("NEW-ENCODED", appUserCaptor.getValue().getPassword());
|
}
|
|
/**
|
* 重置凭证失效时应拒绝更新密码。
|
*/
|
@Test
|
void resetPasswordShouldFailWhenTicketExpired() {
|
AppPasswordResetRequest request = new AppPasswordResetRequest();
|
request.setResetTicket("expired-ticket");
|
request.setPassword("abc123!!");
|
request.setConfirmPassword("abc123!!");
|
|
when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
|
when(valueOperations.get(anyString())).thenReturn(null);
|
|
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
() -> appAuthService.resetPassword(request));
|
|
assertEquals("重置凭证已失效,请重新验证", exception.getMessage());
|
}
|
|
/**
|
* 统一构造启用中的 APP 用户测试桩。
|
*/
|
private AppUser buildEnabledUser(String phone, String password) {
|
AppUser user = new AppUser();
|
user.setId(1L);
|
user.setPhone(phone);
|
user.setPassword(password);
|
user.setNickname("测试用户");
|
user.setStatus(1);
|
user.setType(0);
|
return user;
|
}
|
}
|