|
@@ -0,0 +1,224 @@
|
|
|
|
|
+package com.etotem.cfc.controller.task;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
|
|
+import com.etotem.cfc.entity.Task;
|
|
|
|
|
+import com.etotem.cfc.service.TaskService;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.mockito.Mock;
|
|
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
|
|
+import org.springframework.http.MediaType;
|
|
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
|
|
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * TDD 覆盖驱动(S-cfc-29 / 能力键 task.history.view):
|
|
|
|
|
+ * POST /api/tasks/history 获取任务历史(分页 + 可选 category 过滤)的契约级单测。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 契约(对应 SC-P-94「我的成就·完成任务 → 历史完成任务页面」、API_REFERENCE.md 任务域、
|
|
|
|
|
+ * TaskController.getTaskHistory):
|
|
|
|
|
+ * - 正常路径:body.memberId 明确传入 → taskService.getTaskHistory(memberId, page, size) → Result<Page<Task>> code=200
|
|
|
|
|
+ * - 正常路径·分类:body.category 非空 → 走 taskService.getTaskHistoryByCategory(memberId, page, size, category)
|
|
|
|
|
+ * - 分页边界:body.page/size 显式传入 → 原样传给 Service;不传 → 默认 page=1 / size=10
|
|
|
|
|
+ * - 成员边界:body.memberId 缺省时回退 JwtInterceptor 注入的 currentMemberId 属性
|
|
|
|
|
+ * - 空态:无完成记录 → 统一 Result code=200 且 data.total=0(前端据此展示「暂无已完成任务」空态)
|
|
|
|
|
+ * - 传输层契约:统一 @PostMapping(GET 同路径 → 405); @RequestBody 缺失 → 400
|
|
|
|
|
+ * - 角色/权限口径:getTaskHistory handler 无 @RequestAttribute("role"/"roles") 检查,角色不在本层判定;
|
|
|
|
|
+ * 授权由 JwtInterceptor(有效 token 注入 currentMemberId)+ Service 层成员归属校验承担,
|
|
|
|
|
+ * 故本测试覆盖 memberId 解析与传递正确性而非角色分支(teacher|admin 不适用本 handler)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 测试隔离:MockMvc standalone + mock TaskService,无 Spring 上下文 / 无 DB / 无外部服务(微信等)。
|
|
|
|
|
+ */
|
|
|
|
|
+public class TaskControllerGetTaskHistoryTest {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String URL = "/api/tasks/history";
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private TaskService taskService;
|
|
|
|
|
+
|
|
|
|
|
+ private MockMvc mockMvc;
|
|
|
|
|
+ private TaskController controller;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ public void setUp() {
|
|
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
|
|
+ controller = new TaskController();
|
|
|
|
|
+ // getTaskHistory 仅依赖 taskService;其余 @Resource 字段本方法不触碰,保持 null 无碍
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "taskService", taskService);
|
|
|
|
|
+ mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 正常路径:默认分页 + 空态 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void getTaskHistory_success_defaultPageSize_returnsPage() throws Exception {
|
|
|
|
|
+ Long memberId = 11L;
|
|
|
|
|
+ Page<Task> mockPage = new Page<>();
|
|
|
|
|
+ mockPage.setTotal(0); // 空态:无完成任务
|
|
|
|
|
+ when(taskService.getTaskHistory(eq(memberId), eq(1), eq(10))).thenReturn(mockPage);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
|
|
+ params.put("memberId", memberId);
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post(URL)
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(jsonBody(params)))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
|
|
+ .andExpect(jsonPath("$.message").value("success"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.total").value(0));
|
|
|
|
|
+
|
|
|
|
|
+ verify(taskService).getTaskHistory(eq(memberId), eq(1), eq(10));
|
|
|
|
|
+ verify(taskService, never()).getTaskHistoryByCategory(any(), any(), any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 正常路径:显式分页 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void getTaskHistory_success_explicitPageSize_forwardedToService() throws Exception {
|
|
|
|
|
+ Long memberId = 11L;
|
|
|
|
|
+ Page<Task> mockPage = new Page<>();
|
|
|
|
|
+ mockPage.setTotal(2);
|
|
|
|
|
+ when(taskService.getTaskHistory(eq(memberId), eq(2), eq(20))).thenReturn(mockPage);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
|
|
+ params.put("memberId", memberId);
|
|
|
|
|
+ params.put("page", 2);
|
|
|
|
|
+ params.put("size", 20);
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post(URL)
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(jsonBody(params)))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.total").value(2));
|
|
|
|
|
+
|
|
|
|
|
+ verify(taskService).getTaskHistory(eq(memberId), eq(2), eq(20));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 正常路径:category 过滤 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void getTaskHistory_success_withCategory_usesByCategory() throws Exception {
|
|
|
|
|
+ Long memberId = 11L;
|
|
|
|
|
+ String category = "学习类";
|
|
|
|
|
+ Page<Task> mockPage = new Page<>();
|
|
|
|
|
+ mockPage.setTotal(5);
|
|
|
|
|
+ when(taskService.getTaskHistoryByCategory(eq(memberId), eq(1), eq(10), eq(category))).thenReturn(mockPage);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
|
|
+ params.put("memberId", memberId);
|
|
|
|
|
+ params.put("category", category);
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post(URL)
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(jsonBody(params)))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.total").value(5));
|
|
|
|
|
+
|
|
|
|
|
+ verify(taskService).getTaskHistoryByCategory(eq(memberId), eq(1), eq(10), eq(category));
|
|
|
|
|
+ verify(taskService, never()).getTaskHistory(any(), any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 关键边界:memberId 回退 currentMemberId ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void getTaskHistory_memberIdMissing_fallsBackToCurrentMemberId() throws Exception {
|
|
|
|
|
+ Long currentMemberId = 99L;
|
|
|
|
|
+ Page<Task> mockPage = new Page<>();
|
|
|
|
|
+ mockPage.setTotal(1);
|
|
|
|
|
+ when(taskService.getTaskHistory(eq(currentMemberId), eq(1), eq(10))).thenReturn(mockPage);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> params = new HashMap<>(); // 不传 memberId
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post(URL)
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(jsonBody(params))
|
|
|
|
|
+ .requestAttr("currentMemberId", currentMemberId)) // JwtInterceptor 注入
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200));
|
|
|
|
|
+
|
|
|
|
|
+ verify(taskService).getTaskHistory(eq(currentMemberId), eq(1), eq(10));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 传输层契约 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void getTaskHistory_onlyPostAllowed_getReturns405() throws Exception {
|
|
|
|
|
+ mockMvc.perform(get(URL))
|
|
|
|
|
+ .andExpect(status().isMethodNotAllowed());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void getTaskHistory_missingBody_returns400() throws Exception {
|
|
|
|
|
+ // @RequestBody Map 默认 required=true:无 Body → HttpMessageNotReadableException → 400
|
|
|
|
|
+ mockMvc.perform(post(URL)
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON))
|
|
|
|
|
+ .andExpect(status().isBadRequest());
|
|
|
|
|
+
|
|
|
|
|
+ verify(taskService, never()).getTaskHistory(any(), any(), any());
|
|
|
|
|
+ verify(taskService, never()).getTaskHistoryByCategory(any(), any(), any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 直接调用级:统一 Result<T> ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void getTaskHistory_directCall_uniformResultShape() {
|
|
|
|
|
+ Long memberId = 11L;
|
|
|
|
|
+ Page<Task> mockPage = new Page<>();
|
|
|
|
|
+ mockPage.setTotal(1);
|
|
|
|
|
+ when(taskService.getTaskHistory(memberId, 1, 10)).thenReturn(mockPage);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
|
|
+ params.put("memberId", memberId);
|
|
|
|
|
+
|
|
|
|
|
+ Result<Page<Task>> result = controller.getTaskHistory(params, 99L);
|
|
|
|
|
+
|
|
|
|
|
+ assertNotNull(result);
|
|
|
|
|
+ assertEquals(Integer.valueOf(200), result.getCode());
|
|
|
|
|
+ assertEquals("success", result.getMessage());
|
|
|
|
|
+ assertEquals(mockPage, result.getData());
|
|
|
|
|
+ verify(taskService).getTaskHistory(memberId, 1, 10);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 工具方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private String jsonBody(Map<String, Object> params) {
|
|
|
|
|
+ if (params == null || params.isEmpty()) {
|
|
|
|
|
+ return "{}";
|
|
|
|
|
+ }
|
|
|
|
|
+ StringBuilder sb = new StringBuilder("{");
|
|
|
|
|
+ boolean first = true;
|
|
|
|
|
+ for (Map.Entry<String, Object> entry : params.entrySet()) {
|
|
|
|
|
+ if (!first) {
|
|
|
|
|
+ sb.append(", ");
|
|
|
|
|
+ }
|
|
|
|
|
+ first = false;
|
|
|
|
|
+ Object value = entry.getValue();
|
|
|
|
|
+ sb.append("\"").append(entry.getKey()).append("\": ");
|
|
|
|
|
+ if (value instanceof String) {
|
|
|
|
|
+ sb.append("\"").append(value).append("\"");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ sb.append(value);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return sb.append("}").toString();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|