package com.tcm.training.admin.app.settings.service; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import cn.dev33.satoken.stp.SaTokenInfo; import com.tcm.training.admin.app.auth.service.AppAuthSessionService; import com.tcm.training.admin.app.auth.vo.AppLoginResponse; import com.tcm.training.admin.app.auth.service.AppSmsCodeService; import com.tcm.training.admin.app.auth.service.AppWechatAuthClient; import com.tcm.training.admin.app.settings.dto.AppSettingsPhoneBindRequest; import com.tcm.training.admin.app.settings.mapper.AppUserNotificationSettingMapper; 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 com.tcm.training.admin.upload.ObsUploadService; 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.security.crypto.bcrypt.BCryptPasswordEncoder; 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.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * APP 设置服务手机号绑定测试。 */ @ExtendWith(MockitoExtension.class) class AppSettingsServiceTest { @BeforeAll static void initLambdaCache() { MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); TableInfoHelper.initTableInfo(assistant, AppUser.class); } @Mock private AppAuthSessionService appAuthSessionService; @Mock private UserMapper userMapper; @Mock private AppSmsCodeService appSmsCodeService; @Mock private BCryptPasswordEncoder passwordEncoder; @Mock private AppWechatAuthClient appWechatAuthClient; @Mock private AppShareService appShareService; @Mock private AppUserNotificationSettingMapper appUserNotificationSettingMapper; @Mock private ObsUploadService obsUploadService; @Captor private ArgumentCaptor appUserCaptor; private AppSettingsService appSettingsService; /** * 每个用例前重新构造被测设置服务。 */ @BeforeEach void setUp() { appSettingsService = new AppSettingsService( appAuthSessionService, userMapper, appSmsCodeService, passwordEncoder, appWechatAuthClient, appShareService, appUserNotificationSettingMapper, obsUploadService ); } /** * 微信登录后未绑定手机号的用户,可通过验证码首次绑定手机号。 */ @Test void bindPhoneShouldUpdateCurrentUserWhenPhoneMissing() { AppSettingsPhoneBindRequest request = new AppSettingsPhoneBindRequest(); request.setNewPhone("13800138000"); request.setCode("123456"); AppUser currentUser = buildEnabledUser(null); when(appAuthSessionService.requireLoginUserId()).thenReturn(1L); when(userMapper.selectById(1L)).thenReturn(currentUser); when(userMapper.selectOne(any())).thenReturn(null); doNothing().when(appSmsCodeService).validatePhoneChangeCode("13800138000", "123456"); when(appAuthSessionService.getTokenInfo()).thenReturn(buildToken("bind-token")); AppLoginResponse response = appSettingsService.bindPhone(request); verify(userMapper).updateById(appUserCaptor.capture()); verify(appAuthSessionService).login(1L); assertEquals("13800138000", appUserCaptor.getValue().getPhone()); assertEquals("13800138000", response.getUser().getPhone()); assertEquals("bind-token", response.getToken()); } /** * 已绑定手机号的用户不允许走首次绑定接口。 */ @Test void bindPhoneShouldRejectWhenCurrentUserAlreadyHasPhone() { AppSettingsPhoneBindRequest request = new AppSettingsPhoneBindRequest(); request.setNewPhone("13800138000"); request.setCode("123456"); when(appAuthSessionService.requireLoginUserId()).thenReturn(1L); when(userMapper.selectById(1L)).thenReturn(buildEnabledUser("13900139000")); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> appSettingsService.bindPhone(request)); assertEquals("当前账号已绑定手机号", exception.getMessage()); verify(appSmsCodeService, never()).validatePhoneChangeCode(any(), any()); verify(userMapper, never()).updateById(any()); } /** * 微信临时账号绑定已注册但未绑定微信的手机号时,应合并到手机号账号。 */ @Test void bindPhoneShouldMergeWechatUserIntoExistingPhoneUser() { AppSettingsPhoneBindRequest request = new AppSettingsPhoneBindRequest(); request.setNewPhone("13800138000"); request.setCode("123456"); AppUser currentUser = buildEnabledUser(null); currentUser.setWechatOpenid("wx-openid"); currentUser.setNickname(null); currentUser.setWechatNickname("微信昵称"); AppUser occupiedUser = buildEnabledUser("13800138000"); occupiedUser.setId(2L); occupiedUser.setNickname(null); when(appAuthSessionService.requireLoginUserId()).thenReturn(1L); when(userMapper.selectById(1L)).thenReturn(currentUser); when(userMapper.selectOne(any())).thenReturn(occupiedUser); doNothing().when(appSmsCodeService).validatePhoneChangeCode("13800138000", "123456"); when(appAuthSessionService.getTokenInfo()).thenReturn(buildToken("merged-token")); AppLoginResponse response = appSettingsService.bindPhone(request); verify(userMapper, times(2)).updateById(appUserCaptor.capture()); AppUser mergedUser = appUserCaptor.getAllValues().get(0); AppUser deletedTemporaryUser = appUserCaptor.getAllValues().get(1); verify(appAuthSessionService).login(2L); assertEquals("wx-openid", mergedUser.getWechatOpenid()); assertEquals("微信昵称", mergedUser.getNickname()); assertEquals(Integer.valueOf(1), deletedTemporaryUser.getDeleted()); assertEquals(Long.valueOf(2L), response.getUser().getId()); assertEquals("merged-token", response.getToken()); } /** * 微信临时账号绑定已绑定微信的手机号时,应直接覆盖目标账号微信票据。 */ @Test void bindPhoneShouldOverwriteWechatBindingWhenPhoneUserAlreadyBoundWechat() { AppSettingsPhoneBindRequest request = new AppSettingsPhoneBindRequest(); request.setNewPhone("13800138000"); request.setCode("123456"); AppUser currentUser = buildEnabledUser(null); currentUser.setWechatOpenid("wx-openid"); currentUser.setWechatNickname("新微信昵称"); AppUser occupiedUser = buildEnabledUser("13800138000"); occupiedUser.setId(2L); occupiedUser.setWechatOpenid("other-wx-openid"); when(appAuthSessionService.requireLoginUserId()).thenReturn(1L); when(userMapper.selectById(1L)).thenReturn(currentUser); when(userMapper.selectOne(any())).thenReturn(occupiedUser); doNothing().when(appSmsCodeService).validatePhoneChangeCode("13800138000", "123456"); when(appAuthSessionService.getTokenInfo()).thenReturn(buildToken("wechat-overwrite-token")); AppLoginResponse response = appSettingsService.bindPhone(request); verify(userMapper, times(2)).updateById(appUserCaptor.capture()); AppUser mergedUser = appUserCaptor.getAllValues().get(0); AppUser deletedTemporaryUser = appUserCaptor.getAllValues().get(1); verify(appAuthSessionService).login(2L); assertEquals("wx-openid", mergedUser.getWechatOpenid()); assertEquals("新微信昵称", mergedUser.getWechatNickname()); assertEquals(Integer.valueOf(1), deletedTemporaryUser.getDeleted()); assertEquals(Long.valueOf(2L), response.getUser().getId()); } /** * 苹果临时账号绑定已注册但未绑定苹果的手机号时,应合并到手机号账号。 */ @Test void bindPhoneShouldMergeAppleUserIntoExistingPhoneUser() { AppSettingsPhoneBindRequest request = new AppSettingsPhoneBindRequest(); request.setNewPhone("13800138000"); request.setCode("123456"); AppUser currentUser = buildEnabledUser(null); currentUser.setAppleIdentitySubject("apple-subject"); currentUser.setAppleUserIdentifier("apple-user"); AppUser occupiedUser = buildEnabledUser("13800138000"); occupiedUser.setId(2L); when(appAuthSessionService.requireLoginUserId()).thenReturn(1L); when(userMapper.selectById(1L)).thenReturn(currentUser); when(userMapper.selectOne(any())).thenReturn(occupiedUser); doNothing().when(appSmsCodeService).validatePhoneChangeCode("13800138000", "123456"); when(appAuthSessionService.getTokenInfo()).thenReturn(buildToken("apple-merged-token")); AppLoginResponse response = appSettingsService.bindPhone(request); verify(userMapper, times(2)).updateById(appUserCaptor.capture()); AppUser mergedUser = appUserCaptor.getAllValues().get(0); verify(appAuthSessionService).login(2L); assertEquals("apple-subject", mergedUser.getAppleIdentitySubject()); assertEquals("apple-user", mergedUser.getAppleUserIdentifier()); assertEquals(Long.valueOf(2L), response.getUser().getId()); } /** * 苹果临时账号绑定已绑定苹果的手机号时,应直接覆盖目标账号苹果票据。 */ @Test void bindPhoneShouldOverwriteAppleBindingWhenPhoneUserAlreadyBoundApple() { AppSettingsPhoneBindRequest request = new AppSettingsPhoneBindRequest(); request.setNewPhone("13800138000"); request.setCode("123456"); AppUser currentUser = buildEnabledUser(null); currentUser.setAppleIdentitySubject("apple-subject"); currentUser.setAppleUserIdentifier("apple-user"); AppUser occupiedUser = buildEnabledUser("13800138000"); occupiedUser.setId(2L); occupiedUser.setAppleIdentitySubject("other-apple-subject"); when(appAuthSessionService.requireLoginUserId()).thenReturn(1L); when(userMapper.selectById(1L)).thenReturn(currentUser); when(userMapper.selectOne(any())).thenReturn(occupiedUser); doNothing().when(appSmsCodeService).validatePhoneChangeCode("13800138000", "123456"); when(appAuthSessionService.getTokenInfo()).thenReturn(buildToken("apple-overwrite-token")); AppLoginResponse response = appSettingsService.bindPhone(request); verify(userMapper, times(2)).updateById(appUserCaptor.capture()); AppUser mergedUser = appUserCaptor.getAllValues().get(0); AppUser deletedTemporaryUser = appUserCaptor.getAllValues().get(1); verify(appAuthSessionService).login(2L); assertEquals("apple-subject", mergedUser.getAppleIdentitySubject()); assertEquals("apple-user", mergedUser.getAppleUserIdentifier()); assertEquals(Integer.valueOf(1), deletedTemporaryUser.getDeleted()); assertEquals(Long.valueOf(2L), response.getUser().getId()); } /** * 非第三方临时账号绑定已注册手机号时仍按占用处理。 */ @Test void bindPhoneShouldRejectOccupiedPhoneWithoutThirdPartyBinding() { AppSettingsPhoneBindRequest request = new AppSettingsPhoneBindRequest(); request.setNewPhone("13800138000"); request.setCode("123456"); AppUser occupiedUser = buildEnabledUser("13800138000"); occupiedUser.setId(2L); when(appAuthSessionService.requireLoginUserId()).thenReturn(1L); when(userMapper.selectById(1L)).thenReturn(buildEnabledUser(null)); when(userMapper.selectOne(any())).thenReturn(occupiedUser); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> appSettingsService.bindPhone(request)); assertEquals("该手机号已注册", exception.getMessage()); verify(appSmsCodeService, never()).validatePhoneChangeCode(any(), any()); verify(userMapper, never()).updateById(any()); } /** * 验证码错误时不应更新用户手机号。 */ @Test void bindPhoneShouldNotUpdateWhenSmsCodeInvalid() { AppSettingsPhoneBindRequest request = new AppSettingsPhoneBindRequest(); request.setNewPhone("13800138000"); request.setCode("000000"); when(appAuthSessionService.requireLoginUserId()).thenReturn(1L); when(userMapper.selectById(1L)).thenReturn(buildEnabledUser(null)); when(userMapper.selectOne(any())).thenReturn(null); org.mockito.Mockito.doThrow(new IllegalArgumentException("验证码错误")) .when(appSmsCodeService).validatePhoneChangeCode("13800138000", "000000"); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> appSettingsService.bindPhone(request)); assertEquals("验证码错误", exception.getMessage()); verify(userMapper, never()).updateById(any()); } /** * 构造测试登录态。 */ private SaTokenInfo buildToken(String tokenValue) { SaTokenInfo tokenInfo = new SaTokenInfo(); tokenInfo.setTokenValue(tokenValue); tokenInfo.setTokenTimeout(7200L); return tokenInfo; } /** * 构造启用状态 APP 用户。 */ private AppUser buildEnabledUser(String phone) { AppUser user = new AppUser(); user.setId(1L); user.setNickname("微信用户"); user.setAvatar("avatar.png"); user.setPhone(phone); user.setType(0); user.setStatus(1); user.setDeleted(0); return user; } }