| | |
| | | import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest; |
| | | 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.app.auth.exception.InviteCodeRequiredException; |
| | | import com.tcm.training.admin.app.share.service.AppShareService; |
| | | import com.tcm.training.admin.member.entity.AppUser; |
| | | import com.tcm.training.admin.member.mapper.UserMapper; |
| | |
| | | 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.junit.jupiter.api.Assertions.assertNull; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | |
| | | @Mock |
| | | private StringRedisTemplate stringRedisTemplate; |
| | | |
| | | @Mock |
| | | private ValueOperations<String, String> valueOperations; |
| | | |
| | | private AppAuthService appAuthService; |
| | | |
| | | @Captor |
| | |
| | | } |
| | | |
| | | /** |
| | | * 密码登录成功时应返回统一登录响应并建立 APP 登录态。 |
| | | * 密码登录能力下线后旧客户端调用应返回统一提示。 |
| | | */ |
| | | @Test |
| | | void passwordLoginShouldReturnTokenWhenPasswordMatches() { |
| | | void passwordLoginShouldReturnDisabledMessage() { |
| | | 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()); |
| | | assertEquals("当前版本仅支持验证码登录", exception.getMessage()); |
| | | verify(passwordEncoder, never()).matches(anyString(), anyString()); |
| | | verify(appAuthSessionService, never()).login(anyLong()); |
| | | } |
| | | |
| | | /** |
| | | * 忘记密码发码仅允许已启用用户发送。 |
| | | * 忘记密码发码能力下线后旧客户端调用应返回统一提示。 |
| | | */ |
| | | @Test |
| | | void sendPasswordResetCodeShouldReturnExpireSeconds() { |
| | | void sendPasswordResetCodeShouldReturnDisabledMessage() { |
| | | AppSmsCodeSendRequest request = new AppSmsCodeSendRequest(); |
| | | request.setPhone("13800138001"); |
| | | |
| | | when(userMapper.selectOne(any())).thenReturn(buildEnabledUser("13800138001", "ENCODED")); |
| | | when(appSmsCodeService.createPasswordResetCode("13800138001")).thenReturn(new AppSmsCodeSendResponse(300)); |
| | | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, |
| | | () -> appAuthService.sendPasswordResetCode(request)); |
| | | |
| | | AppSmsCodeSendResponse response = appAuthService.sendPasswordResetCode(request); |
| | | |
| | | assertEquals(300, response.getExpireInSeconds()); |
| | | assertEquals("当前版本仅支持验证码登录", exception.getMessage()); |
| | | verify(appSmsCodeService, never()).createPasswordResetCode(anyString()); |
| | | } |
| | | |
| | | /** |
| | | * 忘记密码验证码校验成功后应签发一次性重置凭证并写入缓存。 |
| | | * 忘记密码验证码校验能力下线后旧客户端调用应返回统一提示。 |
| | | */ |
| | | @Test |
| | | void verifyPasswordResetCodeShouldCacheTicket() { |
| | | void verifyPasswordResetCodeShouldReturnDisabledMessage() { |
| | | 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"); |
| | | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, |
| | | () -> appAuthService.verifyPasswordResetCode(request)); |
| | | |
| | | AppPasswordResetVerifyResponse response = appAuthService.verifyPasswordResetCode(request); |
| | | |
| | | assertNotNull(response.getResetTicket()); |
| | | verify(valueOperations).set(anyString(), anyString(), anyLong(), any(TimeUnit.class)); |
| | | assertEquals("当前版本仅支持验证码登录", exception.getMessage()); |
| | | verify(appSmsCodeService, never()).validatePasswordResetCode(anyString(), anyString()); |
| | | } |
| | | |
| | | /** |
| | | * 使用有效重置凭证时应更新数据库中的密码并销毁凭证。 |
| | | * 重置密码能力下线后旧客户端调用应返回统一提示。 |
| | | */ |
| | | @Test |
| | | void resetPasswordShouldUpdatePasswordAndDeleteTicket() { |
| | | void resetPasswordShouldReturnDisabledMessage() { |
| | | 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()); |
| | | assertEquals("当前版本仅支持验证码登录", exception.getMessage()); |
| | | verify(passwordEncoder, never()).encode(anyString()); |
| | | verify(userMapper, never()).updateById(any(AppUser.class)); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 微信首次登录未携带邀请码时应提示联系管理员获取。 |
| | | * 微信首次登录未携带邀请码时应创建待绑定手机号的临时用户。 |
| | | */ |
| | | @Test |
| | | void wechatLoginShouldRequireInviteCodeWhenOpenidNotExists() { |
| | | void wechatLoginShouldCreatePendingUserWhenInviteCodeMissing() { |
| | | AppWechatLoginRequest request = new AppWechatLoginRequest(); |
| | | request.setAccessToken("access-token"); |
| | | request.setOpenid("openid-new"); |
| | |
| | | |
| | | when(appWechatAuthClient.resolveUserProfile(request)).thenReturn(profile); |
| | | when(userMapper.selectOne(any())).thenReturn(null); |
| | | when(userMapper.insert(any(AppUser.class))).thenAnswer(invocation -> { |
| | | AppUser inserted = invocation.getArgument(0); |
| | | inserted.setId(2001L); |
| | | return 1; |
| | | }); |
| | | when(appAuthSessionService.getTokenInfo()).thenReturn(buildTokenInfo("wechat-pending-token", 7200L)); |
| | | |
| | | InviteCodeRequiredException exception = assertThrows(InviteCodeRequiredException.class, |
| | | () -> appAuthService.wechatLogin(request)); |
| | | AppLoginResponse response = appAuthService.wechatLogin(request); |
| | | |
| | | assertEquals("暂无邀请码,请联系管理员获取", exception.getMessage()); |
| | | verify(userMapper, never()).insert(any()); |
| | | verify(userMapper).insert(appUserCaptor.capture()); |
| | | assertNull(appUserCaptor.getValue().getPhone()); |
| | | assertEquals("openid-new", appUserCaptor.getValue().getWechatOpenid()); |
| | | assertEquals("wechat-pending-token", response.getToken()); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 苹果首次登录未携带邀请码时应提示联系管理员获取。 |
| | | * 苹果首次登录未携带邀请码时应创建待绑定手机号的临时用户。 |
| | | */ |
| | | @Test |
| | | void appleLoginShouldRequireInviteCodeWhenSubjectNotExists() { |
| | | void appleLoginShouldCreatePendingUserWhenInviteCodeMissing() { |
| | | AppAppleLoginRequest request = new AppAppleLoginRequest(); |
| | | request.setIdentityToken("identity-token"); |
| | | request.setUserIdentifier("apple-user-new"); |
| | |
| | | |
| | | when(appAppleAuthClient.resolveUserProfile(request)).thenReturn(profile); |
| | | when(userMapper.selectOne(any())).thenReturn(null); |
| | | when(userMapper.insert(any(AppUser.class))).thenAnswer(invocation -> { |
| | | AppUser inserted = invocation.getArgument(0); |
| | | inserted.setId(3001L); |
| | | return 1; |
| | | }); |
| | | when(appAuthSessionService.getTokenInfo()).thenReturn(buildTokenInfo("apple-pending-token", 7200L)); |
| | | |
| | | InviteCodeRequiredException exception = assertThrows(InviteCodeRequiredException.class, |
| | | () -> appAuthService.appleLogin(request)); |
| | | AppLoginResponse response = appAuthService.appleLogin(request); |
| | | |
| | | assertEquals("暂无邀请码,请联系管理员获取", exception.getMessage()); |
| | | verify(userMapper, never()).insert(any()); |
| | | verify(userMapper).insert(appUserCaptor.capture()); |
| | | assertNull(appUserCaptor.getValue().getPhone()); |
| | | assertEquals("apple-subject-new", appUserCaptor.getValue().getAppleIdentitySubject()); |
| | | assertEquals("apple-pending-token", response.getToken()); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 构造测试用 token 信息。 |
| | | */ |
| | | private SaTokenInfo buildTokenInfo(String tokenValue, long timeout) { |
| | | SaTokenInfo tokenInfo = new SaTokenInfo(); |
| | | tokenInfo.setTokenValue(tokenValue); |
| | | tokenInfo.setTokenTimeout(timeout); |
| | | return tokenInfo; |
| | | } |
| | | |
| | | /** |
| | | * 统一构造启用中的 APP 用户测试桩。 |
| | | */ |
| | | private AppUser buildEnabledUser(String phone, String password) { |