package com.tcm.training.admin.app.auth.service; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; 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.AppAppleLoginRequest; import com.tcm.training.admin.app.auth.dto.AppSmsCodeSendRequest; 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.share.service.AppShareService; 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.security.crypto.bcrypt.BCryptPasswordEncoder; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertNull; 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.never; 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 AppWechatAuthClient appWechatAuthClient; @Mock private AppAppleAuthClient appAppleAuthClient; @Mock private AppShareService appShareService; @Mock private StringRedisTemplate stringRedisTemplate; private AppAuthService appAuthService; @Captor private ArgumentCaptor appUserCaptor; /** * 每个用例前重新构造被测认证服务。 */ @BeforeEach void setUp() { appAuthService = new AppAuthService( userMapper, appSmsCodeService, passwordEncoder, appAuthSessionService, appWechatAuthClient, appAppleAuthClient, appShareService, stringRedisTemplate ); } /** * 密码登录能力下线后旧客户端调用应返回统一提示。 */ @Test void passwordLoginShouldReturnDisabledMessage() { AppPasswordLoginRequest request = new AppPasswordLoginRequest(); request.setPhone("13800138000"); request.setPassword("abc123!!"); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> appAuthService.passwordLogin(request)); assertEquals("当前版本仅支持验证码登录", exception.getMessage()); verify(passwordEncoder, never()).matches(anyString(), anyString()); verify(appAuthSessionService, never()).login(anyLong()); } /** * 忘记密码发码能力下线后旧客户端调用应返回统一提示。 */ @Test void sendPasswordResetCodeShouldReturnDisabledMessage() { AppSmsCodeSendRequest request = new AppSmsCodeSendRequest(); request.setPhone("13800138001"); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> appAuthService.sendPasswordResetCode(request)); assertEquals("当前版本仅支持验证码登录", exception.getMessage()); verify(appSmsCodeService, never()).createPasswordResetCode(anyString()); } /** * 忘记密码验证码校验能力下线后旧客户端调用应返回统一提示。 */ @Test void verifyPasswordResetCodeShouldReturnDisabledMessage() { AppPasswordResetVerifyRequest request = new AppPasswordResetVerifyRequest(); request.setPhone("13800138002"); request.setCode("666666"); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> appAuthService.verifyPasswordResetCode(request)); assertEquals("当前版本仅支持验证码登录", exception.getMessage()); verify(appSmsCodeService, never()).validatePasswordResetCode(anyString(), anyString()); } /** * 重置密码能力下线后旧客户端调用应返回统一提示。 */ @Test void resetPasswordShouldReturnDisabledMessage() { AppPasswordResetRequest request = new AppPasswordResetRequest(); request.setResetTicket("ticket-1"); request.setPassword("abc123!!"); request.setConfirmPassword("abc123!!"); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> appAuthService.resetPassword(request)); assertEquals("当前版本仅支持验证码登录", exception.getMessage()); verify(passwordEncoder, never()).encode(anyString()); verify(userMapper, never()).updateById(any(AppUser.class)); } /** * 微信已绑定用户登录时应更新微信资料并返回统一登录响应。 */ @Test void wechatLoginShouldReuseExistingUser() { AppWechatLoginRequest request = new AppWechatLoginRequest(); request.setAccessToken("access-token"); request.setOpenid("openid-1"); AppWechatUserProfile profile = new AppWechatUserProfile(); profile.setOpenid("openid-1"); profile.setNickname("微信昵称"); profile.setAvatar("https://cdn.example/avatar.png"); profile.setSex(2); AppUser user = buildEnabledUser("13800138000", "ENCODED"); user.setWechatOpenid("openid-1"); user.setNickname(null); user.setAvatar(null); SaTokenInfo tokenInfo = new SaTokenInfo(); tokenInfo.setTokenValue("wechat-token"); tokenInfo.setTokenTimeout(7200L); when(appWechatAuthClient.resolveUserProfile(request)).thenReturn(profile); when(userMapper.selectOne(any())).thenReturn(user); when(appAuthSessionService.getTokenInfo()).thenReturn(tokenInfo); AppLoginResponse response = appAuthService.wechatLogin(request); verify(userMapper).updateById(appUserCaptor.capture()); verify(userMapper, never()).insert(any()); assertEquals("openid-1", appUserCaptor.getValue().getWechatOpenid()); assertEquals("微信昵称", appUserCaptor.getValue().getNickname()); assertEquals("https://cdn.example/avatar.png", appUserCaptor.getValue().getAvatar()); assertEquals(Integer.valueOf(0), appUserCaptor.getValue().getGender()); assertEquals("微信昵称", response.getUser().getNickname()); assertEquals("https://cdn.example/avatar.png", response.getUser().getAvatar()); } /** * 微信首次登录时应自动创建普通用户并返回统一登录响应。 */ @Test void wechatLoginShouldCreateUserWhenOpenidNotExists() { AppWechatLoginRequest request = new AppWechatLoginRequest(); request.setAccessToken("access-token"); request.setOpenid("openid-new"); request.setInviteCode("ABC123"); AppWechatUserProfile profile = new AppWechatUserProfile(); profile.setOpenid("openid-new"); profile.setNickname("新微信用户"); profile.setAvatar("https://cdn.example/new-avatar.png"); profile.setSex(1); SaTokenInfo tokenInfo = new SaTokenInfo(); tokenInfo.setTokenValue("new-token"); tokenInfo.setTokenTimeout(7200L); when(appWechatAuthClient.resolveUserProfile(request)).thenReturn(profile); when(userMapper.selectOne(any())).thenReturn(null); when(appShareService.resolveInviteBindingTarget("ABC123")).thenReturn(buildInviteTarget()); when(appAuthSessionService.getTokenInfo()).thenReturn(tokenInfo); AppLoginResponse response = appAuthService.wechatLogin(request); verify(userMapper).insert(appUserCaptor.capture()); verify(userMapper, never()).updateById(any()); assertNull(appUserCaptor.getValue().getPhone()); assertEquals("openid-new", appUserCaptor.getValue().getWechatOpenid()); assertEquals("新微信用户", appUserCaptor.getValue().getNickname()); assertEquals("https://cdn.example/new-avatar.png", appUserCaptor.getValue().getAvatar()); assertEquals(Integer.valueOf(1), appUserCaptor.getValue().getGender()); assertEquals(Integer.valueOf(0), appUserCaptor.getValue().getType()); assertEquals(Long.valueOf(10L), appUserCaptor.getValue().getClinicId()); assertEquals(Long.valueOf(20L), appUserCaptor.getValue().getSuperiorClinicUserId()); assertEquals("new-token", response.getToken()); } /** * 微信首次登录未携带邀请码时应创建待绑定手机号的临时用户。 */ @Test void wechatLoginShouldCreatePendingUserWhenInviteCodeMissing() { AppWechatLoginRequest request = new AppWechatLoginRequest(); request.setAccessToken("access-token"); request.setOpenid("openid-new"); AppWechatUserProfile profile = new AppWechatUserProfile(); profile.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)); AppLoginResponse response = appAuthService.wechatLogin(request); verify(userMapper).insert(appUserCaptor.capture()); assertNull(appUserCaptor.getValue().getPhone()); assertEquals("openid-new", appUserCaptor.getValue().getWechatOpenid()); assertEquals("wechat-pending-token", response.getToken()); } /** * 苹果已绑定用户登录时应更新苹果绑定信息并返回统一登录响应。 */ @Test void appleLoginShouldReuseExistingUser() { AppAppleLoginRequest request = new AppAppleLoginRequest(); request.setIdentityToken("identity-token"); request.setUserIdentifier("apple-user-1"); AppAppleUserProfile profile = new AppAppleUserProfile(); profile.setSubject("apple-subject-1"); profile.setUserIdentifier("apple-user-1"); AppUser user = buildEnabledUser("13800138009", "ENCODED"); user.setAppleIdentitySubject("apple-subject-1"); user.setAppleUserIdentifier("apple-user-legacy"); user.setNickname("已存在用户"); SaTokenInfo tokenInfo = new SaTokenInfo(); tokenInfo.setTokenValue("apple-token"); tokenInfo.setTokenTimeout(7200L); when(appAppleAuthClient.resolveUserProfile(request)).thenReturn(profile); when(userMapper.selectOne(any())).thenReturn(user); when(appAuthSessionService.getTokenInfo()).thenReturn(tokenInfo); AppLoginResponse response = appAuthService.appleLogin(request); verify(userMapper).updateById(appUserCaptor.capture()); verify(userMapper, never()).insert(any()); assertEquals("apple-subject-1", appUserCaptor.getValue().getAppleIdentitySubject()); assertEquals("apple-user-1", appUserCaptor.getValue().getAppleUserIdentifier()); assertEquals("已存在用户", response.getUser().getNickname()); assertEquals("apple-token", response.getToken()); } /** * 苹果首次登录时应自动创建普通用户并等待后续绑定手机号。 */ @Test void appleLoginShouldCreateUserWhenSubjectNotExists() { AppAppleLoginRequest request = new AppAppleLoginRequest(); request.setIdentityToken("identity-token"); request.setUserIdentifier("apple-user-new"); request.setInviteCode("ABC123"); AppAppleUserProfile profile = new AppAppleUserProfile(); profile.setSubject("apple-subject-new"); profile.setUserIdentifier("apple-user-new"); SaTokenInfo tokenInfo = new SaTokenInfo(); tokenInfo.setTokenValue("apple-new-token"); tokenInfo.setTokenTimeout(7200L); when(appAppleAuthClient.resolveUserProfile(request)).thenReturn(profile); when(userMapper.selectOne(any())).thenReturn(null); when(appShareService.resolveInviteBindingTarget("ABC123")).thenReturn(buildInviteTarget()); when(appAuthSessionService.getTokenInfo()).thenReturn(tokenInfo); AppLoginResponse response = appAuthService.appleLogin(request); verify(userMapper).insert(appUserCaptor.capture()); verify(userMapper, never()).updateById(any()); assertNull(appUserCaptor.getValue().getPhone()); assertEquals("apple-subject-new", appUserCaptor.getValue().getAppleIdentitySubject()); assertEquals("apple-user-new", appUserCaptor.getValue().getAppleUserIdentifier()); assertEquals("苹果用户", appUserCaptor.getValue().getNickname()); assertEquals(Integer.valueOf(0), appUserCaptor.getValue().getType()); assertEquals(Long.valueOf(10L), appUserCaptor.getValue().getClinicId()); assertEquals(Long.valueOf(20L), appUserCaptor.getValue().getSuperiorClinicUserId()); assertEquals("apple-new-token", response.getToken()); } /** * 苹果首次登录未携带邀请码时应创建待绑定手机号的临时用户。 */ @Test void appleLoginShouldCreatePendingUserWhenInviteCodeMissing() { AppAppleLoginRequest request = new AppAppleLoginRequest(); request.setIdentityToken("identity-token"); request.setUserIdentifier("apple-user-new"); AppAppleUserProfile profile = new AppAppleUserProfile(); profile.setSubject("apple-subject-new"); profile.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)); AppLoginResponse response = appAuthService.appleLogin(request); verify(userMapper).insert(appUserCaptor.capture()); assertNull(appUserCaptor.getValue().getPhone()); assertEquals("apple-subject-new", appUserCaptor.getValue().getAppleIdentitySubject()); assertEquals("apple-pending-token", response.getToken()); } /** * 苹果授权无效时应直接拒绝登录。 */ @Test void appleLoginShouldFailWhenIdentityTokenInvalid() { AppAppleLoginRequest request = new AppAppleLoginRequest(); request.setIdentityToken("bad-token"); request.setUserIdentifier("apple-user-bad"); when(appAppleAuthClient.resolveUserProfile(request)).thenThrow(new IllegalArgumentException("苹果登录授权失败")); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> appAuthService.appleLogin(request)); assertEquals("苹果登录授权失败", exception.getMessage()); } /** * 构造邀请绑定目标。 */ private AppShareService.InviteBindingTarget buildInviteTarget() { AppShareService.InviteBindingTarget target = new AppShareService.InviteBindingTarget(); target.setInviteCode("ABC123"); target.setClinicId(10L); target.setSuperiorClinicUserId(20L); return target; } /** * 构造测试用 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) { AppUser user = new AppUser(); user.setId(1L); user.setPhone(phone); user.setPassword(password); user.setNickname("测试用户"); user.setStatus(1); user.setType(0); return user; } }