Files
iotlcm/worker-svr/internal/service/data/executor_accuracy_test.go
T

254 lines
10 KiB
Go

package data
import (
"context"
"errors"
"fmt"
"testing"
"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/model/entity"
)
func TestAccuracyInvalidDuplicateKeepsValidTrafficCutoffRow(t *testing.T) {
for _, invalidFirst := range []bool{true, false} {
for _, invalidInput := range []string{"MSISDN", "IMSI", "authorization", "existing"} {
t.Run(fmt.Sprintf("%s/invalidFirst=%v", invalidInput, invalidFirst), func(t *testing.T) {
operation, err := OperationByBatchType(107)
if err != nil {
t.Fatal(err)
}
card := &Card{ID: 10, ICCID: "same-card", MSISDN: "1001", IMSI: "2001", ChanKey: "channel", DataServiceStatus: 1}
invalid := &Detail{ID: 1, ICCID: card.ICCID, Authorized: true, Card: card}
var wantReason string
switch invalidInput {
case "MSISDN":
invalid.InputMSISDN = "wrong"
wantReason = "MSISDN与卡档案不一致"
case "IMSI":
invalid.InputIMSI = "wrong"
wantReason = "IMSI与卡档案不一致"
case "authorization":
invalid.Authorized = false
wantReason = "ICCID不在当前操作员授权范围内"
case "existing":
wantReason = "导入行格式校验失败"
invalid.Reason = wantReason
}
valid := &Detail{ID: 2, ICCID: card.ICCID, Authorized: true, Card: card}
otherInvalid := &Detail{ID: 3, ICCID: card.ICCID, Reason: "该行独立校验失败"}
duplicate := &Detail{ID: 4, ICCID: card.ICCID, Authorized: true, Card: card}
details := []*Detail{invalid, valid, otherInvalid, duplicate}
if !invalidFirst {
details[0], details[1] = details[1], details[0]
}
task := &Task{Batch: Batch{BatchNo: "ACCURACY-CUT", BatchType: 107}, Operation: operation, ApnName: "CMIOT", Details: details}
base := &accuracyRepository{task: task, saved: map[int64]Outcome{}}
repo := &accuracyDurableRepository{accuracyRepository: base}
if err := NewExecutor(repo, &accuracyNoUpstream{t: t}).Execute(context.Background(), task.Batch.BatchNo); err != nil {
t.Fatal(err)
}
if len(repo.prepared) != 1 || len(repo.prepared[0].DetailIDs) != 1 || repo.prepared[0].DetailIDs[0] != valid.ID {
t.Fatalf("有效行未独立冻结: %+v", repo.prepared)
}
if valid.Reason != "" || valid.DetailStatus == consts.BatchDetailStatusFailed {
t.Fatalf("有效行被无效重复行阻断: %+v", valid)
}
for _, check := range []struct {
detail *Detail
reason string
}{{invalid, wantReason}, {otherInvalid, "该行独立校验失败"}, {duplicate, "重复ICCID,未提交上游"}} {
outcome, ok := base.saved[check.detail.ID]
if !ok || outcome.Success || outcome.Message != check.reason || check.detail.Reason != check.reason {
t.Fatalf("逐行原因被改写: id=%d detail=%+v outcome=%+v", check.detail.ID, check.detail, outcome)
}
}
if repo.prepared[0].Request.ReqData.OperType != "1" || operation.TargetStatus != 0 {
t.Fatalf("107开停方向倒置: %+v", repo.prepared[0])
}
})
}
}
}
func TestAccuracyFrozenRequestsKeepAPNDirection(t *testing.T) {
for _, tc := range []struct {
batchType int
operType string
target int
}{{107, "1", 0}, {109, "0", 1}, {110, "1", 0}} {
t.Run(fmt.Sprint(tc.batchType), func(t *testing.T) {
operation, err := OperationByBatchType(tc.batchType)
if err != nil {
t.Fatal(err)
}
card := &Card{ID: 10, ICCID: "card", MSISDN: "1001", ChanKey: "channel", DataServiceStatus: 1 - tc.target}
task := &Task{
Batch: Batch{BatchNo: "ACCURACY-DIRECTION", BatchType: tc.batchType}, Operation: operation, ApnName: "CMIOT",
Details: []*Detail{{ID: 1, ICCID: card.ICCID, Authorized: true, Card: card}, {ID: 2, ICCID: card.ICCID, Authorized: true, Card: card}},
}
repo := &accuracyDurableRepository{accuracyRepository: &accuracyRepository{task: task, saved: map[int64]Outcome{}}}
if err := NewExecutor(repo, &accuracyNoUpstream{t: t}).Execute(context.Background(), task.Batch.BatchNo); err != nil {
t.Fatal(err)
}
if len(repo.prepared) != 1 || len(repo.prepared[0].DetailIDs) != 1 || repo.prepared[0].DetailIDs[0] != 1 {
t.Fatalf("同ICCID未只保留首个有效行: %+v", repo.prepared)
}
if operation.TargetStatus != tc.target || repo.prepared[0].Request.ReqData.OperType != tc.operType {
t.Fatalf("批次%d开停方向错误: operation=%+v request=%+v", tc.batchType, operation, repo.prepared[0].Request)
}
})
}
}
func TestAccuracyEmptyBatchKeepsValidationReason(t *testing.T) {
for _, batchType := range []int{107, 109, 110} {
t.Run(fmt.Sprint(batchType), func(t *testing.T) {
operation, err := OperationByBatchType(batchType)
if err != nil {
t.Fatal(err)
}
task := &Task{Batch: Batch{BatchNo: "ACCURACY-EMPTY", BatchType: batchType}, Operation: operation}
repo := &accuracyRepository{task: task}
if err := NewExecutor(repo, &accuracyNoUpstream{t: t}).Execute(context.Background(), task.Batch.BatchNo); err != nil {
t.Fatal(err)
}
if !repo.validationSaved || !repo.finalized || task.ValidationOK || task.FailureReason == "" {
t.Fatalf("空明细校验失败原因缺失: task=%+v saved=%v finalized=%v", task, repo.validationSaved, repo.finalized)
}
})
}
}
type accuracyRepository struct {
task *Task
saved map[int64]Outcome
validationSaved bool
finalized bool
}
func (r *accuracyRepository) Load(context.Context, string) (*Task, error) { return r.task, nil }
func (r *accuracyRepository) SaveValidationFailure(context.Context, *Task) error {
r.validationSaved = true
return nil
}
func (r *accuracyRepository) SaveResult(_ context.Context, _ *Task, detail *Detail, outcome Outcome) (Outcome, error) {
r.saved[detail.ID] = outcome
return outcome, nil
}
func (r *accuracyRepository) Finalize(context.Context, *Task) error {
r.finalized = true
return nil
}
type accuracyDurableRepository struct {
*accuracyRepository
SubBatchRepository
prepared []SubBatch
}
func (r *accuracyDurableRepository) ResumeOrPrepare(ctx context.Context, _ string, prepare func(context.Context) error) error {
return prepare(ctx)
}
func (r *accuracyDurableRepository) PrepareSubBatch(_ context.Context, sub SubBatch) error {
r.prepared = append(r.prepared, sub)
return nil
}
func TestAccuracyCanExecuteProcessingRequiresExistingParent(t *testing.T) {
ctx := context.Background()
for _, tc := range []struct {
name string
parent bool
readErr error
wantOK bool
wantErr bool
}{
{name: "旧Processing无凭据不可接管"},
{name: "新批次ready0已持久化凭据", parent: true, wantOK: true},
{name: "缺少迁移查询失败安全拒绝", readErr: errors.New("no such table: data_operation_parent"), wantErr: true},
} {
t.Run(tc.name, func(t *testing.T) {
repo := &accuracyParentRepository{accuracyDurableRepository: &accuracyDurableRepository{accuracyRepository: &accuracyRepository{}}, present: tc.parent, readErr: tc.readErr}
e := NewExecutor(repo, &accuracyNoUpstream{t: t}).(*executor)
ok, err := e.CanExecuteProcessing(ctx, &entity.BatchMain{BatchNo: " APN-EXISTING "})
if ok != tc.wantOK || (err != nil) != tc.wantErr || repo.lookups != 1 || repo.batchNo != "APN-EXISTING" {
t.Fatalf("parent gate ok=%v err=%v lookups=%d batchNo=%q", ok, err, repo.lookups, repo.batchNo)
}
})
}
base := &accuracyRepository{}
for _, tc := range []struct {
name string
repo Repository
upstream Upstream
}{
{name: "缺少持久仓储", repo: base, upstream: &accuracyNoUpstream{t: t}},
{name: "仅同步上游", repo: &accuracyParentRepository{accuracyDurableRepository: &accuracyDurableRepository{accuracyRepository: base}}, upstream: &accuracySyncOnlyUpstream{}},
} {
t.Run(tc.name, func(t *testing.T) {
e := NewExecutor(tc.repo, tc.upstream).(*executor)
ok, err := e.CanExecuteProcessing(ctx, &entity.BatchMain{BatchNo: "APN-EXISTING"})
if ok || err != nil {
t.Fatalf("不支持持久异步不得接管Processing: ok=%v err=%v", ok, err)
}
})
}
}
func TestAccuracyDurableRepositoryWithSyncUpstreamFinalizesValidationFailure(t *testing.T) {
for _, batchType := range []int{107, 109, 110} {
t.Run(fmt.Sprint(batchType), func(t *testing.T) {
operation, err := OperationByBatchType(batchType)
if err != nil {
t.Fatal(err)
}
task := &Task{Batch: Batch{BatchNo: "SYNC-FAILED", BatchType: batchType}, Operation: operation}
base := &accuracyRepository{task: task}
repo := &accuracyDurableRepository{accuracyRepository: base}
if err := NewExecutor(repo, &accuracySyncOnlyUpstream{}).Execute(context.Background(), task.Batch.BatchNo); err != nil {
t.Fatal(err)
}
if !base.validationSaved || !base.finalized || task.FailureReason == "" {
t.Fatalf("同步兜底遗漏校验失败父批: saved=%v finalized=%v task=%+v", base.validationSaved, base.finalized, task)
}
})
}
}
type accuracyParentRepository struct {
*accuracyDurableRepository
present bool
readErr error
lookups int
batchNo string
}
func (r *accuracyParentRepository) HasDurableParent(_ context.Context, batchNo string) (bool, error) {
r.lookups++
r.batchNo = batchNo
return r.present, r.readErr
}
type accuracySyncOnlyUpstream struct{}
func (*accuracySyncOnlyUpstream) ChangeSimApnFunction(context.Context, *model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
panic("校验失败不得发运营商请求")
}
type accuracyNoUpstream struct{ t *testing.T }
func (u *accuracyNoUpstream) ChangeSimApnFunction(context.Context, *model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
u.t.Fatal("该回归不得调用同步上游")
return nil, nil
}
func (u *accuracyNoUpstream) SubmitSimApnFunction(context.Context, *model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
u.t.Fatal("该回归只冻结意图,不得提交上游")
return nil, nil
}
func (u *accuracyNoUpstream) QuerySimApnFunctionResult(context.Context, *model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
u.t.Fatal("该回归不得查询上游")
return nil, nil
}