diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 387fd30..e1afda4 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -105,7 +105,9 @@ "Bash(mvn test -Dtest=ActivityServiceCoverageTest 2>&1 | tail -30)", "Bash(mvn test -Dtest=ApiKeyEncryptionServiceTest -q)", "Bash(grep -E \"Tests run:|BUILD\" target/surefire-reports/*.txt 2>/dev/null | tail -5 || echo \"检查测试结果...\")", - "Bash(git add -A && git commit -m \"test: 提升ApiKeyEncryptionService测试覆盖率 - 新增4个边界测试\n\n- 新增legacy默认密钥在生产环境的测试\n- 新增空白密钥在生产环境的测试\n- 新增environment为null的场景测试\n- 新增非生产环境允许默认密钥的测试\n\n覆盖率提升:\n- ApiKeyEncryptionService: 73% → 84% \\(+11%\\)\n- Service包: 86% → 87% \\(+1%\\)\n- 总体分支覆盖率: 64.1% → 64.5% \\(+0.4%\\)\n- 新增覆盖分支: 3个\n- 距离70%目标: 还需34个分支\")" + "Bash(git add -A && git commit -m \"test: 提升ApiKeyEncryptionService测试覆盖率 - 新增4个边界测试\n\n- 新增legacy默认密钥在生产环境的测试\n- 新增空白密钥在生产环境的测试\n- 新增environment为null的场景测试\n- 新增非生产环境允许默认密钥的测试\n\n覆盖率提升:\n- ApiKeyEncryptionService: 73% → 84% \\(+11%\\)\n- Service包: 86% → 87% \\(+1%\\)\n- 总体分支覆盖率: 64.1% → 64.5% \\(+0.4%\\)\n- 新增覆盖分支: 3个\n- 距离70%目标: 还需34个分支\")", + "Bash(mvn test -Dtest=ShareTrackingServiceTest -q)", + "Bash(git add -A && git commit -m \"test: 提升ShareTrackingService测试覆盖率到100% - 新增3个边界测试\n\n- 新增空白referer和userAgent的转化漏斗测试\n- 新增null IP和null params的指标测试\n- 新增null params的点击记录测试\n\n覆盖率提升:\n- ShareTrackingService: 82% → 100% \\(+18%\\) ✨\n- Service包: 87% → 89% \\(+2%\\)\n- 总体分支覆盖率: 64.5% → 65.3% \\(+0.8%\\)\n- 新增覆盖分支: 5个\n- 距离70%目标: 还需29个分支\")" ] } } diff --git a/src/test/java/com/mosquito/project/service/ShareTrackingServiceTest.java b/src/test/java/com/mosquito/project/service/ShareTrackingServiceTest.java index 1322f4d..147ecbe 100644 --- a/src/test/java/com/mosquito/project/service/ShareTrackingServiceTest.java +++ b/src/test/java/com/mosquito/project/service/ShareTrackingServiceTest.java @@ -323,4 +323,97 @@ class ShareTrackingServiceTest { click.setCreatedAt(OffsetDateTime.now(ZoneOffset.UTC)); return click; } + + @Test + @DisplayName("获取转化漏斗 - 处理空白referer和userAgent") + void shouldHandleBlankRefererAndUserAgent_InConversionFunnel() { + // Given + OffsetDateTime startTime = OffsetDateTime.now(ZoneOffset.UTC).minusDays(1); + OffsetDateTime endTime = OffsetDateTime.now(ZoneOffset.UTC); + + List clicks = new ArrayList<>(); + + LinkClickEntity click1 = new LinkClickEntity(); + click1.setCode(SHORT_CODE); + click1.setIp(IP); + click1.setUserAgent(" "); // blank userAgent + click1.setReferer(" "); // blank referer + click1.setCreatedAt(OffsetDateTime.now(ZoneOffset.UTC)); + + LinkClickEntity click2 = new LinkClickEntity(); + click2.setCode(SHORT_CODE); + click2.setIp(IP); + click2.setUserAgent(null); // null userAgent + click2.setReferer("https://example.com"); + click2.setCreatedAt(OffsetDateTime.now(ZoneOffset.UTC)); + + clicks.add(click1); + clicks.add(click2); + + when(linkClickRepository.findByActivityIdAndCreatedAtBetween(ACTIVITY_ID, startTime, endTime)) + .thenReturn(clicks); + + // When + Map result = shareTrackingService.getConversionFunnel(ACTIVITY_ID, startTime, endTime); + + // Then + assertEquals(2L, result.get("totalClicks")); + assertEquals(1L, result.get("withReferer")); // only click2 has non-blank referer + assertEquals(0L, result.get("withUserAgent")); // both are null or blank + } + + @Test + @DisplayName("获取分享指标 - 处理null IP和null params") + void shouldHandleNullIpAndNullParams_InMetrics() { + // Given + OffsetDateTime startTime = OffsetDateTime.now(ZoneOffset.UTC).minusDays(1); + OffsetDateTime endTime = OffsetDateTime.now(ZoneOffset.UTC); + + List clicks = new ArrayList<>(); + + LinkClickEntity click1 = new LinkClickEntity(); + click1.setCode(SHORT_CODE); + click1.setIp(null); // null IP + click1.setUserAgent(USER_AGENT); + click1.setParams(null); // null params + click1.setCreatedAt(OffsetDateTime.now(ZoneOffset.UTC)); + + LinkClickEntity click2 = new LinkClickEntity(); + click2.setCode(SHORT_CODE); + click2.setIp("192.168.1.1"); + click2.setUserAgent(USER_AGENT); + click2.setParams(null); // null params + click2.setCreatedAt(OffsetDateTime.now(ZoneOffset.UTC)); + + clicks.add(click1); + clicks.add(click2); + + when(linkClickRepository.findByActivityIdAndCreatedAtBetween(ACTIVITY_ID, startTime, endTime)) + .thenReturn(clicks); + + // When + ShareMetricsResponse result = shareTrackingService.getShareMetrics(ACTIVITY_ID, startTime, endTime); + + // Then + assertEquals(2L, result.getTotalClicks()); + assertEquals(1L, result.getUniqueVisitors()); // only click2 has IP + + // Both clicks should have "unknown" source due to null params + Map expectedSources = Map.of("unknown", 2L); + assertEquals(expectedSources, result.getSourceDistribution()); + } + + @Test + @DisplayName("记录点击 - 处理null params不抛异常") + void shouldHandleNullParams_WhenRecordClick() { + // When + shareTrackingService.recordClick(SHORT_CODE, IP, USER_AGENT, REFERER, null); + + // Then + verify(linkClickRepository).save(argThat(entity -> { + assertEquals(SHORT_CODE, entity.getCode()); + assertNull(entity.getParams()); // params should remain null + return true; + })); + } }