742 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			742 lines
		
	
	
		
			24 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"
 | 
						||
	"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)
 | 
						||
			})
 | 
						||
		})
 | 
						||
	}
 | 
						||
}
 |