Compare commits
3 Commits
eb1be3f366
...
b926f7d6a3
| Author | SHA1 | Date | |
|---|---|---|---|
| b926f7d6a3 | |||
| 389c2f9846 | |||
| 55d32dad5f |
@@ -17,8 +17,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -82,17 +80,6 @@ func (m *MockDeviceRepository) Delete(id uint) error {
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// getSilentLogger 创建一个不输出日志的 logs.Logger 实例,用于测试
|
||||
func getSilentLogger() *logs.Logger {
|
||||
discardSyncer := zapcore.AddSync(io.Discard)
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoder := zapcore.NewConsoleEncoder(encoderConfig)
|
||||
core := zapcore.NewCore(encoder, discardSyncer, zap.DebugLevel)
|
||||
zapLogger := zap.New(core)
|
||||
sugaredLogger := zapLogger.Sugar()
|
||||
return &logs.Logger{SugaredLogger: sugaredLogger}
|
||||
}
|
||||
|
||||
// testCase 结构体定义了所有测试用例的通用参数
|
||||
type testCase struct {
|
||||
name string
|
||||
@@ -309,7 +296,7 @@ func TestCreateDevice(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, getSilentLogger()).CreateDevice(ctx)
|
||||
device.NewController(repo, logs.NewSilentLogger()).CreateDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -394,7 +381,7 @@ func TestGetDevice(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, getSilentLogger()).GetDevice(ctx)
|
||||
device.NewController(repo, logs.NewSilentLogger()).GetDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -492,7 +479,7 @@ func TestListDevices(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, getSilentLogger()).ListDevices(ctx)
|
||||
device.NewController(repo, logs.NewSilentLogger()).ListDevices(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -681,7 +668,7 @@ func TestUpdateDevice(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, getSilentLogger()).UpdateDevice(ctx)
|
||||
device.NewController(repo, logs.NewSilentLogger()).UpdateDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -747,7 +734,7 @@ func TestDeleteDevice(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, getSilentLogger()).DeleteDevice(ctx)
|
||||
device.NewController(repo, logs.NewSilentLogger()).DeleteDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,10 +107,31 @@ func NewController(logger *logs.Logger, planRepo repository.PlanRepository) *Con
|
||||
// @Success 200 {object} controller.Response{data=plan.PlanResponse} "业务码为201代表创建成功"
|
||||
// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 500)"
|
||||
// @Router /plans [post]
|
||||
func (pc *Controller) CreatePlan(c *gin.Context) {
|
||||
// 占位符:此处应调用服务层或仓库层来创建计划
|
||||
pc.logger.Infof("收到创建计划请求 (占位符)")
|
||||
controller.SendResponse(c, controller.CodeCreated, "创建计划接口占位符", PlanResponse{ID: 0, Name: "占位计划"})
|
||||
func (c *Controller) CreatePlan(ctx *gin.Context) {
|
||||
var req CreatePlanRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
controller.SendErrorResponse(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 使用已有的转换函数,它已经包含了验证和重排逻辑
|
||||
planToCreate, err := PlanFromCreateRequest(&req)
|
||||
if err != nil {
|
||||
controller.SendErrorResponse(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 调用仓库方法创建计划
|
||||
if err := c.planRepo.CreatePlan(planToCreate); err != nil {
|
||||
controller.SendErrorResponse(ctx, controller.CodeBadRequest, "创建计划失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 使用已有的转换函数将创建后的模型转换为响应对象
|
||||
resp := PlanToResponse(planToCreate)
|
||||
|
||||
// 使用统一的成功响应函数
|
||||
controller.SendResponse(ctx, controller.CodeCreated, "计划创建成功", resp)
|
||||
}
|
||||
|
||||
// GetPlan godoc
|
||||
|
||||
243
internal/app/controller/plan/plan_controller_test.go
Normal file
243
internal/app/controller/plan/plan_controller_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -17,8 +17,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -77,13 +75,7 @@ func TestCreateUser(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode) // 设置 Gin 为测试模式
|
||||
|
||||
// 创建一个不输出日志的真实 logs.Logger 实例
|
||||
discardSyncer := zapcore.AddSync(io.Discard)
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoder := zapcore.NewConsoleEncoder(encoderConfig)
|
||||
core := zapcore.NewCore(encoder, discardSyncer, zap.DebugLevel) // 设置为 DebugLevel 以确保所有日志都被处理(并丢弃)
|
||||
zapLogger := zap.New(core)
|
||||
sugaredLogger := zapLogger.Sugar()
|
||||
silentLogger := &logs.Logger{SugaredLogger: sugaredLogger}
|
||||
silentLogger := logs.NewSilentLogger()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -248,13 +240,7 @@ func TestLogin(t *testing.T) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
// 创建一个不输出日志的真实 logs.Logger 实例
|
||||
discardSyncer := zapcore.AddSync(io.Discard)
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoder := zapcore.NewConsoleEncoder(encoderConfig)
|
||||
core := zapcore.NewCore(encoder, discardSyncer, zap.DebugLevel) // 设置为 DebugLevel 以确保所有日志都被处理(并丢弃)
|
||||
zapLogger := zap.New(core)
|
||||
sugaredLogger := zapLogger.Sugar()
|
||||
silentLogger := &logs.Logger{SugaredLogger: sugaredLogger}
|
||||
silentLogger := logs.NewSilentLogger()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -6,6 +6,7 @@ package logs
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -163,3 +164,15 @@ func (g *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql
|
||||
g.ZapLogger.With(fields...).Debugf("[GORM] trace")
|
||||
// --- 逻辑修复结束 ---
|
||||
}
|
||||
|
||||
// NewSilentLogger 创建一个不输出任何日志的 Logger 实例, 用于测试中屏蔽日志
|
||||
func NewSilentLogger() *Logger {
|
||||
// 创建一个不输出日志的真实 logs.Logger 实例
|
||||
discardSyncer := zapcore.AddSync(io.Discard)
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoder := zapcore.NewConsoleEncoder(encoderConfig)
|
||||
core := zapcore.NewCore(encoder, discardSyncer, zap.DebugLevel) // 设置为 DebugLevel 以确保所有日志都被处理(并丢弃)
|
||||
zapLogger := zap.New(core)
|
||||
sugaredLogger := zapLogger.Sugar()
|
||||
return &Logger{SugaredLogger: sugaredLogger}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user