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 token = new HashMap<>(); token.put("access_token", "jwt-token"); when(tokenService.createToken(any(LoginUser.class))).thenReturn(token); Map result = accountLoginService.phoneLogin(WX_CODE); assertEquals("jwt-token", result.get("access_token")); ArgumentCaptor 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 token = new HashMap<>(); token.put("access_token", "jwt-token"); when(tokenService.createToken(any(LoginUser.class))).thenReturn(token); Map 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()); } }