Files
wenzi/src/test/java/com/mosquito/project/controller/UserExperienceControllerTest.java
Your Name 11a73653aa test: 提升Web包测试覆盖率 - 新增UrlValidator边界测试
新增测试:
- UserExperienceController: maskPhone方法测试
- UrlValidator: IPv6公网地址、0.0.0.0地址、无效主机名、URI异常处理

覆盖率提升:
- 总体分支: 63.6% → 63.8% (+1个分支)
- Web包: 78% → 79% (+1%)
- 新增测试: 5个
- 距离70%目标: 还需39个分支

累计成果(本次会话):
- 新增测试: 17个
- 分支覆盖: +8个 (404→412)
- Controller包: 73% → 89% (+16%)
- Web包: 78% → 79% (+1%)
2026-03-03 12:35:27 +08:00

331 lines
15 KiB
Java

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("<html></html>");
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<String, Object> 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<String, Object> 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"));
}
}