package com.ruoyi.auth.service;
|
|
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.any;
|
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.when;
|
|
import java.util.HashMap;
|
import java.util.Map;
|
import java.util.concurrent.TimeUnit;
|
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.mockito.ArgumentCaptor;
|
import org.mockito.InjectMocks;
|
import org.mockito.Mock;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
import com.ruoyi.auth.enums.SmsSceneEnum;
|
import com.ruoyi.common.core.constant.CacheConstants;
|
import com.ruoyi.common.core.constant.SecurityConstants;
|
import com.ruoyi.common.core.domain.R;
|
import com.ruoyi.common.core.enums.UserStatus;
|
import com.ruoyi.common.core.exception.ServiceException;
|
import com.ruoyi.common.redis.service.RedisService;
|
import com.ruoyi.common.security.service.TokenService;
|
import com.ruoyi.system.api.RemoteAccountUserService;
|
import com.ruoyi.system.api.domain.AccountMember;
|
import com.ruoyi.system.api.model.AccountRegisterRequest;
|
import com.ruoyi.system.api.model.LoginUser;
|
|
/**
|
* C端登录与注册编排服务单元测试
|
*
|
* 覆盖一键登录的未注册/停用/正常分支,以及注册的格式校验、频控锁、锁内查重与失败释放锁。
|
*
|
* @author hugray
|
*/
|
@ExtendWith(MockitoExtension.class)
|
class AccountLoginServiceTest
|
{
|
private static final String PHONE = "13800000000";
|
|
private static final String WX_CODE = "wx-phone-code";
|
|
private static final String REFERRAL_CODE = "REFCODE1";
|
|
private static final String SMS_CODE = "123456";
|
|
@Mock
|
private RedisService redisService;
|
|
@Mock
|
private SmsCodeService smsCodeService;
|
|
@Mock
|
private WeChatMiniappService weChatMiniappService;
|
|
@Mock
|
private RemoteAccountUserService remoteAccountUserService;
|
|
@Mock
|
private TokenService tokenService;
|
|
@InjectMocks
|
private AccountLoginService accountLoginService;
|
|
/** 构造指定状态的会员 */
|
private AccountMember newMember(String status)
|
{
|
AccountMember member = new AccountMember();
|
member.setUserId(1001L);
|
member.setUserName(PHONE);
|
member.setPhone(PHONE);
|
member.setStatus(status);
|
return member;
|
}
|
|
/** 注册锁缓存键:account_register_locks:{phone} */
|
private String lockKey()
|
{
|
return CacheConstants.ACCOUNT_REGISTER_LOCK_KEY + PHONE;
|
}
|
|
@Test
|
@DisplayName("微信授权凭证为空时应拒绝登录")
|
void phoneLogin_shouldThrow_whenCodeEmpty()
|
{
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> accountLoginService.phoneLogin(" "));
|
assertEquals("微信授权凭证不能为空", ex.getMessage());
|
}
|
|
@Test
|
@DisplayName("手机号未注册一键登录应提示先注册")
|
void phoneLogin_shouldThrow_whenNotRegistered()
|
{
|
when(weChatMiniappService.getPhoneNumber(WX_CODE)).thenReturn(PHONE);
|
when(remoteAccountUserService.getMemberByPhone(PHONE, SecurityConstants.INNER)).thenReturn(R.ok());
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> accountLoginService.phoneLogin(WX_CODE));
|
assertEquals("该手机号未注册,请先注册", ex.getMessage());
|
}
|
|
@Test
|
@DisplayName("会员被停用时应拒绝登录")
|
void phoneLogin_shouldThrow_whenDisabled()
|
{
|
AccountMember member = newMember(UserStatus.DISABLE.getCode());
|
when(weChatMiniappService.getPhoneNumber(WX_CODE)).thenReturn(PHONE);
|
when(remoteAccountUserService.getMemberByPhone(PHONE, SecurityConstants.INNER)).thenReturn(R.ok(member));
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> accountLoginService.phoneLogin(WX_CODE));
|
assertEquals("账号已停用,请联系管理员", ex.getMessage());
|
}
|
|
@Test
|
@DisplayName("会员正常时应签发令牌,且账号缺失回退手机号")
|
void phoneLogin_shouldReturnToken_whenValid()
|
{
|
AccountMember member = newMember(UserStatus.OK.getCode());
|
member.setUserName(null);
|
when(weChatMiniappService.getPhoneNumber(WX_CODE)).thenReturn(PHONE);
|
when(remoteAccountUserService.getMemberByPhone(PHONE, SecurityConstants.INNER)).thenReturn(R.ok(member));
|
Map<String, Object> token = new HashMap<>();
|
token.put("access_token", "jwt-token");
|
when(tokenService.createToken(any(LoginUser.class))).thenReturn(token);
|
|
Map<String, Object> result = accountLoginService.phoneLogin(WX_CODE);
|
|
assertEquals("jwt-token", result.get("access_token"));
|
ArgumentCaptor<LoginUser> captor = ArgumentCaptor.forClass(LoginUser.class);
|
verify(tokenService).createToken(captor.capture());
|
assertEquals(PHONE, captor.getValue().getSysUser().getUserName());
|
assertEquals(1001L, captor.getValue().getSysUser().getUserId());
|
}
|
|
@Test
|
@DisplayName("验证码登录手机号格式非法时应拒绝")
|
void phoneCodeLogin_shouldThrow_whenPhoneInvalid()
|
{
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> accountLoginService.phoneCodeLogin("12345", SMS_CODE));
|
assertEquals("请输入正确手机号", ex.getMessage());
|
verify(smsCodeService, never()).verifyAndConsume(anyString(), anyString(), anyString());
|
}
|
|
@Test
|
@DisplayName("验证码登录手机号未注册时应提示先注册且不消费验证码")
|
void phoneCodeLogin_shouldThrow_whenNotRegistered()
|
{
|
when(remoteAccountUserService.getMemberByPhone(PHONE, SecurityConstants.INNER)).thenReturn(R.ok());
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> accountLoginService.phoneCodeLogin(PHONE, SMS_CODE));
|
assertEquals("该手机号未注册,请先注册", ex.getMessage());
|
verify(smsCodeService, never()).verifyAndConsume(anyString(), anyString(), anyString());
|
}
|
|
@Test
|
@DisplayName("验证码登录会员被停用时应拒绝且不消费验证码")
|
void phoneCodeLogin_shouldThrow_whenDisabled()
|
{
|
AccountMember member = newMember(UserStatus.DISABLE.getCode());
|
when(remoteAccountUserService.getMemberByPhone(PHONE, SecurityConstants.INNER)).thenReturn(R.ok(member));
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> accountLoginService.phoneCodeLogin(PHONE, SMS_CODE));
|
assertEquals("账号已停用,请联系管理员", ex.getMessage());
|
verify(smsCodeService, never()).verifyAndConsume(anyString(), anyString(), anyString());
|
}
|
|
@Test
|
@DisplayName("验证码登录正常时应先消费验证码再签发令牌")
|
void phoneCodeLogin_shouldReturnToken_whenValid()
|
{
|
AccountMember member = newMember(UserStatus.OK.getCode());
|
when(remoteAccountUserService.getMemberByPhone(PHONE, SecurityConstants.INNER)).thenReturn(R.ok(member));
|
Map<String, Object> token = new HashMap<>();
|
token.put("access_token", "jwt-token");
|
when(tokenService.createToken(any(LoginUser.class))).thenReturn(token);
|
|
Map<String, Object> result = accountLoginService.phoneCodeLogin(PHONE, SMS_CODE);
|
|
assertEquals("jwt-token", result.get("access_token"));
|
verify(smsCodeService).verifyAndConsume(PHONE, SmsSceneEnum.LOGIN.getCode(), SMS_CODE);
|
verify(tokenService).createToken(any(LoginUser.class));
|
}
|
|
@Test
|
@DisplayName("手机号格式非法时应拒绝注册")
|
void accountRegister_shouldThrow_whenPhoneInvalid()
|
{
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> accountLoginService.accountRegister("12345", SMS_CODE, REFERRAL_CODE));
|
assertEquals("请输入正确手机号", ex.getMessage());
|
}
|
|
@Test
|
@DisplayName("推荐码为空时应拒绝注册")
|
void accountRegister_shouldThrow_whenReferralCodeEmpty()
|
{
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> accountLoginService.accountRegister(PHONE, SMS_CODE, " "));
|
assertEquals("请输入推荐人码", ex.getMessage());
|
}
|
|
@Test
|
@DisplayName("注册锁未抢到时应提示稍后重试")
|
void accountRegister_shouldThrow_whenLockNotAcquired()
|
{
|
when(redisService.setCacheObjectIfAbsent(anyString(), any(), any(), any(TimeUnit.class))).thenReturn(false);
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> accountLoginService.accountRegister(PHONE, SMS_CODE, REFERRAL_CODE));
|
assertEquals("操作过于频繁,请稍后重试", ex.getMessage());
|
verify(remoteAccountUserService, never()).registerMember(any(), anyString());
|
}
|
|
@Test
|
@DisplayName("锁内二次查重命中已注册应拒绝并释放锁")
|
void accountRegister_shouldThrowAndReleaseLock_whenPhoneRegistered()
|
{
|
when(redisService.setCacheObjectIfAbsent(anyString(), any(), any(), any(TimeUnit.class))).thenReturn(true);
|
when(remoteAccountUserService.getMemberByPhone(PHONE, SecurityConstants.INNER))
|
.thenReturn(R.ok(newMember(UserStatus.OK.getCode())));
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> accountLoginService.accountRegister(PHONE, SMS_CODE, REFERRAL_CODE));
|
assertEquals("手机号已注册,请前往登录", ex.getMessage());
|
verify(redisService).deleteObject(lockKey());
|
}
|
|
@Test
|
@DisplayName("正常注册应调用会员服务并在 finally 释放锁")
|
void accountRegister_shouldSucceed_whenValid()
|
{
|
when(redisService.setCacheObjectIfAbsent(anyString(), any(), any(), any(TimeUnit.class))).thenReturn(true);
|
when(remoteAccountUserService.getMemberByPhone(PHONE, SecurityConstants.INNER)).thenReturn(R.ok());
|
when(remoteAccountUserService.registerMember(any(AccountRegisterRequest.class), eq(SecurityConstants.INNER)))
|
.thenReturn(R.ok(newMember(UserStatus.OK.getCode())));
|
|
assertDoesNotThrow(() -> accountLoginService.accountRegister(PHONE, SMS_CODE, REFERRAL_CODE));
|
|
verify(smsCodeService).verifyAndConsume(PHONE, SmsSceneEnum.REGISTER.getCode(), SMS_CODE);
|
verify(redisService).deleteObject(lockKey());
|
}
|
|
@Test
|
@DisplayName("会员服务注册失败时应释放锁并透传错误信息")
|
void accountRegister_shouldReleaseLock_whenFeignFails()
|
{
|
when(redisService.setCacheObjectIfAbsent(anyString(), any(), any(), any(TimeUnit.class))).thenReturn(true);
|
when(remoteAccountUserService.getMemberByPhone(PHONE, SecurityConstants.INNER)).thenReturn(R.ok());
|
when(remoteAccountUserService.registerMember(any(AccountRegisterRequest.class), eq(SecurityConstants.INNER)))
|
.thenReturn(R.fail("推荐人码无效"));
|
|
ServiceException ex = assertThrows(ServiceException.class,
|
() -> accountLoginService.accountRegister(PHONE, SMS_CODE, REFERRAL_CODE));
|
assertEquals("推荐人码无效", ex.getMessage());
|
verify(redisService).deleteObject(lockKey());
|
}
|
}
|