InfoUserControllerTest.java 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. package com.ruoyi.app.user;
  2. import com.auth0.jwt.JWT;
  3. import com.baomidou.mybatisplus.core.MybatisConfiguration;
  4. import com.baomidou.mybatisplus.core.conditions.Wrapper;
  5. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  6. import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  7. import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
  8. import com.ruoyi.common.constant.HttpStatus;
  9. import com.ruoyi.common.core.domain.AjaxResult;
  10. import com.ruoyi.common.core.redis.RedisCache;
  11. import com.ruoyi.common.exception.ServiceException;
  12. import com.ruoyi.app.user.dto.OAuthBindDto;
  13. import com.ruoyi.app.user.dto.OAuthLoginDto;
  14. import com.ruoyi.app.utils.oauth.OAuthVerifyService;
  15. import com.ruoyi.app.utils.oauth.LineOAuthStateService;
  16. import com.ruoyi.system.domain.InfoUser;
  17. import com.ruoyi.system.domain.InfoUserOauth;
  18. import com.ruoyi.system.domain.PosOrder;
  19. import com.ruoyi.system.domain.vo.UserDTO;
  20. import com.ruoyi.system.mapper.InfoUserOauthMapper;
  21. import com.ruoyi.system.service.IInfoUserService;
  22. import com.ruoyi.system.service.IPosOrderService;
  23. import com.ruoyi.system.service.IUserWalletService;
  24. import com.ruoyi.system.service.MerchantStoreAccessService;
  25. import com.ruoyi.system.utils.AuthContext;
  26. import com.ruoyi.system.utils.JwtUtil;
  27. import com.ruoyi.common.utils.spring.SpringUtils;
  28. import org.apache.ibatis.builder.MapperBuilderAssistant;
  29. import org.junit.jupiter.api.AfterAll;
  30. import org.junit.jupiter.params.ParameterizedTest;
  31. import org.junit.jupiter.params.provider.CsvSource;
  32. import org.junit.jupiter.api.AfterEach;
  33. import org.junit.jupiter.api.BeforeAll;
  34. import org.junit.jupiter.api.BeforeEach;
  35. import org.junit.jupiter.api.Test;
  36. import org.mockito.ArgumentCaptor;
  37. import org.mockito.MockedStatic;
  38. import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
  39. import org.springframework.beans.factory.support.DefaultListableBeanFactory;
  40. import org.springframework.context.support.StaticMessageSource;
  41. import org.springframework.mock.web.MockHttpServletRequest;
  42. import org.springframework.test.util.ReflectionTestUtils;
  43. import org.springframework.web.context.request.RequestContextHolder;
  44. import org.springframework.web.context.request.ServletRequestAttributes;
  45. import java.util.ArrayList;
  46. import java.util.Collection;
  47. import java.util.Collections;
  48. import java.util.Locale;
  49. import static org.junit.jupiter.api.Assertions.assertEquals;
  50. import static org.junit.jupiter.api.Assertions.assertNull;
  51. import static org.junit.jupiter.api.Assertions.assertThrows;
  52. import static org.junit.jupiter.api.Assertions.assertTrue;
  53. import static org.mockito.ArgumentMatchers.any;
  54. import static org.mockito.Mockito.mock;
  55. import static org.mockito.Mockito.mockStatic;
  56. import static org.mockito.Mockito.never;
  57. import static org.mockito.Mockito.doThrow;
  58. import static org.mockito.Mockito.verify;
  59. import static org.mockito.Mockito.when;
  60. class InfoUserControllerTest {
  61. private InfoUserController controller;
  62. private IPosOrderService posOrderService;
  63. private IInfoUserService infoUserService;
  64. private IUserWalletService userWalletService;
  65. private MerchantStoreAccessService merchantStoreAccessService;
  66. private MerchantTokenSessionService merchantTokenSessionService;
  67. private BusinessPhoneService businessPhoneService;
  68. private OAuthVerifyService oauthVerifyService;
  69. private InfoUserOauthMapper infoUserOauthMapper;
  70. private RedisCache redisCache;
  71. private static ConfigurableListableBeanFactory originalBeanFactory;
  72. @BeforeAll
  73. static void initializeTableMetadata() {
  74. TableInfoHelper.initTableInfo(
  75. new MapperBuilderAssistant(new MybatisConfiguration(), ""), PosOrder.class);
  76. TableInfoHelper.initTableInfo(
  77. new MapperBuilderAssistant(new MybatisConfiguration(), ""), InfoUser.class);
  78. TableInfoHelper.initTableInfo(
  79. new MapperBuilderAssistant(new MybatisConfiguration(), ""), InfoUserOauth.class);
  80. originalBeanFactory = (ConfigurableListableBeanFactory)
  81. ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
  82. DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
  83. StaticMessageSource messageSource = new StaticMessageSource();
  84. messageSource.addMessage("no.user.audit.reject.reason.required", Locale.getDefault(),
  85. "审核不通过时必须填写审核不通过原因");
  86. messageSource.addMessage("no.action.success", Locale.getDefault(), "操作成功");
  87. messageSource.addMessage("merchant.subaccount.platform.managed", Locale.getDefault(),
  88. "分管账号只能通过专用入口管理");
  89. messageSource.addMessage("no.oauth.phone.blank", Locale.getDefault(), "手机号不能为空");
  90. messageSource.addMessage("no.user.login.success", Locale.getDefault(), "登录成功");
  91. messageSource.addMessage("no.user.stop", Locale.getDefault(), "账号已停用");
  92. messageSource.addMessage("no.user.not.exist", Locale.getDefault(), "账号不存在");
  93. messageSource.addMessage("no.system.error", Locale.getDefault(), "系统错误");
  94. messageSource.addMessage("no.user.jcaptcha.error", Locale.getDefault(), "验证码错误");
  95. messageSource.addMessage("no.oauth.tempkey.expired", Locale.getDefault(), "登录凭证已过期");
  96. messageSource.addMessage("no.oauth.phone.duplicate", Locale.getDefault(), "该手机号关联多个账号,请联系平台");
  97. messageSource.addMessage("no.user.phone.duplicate", Locale.getDefault(), "该手机号关联多个账号,请联系平台");
  98. beanFactory.registerSingleton("messageSource", messageSource);
  99. beanFactory.registerSingleton("redisCache", mock(RedisCache.class));
  100. new SpringUtils().postProcessBeanFactory(beanFactory);
  101. }
  102. @AfterAll
  103. static void restoreBeanFactory() {
  104. new SpringUtils().postProcessBeanFactory(originalBeanFactory);
  105. }
  106. @BeforeEach
  107. void setUp() {
  108. MockHttpServletRequest request = new MockHttpServletRequest();
  109. request.setRemoteAddr("127.0.0.1");
  110. request.addHeader("User-Agent", "JUnit");
  111. RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
  112. controller = new TestInfoUserController();
  113. posOrderService = mock(IPosOrderService.class);
  114. infoUserService = mock(IInfoUserService.class);
  115. userWalletService = mock(IUserWalletService.class);
  116. merchantStoreAccessService = mock(MerchantStoreAccessService.class);
  117. merchantTokenSessionService = mock(MerchantTokenSessionService.class);
  118. businessPhoneService = mock(BusinessPhoneService.class);
  119. oauthVerifyService = mock(OAuthVerifyService.class);
  120. infoUserOauthMapper = mock(InfoUserOauthMapper.class);
  121. redisCache = mock(RedisCache.class);
  122. ReflectionTestUtils.setField(controller, "posOrderService", posOrderService);
  123. ReflectionTestUtils.setField(controller, "infoUserService", infoUserService);
  124. ReflectionTestUtils.setField(controller, "userWalletService", userWalletService);
  125. ReflectionTestUtils.setField(controller, "merchantStoreAccessService", merchantStoreAccessService);
  126. ReflectionTestUtils.setField(controller, "merchantTokenSessionService", merchantTokenSessionService);
  127. ReflectionTestUtils.setField(controller, "businessPhoneService", businessPhoneService);
  128. ReflectionTestUtils.setField(controller, "oauthVerifyService", oauthVerifyService);
  129. ReflectionTestUtils.setField(controller, "infoUserOauthMapper", infoUserOauthMapper);
  130. ReflectionTestUtils.setField(controller, "redisCache", redisCache);
  131. when(infoUserOauthMapper.insert(any(InfoUserOauth.class))).thenReturn(1);
  132. }
  133. @AfterEach
  134. void clearRequestContext() {
  135. RequestContextHolder.resetRequestAttributes();
  136. }
  137. @Test
  138. void refusesDeletionWhenUserHasAnUnfinishedOrder() {
  139. when(posOrderService.exists(any(Wrapper.class))).thenReturn(true);
  140. ServiceException exception = assertThrows(ServiceException.class,
  141. () -> controller.deleuser(tokenFor(42L)));
  142. assertEquals("抱歉,您還有未完成的訂單,無法刪除帳號", exception.getMessage());
  143. verify(infoUserService, never()).deleteInfoUserByUserId(42L);
  144. }
  145. @Test
  146. void checksOnlyLatestUnfinishedStatesForEveryOrderRole() {
  147. when(posOrderService.exists(any(Wrapper.class))).thenReturn(false);
  148. when(infoUserService.deleteInfoUserByUserId(42L)).thenReturn(1);
  149. AjaxResult result = controller.deleuser(tokenFor(42L));
  150. assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
  151. ArgumentCaptor<LambdaQueryWrapper<PosOrder>> captor =
  152. ArgumentCaptor.forClass(LambdaQueryWrapper.class);
  153. verify(posOrderService).exists(captor.capture());
  154. LambdaQueryWrapper<PosOrder> query = captor.getValue();
  155. String sql = query.getSqlSegment();
  156. Collection<Object> parameters = new ArrayList<>(query.getParamNameValuePairs().values());
  157. assertEquals(6, parameters.size());
  158. assertEquals(1, Collections.frequency(parameters, 0L));
  159. assertEquals(1, Collections.frequency(parameters, 1L));
  160. assertEquals(1, Collections.frequency(parameters, 2L));
  161. assertEquals(3, Collections.frequency(parameters, 42L));
  162. assertTrue(sql.contains("user_id"));
  163. assertTrue(sql.contains("qs_id"));
  164. assertTrue(sql.contains("sh_id"));
  165. }
  166. @Test
  167. void rejectsAuditRejectionWithoutReason() {
  168. InfoUser user = new InfoUser();
  169. user.setAuditStatus("2");
  170. user.setAuditRejectReason(" ");
  171. AjaxResult result = controller.edit(user);
  172. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  173. verify(infoUserService, never()).updateInfoUser(any(InfoUser.class));
  174. }
  175. @Test
  176. void rejectsNewAuditRejectionWithoutReason() {
  177. InfoUser user = new InfoUser();
  178. user.setUserName("merchant");
  179. user.setPhone("0912345678");
  180. user.setAuditStatus("2");
  181. user.setAuditRejectReason(" ");
  182. AjaxResult result = controller.add(user);
  183. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  184. verify(infoUserService, never()).insertInfoUser(any(InfoUser.class));
  185. }
  186. @Test
  187. void profileUpdateCannotChangeAuditStatus() {
  188. InfoUser request = new InfoUser();
  189. request.setAuditStatus("1");
  190. when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
  191. controller.setuser(tokenFor(42L), request);
  192. ArgumentCaptor<InfoUser> captor = ArgumentCaptor.forClass(InfoUser.class);
  193. verify(infoUserService).saveOrUpdate(captor.capture());
  194. assertNull(captor.getValue().getAuditStatus());
  195. }
  196. @Test
  197. void resubmittedRejectedProfileReturnsToPendingReview() {
  198. InfoUser request = new InfoUser();
  199. request.setUpdateUserInfo(true);
  200. when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
  201. controller.setuser(tokenFor(42L), request);
  202. ArgumentCaptor<LambdaUpdateWrapper> captor =
  203. ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
  204. verify(infoUserService).update(captor.capture());
  205. LambdaUpdateWrapper<?> update = captor.getValue();
  206. Collection<Object> parameters = update.getParamNameValuePairs().values();
  207. assertTrue(update.getSqlSegment().contains("user_id"));
  208. assertTrue(update.getSqlSegment().contains("audit_status"));
  209. assertTrue(update.getSqlSet().contains("audit_status"));
  210. assertTrue(update.getSqlSet().contains("audit_reject_reason"));
  211. assertTrue(parameters.contains(42L));
  212. assertTrue(parameters.contains("2"));
  213. assertTrue(parameters.contains("0"));
  214. }
  215. @Test
  216. void ordinaryProfileUpdateDoesNotChangeReviewState() {
  217. InfoUser request = new InfoUser();
  218. request.setUpdateUserInfo(false);
  219. when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
  220. controller.setuser(tokenFor(42L), request);
  221. verify(infoUserService, never()).update(any(Wrapper.class));
  222. }
  223. @Test
  224. void trimsAndSavesAuditRejectionReason() {
  225. InfoUser user = new InfoUser();
  226. user.setAuditStatus("2");
  227. user.setAuditRejectReason(" 证件照片模糊 ");
  228. when(infoUserService.updateInfoUser(user)).thenReturn(1);
  229. controller.edit(user);
  230. assertEquals("证件照片模糊", user.getAuditRejectReason());
  231. verify(infoUserService).updateInfoUser(user);
  232. }
  233. @Test
  234. void clearsOldReasonWhenAuditPasses() {
  235. InfoUser user = new InfoUser();
  236. user.setAuditStatus("1");
  237. user.setAuditRejectReason("旧原因");
  238. when(infoUserService.updateInfoUser(user)).thenReturn(1);
  239. controller.edit(user);
  240. assertNull(user.getAuditRejectReason());
  241. verify(infoUserService).updateInfoUser(user);
  242. }
  243. @Test
  244. void platformGenericAddCannotCreateMerchantSubaccount() {
  245. InfoUser user = new InfoUser();
  246. user.setUserType("5");
  247. AjaxResult result = controller.add(user);
  248. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  249. verify(infoUserService, never()).insertInfoUser(any(InfoUser.class));
  250. }
  251. @Test
  252. void platformGenericEditCannotModifyMerchantSubaccount() {
  253. InfoUser existing = new InfoUser();
  254. existing.setUserId(55L);
  255. existing.setUserType("5");
  256. when(infoUserService.selectInfoUserByUserId(55L)).thenReturn(existing);
  257. InfoUser request = new InfoUser();
  258. request.setUserId(55L);
  259. AjaxResult result = controller.edit(request);
  260. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  261. verify(infoUserService, never()).updateInfoUser(request);
  262. }
  263. @Test
  264. void merchantLogoutRevokesOnlyCurrentTokenSession() {
  265. try (MockedStatic<AuthContext> authContext = mockStatic(AuthContext.class)) {
  266. authContext.when(AuthContext::requireJti)
  267. .thenReturn("qtw_tokens:sh:app:936:current-session");
  268. controller.merchantLogout("merchant-token");
  269. }
  270. verify(merchantTokenSessionService)
  271. .logoutCurrent("qtw_tokens:sh:app:936:current-session");
  272. }
  273. @Test
  274. void phoneRegistrationMarksTokenProviderAsPhone() {
  275. UserDTO request = new UserDTO();
  276. request.setPhone("0912345678");
  277. InfoUser created = new InfoUser();
  278. created.setUserId(42L);
  279. created.setPhone(request.getPhone());
  280. when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
  281. when(infoUserService.list(any(Wrapper.class))).thenReturn(java.util.List.of(created));
  282. AjaxResult result = controller.createUser(request);
  283. String token = (String) result.get("token");
  284. assertEquals("phone", JWT.decode(token).getClaim("provider").asString());
  285. }
  286. @Test
  287. void phoneLoginRejectsPhoneLinkedToMultipleAccounts() {
  288. UserDTO request = new UserDTO();
  289. request.setPhone("0912345678");
  290. request.setCode("8888");
  291. when(redisCache.getCacheObject("0912345678")).thenReturn(null);
  292. when(infoUserService.list(any(Wrapper.class))).thenReturn(java.util.List.of(
  293. activeUser(71L, "0"), activeUser(72L, "0")));
  294. ServiceException exception = assertThrows(ServiceException.class,
  295. () -> controller.lodeing(request));
  296. assertEquals("该手机号关联多个账号,请联系平台", exception.getMessage());
  297. verify(infoUserService, never()).saveOrUpdate(any(InfoUser.class));
  298. }
  299. @Test
  300. void phoneLoginOnlyMatchesMemberAccounts() {
  301. UserDTO request = new UserDTO();
  302. request.setPhone("0912345678");
  303. request.setCode("8888");
  304. when(redisCache.getCacheObject("0912345678")).thenReturn(null);
  305. InfoUser created = activeUser(42L, "0");
  306. created.setPhone("0912345678");
  307. // 查询限定 user_type=0:同号商家行不命中,未注册则新建会员(首查空 + 新建回查)
  308. when(infoUserService.list(any(Wrapper.class)))
  309. .thenReturn(java.util.List.of(), java.util.List.of(created));
  310. when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
  311. AjaxResult result = controller.lodeing(request);
  312. assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
  313. ArgumentCaptor<InfoUser> createdCaptor = ArgumentCaptor.forClass(InfoUser.class);
  314. verify(infoUserService).saveOrUpdate(createdCaptor.capture());
  315. assertEquals("0", createdCaptor.getValue().getUserType());
  316. }
  317. @Test
  318. void riderRegistrationRejectsDuplicateBusinessPhone() {
  319. com.ruoyi.system.domain.vo.UserDTO request = new com.ruoyi.system.domain.vo.UserDTO();
  320. request.setTelPhone("0912345678");
  321. org.mockito.Mockito.doThrow(new ServiceException("手机号已存在"))
  322. .when(businessPhoneService).ensureUnique("0912345678", null);
  323. assertThrows(ServiceException.class, () -> controller.createQsUser(request));
  324. verify(infoUserService, never()).insertInfoUser(any(InfoUser.class));
  325. }
  326. @Test
  327. void platformEditChecksBusinessPhoneExcludingCurrentAccount() {
  328. InfoUser existing = new InfoUser();
  329. existing.setUserId(42L);
  330. existing.setUserType("2");
  331. when(infoUserService.selectInfoUserByUserId(42L)).thenReturn(existing);
  332. when(businessPhoneService.isBusinessUserType("2")).thenReturn(true);
  333. when(infoUserService.updateInfoUser(any(InfoUser.class))).thenReturn(1);
  334. InfoUser request = new InfoUser();
  335. request.setUserId(42L);
  336. request.setTelPhone("0912345678");
  337. controller.edit(request);
  338. verify(businessPhoneService).ensureUnique("0912345678", 42L);
  339. }
  340. @Test
  341. void oauthBindPhoneRejectsMissingPhoneWithoutWritingBinding() {
  342. OAuthBindDto request = new OAuthBindDto();
  343. request.setTempKey("temp-key");
  344. when(redisCache.getCacheObject("oauth:bind:temp-key"))
  345. .thenReturn("line_user@line-uid");
  346. when(redisCache.deleteObject("oauth:bind:temp-key")).thenReturn(true);
  347. AjaxResult result = controller.oauthBindPhone(request);
  348. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  349. assertEquals("手机号不能为空", result.get(AjaxResult.MSG_TAG));
  350. verify(infoUserOauthMapper, never()).insert(any(InfoUserOauth.class));
  351. }
  352. @Test
  353. void lineUserBindingCreatesNormalUserInsteadOfBindingLegacyBusinessPhone() {
  354. OAuthBindDto request = new OAuthBindDto();
  355. request.setTempKey("temp-key");
  356. request.setPhone("0912345678");
  357. request.setCode("8888");
  358. when(redisCache.getCacheObject("oauth:bind:temp-key"))
  359. .thenReturn("line_user@line-uid");
  360. when(redisCache.deleteObject("oauth:bind:temp-key")).thenReturn(true);
  361. InfoUser legacyMerchant = activeUser(7L, "1");
  362. legacyMerchant.setPhone("0912345678");
  363. when(infoUserService.getuser("0912345678")).thenReturn(legacyMerchant);
  364. InfoUser createdUser = activeUser(99L, "0");
  365. createdUser.setPhone("0912345678");
  366. when(infoUserService.list(any(Wrapper.class)))
  367. .thenReturn(java.util.List.of(), java.util.List.of(createdUser));
  368. when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
  369. AjaxResult result = controller.oauthBindPhone(request);
  370. assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
  371. ArgumentCaptor<InfoUserOauth> binding = ArgumentCaptor.forClass(InfoUserOauth.class);
  372. verify(infoUserOauthMapper).insert(binding.capture());
  373. assertEquals(99L, binding.getValue().getUserId());
  374. verify(infoUserService, never()).getuser("0912345678");
  375. }
  376. @Test
  377. void profileUpdateCannotChangeOwnRoleOrStatus() {
  378. InfoUser current = activeUser(42L, "0");
  379. when(infoUserService.getById("42")).thenReturn(current);
  380. when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
  381. InfoUser request = new InfoUser();
  382. request.setUserType("2");
  383. request.setStatus("0");
  384. controller.setuser(tokenFor(42L), request);
  385. ArgumentCaptor<InfoUser> saved = ArgumentCaptor.forClass(InfoUser.class);
  386. verify(infoUserService).saveOrUpdate(saved.capture());
  387. assertNull(saved.getValue().getUserType());
  388. assertNull(saved.getValue().getStatus());
  389. }
  390. @Test
  391. void oauthBindPhoneRejectsTempKeyAlreadyClaimedByAnotherRequest() {
  392. OAuthBindDto request = new OAuthBindDto();
  393. request.setTempKey("replayed-key");
  394. request.setPhone("0912345678");
  395. request.setCode("8888");
  396. when(redisCache.getCacheObject("oauth:bind:replayed-key"))
  397. .thenReturn("line_rider@line-uid");
  398. when(redisCache.deleteObject("oauth:bind:replayed-key")).thenReturn(false);
  399. when(infoUserService.getOne(any(Wrapper.class))).thenReturn(activeUser(22L, "2"));
  400. AjaxResult result = controller.oauthBindPhone(request);
  401. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  402. verify(infoUserOauthMapper, never()).insert(any(InfoUserOauth.class));
  403. }
  404. @Test
  405. void lineMerchantBindingRejectsSubaccountWithUnavailableOwner() {
  406. OAuthBindDto request = new OAuthBindDto();
  407. request.setTempKey("merchant-key");
  408. when(redisCache.getCacheObject("oauth:bind:merchant-key"))
  409. .thenReturn("line_merchant@line-uid");
  410. when(redisCache.deleteObject("oauth:bind:merchant-key")).thenReturn(true);
  411. InfoUserOauth binding = new InfoUserOauth();
  412. binding.setUserId(55L);
  413. when(infoUserOauthMapper.selectOne(any())).thenReturn(binding);
  414. InfoUser subaccount = activeUser(55L, "5");
  415. subaccount.setSubaccountStatus("0");
  416. when(infoUserService.getById(55L)).thenReturn(subaccount);
  417. doThrow(new ServiceException("owner unavailable"))
  418. .when(merchantStoreAccessService).resolve(55L);
  419. AjaxResult result = controller.oauthBindPhone(request);
  420. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  421. assertEquals("账号已停用", result.get(AjaxResult.MSG_TAG));
  422. }
  423. @Test
  424. void oauthBindingRaceRejectsDisabledNonLineAccount() {
  425. OAuthBindDto request = new OAuthBindDto();
  426. request.setTempKey("google-key");
  427. when(redisCache.getCacheObject("oauth:bind:google-key"))
  428. .thenReturn("google@google-uid");
  429. when(redisCache.deleteObject("oauth:bind:google-key")).thenReturn(true);
  430. InfoUserOauth binding = new InfoUserOauth();
  431. binding.setUserId(66L);
  432. when(infoUserOauthMapper.selectOne(any())).thenReturn(binding);
  433. InfoUser disabled = activeUser(66L, "0");
  434. disabled.setStatus("1");
  435. when(infoUserService.getById(66L)).thenReturn(disabled);
  436. AjaxResult result = controller.oauthBindPhone(request);
  437. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  438. assertEquals("账号已停用", result.get(AjaxResult.MSG_TAG));
  439. }
  440. @ParameterizedTest
  441. @CsvSource({"apple_rider,2,qtw_tokens:qs:", "google_rider,2,qtw_tokens:qs:",
  442. "apple_merchant,1,qtw_tokens:sh:app:", "google_merchant,3,qtw_tokens:sh:app:"})
  443. void businessOauthBindsExistingBusinessPhoneAndIssuesItsOwnSession(
  444. String provider, String role, String tokenPrefix) {
  445. OAuthBindDto request = oauthRequest(provider);
  446. InfoUser business = activeUser(72L, role);
  447. business.setTelPhone(request.getPhone());
  448. when(infoUserService.list(any(Wrapper.class))).thenReturn(java.util.List.of(business));
  449. when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
  450. AjaxResult result = controller.oauthBindPhone(request);
  451. assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
  452. String token = (String) result.get("token");
  453. assertTrue(JWT.decode(token).getId().startsWith(tokenPrefix));
  454. assertEquals(provider, JWT.decode(token).getClaim("provider").asString());
  455. ArgumentCaptor<InfoUserOauth> binding = ArgumentCaptor.forClass(InfoUserOauth.class);
  456. verify(infoUserOauthMapper).insert(binding.capture());
  457. assertEquals(72L, binding.getValue().getUserId());
  458. assertEquals(provider, binding.getValue().getProvider());
  459. verify(infoUserService, never()).getuser(any());
  460. ArgumentCaptor<Wrapper<InfoUser>> query = ArgumentCaptor.forClass(Wrapper.class);
  461. verify(infoUserService).list(query.capture());
  462. assertTrue(query.getValue().getSqlSegment().contains("tel_phone"));
  463. }
  464. @ParameterizedTest
  465. @CsvSource({"apple_rider", "google_rider", "apple_merchant", "google_merchant"})
  466. void businessOauthCannotCreateAnAccount(String provider) {
  467. AjaxResult result = controller.oauthBindPhone(oauthRequest(provider));
  468. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  469. assertEquals("账号不存在", result.get(AjaxResult.MSG_TAG));
  470. verify(infoUserService, never()).saveOrUpdate(any(InfoUser.class));
  471. verify(infoUserOauthMapper, never()).insert(any(InfoUserOauth.class));
  472. }
  473. @ParameterizedTest
  474. @CsvSource({"apple_rider,1", "google_rider,0", "apple_merchant,2", "google_merchant,0"})
  475. void businessOauthRejectsBindingToTheWrongRole(String provider, String role) {
  476. OAuthBindDto request = oauthRequest(provider);
  477. InfoUserOauth binding = new InfoUserOauth();
  478. binding.setUserId(72L);
  479. when(infoUserOauthMapper.selectOne(any())).thenReturn(binding);
  480. when(infoUserService.getById(72L)).thenReturn(activeUser(72L, role));
  481. AjaxResult result = controller.oauthBindPhone(request);
  482. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  483. assertNull(result.get("token"));
  484. }
  485. @ParameterizedTest
  486. @CsvSource({"apple", "google"})
  487. void existingUserOauthKeepsProviderAndUserSession(String provider) {
  488. OAuthBindDto request = oauthRequest(provider);
  489. InfoUserOauth binding = new InfoUserOauth();
  490. binding.setUserId(72L);
  491. InfoUser user = activeUser(72L, "0");
  492. user.setPhone(request.getPhone());
  493. when(infoUserOauthMapper.selectOne(any())).thenReturn(binding);
  494. when(infoUserService.getById(72L)).thenReturn(user);
  495. AjaxResult result = controller.oauthBindPhone(request);
  496. assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
  497. String token = (String) result.get("token");
  498. assertTrue(JWT.decode(token).getId().startsWith("qtw_tokens:user:"));
  499. assertEquals(provider, JWT.decode(token).getClaim("provider").asString());
  500. }
  501. @ParameterizedTest
  502. @CsvSource({"apple_rider,2,qtw_tokens:qs:", "google_rider,2,qtw_tokens:qs:",
  503. "apple_merchant,4,qtw_tokens:sh:app:", "google_merchant,1,qtw_tokens:sh:app:",
  504. "apple,0,qtw_tokens:user:", "google,0,qtw_tokens:user:", "line,0,qtw_tokens:user:",
  505. "line_user,0,qtw_tokens:user:"})
  506. void boundOauthLoginKeepsEachClientSession(String provider, String role, String tokenPrefix) {
  507. OAuthLoginDto request = new OAuthLoginDto();
  508. request.setProvider(provider);
  509. request.setCredential("credential");
  510. when(oauthVerifyService.verify(provider, "credential")).thenReturn("provider-uid");
  511. InfoUserOauth binding = new InfoUserOauth();
  512. binding.setUserId(72L);
  513. when(infoUserOauthMapper.selectOne(any())).thenReturn(binding);
  514. InfoUser user = activeUser(72L, role);
  515. user.setPhone("0912345678");
  516. when(infoUserService.getOne(any(Wrapper.class))).thenReturn(user);
  517. AjaxResult result = controller.oauthLogin(request);
  518. assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
  519. assertTrue(JWT.decode((String) result.get("token")).getId().startsWith(tokenPrefix));
  520. assertEquals(provider, JWT.decode((String) result.get("token")).getClaim("provider").asString());
  521. if (provider.startsWith("line")) {
  522. ArgumentCaptor<LambdaQueryWrapper<InfoUserOauth>> query = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
  523. verify(infoUserOauthMapper).selectOne(query.capture());
  524. query.getValue().getSqlSegment();
  525. assertTrue(query.getValue().getParamNameValuePairs().containsValue("line"));
  526. assertTrue(query.getValue().getParamNameValuePairs().containsValue("line_user"));
  527. }
  528. }
  529. @ParameterizedTest
  530. @CsvSource({"apple", "google"})
  531. void userFirstBindingStillCreatesAUserAndWallet(String provider) {
  532. OAuthBindDto request = oauthRequest(provider);
  533. InfoUser user = activeUser(72L, "0");
  534. user.setPhone(request.getPhone());
  535. when(infoUserService.list(any(Wrapper.class)))
  536. .thenReturn(java.util.List.of(), java.util.List.of(user));
  537. when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
  538. AjaxResult result = controller.oauthBindPhone(request);
  539. assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
  540. ArgumentCaptor<InfoUser> created = ArgumentCaptor.forClass(InfoUser.class);
  541. verify(infoUserService).saveOrUpdate(created.capture());
  542. assertEquals("0", created.getValue().getUserType());
  543. assertEquals(request.getPhone(), created.getValue().getPhone());
  544. verify(userWalletService).createUserWallet(72L);
  545. }
  546. @ParameterizedTest
  547. @CsvSource({"apple_merchant", "google_merchant"})
  548. void businessOauthRejectsSubaccountWhoseOwnerIsUnavailable(String provider) {
  549. OAuthBindDto request = oauthRequest(provider);
  550. InfoUser user = activeUser(72L, "5");
  551. user.setSubaccountStatus("0");
  552. when(infoUserService.list(any(Wrapper.class))).thenReturn(java.util.List.of(user));
  553. doThrow(new ServiceException("owner unavailable")).when(merchantStoreAccessService).resolve(72L);
  554. AjaxResult result = controller.oauthBindPhone(request);
  555. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  556. verify(infoUserOauthMapper, never()).insert(any(InfoUserOauth.class));
  557. }
  558. @Test
  559. void directBusinessLineLoginCannotBypassStateValidation() {
  560. OAuthLoginDto request = new OAuthLoginDto();
  561. request.setProvider("line_rider");
  562. request.setCredential("code");
  563. LineOAuthStateService stateService = mock(LineOAuthStateService.class);
  564. doThrow(new ServiceException("invalid state")).when(stateService).consume("line_rider", null);
  565. ReflectionTestUtils.setField(controller, "lineOAuthStateService", stateService);
  566. assertThrows(ServiceException.class, () -> controller.oauthLogin(request));
  567. verify(oauthVerifyService, never()).verify(any(), any());
  568. }
  569. @ParameterizedTest
  570. @CsvSource({"apple_rider", "google_merchant", "line_rider", "line_merchant"})
  571. void businessBindingRequiresAnActualSmsCode(String provider) {
  572. OAuthBindDto request = oauthRequest(provider);
  573. request.setCode("8888");
  574. InfoUser user = activeUser(72L, provider.endsWith("rider") ? "2" : "1");
  575. when(infoUserService.getOne(any(Wrapper.class))).thenReturn(user);
  576. when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
  577. AjaxResult result = controller.oauthBindPhone(request);
  578. assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
  579. assertEquals("验证码错误", result.get(AjaxResult.MSG_TAG));
  580. verify(infoUserOauthMapper, never()).insert(any(InfoUserOauth.class));
  581. }
  582. @Test
  583. void oauthBindPhoneRejectsPhoneLinkedToMultipleAccounts() {
  584. OAuthBindDto request = oauthRequest("apple");
  585. when(infoUserService.list(any(Wrapper.class))).thenReturn(java.util.List.of(
  586. activeUser(71L, "0"), activeUser(72L, "0")));
  587. ServiceException exception = assertThrows(ServiceException.class,
  588. () -> controller.oauthBindPhone(request));
  589. assertEquals("该手机号关联多个账号,请联系平台", exception.getMessage());
  590. verify(infoUserOauthMapper, never()).insert(any(InfoUserOauth.class));
  591. verify(infoUserService, never()).saveOrUpdate(any(InfoUser.class));
  592. }
  593. @Test
  594. void merchantOauthBindPhoneRejectsPhoneLinkedToMultipleStores() {
  595. OAuthBindDto request = oauthRequest("line_merchant");
  596. when(infoUserService.list(any(Wrapper.class))).thenReturn(java.util.List.of(
  597. activeUser(71L, "1"), activeUser(72L, "1")));
  598. ServiceException exception = assertThrows(ServiceException.class,
  599. () -> controller.oauthBindPhone(request));
  600. assertEquals("该手机号关联多个账号,请联系平台", exception.getMessage());
  601. verify(infoUserOauthMapper, never()).insert(any(InfoUserOauth.class));
  602. }
  603. private OAuthBindDto oauthRequest(String provider) {
  604. OAuthBindDto request = new OAuthBindDto();
  605. request.setTempKey("business-key");
  606. request.setPhone("0912345678");
  607. request.setCode("456789");
  608. when(redisCache.getCacheObject("oauth:bind:business-key"))
  609. .thenReturn(provider + "@provider-uid");
  610. when(redisCache.deleteObject("oauth:bind:business-key")).thenReturn(true);
  611. when(redisCache.getCacheObject("0912345678")).thenReturn("456789");
  612. return request;
  613. }
  614. private InfoUser activeUser(Long userId, String userType) {
  615. InfoUser user = new InfoUser();
  616. user.setUserId(userId);
  617. user.setUserType(userType);
  618. user.setStatus("0");
  619. user.setDelFlag("0");
  620. user.setUserName("user-" + userId);
  621. return user;
  622. }
  623. private String tokenFor(Long userId) {
  624. return JwtUtil.setToken(String.valueOf(userId), "test-user");
  625. }
  626. private static class TestInfoUserController extends InfoUserController {
  627. @Override
  628. protected AjaxResult toAjax(int rows) {
  629. return new AjaxResult(HttpStatus.SUCCESS, "ok");
  630. }
  631. }
  632. }