package cn.iocoder.yudao.module.system.service.social; import cn.binarywang.wx.miniapp.api.WxMaOrderShippingService; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.WxMaUserService; import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import cn.binarywang.wx.miniapp.bean.shop.request.shipping.WxMaOrderShippingInfoUploadRequest; import cn.binarywang.wx.miniapp.bean.shop.response.WxMaOrderShippingInfoBaseResponse; import cn.hutool.core.util.ReflectUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO; import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO; import cn.iocoder.yudao.module.system.dal.mysql.social.SocialClientMapper; import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; import cn.iocoder.yudao.module.system.framework.justauth.core.AuthRequestFactory; import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; import jakarta.annotation.Resource; import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.zhyd.oauth.config.AuthConfig; import me.zhyd.oauth.model.AuthResponse; import me.zhyd.oauth.model.AuthUser; import me.zhyd.oauth.request.AuthDefaultRequest; import me.zhyd.oauth.request.AuthRequest; import me.zhyd.oauth.utils.AuthStateUtils; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.springframework.context.annotation.Import; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.test.context.bean.override.mockito.MockitoBean; import static cn.hutool.core.util.RandomUtil.randomEle; import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; /** * {@link SocialClientServiceImpl} 的单元测试类 * * @author 芋道源码 */ @Import(SocialClientServiceImpl.class) public class SocialClientServiceImplTest extends BaseDbUnitTest { @Resource private SocialClientServiceImpl socialClientService; @Resource private SocialClientMapper socialClientMapper; @MockitoBean private AuthRequestFactory authRequestFactory; @MockitoBean private WxMpService wxMpService; @MockitoBean private WxMpProperties wxMpProperties; @MockitoBean private StringRedisTemplate stringRedisTemplate; @MockitoBean private WxMaService wxMaService; @MockitoBean private WxMaProperties wxMaProperties; @Test public void testGetAuthorizeUrl() { try (MockedStatic authStateUtilsMock = mockStatic(AuthStateUtils.class)) { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(UserTypeEnum.class).getValue(); String redirectUri = "sss"; // mock 获得对应的 AuthRequest 实现 AuthRequest authRequest = mock(AuthRequest.class); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // mock 方法 authStateUtilsMock.when(AuthStateUtils::createState).thenReturn("aoteman"); when(authRequest.authorize(eq("aoteman"))).thenReturn("https://www.iocoder.cn?redirect_uri=yyy"); // 调用 String url = socialClientService.getAuthorizeUrl(socialType, userType, redirectUri); // 断言 assertEquals("https://www.iocoder.cn?redirect_uri=sss", url); } } @Test public void testAuthSocialUser_success() { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(UserTypeEnum.class).getValue(); String code = randomString(); String state = randomString(); // mock 方法(AuthRequest) AuthRequest authRequest = mock(AuthRequest.class); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // mock 方法(AuthResponse) AuthUser authUser = randomPojo(AuthUser.class); AuthResponse authResponse = new AuthResponse<>(2000, null, authUser); when(authRequest.login(argThat(authCallback -> { assertEquals(code, authCallback.getCode()); assertEquals(state, authCallback.getState()); return true; }))).thenReturn(authResponse); // 调用 AuthUser result = socialClientService.getAuthUser(socialType, userType, code, state); // 断言 assertSame(authUser, result); } @Test public void testAuthSocialUser_fail() { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(UserTypeEnum.class).getValue(); String code = randomString(); String state = randomString(); // mock 方法(AuthRequest) AuthRequest authRequest = mock(AuthRequest.class); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // mock 方法(AuthResponse) AuthResponse authResponse = new AuthResponse<>(0, "模拟失败", null); when(authRequest.login(argThat(authCallback -> { assertEquals(code, authCallback.getCode()); assertEquals(state, authCallback.getState()); return true; }))).thenReturn(authResponse); // 调用并断言 assertServiceException( () -> socialClientService.getAuthUser(socialType, userType, code, state), SOCIAL_USER_AUTH_FAILURE, "模拟失败"); } @Test public void testBuildAuthRequest_clientNull() { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(SocialTypeEnum.class).getType(); // mock 获得对应的 AuthRequest 实现 AuthRequest authRequest = mock(AuthDefaultRequest.class); AuthConfig authConfig = (AuthConfig) ReflectUtil.getFieldValue(authRequest, "config"); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // 调用 AuthRequest result = socialClientService.buildAuthRequest(socialType, userType); // 断言 assertSame(authRequest, result); assertSame(authConfig, ReflectUtil.getFieldValue(authConfig, "config")); } @Test public void testBuildAuthRequest_clientDisable() { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(SocialTypeEnum.class).getType(); // mock 获得对应的 AuthRequest 实现 AuthRequest authRequest = mock(AuthDefaultRequest.class); AuthConfig authConfig = (AuthConfig) ReflectUtil.getFieldValue(authRequest, "config"); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()) .setUserType(userType).setSocialType(socialType)); socialClientMapper.insert(client); // 调用 AuthRequest result = socialClientService.buildAuthRequest(socialType, userType); // 断言 assertSame(authRequest, result); assertSame(authConfig, ReflectUtil.getFieldValue(authConfig, "config")); } @Test public void testBuildAuthRequest_clientEnable() { // 准备参数 Integer socialType = SocialTypeEnum.WECHAT_MP.getType(); Integer userType = randomPojo(SocialTypeEnum.class).getType(); // mock 获得对应的 AuthRequest 实现 AuthConfig authConfig = mock(AuthConfig.class); AuthRequest authRequest = mock(AuthDefaultRequest.class); ReflectUtil.setFieldValue(authRequest, "config", authConfig); when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()) .setUserType(userType).setSocialType(socialType)); socialClientMapper.insert(client); // 调用 AuthRequest result = socialClientService.buildAuthRequest(socialType, userType); // 断言 assertSame(authRequest, result); assertNotSame(authConfig, ReflectUtil.getFieldValue(authRequest, "config")); } // =================== 微信公众号独有 =================== @Test public void testCreateWxMpJsapiSignature() throws WxErrorException { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); String url = randomString(); // mock 方法 WxJsapiSignature signature = randomPojo(WxJsapiSignature.class); when(wxMpService.createJsapiSignature(eq(url))).thenReturn(signature); // 调用 WxJsapiSignature result = socialClientService.createWxMpJsapiSignature(userType, url); // 断言 assertSame(signature, result); } @Test public void testGetWxMpService_clientNull() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 方法 // 调用 WxMpService result = socialClientService.getWxMpService(userType); // 断言 assertSame(wxMpService, result); } @Test public void testGetWxMpService_clientDisable() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()) .setUserType(userType).setSocialType(SocialTypeEnum.WECHAT_MP.getType())); socialClientMapper.insert(client); // 调用 WxMpService result = socialClientService.getWxMpService(userType); // 断言 assertSame(wxMpService, result); } @Test public void testGetWxMpService_clientEnable() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()) .setUserType(userType).setSocialType(SocialTypeEnum.WECHAT_MP.getType())); socialClientMapper.insert(client); // mock 方法 WxMpProperties.ConfigStorage configStorage = mock(WxMpProperties.ConfigStorage.class); when(wxMpProperties.getConfigStorage()).thenReturn(configStorage); // 调用 WxMpService result = socialClientService.getWxMpService(userType); // 断言 assertNotSame(wxMpService, result); assertEquals(client.getClientId(), result.getWxMpConfigStorage().getAppId()); assertEquals(client.getClientSecret(), result.getWxMpConfigStorage().getSecret()); } // =================== 微信小程序独有 =================== @Test public void testGetWxMaPhoneNumberInfo_success() throws WxErrorException { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); String phoneCode = randomString(); // mock 方法 WxMaUserService userService = mock(WxMaUserService.class); when(wxMaService.getUserService()).thenReturn(userService); WxMaPhoneNumberInfo phoneNumber = randomPojo(WxMaPhoneNumberInfo.class); when(userService.getPhoneNumber(eq(phoneCode))).thenReturn(phoneNumber); // 调用 WxMaPhoneNumberInfo result = socialClientService.getWxMaPhoneNumberInfo(userType, phoneCode); // 断言 assertSame(phoneNumber, result); } @Test public void testGetWxMaPhoneNumberInfo_exception() throws WxErrorException { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); String phoneCode = randomString(); // mock 方法 WxMaUserService userService = mock(WxMaUserService.class); when(wxMaService.getUserService()).thenReturn(userService); WxErrorException wxErrorException = new WxErrorException(new NullPointerException()); when(userService.getPhoneNumber(eq(phoneCode))).thenThrow(wxErrorException); // 调用并断言异常 assertServiceException(() -> socialClientService.getWxMaPhoneNumberInfo(userType, phoneCode), SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR); } @Test public void testGetWxMaService_clientNull() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 方法 // 调用 WxMaService result = socialClientService.getWxMaService(userType); // 断言 assertSame(wxMaService, result); } @Test public void testGetWxMaService_clientDisable() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()) .setUserType(userType).setSocialType(SocialTypeEnum.WECHAT_MINI_PROGRAM.getType())); socialClientMapper.insert(client); // 调用 WxMaService result = socialClientService.getWxMaService(userType); // 断言 assertSame(wxMaService, result); } @Test public void testGetWxMaService_clientEnable() { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); // mock 数据 SocialClientDO client = randomPojo(SocialClientDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()) .setUserType(userType).setSocialType(SocialTypeEnum.WECHAT_MINI_PROGRAM.getType())); socialClientMapper.insert(client); // mock 方法 WxMaProperties.ConfigStorage configStorage = mock(WxMaProperties.ConfigStorage.class); when(wxMaProperties.getConfigStorage()).thenReturn(configStorage); // 调用 WxMaService result = socialClientService.getWxMaService(userType); // 断言 assertNotSame(wxMaService, result); assertEquals(client.getClientId(), result.getWxMaConfig().getAppid()); assertEquals(client.getClientSecret(), result.getWxMaConfig().getSecret()); } // =================== 客户端管理 =================== @Test public void testCreateSocialClient_success() { // 准备参数 SocialClientSaveReqVO reqVO = randomPojo(SocialClientSaveReqVO.class, o -> o.setSocialType(randomEle(SocialTypeEnum.values()).getType()) .setUserType(randomEle(UserTypeEnum.values()).getValue()) .setStatus(randomCommonStatus())) .setId(null); // 防止 id 被赋值 // 调用 Long socialClientId = socialClientService.createSocialClient(reqVO); // 断言 assertNotNull(socialClientId); // 校验记录的属性是否正确 SocialClientDO socialClient = socialClientMapper.selectById(socialClientId); assertPojoEquals(reqVO, socialClient, "id"); } @Test public void testUpdateSocialClient_success() { // mock 数据 SocialClientDO dbSocialClient = randomPojo(SocialClientDO.class); socialClientMapper.insert(dbSocialClient);// @Sql: 先插入出一条存在的数据 // 准备参数 SocialClientSaveReqVO reqVO = randomPojo(SocialClientSaveReqVO.class, o -> { o.setId(dbSocialClient.getId()); // 设置更新的 ID o.setSocialType(randomEle(SocialTypeEnum.values()).getType()) .setUserType(randomEle(UserTypeEnum.values()).getValue()) .setStatus(randomCommonStatus()); }); // 调用 socialClientService.updateSocialClient(reqVO); // 校验是否更新正确 SocialClientDO socialClient = socialClientMapper.selectById(reqVO.getId()); // 获取最新的 assertPojoEquals(reqVO, socialClient); } @Test public void testUpdateSocialClient_notExists() { // 准备参数 SocialClientSaveReqVO reqVO = randomPojo(SocialClientSaveReqVO.class); // 调用, 并断言异常 assertServiceException(() -> socialClientService.updateSocialClient(reqVO), SOCIAL_CLIENT_NOT_EXISTS); } @Test public void testDeleteSocialClient_success() { // mock 数据 SocialClientDO dbSocialClient = randomPojo(SocialClientDO.class); socialClientMapper.insert(dbSocialClient);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbSocialClient.getId(); // 调用 socialClientService.deleteSocialClient(id); // 校验数据不存在了 assertNull(socialClientMapper.selectById(id)); } @Test public void testDeleteSocialClient_notExists() { // 准备参数 Long id = randomLongId(); // 调用, 并断言异常 assertServiceException(() -> socialClientService.deleteSocialClient(id), SOCIAL_CLIENT_NOT_EXISTS); } @Test public void testGetSocialClient() { // mock 数据 SocialClientDO dbSocialClient = randomPojo(SocialClientDO.class); socialClientMapper.insert(dbSocialClient);// @Sql: 先插入出一条存在的数据 // 准备参数 Long id = dbSocialClient.getId(); // 调用 SocialClientDO socialClient = socialClientService.getSocialClient(id); // 校验数据正确 assertPojoEquals(dbSocialClient, socialClient); } @Test public void testGetSocialClientPage() { // mock 数据 SocialClientDO dbSocialClient = randomPojo(SocialClientDO.class, o -> { // 等会查询到 o.setName("芋头"); o.setSocialType(SocialTypeEnum.GITEE.getType()); o.setUserType(UserTypeEnum.ADMIN.getValue()); o.setClientId("yudao"); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); }); socialClientMapper.insert(dbSocialClient); // 测试 name 不匹配 socialClientMapper.insert(cloneIgnoreId(dbSocialClient, o -> o.setName(randomString()))); // 测试 socialType 不匹配 socialClientMapper.insert(cloneIgnoreId(dbSocialClient, o -> o.setSocialType(SocialTypeEnum.DINGTALK.getType()))); // 测试 userType 不匹配 socialClientMapper.insert(cloneIgnoreId(dbSocialClient, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); // 测试 clientId 不匹配 socialClientMapper.insert(cloneIgnoreId(dbSocialClient, o -> o.setClientId("dao"))); // 测试 status 不匹配 socialClientMapper.insert(cloneIgnoreId(dbSocialClient, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); // 准备参数 SocialClientPageReqVO reqVO = new SocialClientPageReqVO(); reqVO.setName("芋"); reqVO.setSocialType(SocialTypeEnum.GITEE.getType()); reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); reqVO.setClientId("yu"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 调用 PageResult pageResult = socialClientService.getSocialClientPage(reqVO); // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); assertPojoEquals(dbSocialClient, pageResult.getList().get(0)); } // =================== 微信小程序订单发货 =================== @BeforeAll public static void setUpUploadShippingBackoff() { // 测试场景下把退避数组的每一项改为 1ms,避免拖慢用例(数组引用是 final 但元素可改) long[] backoff = (long[]) ReflectUtil.getFieldValue(SocialClientServiceImpl.class, "UPLOAD_SHIPPING_INFO_RETRY_BACKOFF_MILLIS"); for (int i = 0; i < backoff.length; i++) { backoff[i] = 1L; } } @Test public void testUploadWxaOrderShippingInfo_success() throws WxErrorException { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO(); // mock 方法:首次调用就成功 WxMaOrderShippingService shippingService = mockWxMaOrderShippingService(); when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class))) .thenReturn(new WxMaOrderShippingInfoBaseResponse()); // 调用 socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO); // 断言:仅调用 1 次,无重试 verify(shippingService, times(1)).upload(any(WxMaOrderShippingInfoUploadRequest.class)); } @Test public void testUploadWxaOrderShippingInfo_retryThenSuccess() throws WxErrorException { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO(); // mock 方法:首次抛 10060001,第二次成功 WxMaOrderShippingService shippingService = mockWxMaOrderShippingService(); when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class))) .thenThrow(buildWxErrorException(10060001)) .thenReturn(new WxMaOrderShippingInfoBaseResponse()); // 调用 socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO); // 断言:上传调用了 2 次,触发了 1 次重试 verify(shippingService, times(2)).upload(any(WxMaOrderShippingInfoUploadRequest.class)); } @Test public void testUploadWxaOrderShippingInfo_retryExhausted() throws WxErrorException { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO(); // mock 方法:始终抛 10060001 WxMaOrderShippingService shippingService = mockWxMaOrderShippingService(); when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class))) .thenThrow(buildWxErrorException(10060001)); // 调用并断言:重试用尽抛业务异常 assertServiceException(() -> socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO), SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR); // 断言:共 4 次尝试(1 次首发 + 3 次重试) verify(shippingService, times(4)).upload(any(WxMaOrderShippingInfoUploadRequest.class)); } @Test public void testUploadWxaOrderShippingInfo_otherErrorNoRetry() throws WxErrorException { // 准备参数 Integer userType = randomPojo(UserTypeEnum.class).getValue(); SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO(); // mock 方法:抛非 10060001 错误(如 access_token 失效) WxMaOrderShippingService shippingService = mockWxMaOrderShippingService(); when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class))) .thenThrow(buildWxErrorException(40001)); // 调用并断言:立即抛业务异常,无重试 assertServiceException(() -> socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO), SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR); verify(shippingService, times(1)).upload(any(WxMaOrderShippingInfoUploadRequest.class)); } /** 构造一个发货上传请求 */ private SocialWxaOrderUploadShippingInfoReqDTO randomUploadShippingReqDTO() { return randomPojo(SocialWxaOrderUploadShippingInfoReqDTO.class, o -> o.setLogisticsType(SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_EXPRESS)); } /** mock 出 wxMaService.getWxMaOrderShippingService() 的返回值并返回该 mock */ private WxMaOrderShippingService mockWxMaOrderShippingService() { WxMaOrderShippingService shippingService = mock(WxMaOrderShippingService.class); when(wxMaService.getWxMaOrderShippingService()).thenReturn(shippingService); return shippingService; } /** 构造指定 errorCode 的 WxErrorException */ private WxErrorException buildWxErrorException(int errorCode) { return new WxErrorException(WxError.builder().errorCode(errorCode).errorMsg("mock error").build()); } }