LineCallbackDeviceTrustTest.java 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package com.ruoyi.app.user;
  2. import com.ruoyi.app.user.service.DeviceTrustService;
  3. import com.ruoyi.common.core.redis.RedisCache;
  4. import com.ruoyi.common.utils.spring.SpringUtils;
  5. import com.ruoyi.system.domain.InfoUser;
  6. import com.ruoyi.system.domain.InfoUserDevice;
  7. import com.ruoyi.system.domain.InfoUserOauth;
  8. import com.ruoyi.system.mapper.InfoUserDeviceMapper;
  9. import com.ruoyi.system.mapper.InfoUserOauthMapper;
  10. import com.ruoyi.system.service.IInfoUserService;
  11. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  12. import org.junit.jupiter.api.AfterAll;
  13. import org.junit.jupiter.api.AfterEach;
  14. import org.junit.jupiter.api.BeforeAll;
  15. import org.junit.jupiter.api.BeforeEach;
  16. import org.junit.jupiter.api.Test;
  17. import org.mockito.ArgumentCaptor;
  18. import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
  19. import org.springframework.beans.factory.support.DefaultListableBeanFactory;
  20. import org.springframework.context.support.StaticMessageSource;
  21. import org.springframework.mock.web.MockHttpServletRequest;
  22. import org.springframework.mock.web.MockHttpServletResponse;
  23. import org.springframework.test.util.ReflectionTestUtils;
  24. import org.springframework.web.context.request.RequestContextHolder;
  25. import org.springframework.web.context.request.ServletRequestAttributes;
  26. import java.util.concurrent.TimeUnit;
  27. import static org.junit.jupiter.api.Assertions.assertEquals;
  28. import static org.junit.jupiter.api.Assertions.assertTrue;
  29. import static org.mockito.ArgumentMatchers.any;
  30. import static org.mockito.ArgumentMatchers.anyInt;
  31. import static org.mockito.ArgumentMatchers.anyString;
  32. import static org.mockito.ArgumentMatchers.eq;
  33. import static org.mockito.ArgumentMatchers.startsWith;
  34. import static org.mockito.Mockito.mock;
  35. import static org.mockito.Mockito.never;
  36. import static org.mockito.Mockito.verify;
  37. import static org.mockito.Mockito.when;
  38. /**
  39. * LINE 回调设备信任免绑分支测试(029-device-trust-phone-bind,TDD 先行)。
  40. * state 参数语义:前端把 deviceId 放进 LINE authorize 的 state 带回(见 contracts/api-contract.md §5)。
  41. */
  42. class LineCallbackDeviceTrustTest {
  43. private static ConfigurableListableBeanFactory originalBeanFactory;
  44. private final RedisCache redisCache = mock(RedisCache.class);
  45. private final IInfoUserService infoUserService = mock(IInfoUserService.class);
  46. private final InfoUserOauthMapper infoUserOauthMapper = mock(InfoUserOauthMapper.class);
  47. private final com.ruoyi.app.utils.oauth.OAuthVerifyService oauthVerifyService =
  48. mock(com.ruoyi.app.utils.oauth.OAuthVerifyService.class);
  49. private final InfoUserDeviceMapper deviceMapper = mock(InfoUserDeviceMapper.class);
  50. private LineCallbackController controller;
  51. private MockHttpServletResponse response;
  52. @BeforeAll
  53. static void saveStatics() {
  54. originalBeanFactory = (ConfigurableListableBeanFactory)
  55. ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
  56. }
  57. @AfterAll
  58. static void restoreStatics() {
  59. new SpringUtils().postProcessBeanFactory(originalBeanFactory);
  60. }
  61. @BeforeEach
  62. void setUp() throws Exception {
  63. DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
  64. StaticMessageSource messageSource = new StaticMessageSource();
  65. messageSource.setUseCodeAsDefaultMessage(true);
  66. beanFactory.registerSingleton("messageSource", messageSource);
  67. beanFactory.registerSingleton("redisCache", redisCache);
  68. new SpringUtils().postProcessBeanFactory(beanFactory);
  69. RequestContextHolder.setRequestAttributes(
  70. new ServletRequestAttributes(new MockHttpServletRequest()));
  71. controller = new LineCallbackController();
  72. ReflectionTestUtils.setField(controller, "redisCache", redisCache);
  73. ReflectionTestUtils.setField(controller, "infoUserService", infoUserService);
  74. ReflectionTestUtils.setField(controller, "infoUserOauthMapper", infoUserOauthMapper);
  75. ReflectionTestUtils.setField(controller, "oauthVerifyService", oauthVerifyService);
  76. ReflectionTestUtils.setField(controller, "deviceTrustService", new DeviceTrustService(deviceMapper));
  77. ReflectionTestUtils.setField(controller, "appRedirect", "com.test.app://oauthLogin");
  78. response = new MockHttpServletResponse();
  79. }
  80. @AfterEach
  81. void tearDown() {
  82. RequestContextHolder.resetRequestAttributes();
  83. }
  84. private static InfoUserDevice trust(String deviceId, String phone, long userId) {
  85. InfoUserDevice t = new InfoUserDevice();
  86. t.setDeviceId(deviceId);
  87. t.setPhone(phone);
  88. t.setUserId(userId);
  89. return t;
  90. }
  91. @Test
  92. void unboundWithTrustedDeviceRedirectsDeviceConfirmAndWritesBothKeys() throws Exception {
  93. when(oauthVerifyService.verify("line", "code-1")).thenReturn("line-uid-1");
  94. when(infoUserOauthMapper.selectOne(any())).thenReturn(null);
  95. when(deviceMapper.selectById("dev-1")).thenReturn(trust("dev-1", "0987654321", 100L));
  96. controller.callback("code-1", "dev-1", response);
  97. String location = response.getRedirectedUrl();
  98. assertTrue(location != null && location.startsWith("com.test.app://oauthLogin?deviceConfirm=1&tempKey="),
  99. "应回跳 deviceConfirm 分支,实际: " + location);
  100. assertTrue(location.endsWith("&maskedPhone=098****4321"), "应带脱敏手机号,实际: " + location);
  101. ArgumentCaptor<String> tempKeyCaptor = ArgumentCaptor.forClass(String.class);
  102. verify(redisCache).setCacheObject(startsWith("oauth:bind:"), eq("line@line-uid-1"), eq(5), eq(TimeUnit.MINUTES));
  103. verify(redisCache).setCacheObject(startsWith("oauth:bind:dev:"), eq("dev-1"), eq(5), eq(TimeUnit.MINUTES));
  104. }
  105. @Test
  106. void unboundWithUnknownStateStillRedirectsNeedPhone() throws Exception {
  107. when(oauthVerifyService.verify("line", "code-2")).thenReturn("line-uid-2");
  108. when(infoUserOauthMapper.selectOne(any())).thenReturn(null);
  109. when(deviceMapper.selectById("dev-x")).thenReturn(null);
  110. controller.callback("code-2", "dev-x", response);
  111. String location = response.getRedirectedUrl();
  112. assertTrue(location != null && location.startsWith("com.test.app://oauthLogin?needPhone=1&tempKey="),
  113. "应维持 needPhone 回跳,实际: " + location);
  114. verify(redisCache, never()).setCacheObject(startsWith("oauth:bind:dev:"), anyString(), anyInt(), any(TimeUnit.class));
  115. }
  116. @Test
  117. void boundUserWithDeviceStateRedirectsTokenAndRecordsTrust() throws Exception {
  118. when(oauthVerifyService.verify("line", "code-3")).thenReturn("line-uid-3");
  119. InfoUserOauth bind = new InfoUserOauth();
  120. bind.setUserId(100L);
  121. bind.setProvider("line");
  122. bind.setProviderUid("line-uid-3");
  123. when(infoUserOauthMapper.selectOne(any())).thenReturn(bind);
  124. InfoUser u = new InfoUser();
  125. u.setUserId(100L);
  126. u.setPhone("0987654321");
  127. u.setStatus("0");
  128. when(infoUserService.getOne(any())).thenReturn(u);
  129. when(deviceMapper.selectById("dev-1")).thenReturn(null);
  130. controller.callback("code-3", "dev-1", response);
  131. String location = response.getRedirectedUrl();
  132. assertTrue(location != null && location.startsWith("com.test.app://oauthLogin?token="),
  133. "已绑定应直接回跳 token,实际: " + location);
  134. verify(deviceMapper).insert(any(InfoUserDevice.class));
  135. }
  136. }