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 }