| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- package com.ruoyi.app.user;
- import com.ruoyi.app.user.service.DeviceTrustService;
- import com.ruoyi.common.core.redis.RedisCache;
- import com.ruoyi.common.utils.spring.SpringUtils;
- import com.ruoyi.system.domain.InfoUser;
- import com.ruoyi.system.domain.InfoUserDevice;
- 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.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import org.junit.jupiter.api.AfterAll;
- import org.junit.jupiter.api.AfterEach;
- import org.junit.jupiter.api.BeforeAll;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.Test;
- import org.mockito.ArgumentCaptor;
- import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
- import org.springframework.beans.factory.support.DefaultListableBeanFactory;
- import org.springframework.context.support.StaticMessageSource;
- 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.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.ArgumentMatchers.anyInt;
- import static org.mockito.ArgumentMatchers.anyString;
- import static org.mockito.ArgumentMatchers.eq;
- import static org.mockito.ArgumentMatchers.startsWith;
- import static org.mockito.Mockito.mock;
- import static org.mockito.Mockito.never;
- import static org.mockito.Mockito.verify;
- import static org.mockito.Mockito.when;
- /**
- * LINE 回调设备信任免绑分支测试(029-device-trust-phone-bind,TDD 先行)。
- * state 参数语义:前端把 deviceId 放进 LINE authorize 的 state 带回(见 contracts/api-contract.md §5)。
- */
- class LineCallbackDeviceTrustTest {
- private static ConfigurableListableBeanFactory originalBeanFactory;
- private final RedisCache redisCache = mock(RedisCache.class);
- private final IInfoUserService infoUserService = mock(IInfoUserService.class);
- private final InfoUserOauthMapper infoUserOauthMapper = mock(InfoUserOauthMapper.class);
- private final com.ruoyi.app.utils.oauth.OAuthVerifyService oauthVerifyService =
- mock(com.ruoyi.app.utils.oauth.OAuthVerifyService.class);
- private final InfoUserDeviceMapper deviceMapper = mock(InfoUserDeviceMapper.class);
- private LineCallbackController controller;
- private MockHttpServletResponse response;
- @BeforeAll
- static void saveStatics() {
- originalBeanFactory = (ConfigurableListableBeanFactory)
- ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
- }
- @AfterAll
- static void restoreStatics() {
- new SpringUtils().postProcessBeanFactory(originalBeanFactory);
- }
- @BeforeEach
- void setUp() throws Exception {
- DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
- StaticMessageSource messageSource = new StaticMessageSource();
- messageSource.setUseCodeAsDefaultMessage(true);
- beanFactory.registerSingleton("messageSource", messageSource);
- beanFactory.registerSingleton("redisCache", redisCache);
- new SpringUtils().postProcessBeanFactory(beanFactory);
- RequestContextHolder.setRequestAttributes(
- new ServletRequestAttributes(new MockHttpServletRequest()));
- controller = new LineCallbackController();
- ReflectionTestUtils.setField(controller, "redisCache", redisCache);
- ReflectionTestUtils.setField(controller, "infoUserService", infoUserService);
- ReflectionTestUtils.setField(controller, "infoUserOauthMapper", infoUserOauthMapper);
- ReflectionTestUtils.setField(controller, "oauthVerifyService", oauthVerifyService);
- ReflectionTestUtils.setField(controller, "deviceTrustService", new DeviceTrustService(deviceMapper));
- ReflectionTestUtils.setField(controller, "appRedirect", "com.test.app://oauthLogin");
- response = new MockHttpServletResponse();
- }
- @AfterEach
- void tearDown() {
- RequestContextHolder.resetRequestAttributes();
- }
- private static InfoUserDevice trust(String deviceId, String phone, long userId) {
- InfoUserDevice t = new InfoUserDevice();
- t.setDeviceId(deviceId);
- t.setPhone(phone);
- t.setUserId(userId);
- return t;
- }
- @Test
- void unboundWithTrustedDeviceRedirectsDeviceConfirmAndWritesBothKeys() throws Exception {
- when(oauthVerifyService.verify("line", "code-1")).thenReturn("line-uid-1");
- when(infoUserOauthMapper.selectOne(any())).thenReturn(null);
- when(deviceMapper.selectById("dev-1")).thenReturn(trust("dev-1", "0987654321", 100L));
- controller.callback("code-1", "dev-1", response);
- String location = response.getRedirectedUrl();
- assertTrue(location != null && location.startsWith("com.test.app://oauthLogin?deviceConfirm=1&tempKey="),
- "应回跳 deviceConfirm 分支,实际: " + location);
- assertTrue(location.endsWith("&maskedPhone=098****4321"), "应带脱敏手机号,实际: " + location);
- ArgumentCaptor<String> tempKeyCaptor = ArgumentCaptor.forClass(String.class);
- verify(redisCache).setCacheObject(startsWith("oauth:bind:"), eq("line@line-uid-1"), eq(5), eq(TimeUnit.MINUTES));
- verify(redisCache).setCacheObject(startsWith("oauth:bind:dev:"), eq("dev-1"), eq(5), eq(TimeUnit.MINUTES));
- }
- @Test
- void unboundWithUnknownStateStillRedirectsNeedPhone() throws Exception {
- when(oauthVerifyService.verify("line", "code-2")).thenReturn("line-uid-2");
- when(infoUserOauthMapper.selectOne(any())).thenReturn(null);
- when(deviceMapper.selectById("dev-x")).thenReturn(null);
- controller.callback("code-2", "dev-x", response);
- String location = response.getRedirectedUrl();
- assertTrue(location != null && location.startsWith("com.test.app://oauthLogin?needPhone=1&tempKey="),
- "应维持 needPhone 回跳,实际: " + location);
- verify(redisCache, never()).setCacheObject(startsWith("oauth:bind:dev:"), anyString(), anyInt(), any(TimeUnit.class));
- }
- @Test
- void boundUserWithDeviceStateRedirectsTokenAndRecordsTrust() throws Exception {
- when(oauthVerifyService.verify("line", "code-3")).thenReturn("line-uid-3");
- InfoUserOauth bind = new InfoUserOauth();
- bind.setUserId(100L);
- bind.setProvider("line");
- bind.setProviderUid("line-uid-3");
- when(infoUserOauthMapper.selectOne(any())).thenReturn(bind);
- InfoUser u = new InfoUser();
- u.setUserId(100L);
- u.setPhone("0987654321");
- u.setStatus("0");
- when(infoUserService.getOne(any())).thenReturn(u);
- when(deviceMapper.selectById("dev-1")).thenReturn(null);
- controller.callback("code-3", "dev-1", response);
- String location = response.getRedirectedUrl();
- assertTrue(location != null && location.startsWith("com.test.app://oauthLogin?token="),
- "已绑定应直接回跳 token,实际: " + location);
- verify(deviceMapper).insert(any(InfoUserDevice.class));
- }
- }
|