test: 提升ActivityService测试覆盖率 - 新增21个边界条件和异常处理测试

- 新增calculateReward边界条件测试(null/empty tiers, 无达成层级)
- 新增calculateMultiLevelReward的null规则测试
- 新增generateLeaderboardCsv的topN边界条件测试
- 新增getActivityGraph的maxDepth和limit边界条件测试
- 新增API密钥验证异常路径测试(revoked, invalid hash, missing)
- 新增文件上传null contentType测试
- 新增活动访问权限额外场景测试

覆盖率提升:
- 分支覆盖率: 57.8% → 61% (+3.2%)
- Service包: 74% → 83% (+9%)
- 指令覆盖率: 84% → 85% (+1%)
- 行覆盖率: 90.56% → 92% (+1.44%)

距离70%目标还需55个分支,完成度87%
This commit is contained in:
Your Name
2026-03-03 11:17:33 +08:00
parent 92218e65fe
commit 376bbcd99a
3 changed files with 657 additions and 1 deletions

View File

@@ -394,4 +394,268 @@ class ActivityServiceCoverageTest {
throw new RuntimeException("hash api key failed", e);
}
}
@Test
void calculateReward_shouldReturnZeroWhenNoTiers() {
Activity activity = new Activity();
activity.setRewardTiers(null);
Reward reward = activityService.calculateReward(activity, 5);
assertEquals(new Reward(0), reward);
}
@Test
void calculateReward_shouldReturnZeroWhenEmptyTiers() {
Activity activity = new Activity();
activity.setRewardTiers(List.of());
Reward reward = activityService.calculateReward(activity, 5);
assertEquals(new Reward(0), reward);
}
@Test
void calculateReward_shouldReturnZeroWhenNoTierAchieved() {
Activity activity = new Activity();
activity.setRewardTiers(List.of(
new RewardTier(5, new Reward(100)),
new RewardTier(10, new Reward(200))
));
Reward reward = activityService.calculateReward(activity, 3);
assertEquals(new Reward(0), reward);
}
@Test
void calculateReward_shouldReturnFirstTierInDifferentialMode() {
Activity activity = new Activity();
activity.setRewardTiers(List.of(
new RewardTier(1, new Reward(100))
));
Reward reward = activityService.calculateReward(activity, 1);
assertEquals(new Reward(100), reward);
}
@Test
void calculateMultiLevelReward_shouldReturnZeroWhenRulesNull() {
Activity activity = new Activity();
activity.setMultiLevelRewardRules(null);
Reward reward = activityService.calculateMultiLevelReward(activity, new Reward(100), 2);
assertEquals(new Reward(0), reward);
}
@Test
void generateLeaderboardCsv_shouldHandleNullTopN() {
when(activityRepository.existsById(1L)).thenReturn(true);
when(userInviteRepository.countInvitesByActivityIdGroupByInviter(1L)).thenReturn(List.of(
new Object[]{1L, 5L},
new Object[]{2L, 3L}
));
String csv = activityService.generateLeaderboardCsv(1L, null);
assertNotNull(csv);
assertEquals(3, csv.lines().count());
}
@Test
void generateLeaderboardCsv_shouldHandleZeroTopN() {
when(activityRepository.existsById(1L)).thenReturn(true);
when(userInviteRepository.countInvitesByActivityIdGroupByInviter(1L)).thenReturn(
java.util.Collections.singletonList(new Object[]{1L, 5L})
);
String csv = activityService.generateLeaderboardCsv(1L, 0);
assertNotNull(csv);
// topN < 1 uses entries.size(), so 1 header + 1 data row = 2 lines
assertEquals(2, csv.lines().count());
}
@Test
void generateLeaderboardCsv_shouldHandleTopNLargerThanSize() {
when(activityRepository.existsById(1L)).thenReturn(true);
when(userInviteRepository.countInvitesByActivityIdGroupByInviter(1L)).thenReturn(
java.util.Collections.singletonList(new Object[]{1L, 5L})
);
String csv = activityService.generateLeaderboardCsv(1L, 100);
assertNotNull(csv);
assertEquals(2, csv.lines().count());
}
@Test
void generateLeaderboardCsv_shouldUseDefaultOverload() {
when(activityRepository.existsById(1L)).thenReturn(true);
when(userInviteRepository.countInvitesByActivityIdGroupByInviter(1L)).thenReturn(
java.util.Collections.singletonList(new Object[]{1L, 5L})
);
String csv = activityService.generateLeaderboardCsv(1L);
assertNotNull(csv);
assertEquals(2, csv.lines().count());
}
@Test
void getActivityGraph_shouldHandleNullMaxDepth() {
when(activityRepository.existsById(1L)).thenReturn(true);
UserInviteEntity a = new UserInviteEntity();
a.setActivityId(1L);
a.setInviterUserId(1L);
a.setInviteeUserId(2L);
when(userInviteRepository.findByActivityId(1L)).thenReturn(List.of(a));
var graph = activityService.getActivityGraph(1L, 1L, null, null);
assertEquals(1, graph.getEdges().size());
}
@Test
void getActivityGraph_shouldHandleZeroMaxDepth() {
when(activityRepository.existsById(1L)).thenReturn(true);
UserInviteEntity a = new UserInviteEntity();
a.setActivityId(1L);
a.setInviterUserId(1L);
a.setInviteeUserId(2L);
when(userInviteRepository.findByActivityId(1L)).thenReturn(List.of(a));
var graph = activityService.getActivityGraph(1L, 1L, 0, null);
// maxDepth < 1 uses default 1, so edges will be added
assertEquals(1, graph.getEdges().size());
}
@Test
void getActivityGraph_shouldHandleZeroLimit() {
when(activityRepository.existsById(1L)).thenReturn(true);
UserInviteEntity a = new UserInviteEntity();
a.setActivityId(1L);
a.setInviterUserId(1L);
a.setInviteeUserId(2L);
when(userInviteRepository.findByActivityId(1L)).thenReturn(List.of(a));
var graph = activityService.getActivityGraph(1L, 1L, 1, 0);
// limit < 1 uses default 1000, so edges will be added
assertEquals(1, graph.getEdges().size());
}
@Test
void getActivityGraph_shouldStopAtMaxDepth() {
when(activityRepository.existsById(1L)).thenReturn(true);
UserInviteEntity a = new UserInviteEntity();
a.setActivityId(1L);
a.setInviterUserId(1L);
a.setInviteeUserId(2L);
UserInviteEntity b = new UserInviteEntity();
b.setActivityId(1L);
b.setInviterUserId(2L);
b.setInviteeUserId(3L);
UserInviteEntity c = new UserInviteEntity();
c.setActivityId(1L);
c.setInviterUserId(3L);
c.setInviteeUserId(4L);
when(userInviteRepository.findByActivityId(1L)).thenReturn(List.of(a, b, c));
var graph = activityService.getActivityGraph(1L, 1L, 2, 1000);
assertEquals(2, graph.getEdges().size());
}
@Test
void validateApiKeyByPrefix_shouldRejectRevokedKey() {
String rawKey = "test-api-key-12345";
byte[] salt = new byte[16];
Arrays.fill(salt, (byte) 1);
ApiKeyEntity entity = buildApiKeyEntity(rawKey, salt);
entity.setRevokedAt(java.time.OffsetDateTime.now());
when(apiKeyRepository.findByKeyPrefix(entity.getKeyPrefix())).thenReturn(Optional.of(entity));
assertThrows(InvalidApiKeyException.class, () -> activityService.validateApiKeyByPrefixAndMarkUsed(rawKey));
}
@Test
void validateApiKeyByPrefix_shouldRejectInvalidHash() {
String rawKey = "test-api-key-12345";
String wrongKey = "wrong-key-123456";
byte[] salt = new byte[16];
Arrays.fill(salt, (byte) 1);
ApiKeyEntity entity = buildApiKeyEntity(rawKey, salt);
when(apiKeyRepository.findByKeyPrefix(wrongKey.substring(0, 12))).thenReturn(Optional.of(entity));
assertThrows(InvalidApiKeyException.class, () -> activityService.validateApiKeyByPrefixAndMarkUsed(wrongKey));
}
@Test
void validateApiKeyByPrefix_shouldRejectMissingKey() {
when(apiKeyRepository.findByKeyPrefix("wrong-key-12")).thenReturn(Optional.empty());
assertThrows(InvalidApiKeyException.class, () -> activityService.validateApiKeyByPrefixAndMarkUsed("wrong-key-12345"));
}
@Test
void validateAndMarkApiKeyUsed_shouldRejectRevokedKey() {
String rawKey = "test-api-key-98765";
byte[] salt = new byte[16];
Arrays.fill(salt, (byte) 2);
ApiKeyEntity entity = buildApiKeyEntity(rawKey, salt);
entity.setId(5L);
entity.setRevokedAt(java.time.OffsetDateTime.now());
when(apiKeyRepository.findById(5L)).thenReturn(Optional.of(entity));
assertThrows(InvalidApiKeyException.class, () -> activityService.validateAndMarkApiKeyUsed(5L, rawKey));
}
@Test
void validateAndMarkApiKeyUsed_shouldRejectInvalidHash() {
String rawKey = "test-api-key-98765";
String wrongKey = "wrong-key-987654";
byte[] salt = new byte[16];
Arrays.fill(salt, (byte) 2);
ApiKeyEntity entity = buildApiKeyEntity(rawKey, salt);
entity.setId(5L);
when(apiKeyRepository.findById(5L)).thenReturn(Optional.of(entity));
assertThrows(InvalidApiKeyException.class, () -> activityService.validateAndMarkApiKeyUsed(5L, wrongKey));
}
@Test
void validateAndMarkApiKeyUsed_shouldRejectMissingKey() {
when(apiKeyRepository.findById(99L)).thenReturn(Optional.empty());
assertThrows(com.mosquito.project.exception.ApiKeyNotFoundException.class, () -> activityService.validateAndMarkApiKeyUsed(99L, "any-key"));
}
@Test
void uploadCustomizationImage_shouldRejectNullContentType() {
MockMultipartFile file = new MockMultipartFile("file", "note.txt", null, "hello".getBytes());
assertThrows(FileUploadException.class, () -> activityService.uploadCustomizationImage(1L, file));
}
@Test
void accessActivity_shouldAllowWhenTargetUsersNull() {
Activity activity = new Activity();
activity.setTargetUserIds(null);
User user = new User(3L, "user");
assertDoesNotThrow(() -> activityService.accessActivity(activity, user));
}
@Test
void accessActivity_shouldAllowWhenUserInTargetUsers() {
Activity activity = new Activity();
activity.setTargetUserIds(Set.of(1L, 2L, 3L));
User user = new User(3L, "user");
assertDoesNotThrow(() -> activityService.accessActivity(activity, user));
}
}