Files
junhong_cmp_fiber/tests/integration/iot_card_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

735 lines
25 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package integration
import (
"bytes"
"context"
"encoding/json"
"fmt"
"mime/multipart"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
pkggorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
"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 TestIotCard_ListStandalone(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
cards := []*model.IotCard{
{ICCID: "TEST0012345678901001", CardType: "data_card", CarrierID: 1, Status: 1},
{ICCID: "TEST0012345678901002", CardType: "data_card", CarrierID: 1, Status: 1},
{ICCID: "TEST0012345678901003", CardType: "data_card", CarrierID: 2, Status: 2},
}
for _, card := range cards {
require.NoError(t, env.TX.Create(card).Error)
}
t.Run("获取单卡列表-无过滤", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/iot-cards/standalone?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/iot-cards/standalone?carrier_id=1", 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("获取单卡列表-按ICCID模糊查询", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/iot-cards/standalone?iccid=901001", 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/iot-cards/standalone", 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 TestIotCard_Import(t *testing.T) {
t.Skip("E2E测试需要 Worker 服务运行处理异步导入任务")
env := integ.NewIntegrationTestEnv(t)
t.Run("导入CSV文件", func(t *testing.T) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "test.csv")
require.NoError(t, err)
csvContent := "iccid\nTEST0012345678902001\nTEST0012345678902002\nTEST0012345678902003"
_, err = part.Write([]byte(csvContent))
require.NoError(t, err)
_ = writer.WriteField("carrier_id", "1")
_ = writer.WriteField("carrier_type", "CMCC")
_ = writer.WriteField("batch_no", "TEST_BATCH_001")
writer.Close()
resp, err := env.AsSuperAdmin().RequestWithHeaders("POST", "/api/admin/iot-cards/import", body.Bytes(), map[string]string{
"Content-Type": writer.FormDataContentType(),
})
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
t.Logf("Import response: code=%d, message=%s", result.Code, result.Message)
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, 0, result.Code)
})
t.Run("导入无文件应返回错误", func(t *testing.T) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
_ = writer.WriteField("carrier_id", "1")
_ = writer.WriteField("carrier_type", "CMCC")
writer.Close()
resp, err := env.AsSuperAdmin().RequestWithHeaders("POST", "/api/admin/iot-cards/import", body.Bytes(), map[string]string{
"Content-Type": writer.FormDataContentType(),
})
require.NoError(t, err)
defer resp.Body.Close()
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
t.Logf("No file response: code=%d, message=%s, data=%v", result.Code, result.Message, result.Data)
assert.NotEqual(t, 0, result.Code, "无文件时应返回错误码")
})
}
func TestIotCard_ImportTaskList(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
task := &model.IotCardImportTask{
TaskNo: "TEST20260123001",
Status: model.ImportTaskStatusCompleted,
CarrierID: 1,
CarrierType: "CMCC",
CarrierName: "中国移动",
TotalCount: 100,
}
require.NoError(t, env.TX.Create(task).Error)
t.Run("获取导入任务列表", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/iot-cards/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/iot-cards/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)
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, "CMCC", dataMap["carrier_type"], "任务详情应返回冗余的运营商类型")
assert.Equal(t, "中国移动", dataMap["carrier_name"], "任务详情应返回冗余的运营商名称")
})
}
func TestIotCard_ImportE2E(t *testing.T) {
t.Skip("E2E测试需要 Worker 服务运行处理异步导入任务")
env := integ.NewIntegrationTestEnv(t)
// 准备测试用的 ICCID20位满足 CMCC 要求)
testICCIDPrefix := "E2ETEST"
testBatchNo1 := fmt.Sprintf("E2E_BATCH_%d_001", time.Now().UnixNano())
testBatchNo2 := fmt.Sprintf("E2E_BATCH_%d_002", time.Now().UnixNano())
testICCIDs := []string{
testICCIDPrefix + "1234567890123",
testICCIDPrefix + "1234567890124",
testICCIDPrefix + "1234567890125",
}
t.Run("完整导入流程验证", func(t *testing.T) {
// Step 1: 通过 API 提交导入任务
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "e2e_test.csv")
require.NoError(t, err)
csvContent := "iccid\n" + testICCIDs[0] + "\n" + testICCIDs[1] + "\n" + testICCIDs[2]
_, err = part.Write([]byte(csvContent))
require.NoError(t, err)
_ = writer.WriteField("carrier_id", "1")
_ = writer.WriteField("carrier_type", "CMCC")
_ = writer.WriteField("batch_no", testBatchNo1)
writer.Close()
resp, err := env.AsSuperAdmin().RequestWithHeaders("POST", "/api/admin/iot-cards/import", body.Bytes(), map[string]string{
"Content-Type": writer.FormDataContentType(),
})
require.NoError(t, err)
defer resp.Body.Close()
var apiResult response.Response
err = json.NewDecoder(resp.Body).Decode(&apiResult)
require.NoError(t, err)
require.Equal(t, 0, apiResult.Code, "API 应返回成功: %s", apiResult.Message)
// 从响应中提取 task_id
dataMap, ok := apiResult.Data.(map[string]interface{})
require.True(t, ok, "响应数据应为 map")
taskIDFloat, ok := dataMap["task_id"].(float64)
require.True(t, ok, "task_id 应存在")
taskID := uint(taskIDFloat)
t.Logf("创建的导入任务 ID: %d", taskID)
// Step 2: 等待 Worker 处理完成(轮询检查任务状态)
var importTask model.IotCardImportTask
maxWaitTime := 30 * time.Second
pollInterval := 500 * time.Millisecond
startTime := time.Now()
ctx := context.Background()
skipCtx := pkggorm.SkipDataPermission(ctx)
for {
if time.Since(startTime) > maxWaitTime {
t.Fatalf("等待超时:任务 %d 未在 %v 内完成", taskID, maxWaitTime)
}
err = env.RawDB().WithContext(skipCtx).First(&importTask, taskID).Error
require.NoError(t, err)
t.Logf("任务状态: %d (1=pending, 2=processing, 3=completed, 4=failed)", importTask.Status)
if importTask.Status == model.ImportTaskStatusCompleted || importTask.Status == model.ImportTaskStatusFailed {
break
}
time.Sleep(pollInterval)
}
// Step 3: 验证任务完成状态
assert.Equal(t, model.ImportTaskStatusCompleted, importTask.Status, "任务应完成")
assert.Equal(t, 3, importTask.TotalCount, "总数应为3")
assert.Equal(t, 3, importTask.SuccessCount, "成功数应为3")
assert.Equal(t, 0, importTask.SkipCount, "跳过数应为0")
assert.Equal(t, 0, importTask.FailCount, "失败数应为0")
t.Logf("任务完成: total=%d, success=%d, skip=%d, fail=%d",
importTask.TotalCount, importTask.SuccessCount, importTask.SkipCount, importTask.FailCount)
// Step 4: 验证 IoT 卡已入库
var cards []model.IotCard
err = env.RawDB().WithContext(skipCtx).Where("iccid IN ?", testICCIDs).Find(&cards).Error
require.NoError(t, err)
assert.Len(t, cards, 3, "应创建3张 IoT 卡")
for _, card := range cards {
assert.Equal(t, uint(1), card.CarrierID, "运营商ID应为1")
assert.Equal(t, testBatchNo1, card.BatchNo, "批次号应匹配")
assert.Equal(t, 1, card.Status, "状态应为在库(1)")
t.Logf("已创建 IoT 卡: ICCID=%s, ID=%d", card.ICCID, card.ID)
}
})
t.Run("重复导入应跳过已存在的ICCID", func(t *testing.T) {
// 再次导入相同的 ICCID应该全部跳过
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "e2e_test_dup.csv")
require.NoError(t, err)
csvContent := "iccid\n" + testICCIDs[0] + "\n" + testICCIDs[1]
_, err = part.Write([]byte(csvContent))
require.NoError(t, err)
_ = writer.WriteField("carrier_id", "1")
_ = writer.WriteField("carrier_type", "CMCC")
_ = writer.WriteField("batch_no", testBatchNo2)
writer.Close()
resp, err := env.AsSuperAdmin().RequestWithHeaders("POST", "/api/admin/iot-cards/import", body.Bytes(), map[string]string{
"Content-Type": writer.FormDataContentType(),
})
require.NoError(t, err)
defer resp.Body.Close()
var apiResult response.Response
err = json.NewDecoder(resp.Body).Decode(&apiResult)
require.NoError(t, err)
require.Equal(t, 0, apiResult.Code)
dataMap := apiResult.Data.(map[string]interface{})
taskID := uint(dataMap["task_id"].(float64))
// 等待处理完成
var importTask model.IotCardImportTask
maxWaitTime := 30 * time.Second
startTime := time.Now()
ctx := context.Background()
skipCtx := pkggorm.SkipDataPermission(ctx)
for {
if time.Since(startTime) > maxWaitTime {
t.Fatalf("等待超时")
}
env.RawDB().WithContext(skipCtx).First(&importTask, taskID)
if importTask.Status == model.ImportTaskStatusCompleted || importTask.Status == model.ImportTaskStatusFailed {
break
}
time.Sleep(500 * time.Millisecond)
}
// 验证2条应该全部跳过
assert.Equal(t, model.ImportTaskStatusCompleted, importTask.Status)
assert.Equal(t, 2, importTask.TotalCount)
assert.Equal(t, 0, importTask.SuccessCount, "成功数应为0全部跳过")
assert.Equal(t, 2, importTask.SkipCount, "跳过数应为2")
t.Logf("重复导入结果: success=%d, skip=%d", importTask.SuccessCount, importTask.SkipCount)
})
}
func TestIotCard_CarrierRedundantFields(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
carrierCode := fmt.Sprintf("REDUND_%d", time.Now().UnixNano())
carrier := &model.Carrier{
CarrierCode: carrierCode,
CarrierName: "冗余字段测试运营商",
CarrierType: "CUCC",
Status: 1,
}
require.NoError(t, env.TX.Create(carrier).Error)
testICCID := fmt.Sprintf("8986%016d", time.Now().UnixNano()%10000000000000000)
card := &model.IotCard{
ICCID: testICCID,
CarrierID: carrier.ID,
CarrierType: carrier.CarrierType,
CarrierName: carrier.CarrierName,
CardType: "data_card",
Status: 1,
}
require.NoError(t, env.TX.Create(card).Error)
t.Run("单卡列表应返回冗余字段", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/iot-cards/standalone?iccid="+testICCID, nil)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
var result response.Response
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
require.Equal(t, 0, result.Code, "API应返回成功实际: %v", result.Message)
dataMap, ok := result.Data.(map[string]interface{})
require.True(t, ok, "Data应为map类型实际: %T", result.Data)
items, ok := dataMap["items"].([]interface{})
require.True(t, ok, "items字段应存在且为数组dataMap: %+v", dataMap)
require.GreaterOrEqual(t, len(items), 1, "列表应至少有1条记录ICCID: %s", testICCID)
cardData := items[0].(map[string]interface{})
assert.Equal(t, "CUCC", cardData["carrier_type"], "列表应返回冗余的运营商类型")
assert.Equal(t, "冗余字段测试运营商", cardData["carrier_name"], "列表应返回冗余的运营商名称")
})
t.Run("单卡详情应返回冗余字段", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/iot-cards/by-iccid/%s", card.ICCID)
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 := result.Data.(map[string]interface{})
assert.Equal(t, "CUCC", dataMap["carrier_type"], "详情应返回冗余的运营商类型")
assert.Equal(t, "冗余字段测试运营商", dataMap["carrier_name"], "详情应返回冗余的运营商名称")
})
}
func TestIotCard_GetByICCID(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
carrierCode := fmt.Sprintf("ICCID_%d", time.Now().UnixNano())
carrier := &model.Carrier{
CarrierCode: carrierCode,
CarrierName: "测试运营商",
CarrierType: "CMCC",
Status: 1,
}
require.NoError(t, env.TX.Create(carrier).Error)
testICCID := fmt.Sprintf("8986%016d", time.Now().UnixNano()%10000000000000000)
card := &model.IotCard{
ICCID: testICCID,
CarrierID: carrier.ID,
CarrierType: carrier.CarrierType,
CarrierName: carrier.CarrierName,
MSISDN: "13800000001",
CardType: "physical",
CardCategory: "normal",
CostPrice: 1000,
DistributePrice: 1500,
Status: 1,
}
require.NoError(t, env.TX.Create(card).Error)
t.Run("通过ICCID查询单卡详情-成功", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/iot-cards/by-iccid/%s", card.ICCID)
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, testICCID, dataMap["iccid"])
assert.Equal(t, "13800000001", dataMap["msisdn"])
assert.Equal(t, "CMCC", dataMap["carrier_type"], "应返回冗余的运营商类型")
assert.Equal(t, "测试运营商", dataMap["carrier_name"], "应返回冗余的运营商名称")
})
t.Run("通过不存在的ICCID查询-应返回错误", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/iot-cards/by-iccid/NONEXISTENT_ICCID", 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, "不存在的ICCID应返回错误码")
})
t.Run("未认证请求-应返回错误", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/iot-cards/by-iccid/%s", card.ICCID)
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 TestIotCard_BatchSetSeriesBinding(t *testing.T) {
env := integ.NewIntegrationTestEnv(t)
// 创建测试数据
shop := env.CreateTestShop("测试店铺", 1, nil)
agentAccount := env.CreateTestAccount(fmt.Sprintf("agent_%d", time.Now().UnixNano()), "password123", constants.UserTypeAgent, &shop.ID, nil)
// 创建套餐系列和分配
series := createTestPackageSeries(t, env, "测试系列")
allocation := createTestAllocation(t, env, shop.ID, series.ID, 0)
// 创建测试卡(归属于该店铺)
timestamp := time.Now().Unix() % 1000000
cards := []*model.IotCard{
{ICCID: fmt.Sprintf("TEST%06d001", timestamp), CardType: "data_card", CarrierID: 1, Status: 1, ShopID: &shop.ID},
{ICCID: fmt.Sprintf("TEST%06d002", timestamp), CardType: "data_card", CarrierID: 1, Status: 1, ShopID: &shop.ID},
{ICCID: fmt.Sprintf("TEST%06d003", timestamp), CardType: "data_card", CarrierID: 1, Status: 1, ShopID: &shop.ID},
}
for _, card := range cards {
require.NoError(t, env.TX.Create(card).Error)
}
t.Run("批量设置卡系列绑定-成功", func(t *testing.T) {
body := map[string]interface{}{
"iccids": []string{cards[0].ICCID, cards[1].ICCID},
"series_allocation_id": allocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/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)
// 验证响应数据
dataMap := result.Data.(map[string]interface{})
assert.Equal(t, float64(2), dataMap["success_count"], "应有2张卡成功绑定")
assert.Equal(t, float64(0), dataMap["fail_count"], "应无失败")
// 验证数据库中数据已更新
var updatedCard model.IotCard
err = env.RawDB().Where("iccid = ?", cards[0].ICCID).First(&updatedCard).Error
require.NoError(t, err)
assert.NotNil(t, updatedCard.SeriesAllocationID)
assert.Equal(t, allocation.ID, *updatedCard.SeriesAllocationID)
})
t.Run("清除卡系列绑定-series_allocation_id=0", func(t *testing.T) {
body := map[string]interface{}{
"iccids": []string{cards[0].ICCID},
"series_allocation_id": 0,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/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 updatedCard model.IotCard
err = env.RawDB().Where("iccid = ?", cards[0].ICCID).First(&updatedCard).Error
require.NoError(t, err)
assert.Nil(t, updatedCard.SeriesAllocationID, "系列分配应被清除")
})
t.Run("批量设置-部分卡不存在", func(t *testing.T) {
body := map[string]interface{}{
"iccids": []string{cards[2].ICCID, "NONEXISTENT_ICCID_999"},
"series_allocation_id": allocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/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"], "应有1张卡成功")
assert.Equal(t, float64(1), dataMap["fail_count"], "应有1张卡失败")
// 验证失败列表
failedItems := dataMap["failed_items"].([]interface{})
assert.Len(t, failedItems, 1)
failedItem := failedItems[0].(map[string]interface{})
assert.Equal(t, "NONEXISTENT_ICCID_999", failedItem["iccid"])
})
t.Run("设置不存在的系列分配-应失败", func(t *testing.T) {
body := map[string]interface{}{
"iccids": []string{cards[2].ICCID},
"series_allocation_id": 999999,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/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{}{
"iccids": []string{cards[2].ICCID},
"series_allocation_id": disabledAllocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/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)
otherCard := &model.IotCard{
ICCID: fmt.Sprintf("OTH%010d", time.Now().Unix()%10000000000),
CardType: "data_card",
CarrierID: 1,
Status: 1,
ShopID: &otherShop.ID,
}
require.NoError(t, env.TX.Create(otherCard).Error)
body := map[string]interface{}{
"iccids": []string{otherCard.ICCID},
"series_allocation_id": allocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/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)
anotherCard := &model.IotCard{
ICCID: fmt.Sprintf("ADM%010d", time.Now().Unix()%10000000000),
CardType: "data_card",
CarrierID: 1,
Status: 1,
ShopID: &anotherShop.ID,
}
require.NoError(t, env.TX.Create(anotherCard).Error)
// 为这个店铺创建系列分配
anotherAllocation := createTestAllocation(t, env, anotherShop.ID, series.ID, 0)
body := map[string]interface{}{
"iccids": []string{anotherCard.ICCID},
"series_allocation_id": anotherAllocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsSuperAdmin().Request("PATCH", "/api/admin/iot-cards/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{}{
"iccids": []string{cards[0].ICCID},
"series_allocation_id": allocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.ClearAuth().Request("PATCH", "/api/admin/iot-cards/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("空ICCID列表-返回成功但无操作", func(t *testing.T) {
body := map[string]interface{}{
"iccids": []string{},
"series_allocation_id": allocation.ID,
}
jsonBody, _ := json.Marshal(body)
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/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"], "空列表无成功项")
}
})
}