755 lines
25 KiB
Go
755 lines
25 KiB
Go
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"
|
||
"go.uber.org/zap"
|
||
"go.uber.org/zap/zapcore"
|
||
"gorm.io/datatypes"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// MockDeviceRepository 是 DeviceRepository 接口的模拟实现
|
||
type MockDeviceRepository struct {
|
||
mock.Mock
|
||
}
|
||
|
||
// Create 模拟 DeviceRepository 的 Create 方法
|
||
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)
|
||
}
|
||
|
||
// 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
|
||
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("Create", 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: http.StatusCreated,
|
||
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("Create", 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: http.StatusCreated,
|
||
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: http.StatusBadRequest,
|
||
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("Create", mock.Anything).Return(errors.New("db error")).Once()
|
||
},
|
||
expectedStatus: http.StatusOK,
|
||
expectedCode: http.StatusInternalServerError,
|
||
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) {
|
||
// 期望 Create 方法被调用,并返回一个模拟的数据库错误
|
||
// 这个错误模拟的是数据库层因为 Properties 字段的 JSON 格式无效而拒绝保存
|
||
m.On("Create", 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: http.StatusInternalServerError, // 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, getSilentLogger()).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: http.StatusOK,
|
||
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: http.StatusNotFound,
|
||
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: http.StatusBadRequest,
|
||
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: http.StatusInternalServerError,
|
||
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, getSilentLogger()).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: http.StatusOK,
|
||
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: http.StatusOK,
|
||
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: http.StatusInternalServerError,
|
||
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, getSilentLogger()).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: http.StatusOK,
|
||
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: http.StatusBadRequest,
|
||
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: http.StatusNotFound,
|
||
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: http.StatusBadRequest,
|
||
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: http.StatusInternalServerError,
|
||
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: http.StatusInternalServerError, // 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: http.StatusOK,
|
||
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, getSilentLogger()).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: http.StatusOK,
|
||
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: http.StatusBadRequest,
|
||
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: http.StatusInternalServerError,
|
||
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: http.StatusInternalServerError, // 当前控制器逻辑会将 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, getSilentLogger()).DeleteDevice(ctx)
|
||
})
|
||
})
|
||
}
|
||
}
|