2bec257289
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。 - Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件) - 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、 库名 xiujie_admin → iotlcm、哨兵名占位 - 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异, 不破坏「校验 MSISDN 格式」等测试断言) - 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像 digest 记录 - Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23), 移除写死的 git 凭据 - README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 已知:3 个测试包在源仓同一提交上同样失败(client/upstream、 service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
683 lines
22 KiB
Go
683 lines
22 KiB
Go
package inbound
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/batch"
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/batchresult"
|
|
)
|
|
|
|
func TestAdminSourceTypeContract(t *testing.T) {
|
|
if consts.BatchSourceTypeRange != 1 || consts.BatchSourceTypeBatchInput != 2 || consts.BatchSourceTypeFile != 3 {
|
|
t.Fatalf("source_type contract mismatch: range=%d input=%d file=%d",
|
|
consts.BatchSourceTypeRange, consts.BatchSourceTypeBatchInput, consts.BatchSourceTypeFile)
|
|
}
|
|
}
|
|
|
|
func TestValidateRowRequiresAtLeastOneCardIdentifierAndChanKey(t *testing.T) {
|
|
row := &Row{}
|
|
validateRow(row, "")
|
|
|
|
for _, want := range []string{"ICCID/MSISDN/IMSI至少填写一个", "chanKey不能为空"} {
|
|
if !containsReason(row.Reason, want) {
|
|
t.Fatalf("expected reason %q in %q", want, row.Reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseInputTextAcceptsOneIdentifierPerLine(t *testing.T) {
|
|
rows := parseInputText(strings.Join([]string{
|
|
"89860401780000567308",
|
|
"14100007308",
|
|
"460046527567308",
|
|
}, "\n"))
|
|
|
|
if len(rows) != 3 {
|
|
t.Fatalf("row count = %d, want 3", len(rows))
|
|
}
|
|
if rows[0].ICCID != "89860401780000567308" || rows[1].MSISDN != "14100007308" || rows[2].IMSI != "460046527567308" {
|
|
t.Fatalf("unexpected parsed rows: %+v", rows)
|
|
}
|
|
for _, row := range rows {
|
|
if row.InputCode == "" {
|
|
t.Fatalf("expected input code to be recorded: %+v", row)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateRowRejectsTooShortIdentifiers(t *testing.T) {
|
|
row := &Row{ICCID: "4", MSISDN: "4", IMSI: "4", ChanKey: "chan-a"}
|
|
validateRow(row, "")
|
|
|
|
for _, want := range []string{"ICCID格式不正确", "MSISDN格式不正确", "IMSI格式不正确"} {
|
|
if !containsReason(row.Reason, want) {
|
|
t.Fatalf("expected reason %q in %q", want, row.Reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExpandRangeUsesSelectedMSISDN(t *testing.T) {
|
|
rows, err := expandRange(Range{RangeType: "msisdn", StartMSISDN: "13800008000", EndMSISDN: "13800008001"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 2 || rows[0].MSISDN != "13800008000" || rows[1].MSISDN != "13800008001" {
|
|
t.Fatalf("unexpected MSISDN rows: %+v", rows)
|
|
}
|
|
if rows[0].ICCID != "" || rows[0].IMSI != "" {
|
|
t.Fatalf("unselected identifiers must remain empty: %+v", rows[0])
|
|
}
|
|
}
|
|
|
|
func TestExpandRangeAllowsAlphaNumericICCIDRange(t *testing.T) {
|
|
rows, err := expandRange(Range{RangeType: "iccid", StartICCID: "898604F000000000001", EndICCID: "898604F000000000003"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
want := []string{
|
|
"898604F000000000001",
|
|
"898604F000000000002",
|
|
"898604F000000000003",
|
|
}
|
|
if len(rows) != len(want) {
|
|
t.Fatalf("row count = %d, want %d", len(rows), len(want))
|
|
}
|
|
for i, value := range want {
|
|
if rows[i].ICCID != value {
|
|
t.Fatalf("row %d ICCID = %q, want %q", i, rows[i].ICCID, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExpandRangeRejectsAlphaNumericMSISDNRange(t *testing.T) {
|
|
_, err := expandRange(Range{RangeType: "msisdn", StartMSISDN: "1380013800A", EndMSISDN: "1380013800B"})
|
|
if err == nil || !strings.Contains(err.Error(), "range values must be numeric") {
|
|
t.Fatalf("expected numeric range error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateRowSelectedMSISDNDoesNotRequireOtherIdentifiers(t *testing.T) {
|
|
row := &Row{MSISDN: "13800008000", ChanKey: "chan-a"}
|
|
validateRow(row, "msisdn")
|
|
if row.Reason != "" {
|
|
t.Fatalf("unexpected validation reason: %q", row.Reason)
|
|
}
|
|
}
|
|
|
|
func TestValidateRowAllowsAlphaNumericICCID(t *testing.T) {
|
|
row := &Row{ICCID: "898604F4152000015473", MSISDN: "13800008000", IMSI: "460001234567890", ChanKey: "chan-a"}
|
|
validateRow(row, "")
|
|
if row.Reason != "" {
|
|
t.Fatalf("unexpected validation reason: %q", row.Reason)
|
|
}
|
|
}
|
|
|
|
func TestMarkSameBatchDuplicatesChecksICCIDAndMSISDN(t *testing.T) {
|
|
rows := []*Row{
|
|
{RowNo: 1, ICCID: "8986", MSISDN: "13800008000"},
|
|
{RowNo: 2, ICCID: "8986", MSISDN: "13800008001"},
|
|
{RowNo: 3, ICCID: "8987", MSISDN: "13800008000"},
|
|
}
|
|
|
|
markSameBatchDuplicates(rows)
|
|
|
|
if !containsReason(rows[1].Reason, "同批重复ICCID") {
|
|
t.Fatalf("expected row 2 duplicate ICCID, got %q", rows[1].Reason)
|
|
}
|
|
if !containsReason(rows[2].Reason, "同批重复MSISDN") {
|
|
t.Fatalf("expected row 3 duplicate MSISDN, got %q", rows[2].Reason)
|
|
}
|
|
}
|
|
|
|
func TestMarkSameBatchDuplicatesChecksIMSI(t *testing.T) {
|
|
rows := []*Row{
|
|
{RowNo: 1, IMSI: "460001234567890"},
|
|
{RowNo: 2, IMSI: "460001234567890"},
|
|
}
|
|
|
|
markSameBatchDuplicates(rows)
|
|
|
|
if !containsReason(rows[1].Reason, "同批重复IMSI") {
|
|
t.Fatalf("expected duplicate IMSI, got %q", rows[1].Reason)
|
|
}
|
|
}
|
|
|
|
func TestValidateUpstreamBasicInfoAcceptsMatchingIdentifiers(t *testing.T) {
|
|
rows := []*Row{{RowNo: 1, ICCID: "898604F4152000015473", MSISDN: "1440000615473", IMSI: "460087436015473", ChanKey: "chan-a"}}
|
|
upstream := &basicInfoUpstreamStub{response: []model.SimBaseRes[model.SimBasicInfoItem]{
|
|
{
|
|
Code: 0,
|
|
SimKey: &model.SimKey{ChanKey: "chan-a", ICCID: "898604F4152000015473"},
|
|
ResData: model.SimBasicInfoItem{
|
|
ICCID: "898604F4152000015473",
|
|
MSISDN: "1440000615473",
|
|
IMSI: "460087436015473",
|
|
},
|
|
},
|
|
}}
|
|
|
|
err := (&executor{upstream: upstream}).validateUpstreamBasicInfo(context.Background(), &Task{Rows: rows})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rows[0].Reason != "" {
|
|
t.Fatalf("unexpected reason: %q", rows[0].Reason)
|
|
}
|
|
if upstream.calls != 1 {
|
|
t.Fatalf("upstream calls = %d, want 1", upstream.calls)
|
|
}
|
|
if len(upstream.inputs) != 1 || len(upstream.inputs[0].SimKeys) != 1 {
|
|
t.Fatalf("unexpected upstream inputs: %+v", upstream.inputs)
|
|
}
|
|
requestKey := upstream.inputs[0].SimKeys[0]
|
|
if requestKey.ChanKey != "chan-a" || requestKey.ICCID != "898604F4152000015473" {
|
|
t.Fatalf("unexpected upstream request key: %+v", requestKey)
|
|
}
|
|
if requestKey.MSISDN != "" || requestKey.IMSI != "" {
|
|
t.Fatalf("upstream basic-info request must only carry chanKey and ICCID: %+v", requestKey)
|
|
}
|
|
}
|
|
|
|
func TestValidateUpstreamBasicInfoRequiresCompleteResData(t *testing.T) {
|
|
rows := []*Row{{RowNo: 1, ICCID: "898604F4152000015473", MSISDN: "1440000615473", IMSI: "460087436015473", ChanKey: "chan-a"}}
|
|
upstream := &basicInfoUpstreamStub{response: []model.SimBaseRes[model.SimBasicInfoItem]{
|
|
{
|
|
Code: 0,
|
|
SimKey: &model.SimKey{ChanKey: "chan-a", ICCID: "898604f4152000015473", MSISDN: "1440000615473", IMSI: "460087436015473"},
|
|
ResData: model.SimBasicInfoItem{
|
|
ActiveDate: "2026-08-12 00:00:00",
|
|
OpenDate: "2026-08-12 00:00:00",
|
|
},
|
|
},
|
|
}}
|
|
|
|
err := (&executor{upstream: upstream}).validateUpstreamBasicInfo(context.Background(), &Task{Rows: rows})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !containsReason(rows[0].Reason, "上游未返回三码信息") {
|
|
t.Fatalf("expected missing upstream data failure, got %q", rows[0].Reason)
|
|
}
|
|
}
|
|
|
|
func TestValidateUpstreamBasicInfoPreservesNotFoundMessage(t *testing.T) {
|
|
rows := []*Row{{RowNo: 1, ICCID: "8986112223600594371", ChanKey: "chan-a"}}
|
|
upstream := &basicInfoUpstreamStub{response: []model.SimBaseRes[model.SimBasicInfoItem]{
|
|
{
|
|
Code: 0,
|
|
Message: "未查询到对应SIM卡信息",
|
|
SimKey: &model.SimKey{ChanKey: "chan-a", ICCID: "8986112223600594371"},
|
|
},
|
|
}}
|
|
|
|
err := (&executor{upstream: upstream}).validateUpstreamBasicInfo(context.Background(), &Task{Rows: rows})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rows[0].Reason != "上游三码校验失败: 未查询到对应SIM卡信息" {
|
|
t.Fatalf("unexpected reason: %q", rows[0].Reason)
|
|
}
|
|
}
|
|
|
|
func TestValidateUpstreamBasicInfoMatchesByMSISDNAndBackfillsCodes(t *testing.T) {
|
|
rows := []*Row{{RowNo: 1, MSISDN: "14100007308", ChanKey: "chan-a"}}
|
|
upstream := &basicInfoUpstreamStub{response: []model.SimBaseRes[model.SimBasicInfoItem]{
|
|
{
|
|
Code: 0,
|
|
SimKey: &model.SimKey{ChanKey: "chan-a", MSISDN: "14100007308"},
|
|
ResData: model.SimBasicInfoItem{
|
|
ICCID: "89860401780000567308",
|
|
MSISDN: "14100007308",
|
|
IMSI: "460046527567308",
|
|
},
|
|
},
|
|
}}
|
|
|
|
err := (&executor{upstream: upstream}).validateUpstreamBasicInfo(context.Background(), &Task{Rows: rows})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rows[0].Reason != "" {
|
|
t.Fatalf("unexpected reason: %q", rows[0].Reason)
|
|
}
|
|
if rows[0].ICCID != "89860401780000567308" || rows[0].IMSI != "460046527567308" {
|
|
t.Fatalf("expected upstream codes to be backfilled, got %+v", rows[0])
|
|
}
|
|
requestKey := upstream.inputs[0].SimKeys[0]
|
|
if requestKey.ICCID != "" || requestKey.MSISDN != "14100007308" || requestKey.IMSI != "" {
|
|
t.Fatalf("unexpected upstream request key: %+v", requestKey)
|
|
}
|
|
}
|
|
|
|
func TestValidateBatchInputSingleMSISDNBackfillsBeforeCompletenessCheck(t *testing.T) {
|
|
rows := parseInputText("14100007308")
|
|
repo := &inboundRepositoryStub{}
|
|
upstream := &basicInfoUpstreamStub{response: []model.SimBaseRes[model.SimBasicInfoItem]{
|
|
{
|
|
Code: 0,
|
|
SimKey: &model.SimKey{ChanKey: "chan-a", MSISDN: "14100007308"},
|
|
ResData: model.SimBasicInfoItem{
|
|
ICCID: "89860401780000567308",
|
|
MSISDN: "14100007308",
|
|
IMSI: "460046527567308",
|
|
},
|
|
},
|
|
}}
|
|
task := &Task{
|
|
Batch: Batch{SourceType: consts.BatchSourceTypeBatchInput},
|
|
Options: Options{ChanKey: "chan-a", ValidateUpstreamBasicInfo: "1"},
|
|
Rows: rows,
|
|
}
|
|
|
|
err := (&executor{repository: repo, upstream: upstream}).validate(context.Background(), task)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if task.ValidCount != 1 {
|
|
t.Fatalf("valid count = %d, want 1, reason=%q", task.ValidCount, rows[0].Reason)
|
|
}
|
|
if rows[0].ICCID != "89860401780000567308" || rows[0].IMSI != "460046527567308" {
|
|
t.Fatalf("expected upstream codes to be backfilled, got %+v", rows[0])
|
|
}
|
|
if upstream.calls != 1 {
|
|
t.Fatalf("upstream calls = %d, want 1", upstream.calls)
|
|
}
|
|
}
|
|
|
|
func TestValidateSkipsUpstreamBasicInfoWhenDisabled(t *testing.T) {
|
|
rows := []*Row{{
|
|
RowNo: 1,
|
|
ICCID: "89860401780000567308",
|
|
MSISDN: "14100007308",
|
|
IMSI: "460046527567308",
|
|
ChanKey: "chan-a",
|
|
}}
|
|
upstream := &basicInfoUpstreamStub{err: errors.New("gateway unavailable")}
|
|
task := &Task{
|
|
Batch: Batch{SourceType: consts.BatchSourceTypeFile},
|
|
Options: Options{ValidateUpstreamBasicInfo: "0"},
|
|
Rows: rows,
|
|
}
|
|
|
|
err := (&executor{repository: &inboundRepositoryStub{}, upstream: upstream}).validate(context.Background(), task)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if upstream.calls != 0 {
|
|
t.Fatalf("upstream calls = %d, want 0", upstream.calls)
|
|
}
|
|
if task.ValidCount != 1 {
|
|
t.Fatalf("valid count = %d, want 1, reason=%q", task.ValidCount, rows[0].Reason)
|
|
}
|
|
if task.Options.ValidateUpstreamBasicInfo != "0" {
|
|
t.Fatalf("validate option = %q, want 0", task.Options.ValidateUpstreamBasicInfo)
|
|
}
|
|
}
|
|
|
|
func TestValidateRunsUpstreamBasicInfoWhenEnabled(t *testing.T) {
|
|
rows := []*Row{{
|
|
RowNo: 1,
|
|
ICCID: "89860401780000567308",
|
|
MSISDN: "14100007308",
|
|
IMSI: "460046527567308",
|
|
ChanKey: "chan-a",
|
|
}}
|
|
upstream := &basicInfoUpstreamStub{err: errors.New("gateway unavailable")}
|
|
task := &Task{
|
|
Batch: Batch{SourceType: consts.BatchSourceTypeFile},
|
|
Options: Options{ValidateUpstreamBasicInfo: "1"},
|
|
Rows: rows,
|
|
}
|
|
|
|
err := (&executor{repository: &inboundRepositoryStub{}, upstream: upstream}).validate(context.Background(), task)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if upstream.calls != 1 {
|
|
t.Fatalf("upstream calls = %d, want 1", upstream.calls)
|
|
}
|
|
if task.ValidCount != 0 {
|
|
t.Fatalf("valid count = %d, want 0, reason=%q", task.ValidCount, rows[0].Reason)
|
|
}
|
|
if task.Options.ValidateUpstreamBasicInfo != "1" {
|
|
t.Fatalf("validate option = %q, want 1", task.Options.ValidateUpstreamBasicInfo)
|
|
}
|
|
}
|
|
|
|
func TestPopulateExcelRowsReturnsRetryableErrorWhenSourceIsUnreadable(t *testing.T) {
|
|
sourceErr := errors.New("OSS local file does not exist")
|
|
task := &Task{Source: Source{ImportFileOssID: 1270}}
|
|
repository := &inboundRepositoryStub{readOssErr: sourceErr}
|
|
|
|
err := (&executor{repository: repository}).populateExcelRows(context.Background(), task)
|
|
if !errors.Is(err, sourceErr) || !batch.IsRetryable(err) {
|
|
t.Fatalf("populateExcelRows() error = %v, want retryable wrapper of %v", err, sourceErr)
|
|
}
|
|
}
|
|
|
|
func TestValidateUpstreamBasicInfoWaitsForRateLimitBeforeQuery(t *testing.T) {
|
|
var order []string
|
|
rows := []*Row{{RowNo: 1, ICCID: "89860401780000567308", MSISDN: "14100007308", IMSI: "460046527567308", ChanKey: "chan-a"}}
|
|
limiter := &recordingRateLimiter{
|
|
onWait: func(context.Context, string, int) error {
|
|
order = append(order, "limit")
|
|
return nil
|
|
},
|
|
}
|
|
upstream := &basicInfoUpstreamStub{
|
|
onQuery: func() {
|
|
order = append(order, "query")
|
|
},
|
|
response: []model.SimBaseRes[model.SimBasicInfoItem]{
|
|
{
|
|
Code: 0,
|
|
SimKey: &model.SimKey{ChanKey: "chan-a", ICCID: "89860401780000567308"},
|
|
ResData: model.SimBasicInfoItem{
|
|
ICCID: "89860401780000567308",
|
|
MSISDN: "14100007308",
|
|
IMSI: "460046527567308",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := (&executor{upstream: upstream, rateLimiter: limiter}).validateUpstreamBasicInfo(context.Background(), &Task{
|
|
Batch: Batch{BatchNo: "B-1"},
|
|
Rows: rows,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if limiter.calls != 1 || limiter.chanKey != "chan-a" || limiter.itemCount != 1 {
|
|
t.Fatalf("unexpected limiter call: %+v", limiter)
|
|
}
|
|
if upstream.calls != 1 {
|
|
t.Fatalf("upstream calls = %d, want 1", upstream.calls)
|
|
}
|
|
if len(order) != 2 || order[0] != "limit" || order[1] != "query" {
|
|
t.Fatalf("unexpected call order: %+v", order)
|
|
}
|
|
}
|
|
|
|
func TestValidateUpstreamBasicInfoStopsWhenRateLimitFails(t *testing.T) {
|
|
rows := []*Row{{RowNo: 1, ICCID: "89860401780000567308", ChanKey: "chan-a"}}
|
|
upstream := &basicInfoUpstreamStub{}
|
|
limiter := &recordingRateLimiter{err: errors.New("redis down")}
|
|
|
|
err := (&executor{upstream: upstream, rateLimiter: limiter}).validateUpstreamBasicInfo(context.Background(), &Task{
|
|
Batch: Batch{BatchNo: "B-1"},
|
|
Rows: rows,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if upstream.calls != 0 {
|
|
t.Fatalf("upstream calls = %d, want 0", upstream.calls)
|
|
}
|
|
if !strings.Contains(rows[0].Reason, "redis down") {
|
|
t.Fatalf("expected rate limit failure reason, got %q", rows[0].Reason)
|
|
}
|
|
}
|
|
|
|
func TestValidateUpstreamBasicInfoRejectsMismatch(t *testing.T) {
|
|
rows := []*Row{{RowNo: 1, ICCID: "89860001", MSISDN: "13800008000", IMSI: "460001", ChanKey: "chan-a"}}
|
|
upstream := &basicInfoUpstreamStub{response: []model.SimBaseRes[model.SimBasicInfoItem]{
|
|
{
|
|
Code: 0,
|
|
SimKey: &model.SimKey{ChanKey: "chan-a", ICCID: "89860001"},
|
|
ResData: model.SimBasicInfoItem{ICCID: "89860001", MSISDN: "13800008099", IMSI: "460001"},
|
|
},
|
|
}}
|
|
|
|
err := (&executor{upstream: upstream}).validateUpstreamBasicInfo(context.Background(), &Task{Rows: rows})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(rows[0].Reason, "MSISDN") {
|
|
t.Fatalf("expected MSISDN mismatch reason, got %q", rows[0].Reason)
|
|
}
|
|
}
|
|
|
|
func TestValidateUpstreamBasicInfoRejectsUpstreamError(t *testing.T) {
|
|
rows := []*Row{{RowNo: 1, ICCID: "89860001", ChanKey: "chan-a"}}
|
|
upstream := &basicInfoUpstreamStub{err: errors.New("gateway unavailable")}
|
|
|
|
err := (&executor{upstream: upstream}).validateUpstreamBasicInfo(context.Background(), &Task{Rows: rows})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(rows[0].Reason, "gateway unavailable") {
|
|
t.Fatalf("expected upstream error reason, got %q", rows[0].Reason)
|
|
}
|
|
}
|
|
|
|
func TestParseTableRowsAcceptsChineseInboundHeaders(t *testing.T) {
|
|
rows := parseTableRows([][]string{
|
|
{"ICCID号", "手机号", "国际移动用户识别码", "国际移动设备识别码", "通道编码", "卡类型", "上游套餐编码", "供应商编码"},
|
|
{"898604F4152000015473", "1440000615473", "460087436015473", "865111111111111", "channel_003", "通用插拔 SIM 卡", "UP001", "SUP001"},
|
|
})
|
|
|
|
if len(rows) != 1 {
|
|
t.Fatalf("row count = %d, want 1", len(rows))
|
|
}
|
|
row := rows[0]
|
|
if row.ICCID != "898604F4152000015473" || row.MSISDN != "1440000615473" ||
|
|
row.IMSI != "460087436015473" || row.IMEI != "865111111111111" || row.ChanKey != "channel_003" {
|
|
t.Fatalf("unexpected parsed row: %+v", row)
|
|
}
|
|
if row.CardType != "通用插拔 SIM 卡" || row.UpPkgCode != "UP001" || row.SuppliersCode != "SUP001" {
|
|
t.Fatalf("unexpected inbound options: %+v", row)
|
|
}
|
|
}
|
|
|
|
func TestParseTableRowsAcceptsRequiredMarkerHeaders(t *testing.T) {
|
|
rows := parseTableRows([][]string{
|
|
{"ICCID*", "MSISDN*", "IMSI*", "通道编号*"},
|
|
{"89860401780000567308", "14100007308", "460046527567308", "channel_003"},
|
|
})
|
|
|
|
if len(rows) != 1 {
|
|
t.Fatalf("row count = %d, want 1", len(rows))
|
|
}
|
|
row := rows[0]
|
|
if row.ICCID != "89860401780000567308" || row.MSISDN != "14100007308" ||
|
|
row.IMSI != "460046527567308" || row.ChanKey != "channel_003" {
|
|
t.Fatalf("unexpected parsed row: %+v", row)
|
|
}
|
|
}
|
|
|
|
func TestValidateSkipsUpstreamWhenLocalIdentifierFormatFails(t *testing.T) {
|
|
rows := []*Row{{RowNo: 1, ICCID: "4", MSISDN: "4", IMSI: "4", ChanKey: "chan-a"}}
|
|
upstream := &basicInfoUpstreamStub{}
|
|
repo := &inboundRepositoryStub{}
|
|
task := &Task{
|
|
Rows: rows,
|
|
Options: Options{ChanKey: "chan-a"},
|
|
}
|
|
|
|
err := (&executor{repository: repo, upstream: upstream}).validate(context.Background(), task)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if upstream.calls != 0 {
|
|
t.Fatalf("upstream calls = %d, want 0", upstream.calls)
|
|
}
|
|
if !containsReason(rows[0].Reason, "ICCID格式不正确") {
|
|
t.Fatalf("expected local validation failure, got %q", rows[0].Reason)
|
|
}
|
|
if task.ValidCount != 0 {
|
|
t.Fatalf("valid count = %d, want 0", task.ValidCount)
|
|
}
|
|
}
|
|
|
|
func TestValidateRequiresCompleteIdentifiersAfterUpstreamStep(t *testing.T) {
|
|
rows := []*Row{{RowNo: 1, MSISDN: "14100007308", ChanKey: "chan-a"}}
|
|
repo := &inboundRepositoryStub{}
|
|
task := &Task{
|
|
Rows: rows,
|
|
Source: Source{Range: Range{RangeType: "msisdn"}},
|
|
}
|
|
|
|
err := (&executor{repository: repo}).validate(context.Background(), task)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !containsReason(rows[0].Reason, "三码信息不完整") {
|
|
t.Fatalf("expected incomplete identifiers failure, got %q", rows[0].Reason)
|
|
}
|
|
if task.ValidCount != 0 {
|
|
t.Fatalf("valid count = %d, want 0", task.ValidCount)
|
|
}
|
|
}
|
|
|
|
func TestValidateNormalizesExcelInboundOptionsAndBackfillsBatchInfo(t *testing.T) {
|
|
rows := parseTableRows([][]string{
|
|
{"ICCID*", "MSISDN*", "IMSI*", "通道编号*", "卡类型", "上游套餐编码", "供应商编码", "是否贴片转插拔", "是否运营商达量断网"},
|
|
{"89860401780000567308", "14100007308", "460046527567308", "channel_003", "MP1-消费级插拔物联卡", "UP001", "SUP001", "是", "否"},
|
|
{"89860401780000567309", "14100007309", "460046527567309", "channel_003", "", "UP001", "SUP001", "", ""},
|
|
})
|
|
task := &Task{Rows: rows, Batch: Batch{SourceType: consts.BatchSourceTypeFile}}
|
|
|
|
err := (&executor{repository: &inboundRepositoryStub{}}).validate(context.Background(), task)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if task.ValidCount != 2 {
|
|
t.Fatalf("valid count = %d, want 2", task.ValidCount)
|
|
}
|
|
if task.Options.ChanKey != "channel_003" || task.Options.CardType != "4" ||
|
|
task.Options.UpPkgCode != "UP001" || task.Options.SuppliersCode != "SUP001" {
|
|
t.Fatalf("unexpected task options: %+v", task.Options)
|
|
}
|
|
if rows[1].CardType != "4" || rows[1].InsertToPlugEnabled != "0" || rows[1].CarrierTrafficCutoffEnabled != "1" {
|
|
t.Fatalf("expected blank row options to be backfilled from batch, got %+v", rows[1])
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsMixedInboundOptionsInOneExcelBatch(t *testing.T) {
|
|
rows := parseTableRows([][]string{
|
|
{"ICCID*", "MSISDN*", "IMSI*", "通道编号*", "卡类型"},
|
|
{"89860401780000567308", "14100007308", "460046527567308", "channel_003", "1"},
|
|
{"89860401780000567309", "14100007309", "460046527567309", "channel_003", "2"},
|
|
})
|
|
task := &Task{Rows: rows, Batch: Batch{SourceType: consts.BatchSourceTypeFile}}
|
|
|
|
err := (&executor{repository: &inboundRepositoryStub{}}).validate(context.Background(), task)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if task.ValidCount != 0 {
|
|
t.Fatalf("valid count = %d, want 0", task.ValidCount)
|
|
}
|
|
if task.FailureReason != "同批入库配置必须一致" {
|
|
t.Fatalf("unexpected task failure reason: %q", task.FailureReason)
|
|
}
|
|
if !containsReason(rows[0].Reason, "卡类型必须同批一致") || !containsReason(rows[1].Reason, "卡类型必须同批一致") {
|
|
t.Fatalf("expected consistency errors, got row1=%q row2=%q", rows[0].Reason, rows[1].Reason)
|
|
}
|
|
}
|
|
|
|
func containsReason(actual, want string) bool {
|
|
return strings.Contains(actual, want)
|
|
}
|
|
|
|
type basicInfoUpstreamStub struct {
|
|
calls int
|
|
inputs []*model.SimBasicInfoInput
|
|
response []model.SimBaseRes[model.SimBasicInfoItem]
|
|
err error
|
|
onQuery func()
|
|
}
|
|
|
|
func (s *basicInfoUpstreamStub) QuerySimBasicInfo(_ context.Context, input *model.SimBasicInfoInput) ([]model.SimBaseRes[model.SimBasicInfoItem], error) {
|
|
if s.onQuery != nil {
|
|
s.onQuery()
|
|
}
|
|
s.calls++
|
|
if input == nil || len(input.SimKeys) == 0 {
|
|
return nil, errors.New("missing sim keys")
|
|
}
|
|
s.inputs = append(s.inputs, cloneBasicInfoInput(input))
|
|
if s.err != nil {
|
|
return nil, s.err
|
|
}
|
|
return s.response, nil
|
|
}
|
|
|
|
func cloneBasicInfoInput(input *model.SimBasicInfoInput) *model.SimBasicInfoInput {
|
|
clone := &model.SimBasicInfoInput{
|
|
ReqData: input.ReqData,
|
|
SimKeys: make([]*model.SimKey, 0, len(input.SimKeys)),
|
|
}
|
|
for _, item := range input.SimKeys {
|
|
if item == nil {
|
|
clone.SimKeys = append(clone.SimKeys, nil)
|
|
continue
|
|
}
|
|
copied := *item
|
|
clone.SimKeys = append(clone.SimKeys, &copied)
|
|
}
|
|
return clone
|
|
}
|
|
|
|
type recordingRateLimiter struct {
|
|
calls int
|
|
chanKey string
|
|
itemCount int
|
|
err error
|
|
onWait func(context.Context, string, int) error
|
|
}
|
|
|
|
func (l *recordingRateLimiter) Wait(ctx context.Context, chanKey string, itemCount int) error {
|
|
l.calls++
|
|
l.chanKey = chanKey
|
|
l.itemCount = itemCount
|
|
if l.onWait != nil {
|
|
return l.onWait(ctx, chanKey, itemCount)
|
|
}
|
|
return l.err
|
|
}
|
|
|
|
type inboundRepositoryStub struct {
|
|
readOssContent []byte
|
|
readOssName string
|
|
readOssErr error
|
|
}
|
|
|
|
func (s *inboundRepositoryStub) Load(context.Context, string) (*Task, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
func (s *inboundRepositoryStub) ReadOssFile(context.Context, int64) ([]byte, string, error) {
|
|
if s.readOssErr != nil {
|
|
return nil, "", s.readOssErr
|
|
}
|
|
return s.readOssContent, s.readOssName, nil
|
|
}
|
|
|
|
func (s *inboundRepositoryStub) FindExistingStocks(context.Context, string, []string, []string, []string) ([]Stock, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *inboundRepositoryStub) SaveValidationFailure(context.Context, *Task) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
func (s *inboundRepositoryStub) SaveSuccess(context.Context, *Task) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
func (s *inboundRepositoryStub) SaveBusinessFailure(context.Context, *Task, string) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
func (s *inboundRepositoryStub) SaveResultFile(context.Context, *Task, batchresult.Request) error {
|
|
return errors.New("not implemented")
|
|
}
|