diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b3fdf20..387fd30 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -101,7 +101,11 @@ "Bash(mvn test -Dtest=UserExperienceControllerTest 2>&1 | tail -30)", "Bash(mvn test -Dtest=UrlValidatorTest 2>&1 | tail -30)", "Bash(git add -A && git commit -m \"test: 提升Web包测试覆盖率 - 新增UrlValidator边界测试\n\n新增测试:\n- UserExperienceController: maskPhone方法测试\n- UrlValidator: IPv6公网地址、0.0.0.0地址、无效主机名、URI异常处理\n\n覆盖率提升:\n- 总体分支: 63.6% → 63.8% \\(+1个分支\\)\n- Web包: 78% → 79% \\(+1%\\)\n- 新增测试: 5个\n- 距离70%目标: 还需39个分支\n\n累计成果(本次会话):\n- 新增测试: 17个\n- 分支覆盖: +8个 \\(404→412\\)\n- Controller包: 73% → 89% \\(+16%\\)\n- Web包: 78% → 79% \\(+1%\\)\")", - "Bash(git add -A && git commit -m \"docs: 生成测试覆盖率提升进展报告\n\n生成COVERAGE_PROGRESS_REPORT_2026-03-03.md,包含:\n- 详细的覆盖率提升数据\n- 各包覆盖率分析\n- 达到70%目标的实施计划\n- 投入产出分析\n- 下一步建议\n\n本次会话总成果:\n- 新增测试: 17个\n- 分支覆盖: 62% → 63.8% \\(+1.8%\\)\n- Controller包: 73% → 89% \\(+16%\\)\n- Web包: 78% → 79% \\(+1%\\)\n- 距离70%目标: 还需39个分支(6.2%)\n- 完成度: 91%\")" + "Bash(git add -A && git commit -m \"docs: 生成测试覆盖率提升进展报告\n\n生成COVERAGE_PROGRESS_REPORT_2026-03-03.md,包含:\n- 详细的覆盖率提升数据\n- 各包覆盖率分析\n- 达到70%目标的实施计划\n- 投入产出分析\n- 下一步建议\n\n本次会话总成果:\n- 新增测试: 17个\n- 分支覆盖: 62% → 63.8% \\(+1.8%\\)\n- Controller包: 73% → 89% \\(+16%\\)\n- Web包: 78% → 79% \\(+1%\\)\n- 距离70%目标: 还需39个分支(6.2%)\n- 完成度: 91%\")", + "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个分支\")" ] } } diff --git a/src/test/java/com/mosquito/project/service/ActivityServiceCoverageTest.java b/src/test/java/com/mosquito/project/service/ActivityServiceCoverageTest.java index 0248279..2511357 100644 --- a/src/test/java/com/mosquito/project/service/ActivityServiceCoverageTest.java +++ b/src/test/java/com/mosquito/project/service/ActivityServiceCoverageTest.java @@ -658,4 +658,73 @@ class ActivityServiceCoverageTest { assertDoesNotThrow(() -> activityService.accessActivity(activity, user)); } + + @Test + void updateActivity_shouldUpdateSuccessfully() { + com.mosquito.project.dto.UpdateActivityRequest request = new com.mosquito.project.dto.UpdateActivityRequest(); + request.setName("更新后的活动"); + request.setStartTime(java.time.ZonedDateTime.now()); + request.setEndTime(java.time.ZonedDateTime.now().plusDays(10)); + + com.mosquito.project.persistence.entity.ActivityEntity entity = new com.mosquito.project.persistence.entity.ActivityEntity(); + entity.setId(1L); + entity.setName("原活动"); + when(activityRepository.findById(1L)).thenReturn(Optional.of(entity)); + when(activityRepository.save(entity)).thenReturn(entity); + + Activity result = activityService.updateActivity(1L, request); + + assertNotNull(result); + assertEquals(1L, result.getId()); + assertEquals("更新后的活动", result.getName()); + } + + @Test + void updateActivity_shouldThrowWhenActivityNotFound() { + com.mosquito.project.dto.UpdateActivityRequest request = new com.mosquito.project.dto.UpdateActivityRequest(); + request.setName("更新"); + request.setStartTime(java.time.ZonedDateTime.now()); + request.setEndTime(java.time.ZonedDateTime.now().plusDays(5)); + + when(activityRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThrows(ActivityNotFoundException.class, () -> activityService.updateActivity(999L, request)); + } + + @Test + void updateActivity_shouldThrowWhenEndTimeBeforeStartTime() { + com.mosquito.project.dto.UpdateActivityRequest request = new com.mosquito.project.dto.UpdateActivityRequest(); + request.setName("无效活动"); + request.setStartTime(java.time.ZonedDateTime.now().plusDays(10)); + request.setEndTime(java.time.ZonedDateTime.now()); + + com.mosquito.project.persistence.entity.ActivityEntity entity = new com.mosquito.project.persistence.entity.ActivityEntity(); + entity.setId(1L); + when(activityRepository.findById(1L)).thenReturn(Optional.of(entity)); + + assertThrows(InvalidActivityDataException.class, () -> activityService.updateActivity(1L, request)); + } + + @Test + void getActivityById_shouldReturnActivity() { + com.mosquito.project.persistence.entity.ActivityEntity entity = new com.mosquito.project.persistence.entity.ActivityEntity(); + entity.setId(1L); + entity.setName("测试活动"); + entity.setStartTimeUtc(java.time.OffsetDateTime.now()); + entity.setEndTimeUtc(java.time.OffsetDateTime.now().plusDays(7)); + when(activityRepository.findById(1L)).thenReturn(Optional.of(entity)); + + Activity result = activityService.getActivityById(1L); + + assertNotNull(result); + assertEquals(1L, result.getId()); + assertEquals("测试活动", result.getName()); + } + + @Test + void getActivityById_shouldThrowWhenNotFound() { + when(activityRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThrows(ActivityNotFoundException.class, () -> activityService.getActivityById(999L)); + } } diff --git a/src/test/java/com/mosquito/project/service/ApiKeyEncryptionServiceTest.java b/src/test/java/com/mosquito/project/service/ApiKeyEncryptionServiceTest.java index 3ec4351..cea3282 100644 --- a/src/test/java/com/mosquito/project/service/ApiKeyEncryptionServiceTest.java +++ b/src/test/java/com/mosquito/project/service/ApiKeyEncryptionServiceTest.java @@ -254,4 +254,62 @@ class ApiKeyEncryptionServiceTest { // 加密后应该比原文长(包含IV和tag) assertTrue(encrypted.length() > TEST_PLAIN_TEXT.length()); } + + @Test + @DisplayName("初始化 - 生产环境legacy默认密钥禁止") + void shouldFailInit_WhenLegacyDefaultKeyInProd() { + ApiKeyEncryptionService service = new ApiKeyEncryptionService(); + ReflectionTestUtils.setField(service, "encryptionKey", "default-32-byte-key-for-dev-only!!"); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("prod"); + ReflectionTestUtils.setField(service, "environment", environment); + + IllegalStateException exception = assertThrows(IllegalStateException.class, service::init); + assertEquals("Encryption key must be set in production", exception.getMessage()); + } + + @Test + @DisplayName("初始化 - 生产环境空白密钥禁止") + void shouldFailInit_WhenBlankKeyInProd() { + ApiKeyEncryptionService service = new ApiKeyEncryptionService(); + ReflectionTestUtils.setField(service, "encryptionKey", " "); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("prod"); + ReflectionTestUtils.setField(service, "environment", environment); + + IllegalStateException exception = assertThrows(IllegalStateException.class, service::init); + assertEquals("Encryption key must be set in production", exception.getMessage()); + } + + @Test + @DisplayName("初始化 - 非生产环境无environment对象") + void shouldSucceedInit_WhenEnvironmentIsNull() { + ApiKeyEncryptionService service = new ApiKeyEncryptionService(); + ReflectionTestUtils.setField(service, "encryptionKey", "default-32-byte-key-for-dev-only!"); + ReflectionTestUtils.setField(service, "environment", null); + + assertDoesNotThrow(() -> service.init()); + + // 应该能够正常加密解密 + String encrypted = service.encrypt(TEST_PLAIN_TEXT); + String decrypted = service.decrypt(encrypted); + assertEquals(TEST_PLAIN_TEXT, decrypted); + } + + @Test + @DisplayName("初始化 - 非生产环境允许默认密钥") + void shouldSucceedInit_WhenDefaultKeyInNonProd() { + ApiKeyEncryptionService service = new ApiKeyEncryptionService(); + ReflectionTestUtils.setField(service, "encryptionKey", "default-32-byte-key-for-dev-only!"); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("dev"); + ReflectionTestUtils.setField(service, "environment", environment); + + assertDoesNotThrow(() -> service.init()); + + // 应该能够正常加密解密 + String encrypted = service.encrypt(TEST_PLAIN_TEXT); + String decrypted = service.decrypt(encrypted); + assertEquals(TEST_PLAIN_TEXT, decrypted); + } }