diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index 09e0ed9..cd6c0ab 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -220,9 +220,60 @@ func (c *Controller) ListPlans(ctx *gin.Context) { // @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" // @Router /plans/{id} [put] func (c *Controller) UpdatePlan(ctx *gin.Context) { - // 占位符:此处应调用服务层或仓库层来更新计划 - c.logger.Infof("收到更新计划请求 (占位符)") - controller.SendResponse(ctx, controller.CodeSuccess, "更新计划接口占位符", PlanResponse{ID: 0, Name: "占位计划"}) + // 1. 从 URL 路径中获取 ID + idStr := ctx.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + controller.SendErrorResponse(ctx, controller.CodeBadRequest, "无效的计划ID格式") + return + } + + // 2. 绑定请求体 + var req UpdatePlanRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorResponse(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error()) + return + } + + // 3. 将请求转换为模型(转换函数带校验) + planToUpdate, err := PlanFromUpdateRequest(&req) + if err != nil { + controller.SendErrorResponse(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error()) + return + } + planToUpdate.ID = uint(id) // 确保ID被设置 + + // 4. 检查计划是否存在 + _, err = c.planRepo.GetBasicPlanByID(uint(id)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + controller.SendErrorResponse(ctx, controller.CodeNotFound, "计划不存在") + return + } + c.logger.Errorf("获取计划详情失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "获取计划详情时发生内部错误") + return + } + + // 5. 调用仓库方法更新计划 + if err := c.planRepo.UpdatePlan(planToUpdate); err != nil { + controller.SendErrorResponse(ctx, controller.CodeBadRequest, "更新计划失败: "+err.Error()) + return + } + + // 6. 获取更新后的完整计划用于响应 + updatedPlan, err := c.planRepo.GetPlanByID(uint(id)) + if err != nil { + c.logger.Errorf("获取更新后的计划详情失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "获取更新后计划详情时发生内部错误") + return + } + + // 7. 将模型转换为响应 DTO + resp := PlanToResponse(updatedPlan) + + // 8. 发送成功响应 + controller.SendResponse(ctx, controller.CodeSuccess, "计划更新成功", resp) } // DeletePlan godoc diff --git a/internal/app/controller/plan/plan_controller_test.go b/internal/app/controller/plan/plan_controller_test.go index f4a535a..628a1f7 100644 --- a/internal/app/controller/plan/plan_controller_test.go +++ b/internal/app/controller/plan/plan_controller_test.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "net/http/httptest" + "strconv" "testing" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" @@ -19,35 +20,46 @@ import ( // MockPlanRepository 是 repository.PlanRepository 的一个模拟实现,用于测试 type MockPlanRepository struct { - CreatePlanFunc func(plan *models.Plan) error - GetPlanByIDFunc func(id uint) (*models.Plan, error) + // CreatePlanFunc 模拟 CreatePlan 方法的行为 + CreatePlanFunc func(plan *models.Plan) error + // GetPlanByIDFunc 模拟 GetPlanByID 方法的行为 + GetPlanByIDFunc func(id uint) (*models.Plan, error) + // GetBasicPlanByIDFunc 模拟 GetBasicPlanByID 方法的行为 + GetBasicPlanByIDFunc func(id uint) (*models.Plan, error) + // ListBasicPlansFunc 模拟 ListBasicPlans 方法的行为 ListBasicPlansFunc func() ([]models.Plan, error) - DeletePlanFunc func(id uint) error + // UpdatePlanFunc 模拟 UpdatePlan 方法的行为 + UpdatePlanFunc func(plan *models.Plan) error + // DeletePlanFunc 模拟 DeletePlan 方法的行为 + DeletePlanFunc func(id uint) error } +// ListBasicPlans 实现了 MockPlanRepository 接口的 ListBasicPlans 方法 func (m *MockPlanRepository) ListBasicPlans() ([]models.Plan, error) { return m.ListBasicPlansFunc() } +// GetBasicPlanByID 实现了 MockPlanRepository 接口的 GetBasicPlanByID 方法 func (m *MockPlanRepository) GetBasicPlanByID(id uint) (*models.Plan, error) { - panic("implement me") + return m.GetBasicPlanByIDFunc(id) } +// GetPlanByID 实现了 MockPlanRepository 接口的 GetPlanByID 方法 func (m *MockPlanRepository) GetPlanByID(id uint) (*models.Plan, error) { return m.GetPlanByIDFunc(id) } +// CreatePlan 实现了 MockPlanRepository 接口的 CreatePlan 方法 func (m *MockPlanRepository) CreatePlan(plan *models.Plan) error { - if m.CreatePlanFunc != nil { - return m.CreatePlanFunc(plan) - } - return nil + return m.CreatePlanFunc(plan) } +// UpdatePlan 实现了 MockPlanRepository 接口的 UpdatePlan 方法 func (m *MockPlanRepository) UpdatePlan(plan *models.Plan) error { - panic("implement me") + return m.UpdatePlanFunc(plan) } +// DeletePlan 实现了 MockPlanRepository 接口的 DeletePlan 方法 func (m *MockPlanRepository) DeletePlan(id uint) error { return m.DeletePlanFunc(id) } @@ -61,14 +73,16 @@ func setupTestRouter(repo repository.PlanRepository) *gin.Engine { router.POST("/plans", planController.CreatePlan) router.GET("/plans/:id", planController.GetPlan) router.GET("/plans", planController.ListPlans) + router.PUT("/plans/:id", planController.UpdatePlan) router.DELETE("/plans/:id", planController.DeletePlan) return router } -// TestController_CreatePlan [保持原样,不做任何修改] +// TestController_CreatePlan 测试 CreatePlan 方法 func TestController_CreatePlan(t *testing.T) { t.Run("成功-创建包含任务的计划", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) + // 模拟仓库行为:CreatePlan 成功时,为计划和任务分配ID mockRepo := &MockPlanRepository{ CreatePlanFunc: func(plan *models.Plan) error { plan.ID = 1 @@ -79,8 +93,10 @@ func TestController_CreatePlan(t *testing.T) { return nil }, } + // 设置 Gin 路由器,并注入模拟仓库 router := setupTestRouter(mockRepo) + // 准备请求体 reqBody := CreatePlanRequest{ Name: "Test Plan with Tasks", ExecutionType: models.PlanExecutionTypeManual, @@ -91,23 +107,29 @@ func TestController_CreatePlan(t *testing.T) { } bodyBytes, _ := json.Marshal(reqBody) + // 创建 HTTP 请求 req, _ := http.NewRequest(http.MethodPost, "/plans", bytes.NewBuffer(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - // Act + // Act (执行阶段) + // 发送 HTTP 请求到路由器 router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) + // 验证 HTTP 状态码 assert.Equal(t, http.StatusOK, w.Code) + // 解析响应体 var resp controller.Response err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) + // 验证业务响应码和消息 assert.Equal(t, controller.CodeCreated, resp.Code) assert.Equal(t, "计划创建成功", resp.Message) + // 验证返回数据中的计划ID dataMap, ok := resp.Data.(map[string]interface{}) assert.True(t, ok) assert.Equal(t, float64(1), dataMap["id"]) @@ -117,7 +139,8 @@ func TestController_CreatePlan(t *testing.T) { // TestController_GetPlan 是为 GetPlan 方法新增的单元测试函数 func TestController_GetPlan(t *testing.T) { t.Run("成功-获取计划详情", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) + // 模拟仓库行为:GetPlanByID 成功时返回一个计划 mockRepo := &MockPlanRepository{ GetPlanByIDFunc: func(id uint) (*models.Plan, error) { assert.Equal(t, uint(1), id) @@ -128,14 +151,16 @@ func TestController_GetPlan(t *testing.T) { }, nil }, } + // 设置 Gin 路由器 router := setupTestRouter(mockRepo) w := httptest.NewRecorder() + // 创建 HTTP 请求 req, _ := http.NewRequest(http.MethodGet, "/plans/1", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response @@ -149,7 +174,8 @@ func TestController_GetPlan(t *testing.T) { }) t.Run("成功-获取内容为空的计划详情", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) + // 模拟仓库行为:GetPlanByID 成功时返回一个任务列表为空的计划 mockRepo := &MockPlanRepository{ GetPlanByIDFunc: func(id uint) (*models.Plan, error) { assert.Equal(t, uint(3), id) @@ -165,10 +191,10 @@ func TestController_GetPlan(t *testing.T) { w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans/3", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response @@ -188,7 +214,8 @@ func TestController_GetPlan(t *testing.T) { }) t.Run("失败-计划不存在", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) + // 模拟仓库行为:GetPlanByID 返回记录未找到错误 mockRepo := &MockPlanRepository{ GetPlanByIDFunc: func(id uint) (*models.Plan, error) { return nil, gorm.ErrRecordNotFound @@ -198,10 +225,10 @@ func TestController_GetPlan(t *testing.T) { w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans/999", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response @@ -213,16 +240,18 @@ func TestController_GetPlan(t *testing.T) { }) t.Run("失败-无效的ID格式", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) + // 模拟仓库为空,因为预期不会调用仓库方法 mockRepo := &MockPlanRepository{} router := setupTestRouter(mockRepo) w := httptest.NewRecorder() + // 创建带有无效ID格式的 HTTP 请求 req, _ := http.NewRequest(http.MethodGet, "/plans/abc", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response @@ -234,8 +263,9 @@ func TestController_GetPlan(t *testing.T) { }) t.Run("失败-仓库层内部错误", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) internalErr := errors.New("database connection lost") + // 模拟仓库行为:GetPlanByID 返回内部错误 mockRepo := &MockPlanRepository{ GetPlanByIDFunc: func(id uint) (*models.Plan, error) { return nil, internalErr @@ -245,10 +275,10 @@ func TestController_GetPlan(t *testing.T) { w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans/1", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response @@ -260,14 +290,16 @@ func TestController_GetPlan(t *testing.T) { }) } -// TestController_ListPlans tests the ListPlans method +// TestController_ListPlans 测试 ListPlans 方法 func TestController_ListPlans(t *testing.T) { t.Run("成功-获取计划列表", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) + // 模拟返回的计划列表 mockPlans := []models.Plan{ {Model: gorm.Model{ID: 1}, Name: "Plan 1", ContentType: models.PlanContentTypeTasks}, {Model: gorm.Model{ID: 2}, Name: "Plan 2", ContentType: models.PlanContentTypeTasks}, } + // 模拟仓库行为:ListBasicPlans 成功时返回计划列表 mockRepo := &MockPlanRepository{ ListBasicPlansFunc: func() ([]models.Plan, error) { return mockPlans, nil @@ -277,10 +309,10 @@ func TestController_ListPlans(t *testing.T) { w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response @@ -303,7 +335,8 @@ func TestController_ListPlans(t *testing.T) { }) t.Run("成功-返回空列表", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) + // 模拟仓库行为:ListBasicPlans 返回空列表 mockRepo := &MockPlanRepository{ ListBasicPlansFunc: func() ([]models.Plan, error) { return []models.Plan{}, nil @@ -313,10 +346,10 @@ func TestController_ListPlans(t *testing.T) { w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response @@ -336,8 +369,9 @@ func TestController_ListPlans(t *testing.T) { }) t.Run("失败-仓库层返回错误", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) dbErr := errors.New("db error") + // 模拟仓库行为:ListBasicPlans 返回数据库错误 mockRepo := &MockPlanRepository{ ListBasicPlansFunc: func() ([]models.Plan, error) { return nil, dbErr @@ -347,10 +381,10 @@ func TestController_ListPlans(t *testing.T) { w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/plans", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response @@ -362,24 +396,347 @@ func TestController_ListPlans(t *testing.T) { }) } +// TestController_UpdatePlan 是 UpdatePlan 的测试函数 +func TestController_UpdatePlan(t *testing.T) { + t.Run("成功-更新计划", func(t *testing.T) { + // Arrange (准备阶段) + planID := uint(1) + updatedName := "Updated Plan Name" + // 模拟一个已存在的计划 + mockPlan := &models.Plan{ + Model: gorm.Model{ID: planID}, + Name: "Original Plan", + Description: "Original Description", + ContentType: models.PlanContentTypeTasks, + } + // 配置模拟仓库的行为 + mockRepo := &MockPlanRepository{ + // 模拟 GetBasicPlanByID 成功返回现有计划 + GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) { + assert.Equal(t, planID, id) + return mockPlan, nil + }, + // 模拟 UpdatePlan 成功更新计划,并更新 mockPlan 的名称 + UpdatePlanFunc: func(plan *models.Plan) error { + assert.Equal(t, planID, plan.ID) + assert.Equal(t, updatedName, plan.Name) + mockPlan.Name = plan.Name // 模拟更新操作 + return nil + }, + // 模拟 GetPlanByID 返回更新后的计划 + GetPlanByIDFunc: func(id uint) (*models.Plan, error) { + assert.Equal(t, planID, id) + return mockPlan, nil // 返回已更新的 mockPlan + }, + } + // 设置 Gin 路由器,并注入模拟仓库 + router := setupTestRouter(mockRepo) + + // 准备更新请求体 + reqBody := UpdatePlanRequest{ + Name: updatedName, + Description: "Updated Description", + ExecutionType: models.PlanExecutionTypeAutomatic, + ContentType: models.PlanContentTypeTasks, + } + bodyBytes, _ := json.Marshal(reqBody) + + // 创建 HTTP PUT 请求 + req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + // Act (执行阶段) + // 发送 HTTP 请求到路由器 + router.ServeHTTP(w, req) + + // Assert (断言阶段) + // 验证 HTTP 状态码 + assert.Equal(t, http.StatusOK, w.Code) + + // 解析响应体 + var resp controller.Response + err := json.Unmarshal(w.Body.Bytes(), &resp) + assert.NoError(t, err) + + // 验证业务响应码、消息和返回数据 + assert.Equal(t, controller.CodeSuccess, resp.Code) + assert.Equal(t, "计划更新成功", resp.Message) + + dataMap, ok := resp.Data.(map[string]interface{}) + assert.True(t, ok) + assert.Equal(t, float64(planID), dataMap["id"]) + assert.Equal(t, updatedName, dataMap["name"]) + }) + + t.Run("失败-无效的ID格式", func(t *testing.T) { + // Arrange (准备阶段) + // 模拟仓库为空,因为预期不会调用仓库方法 + mockRepo := &MockPlanRepository{} + router := setupTestRouter(mockRepo) + w := httptest.NewRecorder() + // 创建带有无效ID格式的 HTTP PUT 请求 + req, _ := http.NewRequest(http.MethodPut, "/plans/abc", nil) + + // Act (执行阶段) + router.ServeHTTP(w, req) + + // Assert (断言阶段) + assert.Equal(t, http.StatusOK, w.Code) + + var resp controller.Response + err := json.Unmarshal(w.Body.Bytes(), &resp) + assert.NoError(t, err) + + assert.Equal(t, controller.CodeBadRequest, resp.Code) + assert.Equal(t, "无效的计划ID格式", resp.Message) + }) + + t.Run("失败-请求体绑定失败", func(t *testing.T) { + // Arrange (准备阶段) + planID := uint(1) + // 模拟仓库为空,因为预期不会调用仓库方法(请求体绑定失败发生在控制器内部) + mockRepo := &MockPlanRepository{} + router := setupTestRouter(mockRepo) + + // 准备一个无效的 JSON 请求体,例如 execution_type 类型错误 + reqBody := `{\"name\": \"Updated Plan Name\",}` + req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBufferString(reqBody)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + // Act (执行阶段) + router.ServeHTTP(w, req) + + // Assert (断言阶段) + assert.Equal(t, http.StatusOK, w.Code) + + var resp controller.Response + err := json.Unmarshal(w.Body.Bytes(), &resp) + assert.NoError(t, err) + + assert.Equal(t, controller.CodeBadRequest, resp.Code) + assert.Contains(t, resp.Message, "无效的请求体") + }) + + t.Run("失败-计划不存在", func(t *testing.T) { + // Arrange (准备阶段) + planID := uint(999) + // 模拟仓库行为:GetBasicPlanByID 返回记录未找到错误 + mockRepo := &MockPlanRepository{ + GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) { + assert.Equal(t, planID, id) + return nil, gorm.ErrRecordNotFound + }, + } + router := setupTestRouter(mockRepo) + + // 准备有效的请求体 + reqBody := UpdatePlanRequest{ + Name: "Updated Plan Name", + Description: "Updated Description", + ExecutionType: models.PlanExecutionTypeAutomatic, + ContentType: models.PlanContentTypeTasks, + } + bodyBytes, _ := json.Marshal(reqBody) + + // 创建 HTTP PUT 请求 + req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + // Act (执行阶段) + router.ServeHTTP(w, req) + + // Assert (断言阶段) + assert.Equal(t, http.StatusOK, w.Code) + + var resp controller.Response + err := json.Unmarshal(w.Body.Bytes(), &resp) + assert.NoError(t, err) + + assert.Equal(t, controller.CodeNotFound, resp.Code) + assert.Equal(t, "计划不存在", resp.Message) + }) + + t.Run("失败-计划数据校验失败", func(t *testing.T) { + // Arrange (准备阶段) + planID := uint(1) + // 模拟一个已存在的计划 + mockPlan := &models.Plan{ + Model: gorm.Model{ID: planID}, + Name: "Original Plan", + Description: "Original Description", + ContentType: models.PlanContentTypeTasks, + } + // 配置模拟仓库行为:GetBasicPlanByID 成功返回现有计划 + mockRepo := &MockPlanRepository{ + GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) { + return mockPlan, nil + }, + } + router := setupTestRouter(mockRepo) + + // 准备一个会导致 PlanFromUpdateRequest 校验失败的请求体。 + // 这里通过提供重复的 ExecutionOrder 来触发 ValidateExecutionOrder 错误。 + reqBody := UpdatePlanRequest{ + Name: "Invalid Plan", + ExecutionType: models.PlanExecutionTypeAutomatic, + ContentType: models.PlanContentTypeTasks, // 设置为任务类型 + Tasks: []TaskRequest{ + {Name: "Task 1", ExecutionOrder: 1, Type: models.TaskTypeWaiting}, + {Name: "Task 2", ExecutionOrder: 1, Type: models.TaskTypeWaiting}, // 重复的执行顺序 + }, + } + bodyBytes, _ := json.Marshal(reqBody) + + // 创建 HTTP PUT 请求 + req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + // Act (执行阶段) + router.ServeHTTP(w, req) + + // Assert (断言阶段) + assert.Equal(t, http.StatusOK, w.Code) + + var resp controller.Response + err := json.Unmarshal(w.Body.Bytes(), &resp) + assert.NoError(t, err) + + assert.Equal(t, controller.CodeBadRequest, resp.Code) + assert.Contains(t, resp.Message, "计划数据校验失败") + }) + + t.Run("失败-仓库层更新失败", func(t *testing.T) { + // Arrange (准备阶段) + planID := uint(1) + // 模拟一个已存在的计划 + mockPlan := &models.Plan{ + Model: gorm.Model{ID: planID}, + Name: "Original Plan", + Description: "Original Description", + ContentType: models.PlanContentTypeTasks, + } + updateErr := errors.New("failed to update in repository") + // 配置模拟仓库行为 + mockRepo := &MockPlanRepository{ + // 模拟 GetBasicPlanByID 成功返回现有计划 + GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) { + return mockPlan, nil + }, + // 模拟 UpdatePlan 返回更新失败错误 + UpdatePlanFunc: func(plan *models.Plan) error { + return updateErr // 模拟更新失败 + }, + } + router := setupTestRouter(mockRepo) + + // 准备有效的请求体 + reqBody := UpdatePlanRequest{ + Name: "Updated Plan Name", + Description: "Updated Description", + ExecutionType: models.PlanExecutionTypeAutomatic, + ContentType: models.PlanContentTypeTasks, + } + bodyBytes, _ := json.Marshal(reqBody) + + // 创建 HTTP PUT 请求 + req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + // Act (执行阶段) + router.ServeHTTP(w, req) + + // Assert (断言阶段) + assert.Equal(t, http.StatusOK, w.Code) + + var resp controller.Response + err := json.Unmarshal(w.Body.Bytes(), &resp) + assert.NoError(t, err) + + assert.Equal(t, controller.CodeBadRequest, resp.Code) + assert.Equal(t, "更新计划失败: "+updateErr.Error(), resp.Message) + }) + + t.Run("失败-获取更新后计划失败", func(t *testing.T) { + // Arrange (准备阶段) + planID := uint(1) + // 模拟一个已存在的计划 + mockPlan := &models.Plan{ + Model: gorm.Model{ID: planID}, + Name: "Original Plan", + Description: "Original Description", + ContentType: models.PlanContentTypeTasks, + } + getUpdatedErr := errors.New("failed to get updated plan from repository") + // 配置模拟仓库行为 + mockRepo := &MockPlanRepository{ + // 模拟 GetBasicPlanByID 成功返回现有计划 + GetBasicPlanByIDFunc: func(id uint) (*models.Plan, error) { + return mockPlan, nil + }, + // 模拟 UpdatePlan 成功 + UpdatePlanFunc: func(plan *models.Plan) error { + return nil // 模拟成功更新 + }, + // 模拟 GetPlanByID 返回获取失败错误 + GetPlanByIDFunc: func(id uint) (*models.Plan, error) { + return nil, getUpdatedErr // 模拟获取更新后计划失败 + }, + } + router := setupTestRouter(mockRepo) + + // 准备有效的请求体 + reqBody := UpdatePlanRequest{ + Name: "Updated Plan Name", + Description: "Updated Description", + ExecutionType: models.PlanExecutionTypeAutomatic, + ContentType: models.PlanContentTypeTasks, + } + bodyBytes, _ := json.Marshal(reqBody) + + // 创建 HTTP PUT 请求 + req, _ := http.NewRequest(http.MethodPut, "/plans/"+strconv.Itoa(int(planID)), bytes.NewBuffer(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + // Act (执行阶段) + router.ServeHTTP(w, req) + + // Assert (断言阶段) + assert.Equal(t, http.StatusOK, w.Code) + + var resp controller.Response + err := json.Unmarshal(w.Body.Bytes(), &resp) + assert.NoError(t, err) + + assert.Equal(t, controller.CodeInternalError, resp.Code) + assert.Equal(t, "获取更新后计划详情时发生内部错误", resp.Message) + }) +} + // TestController_DeletePlan 是 DeletePlan 的单元测试 func TestController_DeletePlan(t *testing.T) { t.Run("成功-删除计划", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) + // 模拟仓库行为:DeletePlan 成功 mockRepo := &MockPlanRepository{ DeletePlanFunc: func(id uint) error { assert.Equal(t, uint(1), id) - return nil // Simulate successful deletion + return nil // 模拟成功删除 }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/plans/1", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response @@ -392,20 +749,21 @@ func TestController_DeletePlan(t *testing.T) { }) t.Run("失败-计划不存在", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) + // 模拟仓库行为:DeletePlan 返回记录未找到错误 mockRepo := &MockPlanRepository{ DeletePlanFunc: func(id uint) error { - return gorm.ErrRecordNotFound // Simulate not found + return gorm.ErrRecordNotFound // 模拟未找到记录 }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/plans/999", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response @@ -417,16 +775,18 @@ func TestController_DeletePlan(t *testing.T) { }) t.Run("失败-无效的ID格式", func(t *testing.T) { - // Arrange - mockRepo := &MockPlanRepository{} // No repo call expected + // Arrange (准备阶段) + // 模拟仓库为空,因为预期不会调用仓库方法 + mockRepo := &MockPlanRepository{} router := setupTestRouter(mockRepo) w := httptest.NewRecorder() + // 创建带有无效ID格式的 HTTP DELETE 请求 req, _ := http.NewRequest(http.MethodDelete, "/plans/abc", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response @@ -438,21 +798,23 @@ func TestController_DeletePlan(t *testing.T) { }) t.Run("失败-仓库层内部错误", func(t *testing.T) { - // Arrange + // Arrange (准备阶段) internalErr := errors.New("something went wrong") + // 模拟仓库行为:DeletePlan 返回内部错误 mockRepo := &MockPlanRepository{ + DeletePlanFunc: func(id uint) error { - return internalErr // Simulate internal error + return internalErr // 模拟内部错误 }, } router := setupTestRouter(mockRepo) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/plans/1", nil) - // Act + // Act (执行阶段) router.ServeHTTP(w, req) - // Assert + // Assert (断言阶段) assert.Equal(t, http.StatusOK, w.Code) var resp controller.Response