LineCallbackControllerTest.java 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. package com.ruoyi.app.user;
  2. import com.auth0.jwt.JWT;
  3. import com.ruoyi.app.utils.oauth.LineOAuthProperties;
  4. import com.ruoyi.app.user.service.DeviceTrustService;
  5. import com.ruoyi.app.utils.oauth.OAuthIdentity;
  6. import com.ruoyi.app.utils.oauth.OAuthVerifyService;
  7. import com.ruoyi.app.utils.oauth.LineOAuthStateService;
  8. import com.ruoyi.common.constant.CacheConstants;
  9. import com.ruoyi.common.core.redis.RedisCache;
  10. import com.ruoyi.common.exception.ServiceException;
  11. import com.ruoyi.common.utils.spring.SpringUtils;
  12. import com.ruoyi.system.domain.InfoUser;
  13. import com.ruoyi.system.domain.InfoUserOauth;
  14. import com.ruoyi.system.mapper.InfoUserDeviceMapper;
  15. import com.ruoyi.system.mapper.InfoUserOauthMapper;
  16. import com.ruoyi.system.service.IInfoUserService;
  17. import com.ruoyi.system.service.MerchantStoreAccessService;
  18. import org.junit.jupiter.api.AfterAll;
  19. import org.junit.jupiter.api.AfterEach;
  20. import org.junit.jupiter.api.BeforeAll;
  21. import org.junit.jupiter.api.Test;
  22. import org.mockito.ArgumentCaptor;
  23. import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
  24. import org.springframework.beans.factory.support.DefaultListableBeanFactory;
  25. import org.springframework.mock.web.MockHttpServletRequest;
  26. import org.springframework.mock.web.MockHttpServletResponse;
  27. import org.springframework.test.util.ReflectionTestUtils;
  28. import org.springframework.web.context.request.RequestContextHolder;
  29. import org.springframework.web.context.request.ServletRequestAttributes;
  30. import java.net.URLDecoder;
  31. import java.nio.charset.StandardCharsets;
  32. import java.util.concurrent.TimeUnit;
  33. import static org.junit.jupiter.api.Assertions.assertEquals;
  34. import static org.junit.jupiter.api.Assertions.assertTrue;
  35. import static org.mockito.ArgumentMatchers.any;
  36. import static org.mockito.Mockito.mock;
  37. import static org.mockito.Mockito.never;
  38. import static org.mockito.Mockito.doThrow;
  39. import static org.mockito.Mockito.verify;
  40. import static org.mockito.Mockito.when;
  41. class LineCallbackControllerTest {
  42. private static ConfigurableListableBeanFactory originalBeanFactory;
  43. @BeforeAll
  44. static void installJwtRedisBean() {
  45. originalBeanFactory = (ConfigurableListableBeanFactory)
  46. ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
  47. }
  48. @AfterAll
  49. static void restoreBeanFactory() {
  50. new SpringUtils().postProcessBeanFactory(originalBeanFactory);
  51. }
  52. @AfterEach
  53. void clearRequestContext() {
  54. RequestContextHolder.resetRequestAttributes();
  55. }
  56. @Test
  57. void sharedCallbackUsesProviderSpecificChannelAndKeepsProviderInTempBinding() throws Exception {
  58. OAuthVerifyService verifyService = mock(OAuthVerifyService.class);
  59. IInfoUserService infoUserService = mock(IInfoUserService.class);
  60. InfoUserOauthMapper oauthMapper = mock(InfoUserOauthMapper.class);
  61. RedisCache redisCache = mock(RedisCache.class);
  62. LineOAuthProperties properties = new LineOAuthProperties();
  63. LineOAuthProperties.Channel rider = new LineOAuthProperties.Channel();
  64. rider.setClientId("2011397520");
  65. rider.setClientSecret("secret");
  66. rider.setRedirectUri("https://api.test/auth/line/callback?provider=line_rider");
  67. rider.setAppRedirect("com.twanmsdqs.app://pages/UserCenter/oauthLogin");
  68. properties.setRider(rider);
  69. when(verifyService.verifyIdentity("line_rider", "code")).thenReturn(new OAuthIdentity("line-uid", null));
  70. when(oauthMapper.selectOne(any())).thenReturn(null);
  71. LineCallbackController controller = new LineCallbackController();
  72. ReflectionTestUtils.setField(controller, "lineOAuthStateService", mock(LineOAuthStateService.class));
  73. ReflectionTestUtils.setField(controller, "oauthVerifyService", verifyService);
  74. ReflectionTestUtils.setField(controller, "lineOAuthProperties", properties);
  75. ReflectionTestUtils.setField(controller, "infoUserService", infoUserService);
  76. ReflectionTestUtils.setField(controller, "infoUserOauthMapper", oauthMapper);
  77. ReflectionTestUtils.setField(controller, "redisCache", redisCache);
  78. MockHttpServletResponse response = new MockHttpServletResponse();
  79. controller.callback("line_rider", "code", "state", response);
  80. assertTrue(response.getRedirectedUrl().startsWith(
  81. "com.twanmsdqs.app://pages/UserCenter/oauthLogin?needPhone=1&tempKey="));
  82. verify(redisCache).setCacheObject(any(String.class),
  83. org.mockito.ArgumentMatchers.eq("line_rider@line-uid"),
  84. org.mockito.ArgumentMatchers.eq(5), org.mockito.ArgumentMatchers.eq(TimeUnit.MINUTES));
  85. }
  86. @Test
  87. void boundRiderReceivesRiderSessionToken() throws Exception {
  88. CallbackFixture fixture = boundFixture("2");
  89. fixture.controller.callback("line_rider", "code", "state", fixture.response);
  90. String redirected = fixture.response.getRedirectedUrl();
  91. String token = URLDecoder.decode(redirected.substring(redirected.indexOf("?token=") + 7).split("&", 2)[0],
  92. StandardCharsets.UTF_8);
  93. assertTrue(JWT.decode(token).getId().startsWith(CacheConstants.QS_TOKEN_KEY + "42:"));
  94. assertEquals("line_rider", JWT.decode(token).getClaim("provider").asString());
  95. verify(fixture.redisCache).deleteKeys(CacheConstants.QS_TOKEN_KEY + "42:*");
  96. }
  97. @Test
  98. void boundCallbackRefreshesDisplayNameFromProfile() throws Exception {
  99. CallbackFixture fixture = boundFixture("2");
  100. OAuthVerifyService verifier =
  101. (OAuthVerifyService) ReflectionTestUtils.getField(fixture.controller, "oauthVerifyService");
  102. when(verifier.verifyIdentity("line_rider", "code"))
  103. .thenReturn(new OAuthIdentity("line-uid", "LINE-新名字"));
  104. fixture.controller.callback("line_rider", "code", "state", fixture.response);
  105. InfoUserOauthMapper oauthMapper =
  106. (InfoUserOauthMapper) ReflectionTestUtils.getField(fixture.controller, "infoUserOauthMapper");
  107. ArgumentCaptor<InfoUserOauth> refreshed = ArgumentCaptor.forClass(InfoUserOauth.class);
  108. verify(oauthMapper).updateById(refreshed.capture());
  109. assertEquals("LINE-新名字", refreshed.getValue().getProviderDisplayName());
  110. String redirected = fixture.response.getRedirectedUrl();
  111. String token = URLDecoder.decode(
  112. redirected.substring(redirected.indexOf("?token=") + 7).split("&", 2)[0],
  113. StandardCharsets.UTF_8);
  114. assertEquals("line-uid", JWT.decode(token).getClaim("providerUid").asString());
  115. }
  116. @Test
  117. void riderChannelRejectsBindingToMerchantAccount() throws Exception {
  118. CallbackFixture fixture = boundFixture("1");
  119. fixture.controller.callback("line_rider", "code", "state", fixture.response);
  120. assertEquals("com.twanmsdqs.app://pages/UserCenter/oauthLogin?error=user_stopped&state=state",
  121. fixture.response.getRedirectedUrl());
  122. verify(fixture.redisCache, never()).deleteKeys(any(String.class));
  123. }
  124. @Test
  125. void merchantCallbackRejectsSubaccountWithUnavailableOwner() throws Exception {
  126. OAuthVerifyService verifyService = mock(OAuthVerifyService.class);
  127. IInfoUserService infoUserService = mock(IInfoUserService.class);
  128. InfoUserOauthMapper oauthMapper = mock(InfoUserOauthMapper.class);
  129. RedisCache redisCache = mock(RedisCache.class);
  130. MerchantStoreAccessService accessService = mock(MerchantStoreAccessService.class);
  131. DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
  132. beanFactory.registerSingleton("redisCache", redisCache);
  133. new SpringUtils().postProcessBeanFactory(beanFactory);
  134. MockHttpServletRequest request = new MockHttpServletRequest();
  135. request.setRemoteAddr("127.0.0.1");
  136. request.addHeader("User-Agent", "JUnit");
  137. RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
  138. LineOAuthProperties properties = new LineOAuthProperties();
  139. LineOAuthProperties.Channel merchant = new LineOAuthProperties.Channel();
  140. merchant.setClientId("merchant-client");
  141. merchant.setClientSecret("secret");
  142. merchant.setRedirectUri("https://api.test/auth/line/callback?provider=line_merchant");
  143. merchant.setAppRedirect("com.twanmsdsj.app://pages/UserCenter/oauthLogin");
  144. properties.setMerchant(merchant);
  145. when(verifyService.verifyIdentity("line_merchant", "code")).thenReturn(new OAuthIdentity("line-uid", null));
  146. InfoUserOauth binding = new InfoUserOauth();
  147. binding.setUserId(55L);
  148. when(oauthMapper.selectOne(any())).thenReturn(binding);
  149. InfoUser subaccount = new InfoUser();
  150. subaccount.setUserId(55L);
  151. subaccount.setUserName("subaccount");
  152. subaccount.setUserType("5");
  153. subaccount.setStatus("0");
  154. subaccount.setDelFlag("0");
  155. subaccount.setSubaccountStatus("0");
  156. when(infoUserService.getOne(any())).thenReturn(subaccount);
  157. doThrow(new ServiceException("owner unavailable"))
  158. .when(accessService).resolve(55L);
  159. LineCallbackController controller = new LineCallbackController();
  160. ReflectionTestUtils.setField(controller, "lineOAuthStateService", mock(LineOAuthStateService.class));
  161. ReflectionTestUtils.setField(controller, "oauthVerifyService", verifyService);
  162. ReflectionTestUtils.setField(controller, "lineOAuthProperties", properties);
  163. ReflectionTestUtils.setField(controller, "infoUserService", infoUserService);
  164. ReflectionTestUtils.setField(controller, "infoUserOauthMapper", oauthMapper);
  165. ReflectionTestUtils.setField(controller, "redisCache", redisCache);
  166. ReflectionTestUtils.setField(controller, "merchantStoreAccessService", accessService);
  167. MockHttpServletResponse response = new MockHttpServletResponse();
  168. controller.callback("line_merchant", "code", "state", response);
  169. assertEquals("com.twanmsdsj.app://pages/UserCenter/oauthLogin?error=user_stopped&state=state",
  170. response.getRedirectedUrl());
  171. }
  172. @Test
  173. void oldUserCallbackWithoutProviderKeepsOriginalResponse() throws Exception {
  174. LineOAuthProperties properties = new LineOAuthProperties();
  175. LineOAuthProperties.Channel user = new LineOAuthProperties.Channel();
  176. user.setClientId("user-client");
  177. user.setClientSecret("secret");
  178. user.setRedirectUri("https://api.test/auth/line/callback?provider=line_user");
  179. user.setAppRedirect("com.twanmsdyh.app://pages/UserCenter/oauthLogin");
  180. properties.setUser(user);
  181. OAuthVerifyService verifier = mock(OAuthVerifyService.class);
  182. when(verifier.verifyIdentity("line", "old-code")).thenReturn(new OAuthIdentity("old-user-id", null));
  183. RedisCache redis = mock(RedisCache.class);
  184. LineCallbackController controller = new LineCallbackController();
  185. ReflectionTestUtils.setField(controller, "lineOAuthProperties", properties);
  186. ReflectionTestUtils.setField(controller, "oauthVerifyService", verifier);
  187. ReflectionTestUtils.setField(controller, "infoUserOauthMapper", mock(InfoUserOauthMapper.class));
  188. ReflectionTestUtils.setField(controller, "redisCache", redis);
  189. ReflectionTestUtils.setField(controller, "deviceTrustService",
  190. DeviceTrustTestSupport.service(mock(InfoUserDeviceMapper.class), mock(InfoUserOauthMapper.class)));
  191. MockHttpServletResponse response = new MockHttpServletResponse();
  192. controller.callback(null, "old-code", "line_login", response);
  193. assertTrue(response.getRedirectedUrl().startsWith("com.twanmsdyh.app://pages/UserCenter/oauthLogin?needPhone=1&tempKey="));
  194. assertTrue(!response.getRedirectedUrl().contains("&state="));
  195. verify(verifier).verifyIdentity("line", "old-code");
  196. verify(redis).setCacheObject(any(String.class), org.mockito.ArgumentMatchers.eq("line@old-user-id"),
  197. org.mockito.ArgumentMatchers.eq(5), org.mockito.ArgumentMatchers.eq(TimeUnit.MINUTES));
  198. }
  199. @Test
  200. void businessCallbackRejectsInvalidStateBeforeIssuingSession() throws Exception {
  201. CallbackFixture fixture = boundFixture("2");
  202. LineOAuthStateService stateService = mock(LineOAuthStateService.class);
  203. doThrow(new ServiceException("invalid state")).when(stateService).consume("line_rider", "state");
  204. ReflectionTestUtils.setField(fixture.controller, "lineOAuthStateService", stateService);
  205. OAuthVerifyService verifier = (OAuthVerifyService) ReflectionTestUtils.getField(fixture.controller, "oauthVerifyService");
  206. fixture.controller.callback("line_rider", "code", "state", fixture.response);
  207. assertEquals("com.twanmsdqs.app://pages/UserCenter/oauthLogin?error=fail&state=state", fixture.response.getRedirectedUrl());
  208. verify(verifier, never()).verifyIdentity(any(), any());
  209. verify(fixture.redisCache, never()).deleteKeys(any(String.class));
  210. }
  211. private CallbackFixture boundFixture(String userType) {
  212. OAuthVerifyService verifyService = mock(OAuthVerifyService.class);
  213. IInfoUserService infoUserService = mock(IInfoUserService.class);
  214. InfoUserOauthMapper oauthMapper = mock(InfoUserOauthMapper.class);
  215. RedisCache redisCache = mock(RedisCache.class);
  216. DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
  217. beanFactory.registerSingleton("redisCache", redisCache);
  218. new SpringUtils().postProcessBeanFactory(beanFactory);
  219. MockHttpServletRequest request = new MockHttpServletRequest();
  220. request.setRemoteAddr("127.0.0.1");
  221. request.addHeader("User-Agent", "JUnit");
  222. RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
  223. LineOAuthProperties properties = new LineOAuthProperties();
  224. LineOAuthProperties.Channel rider = new LineOAuthProperties.Channel();
  225. rider.setClientId("2011397520");
  226. rider.setClientSecret("secret");
  227. rider.setRedirectUri("https://api.test/auth/line/callback?provider=line_rider");
  228. rider.setAppRedirect("com.twanmsdqs.app://pages/UserCenter/oauthLogin");
  229. properties.setRider(rider);
  230. InfoUserOauth binding = new InfoUserOauth();
  231. binding.setUserId(42L);
  232. InfoUser user = new InfoUser();
  233. user.setUserId(42L);
  234. user.setUserName("rider");
  235. user.setUserType(userType);
  236. user.setStatus("0");
  237. user.setDelFlag("0");
  238. when(verifyService.verifyIdentity("line_rider", "code")).thenReturn(new OAuthIdentity("line-uid", null));
  239. when(oauthMapper.selectOne(any())).thenReturn(binding);
  240. when(infoUserService.getOne(any())).thenReturn(user);
  241. LineCallbackController controller = new LineCallbackController();
  242. ReflectionTestUtils.setField(controller, "lineOAuthStateService", mock(LineOAuthStateService.class));
  243. ReflectionTestUtils.setField(controller, "oauthVerifyService", verifyService);
  244. ReflectionTestUtils.setField(controller, "lineOAuthProperties", properties);
  245. ReflectionTestUtils.setField(controller, "infoUserService", infoUserService);
  246. ReflectionTestUtils.setField(controller, "infoUserOauthMapper", oauthMapper);
  247. ReflectionTestUtils.setField(controller, "redisCache", redisCache);
  248. return new CallbackFixture(controller, redisCache, new MockHttpServletResponse());
  249. }
  250. private record CallbackFixture(LineCallbackController controller, RedisCache redisCache,
  251. MockHttpServletResponse response) {
  252. }
  253. }