diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a25f516..e60600f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -83,7 +83,14 @@ "Bash(git add -A && git commit -m \"test: 提升ShareConfigService测试覆盖率 - 新增12个边界条件测试\n\n- 新增null参数处理测试(extraParams, utmParams, title, description, imageUrl)\n- 新增空集合处理测试(empty utmParams, empty extraParams)\n- 新增null key/value过滤测试\n- 新增占位符解析测试(timestamp)\n- 新增默认模板回退测试\n- 新增模板注册和获取测试\n\n覆盖率提升:\n- 分支覆盖率: 61% → 62% \\(+1%\\)\n- Service包: 83% → 85% \\(+2%\\)\n\n距离70%目标还需50个分支,完成度89%\")", "Bash(mvn test -Dtest=ActivityControllerContractTest -q)", "Bash(mvn clean test jacoco:report -q 2>&1 | tail -20)", - "Bash(git add -A && git commit -m \"test: 提升ActivityController测试覆盖率 - 新增13个API契约测试\n\n- 新增创建/更新/获取活动测试\n- 新增活动统计和关系图测试\n- 新增排行榜分页测试(topN, page, size边界条件)\n- 新增排行榜CSV导出测试(带/不带topN)\n- 新增null/负数/无效参数处理测试\n- 新增页码超出范围返回空列表测试\n\n覆盖率提升:\n- Controller包: 67% → 73% \\(+6%\\)\n- 指令覆盖率: 85% → 86% \\(+1%\\)\n- 总分支覆盖率: 62% \\(保持\\)\n\n距离70%目标还需47个分支,完成度90%\")" + "Bash(git add -A && git commit -m \"test: 提升ActivityController测试覆盖率 - 新增13个API契约测试\n\n- 新增创建/更新/获取活动测试\n- 新增活动统计和关系图测试\n- 新增排行榜分页测试(topN, page, size边界条件)\n- 新增排行榜CSV导出测试(带/不带topN)\n- 新增null/负数/无效参数处理测试\n- 新增页码超出范围返回空列表测试\n\n覆盖率提升:\n- Controller包: 67% → 73% \\(+6%\\)\n- 指令覆盖率: 85% → 86% \\(+1%\\)\n- 总分支覆盖率: 62% \\(保持\\)\n\n距离70%目标还需47个分支,完成度90%\")", + "Bash(find src/test/java/com/mosquito/project/controller -name \"*Test.java\" -type f -exec basename {} \\\\; | sort)", + "Bash(find src/main/java/com/mosquito/project/controller -name \"*.java\" -type f -exec basename {} \\\\; | sort)", + "Bash(mvn clean test jacoco:report -q && echo \"=== Coverage Report Generated ===\" && ls -lh target/site/jacoco/)", + "Bash(mvn clean test jacoco:report -DskipTests=false 2>&1 | tail -100)", + "Bash(mvn test -Dtest=ShortLinkControllerTest 2>&1 | tail -50)", + "Bash(mvn clean test jacoco:report 2>&1 | grep -A 20 \"Results:\" | head -25)", + "Bash(mvn clean test jacoco:report 2>&1 | tail -100)" ] } } diff --git a/src/test/java/com/mosquito/project/controller/ShortLinkControllerTest.java b/src/test/java/com/mosquito/project/controller/ShortLinkControllerTest.java index 236619f..087c4f6 100644 --- a/src/test/java/com/mosquito/project/controller/ShortLinkControllerTest.java +++ b/src/test/java/com/mosquito/project/controller/ShortLinkControllerTest.java @@ -130,4 +130,45 @@ class ShortLinkControllerTest { mockMvc.perform(get("/r/mal12345")) .andExpect(status().isBadRequest()); } + + @Test + void shouldExtractIpFromXForwardedFor() throws Exception { + ShortLinkEntity e = new ShortLinkEntity(); + e.setCode("ip12345"); + e.setOriginalUrl("https://example.com/page"); + when(shortLinkService.findByCode("ip12345")).thenReturn(Optional.of(e)); + when(urlValidator.isAllowedUrl("https://example.com/page")).thenReturn(true); + + mockMvc.perform(get("/r/ip12345") + .header("X-Forwarded-For", "203.0.113.1, 198.51.100.1")) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "https://example.com/page")); + } + + @Test + void shouldUseRemoteAddrWhenNoXForwardedFor() throws Exception { + ShortLinkEntity e = new ShortLinkEntity(); + e.setCode("remote123"); + e.setOriginalUrl("https://example.com/page"); + when(shortLinkService.findByCode("remote123")).thenReturn(Optional.of(e)); + when(urlValidator.isAllowedUrl("https://example.com/page")).thenReturn(true); + + mockMvc.perform(get("/r/remote123")) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "https://example.com/page")); + } + + @Test + void shouldHandleBlankXForwardedFor() throws Exception { + ShortLinkEntity e = new ShortLinkEntity(); + e.setCode("blank123"); + e.setOriginalUrl("https://example.com/page"); + when(shortLinkService.findByCode("blank123")).thenReturn(Optional.of(e)); + when(urlValidator.isAllowedUrl("https://example.com/page")).thenReturn(true); + + mockMvc.perform(get("/r/blank123") + .header("X-Forwarded-For", " ")) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "https://example.com/page")); + } } diff --git a/src/test/java/com/mosquito/project/controller/UserExperienceControllerTest.java b/src/test/java/com/mosquito/project/controller/UserExperienceControllerTest.java index 71e2b47..5d2cfa3 100644 --- a/src/test/java/com/mosquito/project/controller/UserExperienceControllerTest.java +++ b/src/test/java/com/mosquito/project/controller/UserExperienceControllerTest.java @@ -240,4 +240,74 @@ class UserExperienceControllerTest { .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")); + } }