diff --git a/internal/app/controller/plan/plan_controller_test.go b/internal/app/controller/plan/plan_controller_test.go new file mode 100644 index 0000000..f84a206 --- /dev/null +++ b/internal/app/controller/plan/plan_controller_test.go @@ -0,0 +1,243 @@ +package plan + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +// MockPlanRepository 是 repository.PlanRepository 的一个模拟实现,用于测试 +type MockPlanRepository struct { + CreatePlanFunc func(plan *models.Plan) error + // ... 可以根据需要模拟其他接口方法 +} + +func (m *MockPlanRepository) ListBasicPlans() ([]models.Plan, error) { + panic("implement me") +} + +func (m *MockPlanRepository) GetBasicPlanByID(id uint) (*models.Plan, error) { + panic("implement me") +} + +func (m *MockPlanRepository) GetPlanByID(id uint) (*models.Plan, error) { + panic("implement me") +} + +func (m *MockPlanRepository) CreatePlan(plan *models.Plan) error { + if m.CreatePlanFunc != nil { + return m.CreatePlanFunc(plan) + } + return nil +} + +func (m *MockPlanRepository) UpdatePlan(plan *models.Plan) error { + panic("implement me") +} + +func (m *MockPlanRepository) DeletePlan(id uint) error { + panic("implement me") +} + +// setupTestRouter 创建一个用于测试的 gin 引擎和控制器实例 +func setupTestRouter(repo repository.PlanRepository) (*gin.Engine, *Controller) { + gin.SetMode(gin.TestMode) + router := gin.Default() + planController := NewController(logs.NewSilentLogger(), repo) + router.POST("/plans", planController.CreatePlan) + return router, planController +} + +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) + plan.Tasks[i].PlanID = plan.ID + } + return nil + }, + } + router, _ := setupTestRouter(mockRepo) + + reqBody := CreatePlanRequest{ + Name: "Test Plan with Tasks", + ExecutionType: models.PlanExecutionTypeManual, + ContentType: models.PlanContentTypeTasks, + Tasks: []TaskRequest{ + {Name: "Task 1", ExecutionOrder: 1, Type: models.TaskTypeWaiting}, + }, + } + 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.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) { + // Arrange + mockRepo := &MockPlanRepository{ + CreatePlanFunc: func(plan *models.Plan) error { + return repository.ErrNodeDoesNotExist // 模拟仓库层返回的业务错误 + }, + } + 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") + 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, "创建计划失败: "+repository.ErrNodeDoesNotExist.Error(), 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 // 模拟一个未知的内部错误 + }, + } + 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") + 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, "创建计划失败: "+internalErr.Error(), resp.Message) + }) +}