CancellationLoginConfirmationTest.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. package com.ruoyi.app.user;
  2. import com.baomidou.mybatisplus.core.conditions.Wrapper;
  3. import com.ruoyi.app.user.dto.CancellationLoginConfirmDto;
  4. import com.ruoyi.app.user.dto.OAuthBindDto;
  5. import com.ruoyi.app.user.dto.OAuthDeviceConfirmDto;
  6. import com.ruoyi.app.user.dto.OAuthLoginDto;
  7. import com.ruoyi.app.user.service.DeviceTrustService;
  8. import com.ruoyi.app.utils.oauth.OAuthIdentity;
  9. import com.ruoyi.app.utils.oauth.OAuthVerifyService;
  10. import com.ruoyi.common.core.domain.AjaxResult;
  11. import com.ruoyi.common.core.domain.model.LoginUserDto;
  12. import com.ruoyi.common.core.redis.RedisCache;
  13. import com.ruoyi.common.exception.ServiceException;
  14. import com.ruoyi.common.utils.MessageUtils;
  15. import com.ruoyi.system.domain.InfoUser;
  16. import com.ruoyi.system.domain.InfoUserDevice;
  17. import com.ruoyi.system.domain.InfoUserOauth;
  18. import com.ruoyi.system.domain.vo.UserDTO;
  19. import com.ruoyi.system.mapper.InfoUserMapper;
  20. import com.ruoyi.system.mapper.InfoUserOauthMapper;
  21. import com.ruoyi.system.service.IInfoUserService;
  22. import com.ruoyi.system.service.IVipUserService;
  23. import com.ruoyi.system.utils.JwtUtil;
  24. import org.junit.jupiter.api.AfterEach;
  25. import org.junit.jupiter.api.BeforeEach;
  26. import org.junit.jupiter.api.Test;
  27. import org.junit.jupiter.params.ParameterizedTest;
  28. import org.junit.jupiter.params.provider.EnumSource;
  29. import org.mockito.MockedStatic;
  30. import org.springframework.mock.web.MockHttpServletRequest;
  31. import org.springframework.test.util.ReflectionTestUtils;
  32. import org.springframework.transaction.annotation.Transactional;
  33. import org.springframework.web.context.request.RequestContextHolder;
  34. import org.springframework.web.context.request.ServletRequestAttributes;
  35. import java.util.Date;
  36. import java.util.HashMap;
  37. import java.util.List;
  38. import java.util.Map;
  39. import java.util.concurrent.TimeUnit;
  40. import static org.junit.jupiter.api.Assertions.*;
  41. import static org.mockito.ArgumentMatchers.*;
  42. import static org.mockito.Mockito.*;
  43. class CancellationLoginConfirmationTest {
  44. private final InfoUserController controller = new InfoUserController();
  45. private final InfoUserMapper accounts = mock(InfoUserMapper.class);
  46. private final IInfoUserService users = mock(IInfoUserService.class);
  47. private final RedisCache redis = mock(RedisCache.class);
  48. private final DeviceTrustService devices = mock(DeviceTrustService.class);
  49. private final InfoUserOauthMapper bindings = mock(InfoUserOauthMapper.class);
  50. private final OAuthVerifyService verifier = mock(OAuthVerifyService.class);
  51. private final Map<String, String> cache = new HashMap<>();
  52. private final InfoUser user = new InfoUser();
  53. private final InfoUserOauth binding = new InfoUserOauth();
  54. private MockedStatic<MessageUtils> messages;
  55. private MockedStatic<JwtUtil> jwt;
  56. @BeforeEach void setUp() {
  57. messages = mockStatic(MessageUtils.class, c -> c.getArgument(0));
  58. jwt = mockStatic(JwtUtil.class);
  59. jwt.when(() -> JwtUtil.setToken(anyString(), any(LoginUserDto.class))).thenReturn("new-session");
  60. var request = new MockHttpServletRequest(); request.setRemoteAddr("127.0.0.1");
  61. RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
  62. user.setUserId(7L); user.setUserType("0"); user.setStatus("0"); user.setDelFlag("1");
  63. user.setPhone("0912345678"); user.setCancellationDeadlineAt(new Date(System.currentTimeMillis() + 3600000));
  64. binding.setId(77L); binding.setUserId(7L); binding.setProvider("google"); binding.setProviderUid("verified-id");
  65. when(accounts.selectCancellationUserForUpdate(7L)).thenReturn(user);
  66. when(accounts.restoreCancelledUser(eq(7L), any())).thenReturn(1);
  67. when(users.getOne(any(Wrapper.class))).thenReturn(user);
  68. when(users.list(any(Wrapper.class))).thenReturn(List.of(user));
  69. when(users.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
  70. when(bindings.selectOne(any(Wrapper.class))).thenReturn(binding);
  71. when(bindings.selectByIdForUpdate(77L)).thenReturn(binding);
  72. when(bindings.insert(any(InfoUserOauth.class))).thenReturn(1);
  73. when(verifier.verifyIdentity("google", "one-use-code")).thenReturn(new OAuthIdentity("verified-id", "Name"));
  74. var trust = new InfoUserDevice(); trust.setDeviceId("device"); trust.setPhone(user.getPhone()); trust.setUserId(7L);
  75. when(devices.getTrust("device")).thenReturn(trust);
  76. cache.put(user.getPhone(), "123456");
  77. cache.put("oauth:bind:original", "google@verified-id@Name");
  78. cache.put("oauth:bind:dev:original", "device"); cache.put("oauth:bind:user:original", "7");
  79. when(redis.getCacheObject(anyString())).thenAnswer(call -> cache.get(call.getArgument(0)));
  80. when(redis.deleteObject(anyString())).thenAnswer(call -> cache.remove(call.getArgument(0)) != null);
  81. doAnswer(call -> { cache.put(call.getArgument(0), call.getArgument(1)); return null; })
  82. .when(redis).setCacheObject(anyString(), anyString(), anyInt(), eq(TimeUnit.SECONDS));
  83. ReflectionTestUtils.setField(controller, "userCancellationService", new UserCancellationService(accounts, redis));
  84. ReflectionTestUtils.setField(controller, "infoUserService", users);
  85. ReflectionTestUtils.setField(controller, "redisCache", redis);
  86. ReflectionTestUtils.setField(controller, "deviceTrustService", devices);
  87. ReflectionTestUtils.setField(controller, "infoUserOauthMapper", bindings);
  88. ReflectionTestUtils.setField(controller, "oauthVerifyService", verifier);
  89. ReflectionTestUtils.setField(controller, "vipUserService", mock(IVipUserService.class));
  90. }
  91. @AfterEach void tearDown() { jwt.close(); messages.close(); RequestContextHolder.resetRequestAttributes(); }
  92. private UserCancellationService.LoginConfirmationView prompt(CancellationLoginContext.Flow flow) {
  93. AjaxResult response;
  94. switch (flow) {
  95. case PHONE -> {
  96. UserDTO dto = new UserDTO(); dto.setPhone(user.getPhone()); dto.setCode("123456");
  97. dto.setDeviceId("device"); dto.setCid("new-cid");
  98. response = controller.lodeing(dto);
  99. }
  100. case OAUTH -> {
  101. OAuthLoginDto dto = new OAuthLoginDto(); dto.setProvider("google"); dto.setCredential("one-use-code");
  102. dto.setDeviceId("device"); dto.setCid("new-cid"); response = controller.oauthLogin(dto);
  103. }
  104. case BIND -> {
  105. OAuthBindDto dto = new OAuthBindDto(); dto.setTempKey("original"); dto.setPhone(user.getPhone());
  106. dto.setCode("123456"); dto.setDeviceId("device"); dto.setCid("new-cid");
  107. response = controller.oauthBindPhone(dto);
  108. }
  109. default -> {
  110. OAuthDeviceConfirmDto dto = new OAuthDeviceConfirmDto(); dto.setTempKey("original"); dto.setDeviceId("device");
  111. response = controller.oauthDeviceConfirm(dto);
  112. }
  113. }
  114. assertEquals(200, response.get("code")); assertNull(response.get("token"));
  115. var view = (UserCancellationService.LoginConfirmationView) response.get("data");
  116. assertEquals("cancellationConfirm", view.status()); assertEquals(1, view.remainingDays());
  117. assertEquals("1", user.getDelFlag()); assertNull(user.getCid());
  118. verify(accounts, never()).restoreCancelledUser(anyLong(), any());
  119. verify(users, never()).saveOrUpdate(any(InfoUser.class));
  120. verify(bindings, never()).insert(any(InfoUserOauth.class));
  121. verify(bindings, never()).updateById(any(InfoUserOauth.class));
  122. verify(devices, never()).recordLogin(any(), any(), any());
  123. verify(devices, never()).recordOauthLogin(any(), any(), any(), any());
  124. jwt.verifyNoInteractions();
  125. return view;
  126. }
  127. @ParameterizedTest @EnumSource(CancellationLoginContext.Flow.class)
  128. void everyFlowWaitsForConfirmationAndResumesWithoutReusingOriginalCredential(CancellationLoginContext.Flow flow) {
  129. var view = prompt(flow);
  130. cache.remove(user.getPhone()); cache.remove("oauth:bind:original");
  131. AjaxResult response = controller.confirmCancellationLogin(new CancellationLoginConfirmDto(view.confirmationKey()));
  132. assertEquals("new-session", response.get("token")); assertSame(user, response.get("data"));
  133. assertEquals("0", user.getDelFlag()); assertNull(user.getCancellationDeadlineAt());
  134. if (flow != CancellationLoginContext.Flow.DEVICE) assertEquals("new-cid", user.getCid());
  135. verify(accounts, times(1)).restoreCancelledUser(eq(7L), any());
  136. if (flow == CancellationLoginContext.Flow.OAUTH) verify(verifier, times(1)).verifyIdentity(any(), any());
  137. assertThrows(ServiceException.class, () -> controller.confirmCancellationLogin(new CancellationLoginConfirmDto(view.confirmationKey())));
  138. jwt.verify(() -> JwtUtil.setToken(anyString(), any(LoginUserDto.class)), times(1));
  139. }
  140. @Test void cancellingPromptLeavesLifecycleAndSessionsUnchanged() { prompt(CancellationLoginContext.Flow.PHONE); }
  141. @Test void failedAuthenticationNeverReachesCancellationPrompt() {
  142. UserDTO dto = new UserDTO(); dto.setPhone(user.getPhone()); dto.setCode("wrong");
  143. assertEquals(500, controller.lodeing(dto).get("code"));
  144. verifyNoInteractions(accounts); jwt.verifyNoInteractions();
  145. }
  146. @Test void removedOauthBindingCannotBeRecreatedByConfirmation() {
  147. var view = prompt(CancellationLoginContext.Flow.OAUTH);
  148. when(bindings.selectOne(any(Wrapper.class))).thenReturn(null);
  149. assertThrows(ServiceException.class, () -> controller.confirmCancellationLogin(new CancellationLoginConfirmDto(view.confirmationKey())));
  150. verify(bindings, never()).insert(any(InfoUserOauth.class)); jwt.verifyNoInteractions();
  151. }
  152. @Test void changedTrustedDeviceCannotCompleteConfirmation() {
  153. var view = prompt(CancellationLoginContext.Flow.DEVICE);
  154. doThrow(new ServiceException("changed")).when(devices).requireConfirmation("device", 7L);
  155. assertThrows(ServiceException.class, () -> controller.confirmCancellationLogin(new CancellationLoginConfirmDto(view.confirmationKey())));
  156. jwt.verifyNoInteractions();
  157. }
  158. @Test void tokenFailureThrowsAndConfirmationEndpointHasRollbackTransaction() throws Exception {
  159. var view = prompt(CancellationLoginContext.Flow.PHONE);
  160. jwt.when(() -> JwtUtil.setToken(anyString(), any(LoginUserDto.class))).thenReturn(null);
  161. assertThrows(ServiceException.class, () -> controller.confirmCancellationLogin(new CancellationLoginConfirmDto(view.confirmationKey())));
  162. var annotation = InfoUserController.class.getMethod("confirmCancellationLogin", CancellationLoginConfirmDto.class)
  163. .getAnnotation(Transactional.class);
  164. assertNotNull(annotation); assertTrue(List.of(annotation.rollbackFor()).contains(Exception.class));
  165. }
  166. @ParameterizedTest
  167. @org.junit.jupiter.params.provider.ValueSource(booleans = {false, true})
  168. void databaseRecoveryCommitsOnlyWhenLoginSessionSucceeds(boolean failSession) {
  169. var source = new org.h2.jdbcx.JdbcDataSource();
  170. source.setURL("jdbc:h2:mem:" + java.util.UUID.randomUUID() + ";DB_CLOSE_DELAY=-1");
  171. var jdbc = new org.springframework.jdbc.core.JdbcTemplate(source);
  172. jdbc.execute("CREATE TABLE cancellation_account (id BIGINT PRIMARY KEY, deleted VARCHAR(1), deadline TIMESTAMP)");
  173. jdbc.update("INSERT INTO cancellation_account VALUES (7, '1', ?)", user.getCancellationDeadlineAt());
  174. when(accounts.selectCancellationUserForUpdate(7L)).thenAnswer(call -> jdbc.queryForObject(
  175. "SELECT deleted, deadline FROM cancellation_account WHERE id=7 FOR UPDATE", (row, index) -> {
  176. InfoUser account = new InfoUser(); account.setUserId(7L); account.setPhone(user.getPhone());
  177. account.setUserType("0"); account.setStatus("0"); account.setDelFlag(row.getString(1));
  178. account.setCancellationDeadlineAt(row.getTimestamp(2)); return account;
  179. }));
  180. when(accounts.restoreCancelledUser(eq(7L), any())).thenAnswer(call -> jdbc.update(
  181. "UPDATE cancellation_account SET deleted='0', deadline=NULL WHERE id=7 AND deleted='1' AND deadline>?",
  182. call.getArgument(1, Date.class)));
  183. var view = prompt(CancellationLoginContext.Flow.PHONE);
  184. when(users.getOne(any(Wrapper.class))).thenAnswer(call -> accounts.selectCancellationUserForUpdate(7L));
  185. var transactionManager = new org.springframework.jdbc.datasource.DataSourceTransactionManager(source);
  186. var interceptor = new org.springframework.transaction.interceptor.TransactionInterceptor(transactionManager,
  187. new org.springframework.transaction.annotation.AnnotationTransactionAttributeSource());
  188. var factory = new org.springframework.aop.framework.ProxyFactory(controller);
  189. factory.setProxyTargetClass(true); factory.addAdvice(interceptor);
  190. var transactionalController = (InfoUserController) factory.getProxy();
  191. if (failSession) {
  192. jwt.when(() -> JwtUtil.setToken(anyString(), any(LoginUserDto.class))).thenReturn(null);
  193. assertThrows(ServiceException.class, () -> transactionalController.confirmCancellationLogin(
  194. new CancellationLoginConfirmDto(view.confirmationKey())));
  195. assertEquals("1", jdbc.queryForObject("SELECT deleted FROM cancellation_account WHERE id=7", String.class));
  196. assertNotNull(jdbc.queryForObject("SELECT deadline FROM cancellation_account WHERE id=7", Date.class));
  197. } else {
  198. assertEquals("new-session", transactionalController.confirmCancellationLogin(
  199. new CancellationLoginConfirmDto(view.confirmationKey())).get("token"));
  200. assertEquals("0", jdbc.queryForObject("SELECT deleted FROM cancellation_account WHERE id=7", String.class));
  201. assertNull(jdbc.queryForObject("SELECT deadline FROM cancellation_account WHERE id=7", Date.class));
  202. }
  203. }
  204. }