Files
junhong_cmp_fiber/tests/integration/iot_card_test.go
huang 23eb0307bb
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m30s
feat: 实现门店套餐分配功能并统一测试基础设施
新增功能:
- 门店套餐分配管理(shop_package_allocation):支持门店套餐库存管理
- 门店套餐系列分配管理(shop_series_allocation):支持套餐系列分配和佣金层级设置
- 我的套餐查询(my_package):支持门店查询自己的套餐分配情况

测试改进:
- 统一集成测试基础设施,新增 testutils.NewIntegrationTestEnv
- 重构所有集成测试使用新的测试环境设置
- 移除旧的测试辅助函数和冗余测试文件
- 新增 test_helpers_test.go 统一任务测试辅助

技术细节:
- 新增数据库迁移 000025_create_shop_allocation_tables
- 新增 3 个 Handler、Service、Store 和对应的单元测试
- 更新 OpenAPI 文档和文档生成器
- 测试覆盖率:Service 层 > 90%

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-28 10:45:16 +08:00

477 lines
16 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"
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, "未认证请求应返回错误码")
})
}