package com.starry.admin.api; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.starry.admin.common.apitest.ApiTestDataSeeder; import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity; import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService; import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService; import com.starry.admin.modules.weichat.service.WxTokenService; import com.starry.admin.utils.SecurityUtils; import com.starry.common.constant.Constants; import com.starry.common.redis.RedisCache; import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.bean.WxOAuth2UserInfo; import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken; import me.chanjar.weixin.common.service.WxOAuth2Service; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.result.WxMpUser; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; class WxOauthControllerApiTest extends AbstractApiTest { @Autowired private WxTokenService wxTokenService; @Autowired private IPlayCustomUserInfoService customUserInfoService; @Autowired private IPlayClerkUserInfoService clerkUserInfoService; @Autowired private WxMpService wxMpService; @Autowired private RedisCache redisCache; @Test void getConfigAddressUsesDefaultWhenUrlMissing__covers_OAUTH_001() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); when(wxMpService.createJsapiSignature(anyString())).thenReturn(new WxJsapiSignature()); mockMvc.perform(post("/wx/oauth2/getConfigAddress") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .contentType(MediaType.APPLICATION_JSON) .content("{\"url\":\"\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); } @Test void getConfigAddressUsesProvidedUrlWhenPresent__covers_OAUTH_002() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); when(wxMpService.createJsapiSignature(anyString())).thenReturn(new WxJsapiSignature()); mockMvc.perform(post("/wx/oauth2/getConfigAddress") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .contentType(MediaType.APPLICATION_JSON) .content("{\"url\":\"https://example.com/custom\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); } @Test void getClerkLoginAddressBuildsAuthorizationUrl__covers_OAUTH_003() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); when(wxMpService.getOAuth2Service().buildAuthorizationUrl(anyString(), anyString(), anyString())) .thenReturn("https://wx.example/auth?scope=snsapi_userinfo"); mockMvc.perform(post("/wx/oauth2/getClerkLoginAddress") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .contentType(MediaType.APPLICATION_JSON) .content("{\"url\":\"https://example.com/callback\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").value("https://wx.example/auth?scope=snsapi_userinfo")); } @Test void getCustomLoginAddressBuildsAuthorizationUrl__covers_OAUTH_004() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); when(wxMpService.getOAuth2Service().buildAuthorizationUrl(anyString(), anyString(), anyString())) .thenReturn("https://wx.example/auth?scope=snsapi_userinfo"); mockMvc.perform(post("/wx/oauth2/getCustomLoginAddress") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .contentType(MediaType.APPLICATION_JSON) .content("{\"url\":\"https://example.com/callback\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").value("https://wx.example/auth?scope=snsapi_userinfo")); } @Test void customLoginPersistsTokenAndReturnsPayload__covers_OAUTH_005() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); WxOAuth2AccessToken token = new WxOAuth2AccessToken(); token.setOpenId(ApiTestDataSeeder.DEFAULT_CUSTOMER_OPEN_ID); WxOAuth2Service oAuth2Service = wxMpService.getOAuth2Service(); Mockito.doReturn(token).when(oAuth2Service).getAccessToken(anyString()); WxOAuth2UserInfo userInfo = new WxOAuth2UserInfo(); userInfo.setOpenid(ApiTestDataSeeder.DEFAULT_CUSTOMER_OPEN_ID); userInfo.setNickname("API Test Customer"); userInfo.setHeadImgUrl("https://example.com/avatar.png"); Mockito.doReturn(userInfo).when(oAuth2Service).getUserInfo(eq(token), eq(null)); mockMvc.perform(post("/wx/oauth2/custom/login") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .contentType(MediaType.APPLICATION_JSON) .content("{\"code\":\"apitest-code\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.tokenValue").isString()) .andExpect(jsonPath("$.data.tokenName").value(Constants.CUSTOM_USER_LOGIN_TOKEN)); PlayCustomUserInfoEntity customer = customUserInfoService.selectById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); if (customer == null || customer.getToken() == null || customer.getToken().isEmpty()) { throw new AssertionError("Expected customer token to be persisted after login"); } Object cachedTenantId = redisCache.getCacheObject("TENANT_INFO:" + ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); if (!ApiTestDataSeeder.DEFAULT_TENANT_ID.equals(cachedTenantId)) { throw new AssertionError("Expected Redis TENANT_INFO to be cached after login"); } } @Test void customLoginReturnsUnauthorizedWhenWxOauthFails__covers_OAUTH_006() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); WxOAuth2Service oAuth2Service = wxMpService.getOAuth2Service(); Mockito.doThrow(new RuntimeException("wx-fail")).when(oAuth2Service).getAccessToken(eq("fail-code")); mockMvc.perform(post("/wx/oauth2/custom/login") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .contentType(MediaType.APPLICATION_JSON) .content("{\"code\":\"fail-code\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(401)); } @Test void customLogoutInvalidatesToken__covers_OAUTH_008() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); String token = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, token); mockMvc.perform(get("/wx/oauth2/custom/logout") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + token)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); PlayCustomUserInfoEntity customer = customUserInfoService.selectById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); if (customer == null) { throw new AssertionError("Customer missing"); } if (!"empty".equals(customer.getToken())) { throw new AssertionError("Expected token to be invalidated to 'empty'"); } } @Test void clerkLoginPersistsTokenAndCachesTenant__covers_OAUTH_007() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); String clerkId = "clerk-oauth-apitest"; String clerkOpenId = "openid-clerk-oauth-apitest"; PlayClerkUserInfoEntity existing = clerkUserInfoService.getById(clerkId); if (existing == null) { PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity(); entity.setId(clerkId); entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); entity.setOpenid(clerkOpenId); entity.setNickname("API Test Clerk OAuth"); entity.setAvatar("https://example.com/avatar.png"); entity.setSysUserId(""); entity.setOnboardingState("1"); entity.setListingState("1"); entity.setClerkState("1"); entity.setOnlineState("1"); clerkUserInfoService.save(entity); } else { PlayClerkUserInfoEntity patch = new PlayClerkUserInfoEntity(); patch.setId(clerkId); patch.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); patch.setOpenid(clerkOpenId); patch.setAvatar("https://example.com/avatar.png"); patch.setSysUserId(""); patch.setOnboardingState("1"); patch.setListingState("1"); patch.setClerkState("1"); patch.setOnlineState("1"); patch.setDeleted(Boolean.FALSE); clerkUserInfoService.updateById(patch); } WxOAuth2AccessToken token = new WxOAuth2AccessToken(); token.setOpenId(clerkOpenId); WxOAuth2Service oAuth2Service = wxMpService.getOAuth2Service(); Mockito.doReturn(token).when(oAuth2Service).getAccessToken(anyString()); WxOAuth2UserInfo userInfo = new WxOAuth2UserInfo(); userInfo.setOpenid(clerkOpenId); userInfo.setNickname("API Test Clerk"); userInfo.setHeadImgUrl("https://example.com/avatar.png"); Mockito.doReturn(userInfo).when(oAuth2Service).getUserInfo(eq(token), eq(null)); mockMvc.perform(post("/wx/oauth2/clerk/login") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .contentType(MediaType.APPLICATION_JSON) .content("{\"code\":\"apitest-code\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.tokenValue").isString()) .andExpect(jsonPath("$.data.tokenName").value(Constants.CLERK_USER_LOGIN_TOKEN)) .andExpect(jsonPath("$.data.pcData.token").value("")) .andExpect(jsonPath("$.data.pcData.role").value("")); PlayClerkUserInfoEntity clerk = clerkUserInfoService.selectById(clerkId); if (clerk == null || clerk.getToken() == null || clerk.getToken().isEmpty() || "empty".equals(clerk.getToken())) { throw new AssertionError("Expected clerk token to be persisted after login"); } Object cachedTenantId = redisCache.getCacheObject("TENANT_INFO:" + clerkId); if (!ApiTestDataSeeder.DEFAULT_TENANT_ID.equals(cachedTenantId)) { throw new AssertionError("Expected clerk TENANT_INFO to be cached after login"); } } @Test void clerkLogoutInvalidatesToken__covers_OAUTH_007() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); String token = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID); clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, token); mockMvc.perform(get("/wx/oauth2/clerk/logout") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + token)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); PlayClerkUserInfoEntity clerk = clerkUserInfoService.selectById(ApiTestDataSeeder.DEFAULT_CLERK_ID); if (clerk == null) { throw new AssertionError("Clerk missing"); } if (!"empty".equals(clerk.getToken())) { throw new AssertionError("Expected clerk token to be invalidated to 'empty'"); } } @Test void checkSubscribeReturnsBoolean__covers_OAUTH_011() throws Exception { SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); String token = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, token); WxMpUser wxMpUser = new WxMpUser(); wxMpUser.setSubscribe(true); when(wxMpService.getUserService().userInfo(ApiTestDataSeeder.DEFAULT_CUSTOMER_OPEN_ID)).thenReturn(wxMpUser); mockMvc.perform(get("/wx/oauth2/checkSubscribe") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + token)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").value(true)); } }