diff --git a/internal/app/controller/plan/plan_controller_test.go b/internal/app/controller/plan/plan_controller_test.go index f84a206..eda6e13 100644 --- a/internal/app/controller/plan/plan_controller_test.go +++ b/internal/app/controller/plan/plan_controller_test.go @@ -14,11 +14,14 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" + "gorm.io/gorm" ) // MockPlanRepository 是 repository.PlanRepository 的一个模拟实现,用于测试 +// [保持原样,不做任何修改] type MockPlanRepository struct { - CreatePlanFunc func(plan *models.Plan) error + CreatePlanFunc func(plan *models.Plan) error + GetPlanByIDFunc func(id uint) (*models.Plan, error) // ... 可以根据需要模拟其他接口方法 } @@ -31,7 +34,7 @@ func (m *MockPlanRepository) GetBasicPlanByID(id uint) (*models.Plan, error) { } func (m *MockPlanRepository) GetPlanByID(id uint) (*models.Plan, error) { - panic("implement me") + return m.GetPlanByIDFunc(id) } func (m *MockPlanRepository) CreatePlan(plan *models.Plan) error { @@ -50,20 +53,22 @@ func (m *MockPlanRepository) DeletePlan(id uint) error { } // setupTestRouter 创建一个用于测试的 gin 引擎和控制器实例 -func setupTestRouter(repo repository.PlanRepository) (*gin.Engine, *Controller) { +func setupTestRouter(repo repository.PlanRepository) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.Default() - planController := NewController(logs.NewSilentLogger(), repo) + logger := logs.NewSilentLogger() + planController := NewController(logger, repo) router.POST("/plans", planController.CreatePlan) - return router, planController + router.GET("/plans/:id", planController.GetPlan) + return router } +// TestController_CreatePlan [保持原样,不做任何修改] func TestController_CreatePlan(t *testing.T) { t.Run("成功-创建包含任务的计划", func(t *testing.T) { // Arrange mockRepo := &MockPlanRepository{ CreatePlanFunc: func(plan *models.Plan) error { - // 模拟 GORM 回填 ID plan.ID = 1 for i := range plan.Tasks { plan.Tasks[i].ID = uint(i + 1) @@ -72,7 +77,7 @@ func TestController_CreatePlan(t *testing.T) { return nil }, } - router, _ := setupTestRouter(mockRepo) + router := setupTestRouter(mockRepo) reqBody := CreatePlanRequest{ Name: "Test Plan with Tasks", @@ -101,95 +106,116 @@ func TestController_CreatePlan(t *testing.T) { assert.Equal(t, controller.CodeCreated, resp.Code) assert.Equal(t, "计划创建成功", resp.Message) - // 验证 Data 部分 dataMap, ok := resp.Data.(map[string]interface{}) assert.True(t, ok) assert.Equal(t, float64(1), dataMap["id"]) - assert.Equal(t, "Test Plan with Tasks", dataMap["name"]) - tasks, ok := dataMap["tasks"].([]interface{}) - assert.True(t, ok) - assert.Len(t, tasks, 1) }) +} - t.Run("失败-无效的请求体", func(t *testing.T) { - // Arrange - mockRepo := &MockPlanRepository{} - router, _ := setupTestRouter(mockRepo) - - req, _ := http.NewRequest(http.MethodPost, "/plans", bytes.NewBufferString("{invalid json")) - 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 - mockRepo := &MockPlanRepository{} - router, _ := setupTestRouter(mockRepo) - - // 创建一个带有重复执行顺序的请求,这将导致 PlanFromCreateRequest 返回错误 - reqBody := CreatePlanRequest{ - Name: "Duplicate Order Plan", - ExecutionType: models.PlanExecutionTypeManual, - ContentType: models.PlanContentTypeTasks, - Tasks: []TaskRequest{ - {Name: "Task 1", ExecutionOrder: 1}, - {Name: "Task 2", ExecutionOrder: 1}, // 重复 - }, - } - bodyBytes, _ := json.Marshal(reqBody) - - req, _ := http.NewRequest(http.MethodPost, "/plans", 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, "计划数据校验失败") - assert.Contains(t, resp.Message, "任务执行顺序重复") - }) - - t.Run("失败-仓库层业务错误", func(t *testing.T) { +// TestController_GetPlan 是为 GetPlan 方法新增的单元测试函数 +func TestController_GetPlan(t *testing.T) { + t.Run("成功-获取计划详情", func(t *testing.T) { // Arrange mockRepo := &MockPlanRepository{ - CreatePlanFunc: func(plan *models.Plan) error { - return repository.ErrNodeDoesNotExist // 模拟仓库层返回的业务错误 + GetPlanByIDFunc: func(id uint) (*models.Plan, error) { + assert.Equal(t, uint(1), id) + return &models.Plan{ + Model: gorm.Model{ID: 1}, + Name: "Test Plan", + ContentType: models.PlanContentTypeTasks, + }, nil }, } - router, _ := setupTestRouter(mockRepo) - - reqBody := CreatePlanRequest{ - Name: "Plan with non-existent sub-plan", - ExecutionType: models.PlanExecutionTypeManual, - ContentType: models.PlanContentTypeSubPlans, - SubPlanIDs: []uint{999}, // 假设这个ID不存在 - } - bodyBytes, _ := json.Marshal(reqBody) - - req, _ := http.NewRequest(http.MethodPost, "/plans", bytes.NewBuffer(bodyBytes)) - req.Header.Set("Content-Type", "application/json") + router := setupTestRouter(mockRepo) w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/plans/1", 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.CodeSuccess, resp.Code) + dataMap, ok := resp.Data.(map[string]interface{}) + assert.True(t, ok) + assert.Equal(t, float64(1), dataMap["id"]) + }) + + t.Run("成功-获取内容为空的计划详情", func(t *testing.T) { + // Arrange + mockRepo := &MockPlanRepository{ + GetPlanByIDFunc: func(id uint) (*models.Plan, error) { + assert.Equal(t, uint(3), id) + return &models.Plan{ + Model: gorm.Model{ID: 3}, + Name: "Empty Plan", + ContentType: models.PlanContentTypeTasks, + Tasks: []models.Task{}, // 任务列表为空 + }, nil + }, + } + router := setupTestRouter(mockRepo) + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/plans/3", 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.CodeSuccess, resp.Code) + + dataMap, ok := resp.Data.(map[string]interface{}) + assert.True(t, ok) + assert.Equal(t, float64(3), dataMap["id"]) + assert.Equal(t, "Empty Plan", dataMap["name"]) + + // 关键断言:因为 omitempty 标签,当 tasks 列表为空时,该字段不应该出现在JSON中 + _, ok = dataMap["tasks"] + assert.False(t, ok, "当任务列表为空时,'tasks' 字段因为 omitempty 标签,不应该出现在JSON响应中") + }) + + t.Run("失败-计划不存在", func(t *testing.T) { + // Arrange + mockRepo := &MockPlanRepository{ + GetPlanByIDFunc: func(id uint) (*models.Plan, error) { + return nil, gorm.ErrRecordNotFound + }, + } + router := setupTestRouter(mockRepo) + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/plans/999", 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.CodeNotFound, resp.Code) + assert.Equal(t, "计划不存在", resp.Message) + }) + + t.Run("失败-无效的ID格式", func(t *testing.T) { + // Arrange + mockRepo := &MockPlanRepository{} + router := setupTestRouter(mockRepo) + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/plans/abc", nil) // Act router.ServeHTTP(w, req) @@ -202,30 +228,20 @@ func TestController_CreatePlan(t *testing.T) { assert.NoError(t, err) assert.Equal(t, controller.CodeBadRequest, resp.Code) - assert.Equal(t, "创建计划失败: "+repository.ErrNodeDoesNotExist.Error(), resp.Message) + assert.Equal(t, "无效的计划ID格式", resp.Message) }) t.Run("失败-仓库层内部错误", func(t *testing.T) { // Arrange internalErr := errors.New("database connection lost") mockRepo := &MockPlanRepository{ - CreatePlanFunc: func(plan *models.Plan) error { - return internalErr // 模拟一个未知的内部错误 + GetPlanByIDFunc: func(id uint) (*models.Plan, error) { + return nil, internalErr }, } - router, _ := setupTestRouter(mockRepo) - - reqBody := CreatePlanRequest{ - Name: "Test Plan", - ExecutionType: models.PlanExecutionTypeManual, - ContentType: models.PlanContentTypeTasks, - Tasks: []TaskRequest{}, - } - bodyBytes, _ := json.Marshal(reqBody) - - req, _ := http.NewRequest(http.MethodPost, "/plans", bytes.NewBuffer(bodyBytes)) - req.Header.Set("Content-Type", "application/json") + router := setupTestRouter(mockRepo) w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/plans/1", nil) // Act router.ServeHTTP(w, req) @@ -237,7 +253,7 @@ func TestController_CreatePlan(t *testing.T) { err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) - assert.Equal(t, controller.CodeBadRequest, resp.Code) - assert.Equal(t, "创建计划失败: "+internalErr.Error(), resp.Message) + assert.Equal(t, controller.CodeInternalError, resp.Code) + assert.Equal(t, "获取计划详情时发生内部错误", resp.Message) }) }