package com.mosquito.project.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.mosquito.project.persistence.entity.ShortLinkEntity; import com.mosquito.project.persistence.entity.UserInviteEntity; import com.mosquito.project.persistence.repository.UserInviteRepository; import com.mosquito.project.service.PosterRenderService; import com.mosquito.project.service.ShareConfigService; import com.mosquito.project.service.ShortLinkService; import com.mosquito.project.support.TestAuthSupport; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; import java.util.List; import java.util.Map; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(UserExperienceController.class) @Import(com.mosquito.project.config.ControllerTestConfig.class) @TestPropertySource(properties = { "spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration," + "org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration," + "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration" }) class UserExperienceControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @MockBean private ShortLinkService shortLinkService; @MockBean private UserInviteRepository userInviteRepository; @MockBean private PosterRenderService posterRenderService; @MockBean private ShareConfigService shareConfigService; @MockBean private com.mosquito.project.persistence.repository.UserRewardRepository userRewardRepository; @Test void shouldReturnInvitationInfo_withShortLink() throws Exception { ShortLinkEntity e = new ShortLinkEntity(); e.setCode("inv12345"); e.setOriginalUrl("https://example.com/landing?activityId=1&inviter=2"); when(shortLinkService.create(anyString())).thenReturn(e); when(shareConfigService.buildShareUrl(anyLong(), anyLong(), anyString(), any())).thenReturn("https://example.com/landing?activityId=1&inviter=2"); mockMvc.perform(get("/api/v1/me/invitation-info") .param("activityId", "1") .param("userId", "2") .accept(MediaType.APPLICATION_JSON) .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.code").value("inv12345")) .andExpect(jsonPath("$.data.path").value("/r/inv12345")); } @Test void shouldReturnInvitedFriends_withPagination() throws Exception { UserInviteEntity a = new UserInviteEntity(); a.setInviteeUserId(10L); a.setStatus("clicked"); UserInviteEntity b = new UserInviteEntity(); b.setInviteeUserId(11L); b.setStatus("registered"); UserInviteEntity c = new UserInviteEntity(); c.setInviteeUserId(12L); c.setStatus("ordered"); when(userInviteRepository.findByActivityIdAndInviterUserId(anyLong(), anyLong())).thenReturn(List.of(a,b,c)); mockMvc.perform(get("/api/v1/me/invited-friends") .param("activityId", "1") .param("userId", "2") .param("page", "1") .param("size", "1") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data[0].status").value("registered")); } @Test void shouldReturnPosterImage() throws Exception { when(posterRenderService.renderPoster(anyLong(), anyLong(), anyString())).thenReturn("placeholder".getBytes()); mockMvc.perform(get("/api/v1/me/poster/image") .param("activityId", "1") .param("userId", "2") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(header().string("Content-Type", "image/png")); } @Test void posterImage_shouldReturn500_whenRenderFails() throws Exception { when(posterRenderService.renderPoster(anyLong(), anyLong(), anyString())) .thenThrow(new RuntimeException("render failed")); mockMvc.perform(get("/api/v1/me/poster/image") .param("activityId", "1") .param("userId", "2") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isInternalServerError()); } @Test void shouldReturnPosterConfig() throws Exception { mockMvc.perform(get("/api/v1/me/poster/config") .param("template", "default") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.template").value("default")); } @Test void shouldReturnPosterHtml() throws Exception { when(posterRenderService.renderPosterHtml(anyLong(), anyLong(), anyString())).thenReturn(""); mockMvc.perform(get("/api/v1/me/poster/html") .param("activityId", "1") .param("userId", "2") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.TEXT_HTML)); } @Test void posterHtml_shouldReturn500_whenRenderFails() throws Exception { when(posterRenderService.renderPosterHtml(anyLong(), anyLong(), anyString())) .thenThrow(new RuntimeException("render failed")); mockMvc.perform(get("/api/v1/me/poster/html") .param("activityId", "1") .param("userId", "2") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isInternalServerError()); } @Test void shouldReturnRewards_withPagination() throws Exception { var r1 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r1.setType("points"); r1.setPoints(100); r1.setCreatedAt(java.time.OffsetDateTime.now()); var r2 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r2.setType("coupon"); r2.setPoints(0); r2.setCreatedAt(java.time.OffsetDateTime.now().minusDays(1)); when(userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(anyLong(), anyLong())).thenReturn(java.util.List.of(r1, r2)); mockMvc.perform(get("/api/v1/me/rewards") .param("activityId", "1") .param("userId", "2") .param("page", "0") .param("size", "1") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data[0].type").value("points")); } @Test void shouldReturnEmptyList_whenInvitedFriendsPaginationExceedsSize() throws Exception { UserInviteEntity a = new UserInviteEntity(); a.setInviteeUserId(10L); a.setStatus("clicked"); when(userInviteRepository.findByActivityIdAndInviterUserId(anyLong(), anyLong())).thenReturn(List.of(a)); mockMvc.perform(get("/api/v1/me/invited-friends") .param("activityId", "1") .param("userId", "2") .param("page", "10") .param("size", "20") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").isEmpty()); } @Test void shouldReturnEmptyList_whenRewardsPaginationExceedsSize() throws Exception { var r1 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r1.setType("points"); r1.setPoints(100); r1.setCreatedAt(java.time.OffsetDateTime.now()); when(userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(anyLong(), anyLong())).thenReturn(java.util.List.of(r1)); mockMvc.perform(get("/api/v1/me/rewards") .param("activityId", "1") .param("userId", "2") .param("page", "10") .param("size", "20") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").isEmpty()); } @Test void shouldReturnShareMeta() throws Exception { Map meta = Map.of("title", "活动标题", "description", "活动描述"); when(shareConfigService.getShareMeta(anyLong(), anyLong(), anyString())).thenReturn(meta); mockMvc.perform(get("/api/v1/me/share-meta") .param("activityId", "1") .param("userId", "2") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.title").value("活动标题")) .andExpect(jsonPath("$.data.description").value("活动描述")); } @Test void shouldReturnShareMeta_withCustomTemplate() throws Exception { Map meta = Map.of("title", "自定义模板"); when(shareConfigService.getShareMeta(anyLong(), anyLong(), eq("custom"))).thenReturn(meta); mockMvc.perform(get("/api/v1/me/share-meta") .param("activityId", "1") .param("userId", "2") .param("template", "custom") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.title").value("自定义模板")); } @Test void shouldHandleZeroOrNegativeSize_inInvitedFriends() throws Exception { UserInviteEntity a = new UserInviteEntity(); a.setInviteeUserId(10L); a.setStatus("clicked"); UserInviteEntity b = new UserInviteEntity(); b.setInviteeUserId(11L); b.setStatus("registered"); when(userInviteRepository.findByActivityIdAndInviterUserId(anyLong(), anyLong())).thenReturn(List.of(a, b)); mockMvc.perform(get("/api/v1/me/invited-friends") .param("activityId", "1") .param("userId", "2") .param("page", "0") .param("size", "0") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").isEmpty()); } @Test void shouldHandleZeroOrNegativeSize_inRewards() throws Exception { var r1 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r1.setType("points"); r1.setPoints(100); r1.setCreatedAt(java.time.OffsetDateTime.now()); var r2 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r2.setType("coupon"); r2.setPoints(0); r2.setCreatedAt(java.time.OffsetDateTime.now().minusDays(1)); when(userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(anyLong(), anyLong())).thenReturn(java.util.List.of(r1, r2)); mockMvc.perform(get("/api/v1/me/rewards") .param("activityId", "1") .param("userId", "2") .param("page", "0") .param("size", "0") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").isEmpty()); } @Test void shouldHandleNegativePage_inInvitedFriends() throws Exception { UserInviteEntity a = new UserInviteEntity(); a.setInviteeUserId(10L); a.setStatus("clicked"); when(userInviteRepository.findByActivityIdAndInviterUserId(anyLong(), anyLong())).thenReturn(List.of(a)); mockMvc.perform(get("/api/v1/me/invited-friends") .param("activityId", "1") .param("userId", "2") .param("page", "-1") .param("size", "20") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data[0].status").value("clicked")); } @Test void shouldHandleNegativePage_inRewards() throws Exception { var r1 = new com.mosquito.project.persistence.entity.UserRewardEntity(); r1.setType("points"); r1.setPoints(100); r1.setCreatedAt(java.time.OffsetDateTime.now()); when(userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(anyLong(), anyLong())).thenReturn(java.util.List.of(r1)); mockMvc.perform(get("/api/v1/me/rewards") .param("activityId", "1") .param("userId", "2") .param("page", "-1") .param("size", "20") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data[0].type").value("points")); } @Test void shouldMaskPhoneNumber_inInvitedFriends() throws Exception { UserInviteEntity a = new UserInviteEntity(); a.setInviteeUserId(123L); a.setStatus("registered"); when(userInviteRepository.findByActivityIdAndInviterUserId(anyLong(), anyLong())).thenReturn(List.of(a)); mockMvc.perform(get("/api/v1/me/invited-friends") .param("activityId", "1") .param("userId", "2") .header("X-API-Key", TestAuthSupport.RAW_API_KEY) .header("Authorization", "Bearer test-token")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data[0].maskedPhone").value("138****0123")); } }