All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m37s
- 添加 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>
504 lines
17 KiB
Go
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"], "空列表无成功项")
|
|
}
|
|
})
|
|
}
|