From f815fdf5b846a17cd88e950489e9762e11212db1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 3 Mar 2026 11:33:49 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=8F=90=E5=8D=87ActivityController?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96=E7=8E=87=20-=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E13=E4=B8=AAAPI=E5=A5=91=E7=BA=A6=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增创建/更新/获取活动测试 - 新增活动统计和关系图测试 - 新增排行榜分页测试(topN, page, size边界条件) - 新增排行榜CSV导出测试(带/不带topN) - 新增null/负数/无效参数处理测试 - 新增页码超出范围返回空列表测试 覆盖率提升: - Controller包: 67% → 73% (+6%) - 指令覆盖率: 85% → 86% (+1%) - 总分支覆盖率: 62% (保持) 距离70%目标还需47个分支,完成度90% --- .claude/settings.local.json | 5 +- .../ActivityControllerContractTest.java | 241 +++++++++++++++++- 2 files changed, 242 insertions(+), 4 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8f01af5..a25f516 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -80,7 +80,10 @@ "Bash(git commit -m \"test: 提升ActivityService测试覆盖率 - 新增21个边界条件和异常处理测试\n\n- 新增calculateReward边界条件测试(null/empty tiers, 无达成层级)\n- 新增calculateMultiLevelReward的null规则测试\n- 新增generateLeaderboardCsv的topN边界条件测试\n- 新增getActivityGraph的maxDepth和limit边界条件测试\n- 新增API密钥验证异常路径测试(revoked, invalid hash, missing)\n- 新增文件上传null contentType测试\n- 新增活动访问权限额外场景测试\n\n覆盖率提升:\n- 分支覆盖率: 57.8% → 61% \\(+3.2%\\)\n- Service包: 74% → 83% \\(+9%\\)\n- 指令覆盖率: 84% → 85% \\(+1%\\)\n- 行覆盖率: 90.56% → 92% \\(+1.44%\\)\n\n距离70%目标还需55个分支,完成度87%\")", "Bash(mvn test -Dtest=ShareConfigServiceTest -q)", "Bash(mvn clean test jacoco:report -q 2>&1 | grep -A 5 \"Tests run:\" | tail -20)", - "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(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%\")" ] } } diff --git a/src/test/java/com/mosquito/project/controller/ActivityControllerContractTest.java b/src/test/java/com/mosquito/project/controller/ActivityControllerContractTest.java index 897b943..e4e4873 100644 --- a/src/test/java/com/mosquito/project/controller/ActivityControllerContractTest.java +++ b/src/test/java/com/mosquito/project/controller/ActivityControllerContractTest.java @@ -1,6 +1,12 @@ package com.mosquito.project.controller; +import com.fasterxml.jackson.databind.ObjectMapper; import com.mosquito.project.domain.Activity; +import com.mosquito.project.domain.LeaderboardEntry; +import com.mosquito.project.dto.ActivityGraphResponse; +import com.mosquito.project.dto.ActivityStatsResponse; +import com.mosquito.project.dto.CreateActivityRequest; +import com.mosquito.project.dto.UpdateActivityRequest; import com.mosquito.project.service.ActivityService; import com.mosquito.project.support.TestAuthSupport; import org.junit.jupiter.api.Test; @@ -12,10 +18,16 @@ import org.springframework.http.MediaType; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; 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.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(ActivityController.class) @Import(com.mosquito.project.config.ControllerTestConfig.class) @@ -29,6 +41,9 @@ class ActivityControllerContractTest { @Autowired private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @MockBean private ActivityService activityService; @@ -47,4 +62,224 @@ class ActivityControllerContractTest { .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.id").value(1)); } + + @Test + void shouldCreateActivity() throws Exception { + CreateActivityRequest request = new CreateActivityRequest(); + request.setName("新活动"); + request.setStartTime(ZonedDateTime.now()); + request.setEndTime(ZonedDateTime.now().plusDays(7)); + + Activity activity = new Activity(); + activity.setId(1L); + activity.setName("新活动"); + when(activityService.createActivity(any(CreateActivityRequest.class))).thenReturn(activity); + + mockMvc.perform(post("/api/v1/activities") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header("X-API-Key", TestAuthSupport.RAW_API_KEY) + .header("Authorization", "Bearer test-token")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.code").value(201)) + .andExpect(jsonPath("$.data.id").value(1)); + } + + @Test + void shouldGetActivities() throws Exception { + List activities = new ArrayList<>(); + Activity activity = new Activity(); + activity.setId(1L); + activity.setName("活动1"); + activities.add(activity); + when(activityService.getAllActivities()).thenReturn(activities); + + mockMvc.perform(get("/api/v1/activities") + .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[0].id").value(1)); + } + + @Test + void shouldUpdateActivity() throws Exception { + UpdateActivityRequest request = new UpdateActivityRequest(); + request.setName("更新活动"); + request.setStartTime(ZonedDateTime.now()); + request.setEndTime(ZonedDateTime.now().plusDays(7)); + + Activity activity = new Activity(); + activity.setId(1L); + activity.setName("更新活动"); + when(activityService.updateActivity(eq(1L), any(UpdateActivityRequest.class))).thenReturn(activity); + + mockMvc.perform(put("/api/v1/activities/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header("X-API-Key", TestAuthSupport.RAW_API_KEY) + .header("Authorization", "Bearer test-token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.id").value(1)); + } + + @Test + void shouldGetActivityStats() throws Exception { + ActivityStatsResponse stats = new ActivityStatsResponse(100L, 50L, new ArrayList<>()); + when(activityService.getActivityStats(1L)).thenReturn(stats); + + mockMvc.perform(get("/api/v1/activities/1/stats") + .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.totalParticipants").value(100)); + } + + @Test + void shouldGetActivityGraph() throws Exception { + ActivityGraphResponse graph = new ActivityGraphResponse(new ArrayList<>(), new ArrayList<>()); + when(activityService.getActivityGraph(anyLong(), any(), any(), any())).thenReturn(graph); + + mockMvc.perform(get("/api/v1/activities/1/graph") + .param("rootUserId", "1") + .param("maxDepth", "3") + .param("limit", "1000") + .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)); + } + + @Test + void shouldGetLeaderboard_withTopN() throws Exception { + List entries = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + entries.add(new LeaderboardEntry((long) i, "用户" + i, 100 - i)); + } + when(activityService.getLeaderboard(1L)).thenReturn(entries); + + mockMvc.perform(get("/api/v1/activities/1/leaderboard") + .param("topN", "5") + .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.length()").value(5)); + } + + @Test + void shouldGetLeaderboard_withPagination() throws Exception { + List entries = new ArrayList<>(); + for (int i = 1; i <= 50; i++) { + entries.add(new LeaderboardEntry((long) i, "用户" + i, 100 - i)); + } + when(activityService.getLeaderboard(1L)).thenReturn(entries); + + mockMvc.perform(get("/api/v1/activities/1/leaderboard") + .param("page", "1") + .param("size", "20") + .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.length()").value(20)) + .andExpect(jsonPath("$.meta.pagination.page").value(1)) + .andExpect(jsonPath("$.meta.pagination.size").value(20)) + .andExpect(jsonPath("$.meta.pagination.total").value(50)); + } + + @Test + void shouldGetLeaderboard_returnEmptyWhenPageExceedsTotal() throws Exception { + List entries = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + entries.add(new LeaderboardEntry((long) i, "用户" + i, 100 - i)); + } + when(activityService.getLeaderboard(1L)).thenReturn(entries); + + mockMvc.perform(get("/api/v1/activities/1/leaderboard") + .param("page", "10") + .param("size", "20") + .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.length()").value(0)); + } + + @Test + void shouldGetLeaderboard_handleNullPage() throws Exception { + List entries = new ArrayList<>(); + entries.add(new LeaderboardEntry(1L, "用户1", 100)); + when(activityService.getLeaderboard(1L)).thenReturn(entries); + + mockMvc.perform(get("/api/v1/activities/1/leaderboard") + .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("$.meta.pagination.page").value(0)); + } + + @Test + void shouldGetLeaderboard_handleNegativePage() throws Exception { + List entries = new ArrayList<>(); + entries.add(new LeaderboardEntry(1L, "用户1", 100)); + when(activityService.getLeaderboard(1L)).thenReturn(entries); + + mockMvc.perform(get("/api/v1/activities/1/leaderboard") + .param("page", "-1") + .accept(MediaType.APPLICATION_JSON) + .header("X-API-Key", TestAuthSupport.RAW_API_KEY) + .header("Authorization", "Bearer test-token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.meta.pagination.page").value(0)); + } + + @Test + void shouldGetLeaderboard_handleInvalidSize() throws Exception { + List entries = new ArrayList<>(); + entries.add(new LeaderboardEntry(1L, "用户1", 100)); + when(activityService.getLeaderboard(1L)).thenReturn(entries); + + mockMvc.perform(get("/api/v1/activities/1/leaderboard") + .param("size", "0") + .accept(MediaType.APPLICATION_JSON) + .header("X-API-Key", TestAuthSupport.RAW_API_KEY) + .header("Authorization", "Bearer test-token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.meta.pagination.size").value(20)); + } + + @Test + void shouldExportLeaderboard_withoutTopN() throws Exception { + when(activityService.generateLeaderboardCsv(1L)).thenReturn("userId,userName,score\n1,用户1,100\n"); + + mockMvc.perform(get("/api/v1/activities/1/leaderboard/export") + .header("X-API-Key", TestAuthSupport.RAW_API_KEY) + .header("Authorization", "Bearer test-token")) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Type", "text/csv;charset=UTF-8")) + .andExpect(header().string("Content-Disposition", "attachment; filename=\"leaderboard_1.csv\"")); + } + + @Test + void shouldExportLeaderboard_withTopN() throws Exception { + when(activityService.generateLeaderboardCsv(1L, 10)).thenReturn("userId,userName,score\n1,用户1,100\n"); + + mockMvc.perform(get("/api/v1/activities/1/leaderboard/export") + .param("topN", "10") + .header("X-API-Key", TestAuthSupport.RAW_API_KEY) + .header("Authorization", "Bearer test-token")) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Type", "text/csv;charset=UTF-8")); + } }