Files
pig-farm-controller/internal/app/controller/plan/plan_controller_test.go
2025-09-14 17:02:36 +08:00

466 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
"gorm.io/gorm"
)
// MockPlanRepository 是 repository.PlanRepository 的一个模拟实现,用于测试
type MockPlanRepository struct {
CreatePlanFunc func(plan *models.Plan) error
GetPlanByIDFunc func(id uint) (*models.Plan, error)
ListBasicPlansFunc func() ([]models.Plan, error)
DeletePlanFunc func(id uint) error
}
func (m *MockPlanRepository) ListBasicPlans() ([]models.Plan, error) {
return m.ListBasicPlansFunc()
}
func (m *MockPlanRepository) GetBasicPlanByID(id uint) (*models.Plan, error) {
panic("implement me")
}
func (m *MockPlanRepository) GetPlanByID(id uint) (*models.Plan, error) {
return m.GetPlanByIDFunc(id)
}
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 {
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.DELETE("/plans/:id", planController.DeletePlan)
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 {
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)
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
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
},
}
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)
// 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")
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 tests the ListPlans method
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},
}
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
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")
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_DeletePlan 是 DeletePlan 的单元测试
func TestController_DeletePlan(t *testing.T) {
t.Run("成功-删除计划", func(t *testing.T) {
// Arrange
mockRepo := &MockPlanRepository{
DeletePlanFunc: func(id uint) error {
assert.Equal(t, uint(1), id)
return nil // Simulate successful deletion
},
}
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
mockRepo := &MockPlanRepository{
DeletePlanFunc: func(id uint) error {
return gorm.ErrRecordNotFound // Simulate not found
},
}
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{} // No repo call expected
router := setupTestRouter(mockRepo)
w := httptest.NewRecorder()
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")
mockRepo := &MockPlanRepository{
DeletePlanFunc: func(id uint) error {
return internalErr // Simulate internal error
},
}
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)
})
}