package com.ruoyi.app.user; import com.auth0.jwt.JWT; import com.ruoyi.app.utils.oauth.LineOAuthProperties; import com.ruoyi.app.user.service.DeviceTrustService; import com.ruoyi.app.utils.oauth.OAuthVerifyService; import com.ruoyi.app.utils.oauth.LineOAuthStateService; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.system.domain.InfoUser; import com.ruoyi.system.domain.InfoUserOauth; import com.ruoyi.system.mapper.InfoUserDeviceMapper; import com.ruoyi.system.mapper.InfoUserOauthMapper; import com.ruoyi.system.service.IInfoUserService; import com.ruoyi.system.service.MerchantStoreAccessService; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class LineCallbackControllerTest { private static ConfigurableListableBeanFactory originalBeanFactory; @BeforeAll static void installJwtRedisBean() { originalBeanFactory = (ConfigurableListableBeanFactory) ReflectionTestUtils.getField(SpringUtils.class, "beanFactory"); } @AfterAll static void restoreBeanFactory() { new SpringUtils().postProcessBeanFactory(originalBeanFactory); } @AfterEach void clearRequestContext() { RequestContextHolder.resetRequestAttributes(); } @Test void sharedCallbackUsesProviderSpecificChannelAndKeepsProviderInTempBinding() throws Exception { OAuthVerifyService verifyService = mock(OAuthVerifyService.class); IInfoUserService infoUserService = mock(IInfoUserService.class); InfoUserOauthMapper oauthMapper = mock(InfoUserOauthMapper.class); RedisCache redisCache = mock(RedisCache.class); LineOAuthProperties properties = new LineOAuthProperties(); LineOAuthProperties.Channel rider = new LineOAuthProperties.Channel(); rider.setClientId("2011397520"); rider.setClientSecret("secret"); rider.setRedirectUri("https://api.test/auth/line/callback?provider=line_rider"); rider.setAppRedirect("com.twanmsdqs.app://pages/UserCenter/oauthLogin"); properties.setRider(rider); when(verifyService.verify("line_rider", "code")).thenReturn("line-uid"); when(oauthMapper.selectOne(any())).thenReturn(null); LineCallbackController controller = new LineCallbackController(); ReflectionTestUtils.setField(controller, "lineOAuthStateService", mock(LineOAuthStateService.class)); ReflectionTestUtils.setField(controller, "oauthVerifyService", verifyService); ReflectionTestUtils.setField(controller, "lineOAuthProperties", properties); ReflectionTestUtils.setField(controller, "infoUserService", infoUserService); ReflectionTestUtils.setField(controller, "infoUserOauthMapper", oauthMapper); ReflectionTestUtils.setField(controller, "redisCache", redisCache); MockHttpServletResponse response = new MockHttpServletResponse(); controller.callback("line_rider", "code", "state", response); assertTrue(response.getRedirectedUrl().startsWith( "com.twanmsdqs.app://pages/UserCenter/oauthLogin?needPhone=1&tempKey=")); verify(redisCache).setCacheObject(any(String.class), org.mockito.ArgumentMatchers.eq("line_rider@line-uid"), org.mockito.ArgumentMatchers.eq(5), org.mockito.ArgumentMatchers.eq(TimeUnit.MINUTES)); } @Test void boundRiderReceivesRiderSessionToken() throws Exception { CallbackFixture fixture = boundFixture("2"); fixture.controller.callback("line_rider", "code", "state", fixture.response); String redirected = fixture.response.getRedirectedUrl(); String token = URLDecoder.decode(redirected.substring(redirected.indexOf("?token=") + 7).split("&", 2)[0], StandardCharsets.UTF_8); assertTrue(JWT.decode(token).getId().startsWith(CacheConstants.QS_TOKEN_KEY + "42:")); assertEquals("line_rider", JWT.decode(token).getClaim("provider").asString()); verify(fixture.redisCache).deleteKeys(CacheConstants.QS_TOKEN_KEY + "42:*"); } @Test void riderChannelRejectsBindingToMerchantAccount() throws Exception { CallbackFixture fixture = boundFixture("1"); fixture.controller.callback("line_rider", "code", "state", fixture.response); assertEquals("com.twanmsdqs.app://pages/UserCenter/oauthLogin?error=user_stopped&state=state", fixture.response.getRedirectedUrl()); verify(fixture.redisCache, never()).deleteKeys(any(String.class)); } @Test void merchantCallbackRejectsSubaccountWithUnavailableOwner() throws Exception { OAuthVerifyService verifyService = mock(OAuthVerifyService.class); IInfoUserService infoUserService = mock(IInfoUserService.class); InfoUserOauthMapper oauthMapper = mock(InfoUserOauthMapper.class); RedisCache redisCache = mock(RedisCache.class); MerchantStoreAccessService accessService = mock(MerchantStoreAccessService.class); DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerSingleton("redisCache", redisCache); new SpringUtils().postProcessBeanFactory(beanFactory); MockHttpServletRequest request = new MockHttpServletRequest(); request.setRemoteAddr("127.0.0.1"); request.addHeader("User-Agent", "JUnit"); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); LineOAuthProperties properties = new LineOAuthProperties(); LineOAuthProperties.Channel merchant = new LineOAuthProperties.Channel(); merchant.setClientId("merchant-client"); merchant.setClientSecret("secret"); merchant.setRedirectUri("https://api.test/auth/line/callback?provider=line_merchant"); merchant.setAppRedirect("com.twanmsdsj.app://pages/UserCenter/oauthLogin"); properties.setMerchant(merchant); when(verifyService.verify("line_merchant", "code")).thenReturn("line-uid"); InfoUserOauth binding = new InfoUserOauth(); binding.setUserId(55L); when(oauthMapper.selectOne(any())).thenReturn(binding); InfoUser subaccount = new InfoUser(); subaccount.setUserId(55L); subaccount.setUserName("subaccount"); subaccount.setUserType("5"); subaccount.setStatus("0"); subaccount.setDelFlag("0"); subaccount.setSubaccountStatus("0"); when(infoUserService.getOne(any())).thenReturn(subaccount); doThrow(new ServiceException("owner unavailable")) .when(accessService).resolve(55L); LineCallbackController controller = new LineCallbackController(); ReflectionTestUtils.setField(controller, "lineOAuthStateService", mock(LineOAuthStateService.class)); ReflectionTestUtils.setField(controller, "oauthVerifyService", verifyService); ReflectionTestUtils.setField(controller, "lineOAuthProperties", properties); ReflectionTestUtils.setField(controller, "infoUserService", infoUserService); ReflectionTestUtils.setField(controller, "infoUserOauthMapper", oauthMapper); ReflectionTestUtils.setField(controller, "redisCache", redisCache); ReflectionTestUtils.setField(controller, "merchantStoreAccessService", accessService); MockHttpServletResponse response = new MockHttpServletResponse(); controller.callback("line_merchant", "code", "state", response); assertEquals("com.twanmsdsj.app://pages/UserCenter/oauthLogin?error=user_stopped&state=state", response.getRedirectedUrl()); } @Test void oldUserCallbackWithoutProviderKeepsOriginalResponse() throws Exception { LineOAuthProperties properties = new LineOAuthProperties(); LineOAuthProperties.Channel user = new LineOAuthProperties.Channel(); user.setClientId("user-client"); user.setClientSecret("secret"); user.setRedirectUri("https://api.test/auth/line/callback?provider=line_user"); user.setAppRedirect("com.twanmsdyh.app://pages/UserCenter/oauthLogin"); properties.setUser(user); OAuthVerifyService verifier = mock(OAuthVerifyService.class); when(verifier.verify("line", "old-code")).thenReturn("old-user-id"); RedisCache redis = mock(RedisCache.class); LineCallbackController controller = new LineCallbackController(); ReflectionTestUtils.setField(controller, "lineOAuthProperties", properties); ReflectionTestUtils.setField(controller, "oauthVerifyService", verifier); ReflectionTestUtils.setField(controller, "infoUserOauthMapper", mock(InfoUserOauthMapper.class)); ReflectionTestUtils.setField(controller, "redisCache", redis); ReflectionTestUtils.setField(controller, "deviceTrustService", new DeviceTrustService(mock(InfoUserDeviceMapper.class))); MockHttpServletResponse response = new MockHttpServletResponse(); controller.callback(null, "old-code", "line_login", response); assertTrue(response.getRedirectedUrl().startsWith("com.twanmsdyh.app://pages/UserCenter/oauthLogin?needPhone=1&tempKey=")); assertTrue(!response.getRedirectedUrl().contains("&state=")); verify(verifier).verify("line", "old-code"); verify(redis).setCacheObject(any(String.class), org.mockito.ArgumentMatchers.eq("line@old-user-id"), org.mockito.ArgumentMatchers.eq(5), org.mockito.ArgumentMatchers.eq(TimeUnit.MINUTES)); } @Test void businessCallbackRejectsInvalidStateBeforeIssuingSession() throws Exception { CallbackFixture fixture = boundFixture("2"); LineOAuthStateService stateService = mock(LineOAuthStateService.class); doThrow(new ServiceException("invalid state")).when(stateService).consume("line_rider", "state"); ReflectionTestUtils.setField(fixture.controller, "lineOAuthStateService", stateService); OAuthVerifyService verifier = (OAuthVerifyService) ReflectionTestUtils.getField(fixture.controller, "oauthVerifyService"); fixture.controller.callback("line_rider", "code", "state", fixture.response); assertEquals("com.twanmsdqs.app://pages/UserCenter/oauthLogin?error=fail&state=state", fixture.response.getRedirectedUrl()); verify(verifier, never()).verify(any(), any()); verify(fixture.redisCache, never()).deleteKeys(any(String.class)); } private CallbackFixture boundFixture(String userType) { OAuthVerifyService verifyService = mock(OAuthVerifyService.class); IInfoUserService infoUserService = mock(IInfoUserService.class); InfoUserOauthMapper oauthMapper = mock(InfoUserOauthMapper.class); RedisCache redisCache = mock(RedisCache.class); DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerSingleton("redisCache", redisCache); new SpringUtils().postProcessBeanFactory(beanFactory); MockHttpServletRequest request = new MockHttpServletRequest(); request.setRemoteAddr("127.0.0.1"); request.addHeader("User-Agent", "JUnit"); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); LineOAuthProperties properties = new LineOAuthProperties(); LineOAuthProperties.Channel rider = new LineOAuthProperties.Channel(); rider.setClientId("2011397520"); rider.setClientSecret("secret"); rider.setRedirectUri("https://api.test/auth/line/callback?provider=line_rider"); rider.setAppRedirect("com.twanmsdqs.app://pages/UserCenter/oauthLogin"); properties.setRider(rider); InfoUserOauth binding = new InfoUserOauth(); binding.setUserId(42L); InfoUser user = new InfoUser(); user.setUserId(42L); user.setUserName("rider"); user.setUserType(userType); user.setStatus("0"); user.setDelFlag("0"); when(verifyService.verify("line_rider", "code")).thenReturn("line-uid"); when(oauthMapper.selectOne(any())).thenReturn(binding); when(infoUserService.getOne(any())).thenReturn(user); LineCallbackController controller = new LineCallbackController(); ReflectionTestUtils.setField(controller, "lineOAuthStateService", mock(LineOAuthStateService.class)); ReflectionTestUtils.setField(controller, "oauthVerifyService", verifyService); ReflectionTestUtils.setField(controller, "lineOAuthProperties", properties); ReflectionTestUtils.setField(controller, "infoUserService", infoUserService); ReflectionTestUtils.setField(controller, "infoUserOauthMapper", oauthMapper); ReflectionTestUtils.setField(controller, "redisCache", redisCache); return new CallbackFixture(controller, redisCache, new MockHttpServletResponse()); } private record CallbackFixture(LineCallbackController controller, RedisCache redisCache, MockHttpServletResponse response) { } }