828 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			828 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package plan
 | 
						||
 | 
						||
import (
 | 
						||
	"bytes"
 | 
						||
	"encoding/json"
 | 
						||
	"errors"
 | 
						||
	"net/http"
 | 
						||
	"net/http/httptest"
 | 
						||
	"strconv"
 | 
						||
	"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"
 | 
						||
	"gorm.io/gorm"
 | 
						||
)
 | 
						||
 | 
						||
// MockPlanRepository 是 repository.PlanRepository 的一个模拟实现,用于测试
 | 
						||
type MockPlanRepository struct {
 | 
						||
	// 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)
 | 
						||
	// 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) {
 | 
						||
	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 {
 | 
						||
	return m.CreatePlanFunc(plan)
 | 
						||
}
 | 
						||
 | 
						||
// UpdatePlan 实现了 MockPlanRepository 接口的 UpdatePlan 方法
 | 
						||
func (m *MockPlanRepository) UpdatePlan(plan *models.Plan) error {
 | 
						||
	return m.UpdatePlanFunc(plan)
 | 
						||
}
 | 
						||
 | 
						||
// DeletePlan 实现了 MockPlanRepository 接口的 DeletePlan 方法
 | 
						||
func (m *MockPlanRepository) DeletePlan(id uint) error {
 | 
						||
	return m.DeletePlanFunc(id)
 | 
						||
}
 | 
						||
 | 
						||
// setupTestRouter 创建一个用于测试的 gin 引擎和控制器实例
 | 
						||
func setupTestRouter(repo repository.PlanRepository) *gin.Engine {
 | 
						||
	gin.SetMode(gin.TestMode)
 | 
						||
	router := gin.Default()
 | 
						||
	logger := logs.NewSilentLogger()
 | 
						||
	planController := NewController(logger, repo)
 | 
						||
	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 测试 CreatePlan 方法
 | 
						||
func TestController_CreatePlan(t *testing.T) {
 | 
						||
	t.Run("成功-创建包含任务的计划", func(t *testing.T) {
 | 
						||
		// Arrange (准备阶段)
 | 
						||
		// 模拟仓库行为:CreatePlan 成功时,为计划和任务分配ID
 | 
						||
		mockRepo := &MockPlanRepository{
 | 
						||
			CreatePlanFunc: func(plan *models.Plan) error {
 | 
						||
				plan.ID = 1
 | 
						||
				for i := range plan.Tasks {
 | 
						||
					plan.Tasks[i].ID = uint(i + 1)
 | 
						||
					plan.Tasks[i].PlanID = plan.ID
 | 
						||
				}
 | 
						||
				return nil
 | 
						||
			},
 | 
						||
		}
 | 
						||
		// 设置 Gin 路由器,并注入模拟仓库
 | 
						||
		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)
 | 
						||
 | 
						||
		// 创建 HTTP 请求
 | 
						||
		req, _ := http.NewRequest(http.MethodPost, "/plans", 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.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"])
 | 
						||
	})
 | 
						||
}
 | 
						||
 | 
						||
// TestController_GetPlan 是为 GetPlan 方法新增的单元测试函数
 | 
						||
func TestController_GetPlan(t *testing.T) {
 | 
						||
	t.Run("成功-获取计划详情", func(t *testing.T) {
 | 
						||
		// Arrange (准备阶段)
 | 
						||
		// 模拟仓库行为:GetPlanByID 成功时返回一个计划
 | 
						||
		mockRepo := &MockPlanRepository{
 | 
						||
			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
 | 
						||
			},
 | 
						||
		}
 | 
						||
		// 设置 Gin 路由器
 | 
						||
		router := setupTestRouter(mockRepo)
 | 
						||
		w := httptest.NewRecorder()
 | 
						||
		// 创建 HTTP 请求
 | 
						||
		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 (准备阶段)
 | 
						||
		// 模拟仓库行为:GetPlanByID 成功时返回一个任务列表为空的计划
 | 
						||
		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 (准备阶段)
 | 
						||
		// 模拟仓库行为:GetPlanByID 返回记录未找到错误
 | 
						||
		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()
 | 
						||
		// 创建带有无效ID格式的 HTTP 请求
 | 
						||
		req, _ := http.NewRequest(http.MethodGet, "/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 (准备阶段)
 | 
						||
		internalErr := errors.New("database connection lost")
 | 
						||
		// 模拟仓库行为:GetPlanByID 返回内部错误
 | 
						||
		mockRepo := &MockPlanRepository{
 | 
						||
			GetPlanByIDFunc: func(id uint) (*models.Plan, error) {
 | 
						||
				return nil, internalErr
 | 
						||
			},
 | 
						||
		}
 | 
						||
		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.CodeInternalError, resp.Code)
 | 
						||
		assert.Equal(t, "获取计划详情时发生内部错误", resp.Message)
 | 
						||
	})
 | 
						||
}
 | 
						||
 | 
						||
// TestController_ListPlans 测试 ListPlans 方法
 | 
						||
func TestController_ListPlans(t *testing.T) {
 | 
						||
	t.Run("成功-获取计划列表", func(t *testing.T) {
 | 
						||
		// 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
 | 
						||
			},
 | 
						||
		}
 | 
						||
		router := setupTestRouter(mockRepo)
 | 
						||
		w := httptest.NewRecorder()
 | 
						||
		req, _ := http.NewRequest(http.MethodGet, "/plans", 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)
 | 
						||
		assert.Equal(t, "获取计划列表成功", resp.Message)
 | 
						||
 | 
						||
		dataBytes, err := json.Marshal(resp.Data)
 | 
						||
		assert.NoError(t, err)
 | 
						||
		var listResp ListPlansResponse
 | 
						||
		err = json.Unmarshal(dataBytes, &listResp)
 | 
						||
		assert.NoError(t, err)
 | 
						||
 | 
						||
		assert.Equal(t, 2, listResp.Total)
 | 
						||
		assert.Len(t, listResp.Plans, 2)
 | 
						||
		assert.Equal(t, uint(1), listResp.Plans[0].ID)
 | 
						||
		assert.Equal(t, "Plan 1", listResp.Plans[0].Name)
 | 
						||
	})
 | 
						||
 | 
						||
	t.Run("成功-返回空列表", func(t *testing.T) {
 | 
						||
		// Arrange (准备阶段)
 | 
						||
		// 模拟仓库行为:ListBasicPlans 返回空列表
 | 
						||
		mockRepo := &MockPlanRepository{
 | 
						||
			ListBasicPlansFunc: func() ([]models.Plan, error) {
 | 
						||
				return []models.Plan{}, nil
 | 
						||
			},
 | 
						||
		}
 | 
						||
		router := setupTestRouter(mockRepo)
 | 
						||
		w := httptest.NewRecorder()
 | 
						||
		req, _ := http.NewRequest(http.MethodGet, "/plans", 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)
 | 
						||
 | 
						||
		dataBytes, err := json.Marshal(resp.Data)
 | 
						||
		assert.NoError(t, err)
 | 
						||
		var listResp ListPlansResponse
 | 
						||
		err = json.Unmarshal(dataBytes, &listResp)
 | 
						||
		assert.NoError(t, err)
 | 
						||
 | 
						||
		assert.Equal(t, 0, listResp.Total)
 | 
						||
		assert.Len(t, listResp.Plans, 0)
 | 
						||
	})
 | 
						||
 | 
						||
	t.Run("失败-仓库层返回错误", func(t *testing.T) {
 | 
						||
		// Arrange (准备阶段)
 | 
						||
		dbErr := errors.New("db error")
 | 
						||
		// 模拟仓库行为:ListBasicPlans 返回数据库错误
 | 
						||
		mockRepo := &MockPlanRepository{
 | 
						||
			ListBasicPlansFunc: func() ([]models.Plan, error) {
 | 
						||
				return nil, dbErr
 | 
						||
			},
 | 
						||
		}
 | 
						||
		router := setupTestRouter(mockRepo)
 | 
						||
		w := httptest.NewRecorder()
 | 
						||
		req, _ := http.NewRequest(http.MethodGet, "/plans", 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.CodeInternalError, resp.Code)
 | 
						||
		assert.Equal(t, "获取计划列表时发生内部错误", resp.Message)
 | 
						||
	})
 | 
						||
}
 | 
						||
 | 
						||
// 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 (准备阶段)
 | 
						||
		// 模拟仓库行为:DeletePlan 成功
 | 
						||
		mockRepo := &MockPlanRepository{
 | 
						||
			DeletePlanFunc: func(id uint) error {
 | 
						||
				assert.Equal(t, uint(1), id)
 | 
						||
				return nil // 模拟成功删除
 | 
						||
			},
 | 
						||
		}
 | 
						||
		router := setupTestRouter(mockRepo)
 | 
						||
		w := httptest.NewRecorder()
 | 
						||
		req, _ := http.NewRequest(http.MethodDelete, "/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)
 | 
						||
		assert.Equal(t, "计划删除成功", resp.Message)
 | 
						||
		assert.Nil(t, resp.Data)
 | 
						||
	})
 | 
						||
 | 
						||
	t.Run("失败-计划不存在", func(t *testing.T) {
 | 
						||
		// Arrange (准备阶段)
 | 
						||
		// 模拟仓库行为:DeletePlan 返回记录未找到错误
 | 
						||
		mockRepo := &MockPlanRepository{
 | 
						||
			DeletePlanFunc: func(id uint) error {
 | 
						||
				return gorm.ErrRecordNotFound // 模拟未找到记录
 | 
						||
			},
 | 
						||
		}
 | 
						||
		router := setupTestRouter(mockRepo)
 | 
						||
		w := httptest.NewRecorder()
 | 
						||
		req, _ := http.NewRequest(http.MethodDelete, "/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.CodeInternalError, resp.Code)
 | 
						||
		assert.Equal(t, "删除计划时发生内部错误", resp.Message)
 | 
						||
	})
 | 
						||
 | 
						||
	t.Run("失败-无效的ID格式", func(t *testing.T) {
 | 
						||
		// Arrange (准备阶段)
 | 
						||
		// 模拟仓库为空,因为预期不会调用仓库方法
 | 
						||
		mockRepo := &MockPlanRepository{}
 | 
						||
		router := setupTestRouter(mockRepo)
 | 
						||
		w := httptest.NewRecorder()
 | 
						||
		// 创建带有无效ID格式的 HTTP DELETE 请求
 | 
						||
		req, _ := http.NewRequest(http.MethodDelete, "/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 (准备阶段)
 | 
						||
		internalErr := errors.New("something went wrong")
 | 
						||
		// 模拟仓库行为:DeletePlan 返回内部错误
 | 
						||
		mockRepo := &MockPlanRepository{
 | 
						||
 | 
						||
			DeletePlanFunc: func(id uint) error {
 | 
						||
				return internalErr // 模拟内部错误
 | 
						||
			},
 | 
						||
		}
 | 
						||
		router := setupTestRouter(mockRepo)
 | 
						||
		w := httptest.NewRecorder()
 | 
						||
		req, _ := http.NewRequest(http.MethodDelete, "/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.CodeInternalError, resp.Code)
 | 
						||
		assert.Equal(t, "删除计划时发生内部错误", resp.Message)
 | 
						||
	})
 | 
						||
}
 |