hejianhao
2026-08-28 16a57a8fc6868cea9171a1119698572407a2fc03
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java
@@ -10,7 +10,6 @@
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.share.service.AppShareService;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.member.mapper.UserMapper;
@@ -25,13 +24,9 @@
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;
@@ -78,9 +73,6 @@
    @Mock
    private StringRedisTemplate stringRedisTemplate;
    @Mock
    private ValueOperations<String, String> valueOperations;
    private AppAuthService appAuthService;
    @Captor
@@ -104,128 +96,69 @@
    }
    /**
     * 密码登录成功时应返回统一登录响应并建立 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));
    }
    /**
@@ -276,6 +209,7 @@
        AppWechatLoginRequest request = new AppWechatLoginRequest();
        request.setAccessToken("access-token");
        request.setOpenid("openid-new");
        request.setInviteCode("ABC123");
        AppWechatUserProfile profile = new AppWechatUserProfile();
        profile.setOpenid("openid-new");
@@ -289,6 +223,7 @@
        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);
@@ -301,7 +236,38 @@
        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());
    }
    /**
@@ -348,6 +314,7 @@
        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");
@@ -359,6 +326,7 @@
        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);
@@ -370,7 +338,39 @@
        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());
    }
    /**
@@ -391,6 +391,27 @@
    }
    /**
     * 构造邀请绑定目标。
     */
    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) {