Files
junhong_cmp_fiber/tests/integration/device_test.go
huang a945a4f554
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m37s
feat: 实现卡和设备的套餐系列绑定功能
- 添加 Device 和 IotCard 模型的 SeriesID 字段
- 实现 DeviceService 和 IotCardService 的套餐系列绑定逻辑
- 添加 DeviceStore 和 IotCardStore 的数据库操作方法
- 更新 API 接口和路由支持套餐系列绑定
- 创建数据库迁移脚本(000027_add_series_binding_fields)
- 添加完整的单元测试和集成测试
- 更新 OpenAPI 文档
- 归档 OpenSpec 变更文档

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-28 19:49:45 +08:00

504 lines
17 KiB
Go

package integration
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/response"
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDevice_List(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
// 创建测试设备
devices := []*model.Device{
{DeviceNo: "TEST_DEVICE_001", DeviceName: "测试设备1", DeviceType: "router", MaxSimSlots: 4, Status: constants.DeviceStatusInStock},
{DeviceNo: "TEST_DEVICE_002", DeviceName: "测试设备2", DeviceType: "router", MaxSimSlots: 2, Status: constants.DeviceStatusInStock},
{DeviceNo: "TEST_DEVICE_003", DeviceName: "测试设备3", DeviceType: "mifi", MaxSimSlots: 1, Status: constants.DeviceStatusDistributed},
}
for _, device := range devices {
require.NoError(t, env.TX.Create(device).Error)
}
t.Run("获取设备列表-无过滤", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/devices?page=1&page_size=20", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
t.Run("获取设备列表-按设备类型过滤", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/devices?device_type=router", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
t.Run("获取设备列表-按状态过滤", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/devices?status=%d", constants.DeviceStatusInStock), nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
t.Run("未认证请求应返回错误", func(t *testing.T) {
resp, err := env.ClearAuth().Request("GET", "/api/admin/devices", nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "未认证请求应返回错误码")
})
}
func TestDevice_GetByID(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
// 创建测试设备
device := &model.Device{
DeviceNo: "TEST_DEVICE_GET_001",
DeviceName: "测试设备详情",
DeviceType: "router",
MaxSimSlots: 4,
Status: constants.DeviceStatusInStock,
}
require.NoError(t, env.TX.Create(device).Error)
t.Run("获取设备详情-成功", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/devices/%d", device.ID)
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
// 验证返回数据
dataMap, ok := result.Data.(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "TEST_DEVICE_GET_001", dataMap["device_no"])
})
t.Run("获取不存在的设备-应返回错误", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/devices/999999", nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "不存在的设备应返回错误码")
})
}
func TestDevice_Delete(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
device := &model.Device{
DeviceNo: "TEST_DEVICE_DEL_001",
DeviceName: "测试删除设备",
DeviceType: "router",
MaxSimSlots: 4,
Status: constants.DeviceStatusInStock,
}
require.NoError(t, env.TX.Create(device).Error)
t.Run("删除设备-成功", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/devices/%d", device.ID)
resp, err := env.AsSuperAdmin().Request("DELETE", url, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
var deletedDevice model.Device
err = env.RawDB().Unscoped().First(&deletedDevice, device.ID).Error
require.NoError(t, err)
assert.NotNil(t, deletedDevice.DeletedAt)
})
}
func TestDeviceImport_TaskList(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
task := &model.DeviceImportTask{
TaskNo: "TEST_DEVICE_IMPORT_001",
Status: model.ImportTaskStatusCompleted,
BatchNo: "TEST_BATCH_001",
TotalCount: 100,
}
require.NoError(t, env.TX.Create(task).Error)
t.Run("获取导入任务列表", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/devices/import/tasks?page=1&page_size=20", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
t.Run("获取导入任务详情", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/devices/import/tasks/%d", task.ID)
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
})
}
func TestDevice_GetByIMEI(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
// 创建测试设备
device := &model.Device{
DeviceNo: "TEST_IMEI_001",
DeviceName: "测试IMEI查询设备",
DeviceType: "router",
MaxSimSlots: 4,
Status: constants.DeviceStatusInStock,
}
require.NoError(t, env.TX.Create(device).Error)
t.Run("通过IMEI查询设备详情-成功", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/devices/by-imei/%s", device.DeviceNo)
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
// 验证返回数据
dataMap, ok := result.Data.(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "TEST_IMEI_001", dataMap["device_no"])
assert.Equal(t, "测试IMEI查询设备", dataMap["device_name"])
})
t.Run("通过不存在的IMEI查询-应返回错误", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/devices/by-imei/NONEXISTENT_IMEI", nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "不存在的IMEI应返回错误码")
})
t.Run("未认证请求-应返回错误", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/devices/by-imei/%s", device.DeviceNo)
resp, err := env.ClearAuth().Request("GET", url, nil)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "未认证请求应返回错误码")
})
}
func TestDevice_BatchSetSeriesBinding(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
shop := env.CreateTestShop("测试店铺", 1, nil)
agentAccount := env.CreateTestAccount(fmt.Sprintf("agent_dev_%d", time.Now().UnixNano()), "password123", constants.UserTypeAgent, &shop.ID, nil)
series := createTestPackageSeries(t, env, "测试系列")
allocation := createTestAllocation(t, env, shop.ID, series.ID, 0)
devices := []*model.Device{
{DeviceNo: fmt.Sprintf("DEV_%d_001", time.Now().UnixNano()), DeviceName: "测试设备1", DeviceType: "router", MaxSimSlots: 4, Status: constants.DeviceStatusInStock, ShopID: &shop.ID},
{DeviceNo: fmt.Sprintf("DEV_%d_002", time.Now().UnixNano()), DeviceName: "测试设备2", DeviceType: "mifi", MaxSimSlots: 2, Status: constants.DeviceStatusInStock, ShopID: &shop.ID},
{DeviceNo: fmt.Sprintf("DEV_%d_003", time.Now().UnixNano()), DeviceName: "测试设备3", DeviceType: "router", MaxSimSlots: 4, Status: constants.DeviceStatusInStock, ShopID: &shop.ID},
}
for _, device := range devices {
require.NoError(t, env.TX.Create(device).Error)
}
t.Run("批量设置设备系列绑定-成功", func(t *testing.T) {
body := map[string]interface{}{
"device_ids": []uint{devices[0].ID, devices[1].ID},
"series_allocation_id": allocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
if result.Data != nil {
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, float64(2), dataMap["success_count"], "应有2个设备成功绑定")
assert.Equal(t, float64(0), dataMap["fail_count"], "应无失败")
} else {
t.Logf("Response data is nil: code=%d, message=%s", result.Code, result.Message)
}
var updatedDevice model.Device
err = env.RawDB().Where("id = ?", devices[0].ID).First(&updatedDevice).Error
require.NoError(t, err)
assert.NotNil(t, updatedDevice.SeriesAllocationID)
assert.Equal(t, allocation.ID, *updatedDevice.SeriesAllocationID)
})
t.Run("清除设备系列绑定-series_allocation_id=0", func(t *testing.T) {
body := map[string]interface{}{
"device_ids": []uint{devices[0].ID},
"series_allocation_id": 0,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
var updatedDevice model.Device
err = env.RawDB().Where("id = ?", devices[0].ID).First(&updatedDevice).Error
require.NoError(t, err)
assert.Nil(t, updatedDevice.SeriesAllocationID, "系列分配应被清除")
})
t.Run("批量设置-部分设备不存在", func(t *testing.T) {
body := map[string]interface{}{
"device_ids": []uint{devices[2].ID, 999999},
"series_allocation_id": allocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code)
if result.Data != nil {
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, float64(1), dataMap["success_count"], "应有1个设备成功")
assert.Equal(t, float64(1), dataMap["fail_count"], "应有1个设备失败")
if dataMap["failed_items"] != nil {
failedItems := dataMap["failed_items"].([]interface{})
assert.Len(t, failedItems, 1)
failedItem := failedItems[0].(map[string]interface{})
assert.Equal(t, float64(999999), failedItem["device_id"])
}
}
})
t.Run("设置不存在的系列分配-应失败", func(t *testing.T) {
body := map[string]interface{}{
"device_ids": []uint{devices[2].ID},
"series_allocation_id": 999999,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "不存在的系列分配应返回错误")
})
t.Run("设置禁用的系列分配-应失败", func(t *testing.T) {
disabledSeries := createTestPackageSeries(t, env, "禁用系列")
disabledAllocation := createTestAllocation(t, env, shop.ID, disabledSeries.ID, 0)
env.TX.Model(&model.ShopSeriesAllocation{}).Where("id = ?", disabledAllocation.ID).Update("status", constants.StatusDisabled)
body := map[string]interface{}{
"device_ids": []uint{devices[2].ID},
"series_allocation_id": disabledAllocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "禁用的系列分配应返回错误")
})
t.Run("代理商设置其他店铺的设备-应失败", func(t *testing.T) {
otherShop := env.CreateTestShop("其他店铺", 1, nil)
otherDevice := &model.Device{
DeviceNo: fmt.Sprintf("OTHER_%d", time.Now().UnixNano()),
DeviceName: "其他设备",
DeviceType: "router",
MaxSimSlots: 4,
Status: constants.DeviceStatusInStock,
ShopID: &otherShop.ID,
}
require.NoError(t, env.TX.Create(otherDevice).Error)
body := map[string]interface{}{
"device_ids": []uint{otherDevice.ID},
"series_allocation_id": allocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, float64(0), dataMap["success_count"], "不应有成功")
assert.Equal(t, float64(1), dataMap["fail_count"], "应全部失败")
})
t.Run("超级管理员可以设置任意店铺的设备", func(t *testing.T) {
anotherShop := env.CreateTestShop("另一个店铺", 1, nil)
anotherDevice := &model.Device{
DeviceNo: fmt.Sprintf("ADMIN_%d", time.Now().UnixNano()),
DeviceName: "管理员设备",
DeviceType: "router",
MaxSimSlots: 4,
Status: constants.DeviceStatusInStock,
ShopID: &anotherShop.ID,
}
require.NoError(t, env.TX.Create(anotherDevice).Error)
anotherAllocation := createTestAllocation(t, env, anotherShop.ID, series.ID, 0)
body := map[string]interface{}{
"device_ids": []uint{anotherDevice.ID},
"series_allocation_id": anotherAllocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "超级管理员应能设置任意店铺的设备")
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, float64(1), dataMap["success_count"])
})
t.Run("未认证请求应返回错误", func(t *testing.T) {
body := map[string]interface{}{
"device_ids": []uint{devices[0].ID},
"series_allocation_id": allocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.ClearAuth().Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.NotEqual(t, 0, result.Code, "未认证请求应返回错误码")
})
t.Run("空设备ID列表-返回成功但无操作", func(t *testing.T) {
body := map[string]interface{}{
"device_ids": []uint{},
"series_allocation_id": allocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, 0, result.Code, "当前实现:空列表返回成功")
if result.Data != nil {
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, float64(0), dataMap["success_count"], "空列表无成功项")
}
})
}