生成openspace任务列表
This commit is contained in:
@@ -1,741 +0,0 @@
|
||||
package device_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/device"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MockDeviceRepository 是 DeviceRepository 接口的模拟实现
|
||||
type MockDeviceRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CreateTx 模拟 DeviceRepository 的 CreateTx 方法
|
||||
func (m *MockDeviceRepository) Create(device *models.Device) error {
|
||||
args := m.Called(device)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// FindByID 模拟 DeviceRepository 的 FindByID 方法
|
||||
func (m *MockDeviceRepository) FindByID(id uint) (*models.Device, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*models.Device), args.Error(1)
|
||||
}
|
||||
|
||||
// FindByIDString 模拟 DeviceRepository 的 FindByIDString 方法
|
||||
func (m *MockDeviceRepository) FindByIDString(id string) (*models.Device, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*models.Device), args.Error(1)
|
||||
}
|
||||
|
||||
// ListAll 模拟 DeviceRepository 的 ListAll 方法
|
||||
func (m *MockDeviceRepository) ListAll() ([]*models.Device, error) {
|
||||
args := m.Called()
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]*models.Device), args.Error(1)
|
||||
}
|
||||
|
||||
// ListByParentID 模拟 DeviceRepository 的 ListByParentID 方法
|
||||
func (m *MockDeviceRepository) ListByParentID(parentID *uint) ([]*models.Device, error) {
|
||||
args := m.Called(parentID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]*models.Device), args.Error(1)
|
||||
}
|
||||
|
||||
// Update 模拟 DeviceRepository 的 Update 方法
|
||||
func (m *MockDeviceRepository) Update(device *models.Device) error {
|
||||
args := m.Called(device)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Delete 模拟 DeviceRepository 的 Delete 方法
|
||||
func (m *MockDeviceRepository) Delete(id uint) error {
|
||||
args := m.Called(id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// testCase 结构体定义了所有测试用例的通用参数
|
||||
type testCase struct {
|
||||
name string
|
||||
httpMethod string // 新增字段:HTTP 方法
|
||||
requestBody interface{}
|
||||
paramID string // URL 中的 ID 参数
|
||||
mockRepoSetup func(*MockDeviceRepository)
|
||||
expectedStatus int // HTTP 状态码
|
||||
expectedCode int // 业务状态码
|
||||
expectedMessage string
|
||||
expectedDataFunc func(interface{}) bool // 用于验证 data 字段的函数
|
||||
}
|
||||
|
||||
// runTest 是一个辅助函数,用于执行单个测试用例
|
||||
func runTest(t *testing.T, tc testCase, controllerMethod func(*gin.Context, *MockDeviceRepository)) {
|
||||
// 初始化 Gin 上下文
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
|
||||
// 设置请求体和 HTTP 方法
|
||||
if tc.requestBody != nil {
|
||||
jsonBody, _ := json.Marshal(tc.requestBody)
|
||||
ctx.Request = httptest.NewRequest(tc.httpMethod, "/", io.NopCloser(bytes.NewBuffer(jsonBody)))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
// 对于没有请求体的请求 (GET, DELETE, 或没有 body 的 POST/PUT)
|
||||
ctx.Request = httptest.NewRequest(tc.httpMethod, "/", nil)
|
||||
}
|
||||
|
||||
// 设置 URL 参数
|
||||
if tc.paramID != "" {
|
||||
ctx.Params = append(ctx.Params, gin.Param{Key: "id", Value: tc.paramID})
|
||||
}
|
||||
|
||||
// 创建 Mock Repository
|
||||
mockRepo := new(MockDeviceRepository)
|
||||
// 设置 Mock 行为
|
||||
tc.mockRepoSetup(mockRepo)
|
||||
|
||||
// 调用被测试的方法,并传入 mockRepo
|
||||
controllerMethod(ctx, mockRepo)
|
||||
|
||||
// 解析响应体
|
||||
var responseBody controller.Response
|
||||
err := json.Unmarshal(w.Body.Bytes(), &responseBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 断言 HTTP 状态码始终为 200 OK
|
||||
assert.Equal(t, tc.expectedStatus, w.Code)
|
||||
|
||||
// 断言业务状态码和消息
|
||||
assert.Equal(t, tc.expectedCode, responseBody.Code)
|
||||
assert.Equal(t, tc.expectedMessage, responseBody.Message)
|
||||
|
||||
// 断言数据字段
|
||||
if tc.expectedDataFunc != nil {
|
||||
var data interface{}
|
||||
// 只有当 responseBody.Data 不为 nil 且其底层类型为 []byte 时才尝试 Unmarshal
|
||||
if responseBody.Data != nil {
|
||||
if byteData, ok := responseBody.Data.([]byte); ok {
|
||||
err = json.Unmarshal(byteData, &data)
|
||||
assert.NoError(t, err, "无法解析响应数据") // 增加对 Unmarshal 错误的断言
|
||||
} else {
|
||||
// 如果 Data 不为 nil 但也不是 []byte,这通常不应该发生
|
||||
// 但为了健壮性,直接将原始 interface{} 赋值给 data
|
||||
data = responseBody.Data
|
||||
}
|
||||
}
|
||||
assert.True(t, tc.expectedDataFunc(data), "数据字段验证失败")
|
||||
}
|
||||
|
||||
// 验证 Mock 期望是否都已满足
|
||||
mockRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestCreateDevice(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "成功创建区域主控",
|
||||
httpMethod: http.MethodPost,
|
||||
requestBody: device.CreateDeviceRequest{
|
||||
Name: "主控A",
|
||||
Type: models.DeviceTypeAreaController,
|
||||
Location: "猪舍1",
|
||||
Properties: controller.Properties(`{"lora_address":"0x1234"}`),
|
||||
},
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("CreateTx", mock.MatchedBy(func(dev *models.Device) bool {
|
||||
// 检查 Name 字段
|
||||
nameMatch := dev.Name == "主控A"
|
||||
// 检查 Type 字段
|
||||
typeMatch := dev.Type == models.DeviceTypeAreaController
|
||||
// 检查 Location 字段
|
||||
locationMatch := dev.Location == "猪舍1"
|
||||
// 检查 Properties 字段的字节内容
|
||||
expectedProperties := controller.Properties(`{"lora_address":"0x1234"}`)
|
||||
propertiesMatch := bytes.Equal(dev.Properties, expectedProperties)
|
||||
|
||||
return nameMatch && typeMatch && locationMatch && propertiesMatch
|
||||
})).Return(nil).Run(func(args mock.Arguments) {
|
||||
// 模拟 GORM 自动填充 ID
|
||||
arg := args.Get(0).(*models.Device)
|
||||
arg.ID = 1
|
||||
arg.CreatedAt = time.Now()
|
||||
arg.UpdatedAt = time.Now()
|
||||
}).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeCreated,
|
||||
expectedMessage: "设备创建成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return dataMap["id"] != nil &&
|
||||
dataMap["name"] == "主控A" &&
|
||||
dataMap["type"] == string(models.DeviceTypeAreaController) &&
|
||||
dataMap["properties"] != nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "成功创建普通设备",
|
||||
httpMethod: http.MethodPost,
|
||||
requestBody: device.CreateDeviceRequest{
|
||||
Name: "温度传感器",
|
||||
Type: models.DeviceTypeDevice,
|
||||
SubType: models.SubTypeSensorTemp,
|
||||
ParentID: func() *uint { id := uint(1); return &id }(),
|
||||
Location: "猪舍1-A区",
|
||||
Properties: controller.Properties(`{"bus_id":1,"bus_address":10}`),
|
||||
},
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("CreateTx", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
|
||||
arg := args.Get(0).(*models.Device)
|
||||
arg.ID = 2
|
||||
arg.CreatedAt = time.Now()
|
||||
arg.UpdatedAt = time.Now()
|
||||
}).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeCreated,
|
||||
expectedMessage: "设备创建成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return dataMap["id"] != nil &&
|
||||
dataMap["name"] == "温度传感器" &&
|
||||
dataMap["type"] == string(models.DeviceTypeDevice) &&
|
||||
dataMap["sub_type"] == string(models.SubTypeSensorTemp) &&
|
||||
dataMap["parent_id"] != nil &&
|
||||
dataMap["properties"] != nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败",
|
||||
httpMethod: http.MethodPost,
|
||||
requestBody: device.CreateDeviceRequest{
|
||||
Name: "", // 缺少必填字段 Name
|
||||
Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeBadRequest,
|
||||
expectedMessage: "Key: 'CreateDeviceRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "数据库创建失败",
|
||||
httpMethod: http.MethodPost,
|
||||
requestBody: device.CreateDeviceRequest{
|
||||
Name: "失败设备",
|
||||
Type: models.DeviceTypeDevice,
|
||||
},
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("CreateTx", mock.Anything).Return(errors.New("db error")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError,
|
||||
expectedMessage: "创建设备失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
// 新增:Properties字段JSON格式无效
|
||||
{
|
||||
name: "Properties字段JSON格式无效",
|
||||
httpMethod: http.MethodPost,
|
||||
requestBody: device.CreateDeviceRequest{
|
||||
Name: "无效JSON设备",
|
||||
Type: models.DeviceTypeDevice,
|
||||
Properties: controller.Properties(`{invalid json}`),
|
||||
},
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
// 期望 CreateTx 方法被调用,并返回一个模拟的数据库错误
|
||||
// 这个错误模拟的是数据库层因为 Properties 字段的 JSON 格式无效而拒绝保存
|
||||
m.On("CreateTx", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) {
|
||||
dev := args.Get(0).(*models.Device)
|
||||
assert.Equal(t, "无效JSON设备", dev.Name)
|
||||
assert.Equal(t, models.DeviceTypeDevice, dev.Type)
|
||||
expectedProperties := controller.Properties(`{invalid json}`)
|
||||
assert.True(t, bytes.Equal(dev.Properties, expectedProperties), "Properties should match")
|
||||
}).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK, // HTTP status is 200 OK for business errors
|
||||
expectedCode: controller.CodeInternalError, // Business code for internal server error
|
||||
expectedMessage: "创建设备失败", // The message returned by the controller
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, logs.NewSilentLogger()).CreateDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDevice(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "成功获取设备",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "1").Return(&models.Device{
|
||||
Model: gorm.Model{
|
||||
ID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: "测试设备",
|
||||
Type: models.DeviceTypeAreaController,
|
||||
Location: "测试地点",
|
||||
Properties: datatypes.JSON(`{"key":"value"}`),
|
||||
}, nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "获取设备信息成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return dataMap["id"] == float64(1) &&
|
||||
dataMap["name"] == "测试设备" &&
|
||||
dataMap["properties"] != nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "设备未找到",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "999",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "999").Return(nil, gorm.ErrRecordNotFound).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeNotFound,
|
||||
expectedMessage: "设备未找到",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "ID格式无效",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "abc",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "abc").Return(nil, errors.New("无效的设备ID格式")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeBadRequest,
|
||||
expectedMessage: "无效的设备ID格式",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "数据库查询失败",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "1").Return(nil, errors.New("db error")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError,
|
||||
expectedMessage: "获取设备信息失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, logs.NewSilentLogger()).GetDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListDevices(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "成功获取空列表",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("ListAll").Return([]*models.Device{}, nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "获取设备列表成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
s, ok := data.([]interface{})
|
||||
return ok && len(s) == 0
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "成功获取包含设备的列表",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("ListAll").Return([]*models.Device{
|
||||
{
|
||||
Model: gorm.Model{
|
||||
ID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: "设备1",
|
||||
Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
{
|
||||
Model: gorm.Model{
|
||||
ID: 2,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: "设备2",
|
||||
Type: models.DeviceTypeDevice,
|
||||
SubType: models.SubTypeFan,
|
||||
ParentID: func() *uint { id := uint(1); return &id }(),
|
||||
},
|
||||
}, nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "获取设备列表成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataList, ok := data.([]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// 检查长度
|
||||
if len(dataList) != 2 {
|
||||
return false
|
||||
}
|
||||
// 检查第一个设备
|
||||
item1, ok1 := dataList[0].(map[string]interface{})
|
||||
if !ok1 || item1["id"] != float64(1) || item1["name"] != "设备1" {
|
||||
return false
|
||||
}
|
||||
// 检查第二个设备
|
||||
item2, ok2 := dataList[1].(map[string]interface{})
|
||||
if !ok2 || item2["id"] != float64(2) || item2["name"] != "设备2" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "数据库查询失败",
|
||||
httpMethod: http.MethodGet,
|
||||
requestBody: nil,
|
||||
paramID: "",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("ListAll").Return(nil, errors.New("db error")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError,
|
||||
expectedMessage: "获取设备列表失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, logs.NewSilentLogger()).ListDevices(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateDevice(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "成功更新设备",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "更新后的主控",
|
||||
Type: models.DeviceTypeAreaController,
|
||||
Location: "新地点",
|
||||
Properties: controller.Properties(`{"lora_address":"0x5678"}`),
|
||||
},
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
// 模拟 FindByIDString 找到设备
|
||||
m.On("FindByIDString", "1").Return(&models.Device{
|
||||
Model: gorm.Model{
|
||||
ID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: "旧主控",
|
||||
Type: models.DeviceTypeAreaController,
|
||||
Location: "旧地点",
|
||||
Properties: datatypes.JSON(`{"lora_address":"0x1234"}`),
|
||||
}, nil).Once()
|
||||
// 模拟 Update 成功
|
||||
m.On("Update", mock.AnythingOfType("*models.Device")).Return(nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "设备更新成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return dataMap["id"] == float64(1) &&
|
||||
dataMap["name"] == "更新后的主控" &&
|
||||
dataMap["location"] == "新地点" &&
|
||||
dataMap["properties"] != nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "", // 缺少必填字段 Name
|
||||
Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
// 模拟 FindByIDString 找到设备,以便进入参数绑定阶段
|
||||
m.On("FindByIDString", "1").Return(&models.Device{Model: gorm.Model{ID: 1}}, nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeBadRequest,
|
||||
expectedMessage: "Key: 'UpdateDeviceRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "设备未找到",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "任意名称", Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
paramID: "999",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "999").Return(nil, gorm.ErrRecordNotFound).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeNotFound,
|
||||
expectedMessage: "设备未找到",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "ID格式无效",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "任意名称", Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
paramID: "abc",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "abc").Return(nil, errors.New("无效的设备ID格式")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeBadRequest,
|
||||
expectedMessage: "无效的设备ID格式",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "数据库更新失败",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "更新失败设备", Type: models.DeviceTypeAreaController,
|
||||
},
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("FindByIDString", "1").Return(&models.Device{Model: gorm.Model{ID: 1}}, nil).Once()
|
||||
m.On("Update", mock.AnythingOfType("*models.Device")).Return(errors.New("db error")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError,
|
||||
expectedMessage: "更新设备失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
// 新增:Properties字段JSON格式无效
|
||||
{
|
||||
name: "Properties字段JSON格式无效",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "无效JSON设备",
|
||||
Type: models.DeviceTypeDevice,
|
||||
Properties: controller.Properties(`{invalid json}`),
|
||||
},
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
// 模拟 FindByIDString 找到设备,以便进入参数绑定阶段
|
||||
m.On("FindByIDString", "1").Return(&models.Device{Model: gorm.Model{ID: 1}}, nil).Once()
|
||||
// 期望 Update 方法被调用,并返回一个模拟的数据库错误
|
||||
m.On("Update", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) {
|
||||
dev := args.Get(0).(*models.Device)
|
||||
assert.Equal(t, "无效JSON设备", dev.Name)
|
||||
assert.Equal(t, models.DeviceTypeDevice, dev.Type)
|
||||
expectedProperties := controller.Properties(`{invalid json}`)
|
||||
assert.True(t, bytes.Equal(dev.Properties, expectedProperties), "Properties should match")
|
||||
}).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError, // Expected to be internal server error due to DB error
|
||||
expectedMessage: "更新设备失败", // The message returned by the controller
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
// 新增:成功更新设备的ParentID
|
||||
{
|
||||
name: "成功更新设备的ParentID",
|
||||
httpMethod: http.MethodPut,
|
||||
requestBody: device.UpdateDeviceRequest{
|
||||
Name: "更新ParentID设备",
|
||||
Type: models.DeviceTypeDevice,
|
||||
ParentID: func() *uint { id := uint(10); return &id }(),
|
||||
Location: "新地点",
|
||||
Properties: controller.Properties(`{"key":"value"}`),
|
||||
},
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
// 模拟 FindByIDString 找到设备
|
||||
m.On("FindByIDString", "1").Return(&models.Device{
|
||||
Model: gorm.Model{
|
||||
ID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: "旧设备",
|
||||
Type: models.DeviceTypeDevice,
|
||||
ParentID: func() *uint { id := uint(1); return &id }(),
|
||||
Location: "旧地点",
|
||||
Properties: datatypes.JSON(`{"old_key":"old_value"}`),
|
||||
}, nil).Once()
|
||||
// 模拟 Update 成功,并验证 ParentID 被更新
|
||||
m.On("Update", mock.MatchedBy(func(dev *models.Device) bool {
|
||||
return dev.ID == 1 && *dev.ParentID == 10
|
||||
})).Return(nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "设备更新成功",
|
||||
expectedDataFunc: func(data interface{}) bool {
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return dataMap["id"] == float64(1) &&
|
||||
dataMap["parent_id"] == float64(10) &&
|
||||
dataMap["properties"] != nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, logs.NewSilentLogger()).UpdateDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteDevice(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "成功删除设备",
|
||||
httpMethod: http.MethodDelete,
|
||||
requestBody: nil,
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("Delete", uint(1)).Return(nil).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeSuccess,
|
||||
expectedMessage: "设备删除成功",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "ID格式无效",
|
||||
httpMethod: http.MethodDelete,
|
||||
requestBody: nil,
|
||||
paramID: "abc",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeBadRequest,
|
||||
expectedMessage: "无效的设备ID格式",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
{
|
||||
name: "数据库删除失败",
|
||||
httpMethod: http.MethodDelete,
|
||||
requestBody: nil,
|
||||
paramID: "1",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("Delete", uint(1)).Return(errors.New("db error")).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError,
|
||||
expectedMessage: "删除设备失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
// 新增:删除设备未找到
|
||||
{
|
||||
name: "删除设备未找到",
|
||||
httpMethod: http.MethodDelete,
|
||||
requestBody: nil,
|
||||
paramID: "999",
|
||||
mockRepoSetup: func(m *MockDeviceRepository) {
|
||||
m.On("Delete", uint(999)).Return(gorm.ErrRecordNotFound).Once()
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedCode: controller.CodeInternalError, // 当前控制器逻辑会将 ErrRecordNotFound 视为内部错误
|
||||
expectedMessage: "删除设备失败",
|
||||
expectedDataFunc: func(data interface{}) bool { return data == nil },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runTest(t, tc, func(ctx *gin.Context, repo *MockDeviceRepository) {
|
||||
device.NewController(repo, logs.NewSilentLogger()).DeleteDevice(ctx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,827 +0,0 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -1,450 +0,0 @@
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MockUserRepository 是 UserRepository 接口的模拟实现
|
||||
type MockUserRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CreateTx 模拟 UserRepository 的 CreateTx 方法
|
||||
func (m *MockUserRepository) Create(user *models.User) error {
|
||||
args := m.Called(user)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// FindByUsername 模拟 UserRepository 的 FindByUsername 方法
|
||||
// 返回类型改回 *models.User
|
||||
func (m *MockUserRepository) FindByUsername(username string) (*models.User, error) {
|
||||
args := m.Called(username)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
// FindByID 模拟 UserRepository 的 FindByID 方法
|
||||
func (m *MockUserRepository) FindByID(id uint) (*models.User, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
// MockTokenService 是 token.TokenService 接口的模拟实现
|
||||
type MockTokenService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GenerateToken 模拟 TokenService 的 GenerateToken 方法
|
||||
func (m *MockTokenService) GenerateToken(userID uint) (string, error) {
|
||||
args := m.Called(userID)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
// ParseToken 模拟 TokenService 的 ParseToken 方法
|
||||
func (m *MockTokenService) ParseToken(tokenString string) (*token.Claims, error) {
|
||||
args := m.Called(tokenString)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*token.Claims), args.Error(1)
|
||||
}
|
||||
|
||||
// TestCreateUser 测试 CreateUser 方法
|
||||
func TestCreateUser(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode) // 设置 Gin 为测试模式
|
||||
|
||||
// 创建一个不输出日志的真实 logs.Logger 实例
|
||||
silentLogger := logs.NewSilentLogger()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
requestBody user.CreateUserRequest
|
||||
mockRepoSetup func(*MockUserRepository)
|
||||
expectedResponse map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "成功创建用户",
|
||||
requestBody: user.CreateUserRequest{
|
||||
Username: "testuser",
|
||||
Password: "password123",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
// 模拟 CreateTx 成功
|
||||
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(nil).Run(func(args mock.Arguments) {
|
||||
// 模拟数据库自动填充 ID
|
||||
userArg := args.Get(0).(*models.User)
|
||||
userArg.ID = 1 // 设置一个非零的 ID
|
||||
}).Once()
|
||||
// 在成功创建用户的路径下,FindByUsername 不会被调用,因此这里不需要设置其期望
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeCreated), // 修改这里:使用自定义状态码
|
||||
"message": "用户创建成功",
|
||||
"data": map[string]interface{}{
|
||||
"username": "testuser",
|
||||
// "id": mock.Anything, // 移除这里的 id,在断言时单独检查
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败_密码过短",
|
||||
requestBody: user.CreateUserRequest{
|
||||
Username: "testuser2",
|
||||
Password: "123", // 密码少于6位
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
// 不会调用 CreateTx 或 FindByUsername
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeBadRequest),
|
||||
"message": "Key: 'CreateUserRequest.Password' Error:Field validation for 'Password' failed on the 'min' tag",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败_缺少用户名",
|
||||
requestBody: user.CreateUserRequest{
|
||||
Password: "password123",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
// 不会调用 CreateTx 或 FindByUsername
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeBadRequest),
|
||||
"message": "Key: 'CreateUserRequest.Username' Error:Field validation for 'Username' failed on the 'required' tag",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "用户名已存在",
|
||||
requestBody: user.CreateUserRequest{
|
||||
Username: "existinguser",
|
||||
Password: "password123",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
// 模拟 CreateTx 失败,因为用户名已存在
|
||||
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("duplicate entry")).Once()
|
||||
// 模拟 FindByUsername 找到用户,确认是用户名重复
|
||||
m.On("FindByUsername", "existinguser").Return(&models.User{Username: "existinguser"}, nil).Once()
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeConflict),
|
||||
"message": "用户名已存在",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "创建用户失败_通用数据库错误",
|
||||
requestBody: user.CreateUserRequest{
|
||||
Username: "db_error_user",
|
||||
Password: "password123",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
// 模拟 CreateTx 失败,通用数据库错误
|
||||
m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("database error")).Once()
|
||||
// 模拟 FindByUsername 找不到用户,确认不是用户名重复
|
||||
m.On("FindByUsername", "db_error_user").Return(nil, gorm.ErrRecordNotFound).Once()
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeInternalError),
|
||||
"message": "创建用户失败",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 初始化 Gin 上下文和记录器
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/users", nil) // URL 路径不重要,因为我们不测试路由
|
||||
|
||||
// 设置请求体
|
||||
jsonBody, _ := json.Marshal(tt.requestBody)
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(jsonBody))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 创建 Mock UserRepository
|
||||
mockRepo := new(MockUserRepository)
|
||||
|
||||
// 设置 Mock UserRepository 行为
|
||||
tt.mockRepoSetup(mockRepo)
|
||||
|
||||
// 创建控制器实例,使用静默日志器
|
||||
userController := user.NewController(mockRepo, silentLogger, nil) // tokenService 在 CreateUser 中未使用,设为 nil
|
||||
|
||||
// 调用被测试的方法
|
||||
userController.CreateUser(ctx)
|
||||
|
||||
// 解析响应体
|
||||
var responseBody map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &responseBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 断言响应体中的 code 字段
|
||||
assert.Equal(t, tt.expectedResponse["code"], responseBody["code"])
|
||||
|
||||
// 断言响应内容 (除了 code 字段)
|
||||
if tt.expectedResponse["code"] == float64(controller.CodeCreated) {
|
||||
// 确保 data 字段存在且是 map[string]interface{} 类型
|
||||
data, ok := responseBody["data"].(map[string]interface{})
|
||||
assert.True(t, ok, "响应体中的 data 字段应为 map[string]interface{}")
|
||||
// 确保 id 字段存在且不为零
|
||||
id, idOk := data["id"].(float64)
|
||||
assert.True(t, idOk, "响应体中的 data.id 字段应为 float64 类型")
|
||||
assert.NotEqual(t, float64(0), id, "响应体中的 data.id 不应为零")
|
||||
|
||||
// 移除 ID 字段以便进行通用断言
|
||||
delete(responseBody["data"].(map[string]interface{}), "id")
|
||||
// 移除 expectedResponse 中的 id 字段,因为我们已经单独验证了
|
||||
if expectedData, ok := tt.expectedResponse["data"].(map[string]interface{}); ok {
|
||||
delete(expectedData, "id")
|
||||
}
|
||||
}
|
||||
// 移除 code 字段以便进行通用断言
|
||||
delete(responseBody, "code")
|
||||
delete(tt.expectedResponse, "code")
|
||||
assert.Equal(t, tt.expectedResponse, responseBody)
|
||||
|
||||
// 验证 Mock 期望是否都已满足
|
||||
mockRepo.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLogin 测试 Login 方法
|
||||
func TestLogin(t *testing.T) {
|
||||
// 设置release模式阻止废话日志
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
// 创建一个不输出日志的真实 logs.Logger 实例
|
||||
silentLogger := logs.NewSilentLogger()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
requestBody user.LoginRequest
|
||||
mockRepoSetup func(*MockUserRepository)
|
||||
mockTokenServiceSetup func(*MockTokenService)
|
||||
expectedResponse map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "成功登录",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "loginuser",
|
||||
Password: "correctpassword",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
mockUser := &models.User{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Username: "loginuser",
|
||||
Password: "correctpassword", // 明文密码,BeforeCreate 会哈希它
|
||||
}
|
||||
// 调用 BeforeCreate 钩子来哈希密码
|
||||
_ = mockUser.BeforeCreate(nil)
|
||||
m.On("FindByUsername", "loginuser").Return(mockUser, nil).Once()
|
||||
},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {
|
||||
m.On("GenerateToken", uint(1)).Return("mocked_token", nil).Once()
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeSuccess),
|
||||
"message": "登录成功",
|
||||
"data": map[string]interface{}{
|
||||
"username": "loginuser",
|
||||
"id": float64(1),
|
||||
"token": "mocked_token",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败_缺少用户名",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "", // 缺少用户名
|
||||
Password: "password",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeBadRequest),
|
||||
"message": "Key: 'LoginRequest.Username' Error:Field validation for 'Username' failed on the 'required' tag",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "请求参数绑定失败_缺少密码",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "", // 缺少密码
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeBadRequest),
|
||||
"message": "Key: 'LoginRequest.Password' Error:Field validation for 'Password' failed on the 'required' tag",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "用户不存在",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "nonexistent",
|
||||
Password: "anypassword",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
m.On("FindByUsername", "nonexistent").Return(nil, gorm.ErrRecordNotFound).Once()
|
||||
},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeUnauthorized),
|
||||
"message": "用户名或密码不正确",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "查询用户失败_通用数据库错误",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "dberroruser",
|
||||
Password: "password",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
m.On("FindByUsername", "dberroruser").Return(nil, errors.New("database connection error")).Once()
|
||||
},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {}, expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeInternalError),
|
||||
"message": "登录失败",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "密码不正确",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "loginuser",
|
||||
Password: "wrongpassword",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
mockUser := &models.User{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Username: "loginuser",
|
||||
Password: "correctpassword", // 明文密码,BeforeCreate 会哈希它
|
||||
}
|
||||
// 调用 BeforeCreate 钩子来哈希密码
|
||||
_ = mockUser.BeforeCreate(nil)
|
||||
m.On("FindByUsername", "loginuser").Return(mockUser, nil).Once()
|
||||
},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeUnauthorized),
|
||||
"message": "用户名或密码不正确",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "生成Token失败",
|
||||
requestBody: user.LoginRequest{
|
||||
Username: "loginuser",
|
||||
Password: "correctpassword",
|
||||
},
|
||||
mockRepoSetup: func(m *MockUserRepository) {
|
||||
mockUser := &models.User{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Username: "loginuser",
|
||||
Password: "correctpassword", // 明文密码,BeforeCreate 会哈希它
|
||||
}
|
||||
// 调用 BeforeCreate 钩子来哈希密码
|
||||
_ = mockUser.BeforeCreate(nil)
|
||||
m.On("FindByUsername", "loginuser").Return(mockUser, nil).Once()
|
||||
},
|
||||
mockTokenServiceSetup: func(m *MockTokenService) {
|
||||
m.On("GenerateToken", uint(1)).Return("", errors.New("jwt error")).Once()
|
||||
},
|
||||
expectedResponse: map[string]interface{}{
|
||||
"code": float64(controller.CodeInternalError),
|
||||
"message": "登录失败,无法生成认证信息",
|
||||
"data": nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 初始化 Gin 上下文和记录器
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/login", nil) // URL 路径不重要,因为我们不测试路由
|
||||
|
||||
// 设置请求体
|
||||
jsonBody, _ := json.Marshal(tt.requestBody)
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(jsonBody))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 创建 Mock
|
||||
mockRepo := new(MockUserRepository)
|
||||
mockTokenService := new(MockTokenService)
|
||||
|
||||
// 设置 Mock 行为
|
||||
tt.mockRepoSetup(mockRepo)
|
||||
tt.mockTokenServiceSetup(mockTokenService)
|
||||
|
||||
// 创建控制器实例
|
||||
userController := user.NewController(mockRepo, silentLogger, mockTokenService)
|
||||
|
||||
// 调用被测试的方法
|
||||
userController.Login(ctx)
|
||||
|
||||
// 解析响应体
|
||||
var responseBody map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &responseBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 断言响应体中的 code 字段
|
||||
assert.Equal(t, tt.expectedResponse["code"], responseBody["code"])
|
||||
|
||||
// 断言响应内容 (除了 code 字段)
|
||||
if tt.expectedResponse["code"] == float64(controller.CodeSuccess) {
|
||||
// 确保 data 字段存在且是 map[string]interface{} 类型
|
||||
data, ok := responseBody["data"].(map[string]interface{})
|
||||
assert.True(t, ok, "响应体中的 data 字段应为 map[string]interface{}")
|
||||
|
||||
// 验证 id 和 token 存在
|
||||
assert.NotNil(t, data["id"])
|
||||
assert.NotNil(t, data["token"])
|
||||
|
||||
// 移除 ID 和 Token 字段以便进行通用断言
|
||||
delete(responseBody["data"].(map[string]interface{}), "id")
|
||||
delete(tt.expectedResponse["data"].(map[string]interface{}), "id")
|
||||
delete(responseBody["data"].(map[string]interface{}), "token")
|
||||
delete(tt.expectedResponse["data"].(map[string]interface{}), "token")
|
||||
}
|
||||
// 移除 code 字段以便进行通用断言
|
||||
delete(responseBody, "code")
|
||||
delete(tt.expectedResponse, "code")
|
||||
assert.Equal(t, tt.expectedResponse, responseBody)
|
||||
|
||||
// 验证 Mock 期望是否都已满足
|
||||
mockRepo.AssertExpectations(t)
|
||||
mockTokenService.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user