202 lines
7.2 KiB
Go
202 lines
7.2 KiB
Go
package data
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
|
)
|
|
|
|
// reviewBatchFake 复用 durableFake 的持久化语义,只补手动同步需要的仓储能力。
|
|
type reviewBatchFake struct {
|
|
*durableFake
|
|
candidates []string
|
|
finalized []string
|
|
}
|
|
|
|
func newReviewBatchFake(candidates ...string) *reviewBatchFake {
|
|
return &reviewBatchFake{durableFake: newDurableFake(), candidates: candidates}
|
|
}
|
|
|
|
func (f *reviewBatchFake) ListReviewSubBatches(_ context.Context, _ string) ([]string, error) {
|
|
return append([]string(nil), f.candidates...), nil
|
|
}
|
|
|
|
// 手动同步不触碰校验/终态写入路径,这里只补齐 Repository 契约。
|
|
func (f *reviewBatchFake) Load(context.Context, string) (*Task, error) { return &Task{}, nil }
|
|
func (f *reviewBatchFake) SaveValidationFailure(context.Context, *Task) error { return nil }
|
|
func (f *reviewBatchFake) SaveResult(context.Context, *Task, *Detail, Outcome) (Outcome, error) {
|
|
return Outcome{}, nil
|
|
}
|
|
func (f *reviewBatchFake) Finalize(context.Context, *Task) error { return nil }
|
|
|
|
// manualSyncUpstream 补上同步办理入口,确保手动同步一旦误走同步路径立即暴露。
|
|
type manualSyncUpstream struct {
|
|
*asyncFake
|
|
}
|
|
|
|
func (u *manualSyncUpstream) ChangeSimApnFunction(context.Context, *model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
|
|
return nil, errors.New("手动同步不得调用同步办理入口")
|
|
}
|
|
|
|
func (f *reviewBatchFake) FinalizeReadyBatch(_ context.Context, batchNo string) error {
|
|
f.finalized = append(f.finalized, batchNo)
|
|
return nil
|
|
}
|
|
|
|
func (f *reviewBatchFake) ClaimReviewSubBatch(_ context.Context, operationID, token string, now time.Time) (*SubBatch, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
sub, ok := f.batches[operationID]
|
|
if !ok || (sub.Status != "review" && sub.Status != "review_query") {
|
|
return nil, nil
|
|
}
|
|
sub.Status = "review_query"
|
|
sub.LeaseToken = token
|
|
sub.LeaseUntil = now.Add(subBatchLease)
|
|
f.batches[operationID] = sub
|
|
out := sub
|
|
return &out, nil
|
|
}
|
|
|
|
// 手动同步只用既有受理号只读补查:不重发办理、不清零自动补查计数。
|
|
func TestManualSyncQueriesFrozenIntentsWithoutResubmitting(t *testing.T) {
|
|
ctx := context.Background()
|
|
sub := fixtureSub()
|
|
repo := newReviewBatchFake(sub.OperationID)
|
|
sub.Status = "review"
|
|
sub.JobID = "job-1"
|
|
sub.PollCount = 4
|
|
repo.batches[sub.OperationID] = sub
|
|
|
|
upstream := &manualSyncUpstream{asyncFake: &asyncFake{
|
|
submit: func(*model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
|
|
t.Fatal("手动同步不得重发办理")
|
|
return nil, nil
|
|
},
|
|
query: func(in *model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
|
|
if in.ReqData.QueryJobID != "job-1" {
|
|
t.Fatalf("手动同步必须按既有 jobId 查询: %+v", in.ReqData)
|
|
}
|
|
out := make([]model.SimBaseRes[model.SimCardApnChangeOutputItem], 0, len(in.SimKeys))
|
|
for _, key := range in.SimKeys {
|
|
out = append(out, model.SimBaseRes[model.SimCardApnChangeOutputItem]{SimKey: key, Code: 0, ResData: model.SimCardApnChangeOutputItem{OrderNum: "job-1"}})
|
|
}
|
|
return out, nil
|
|
},
|
|
}}
|
|
executor := &executor{repository: repo, upstream: upstream}
|
|
|
|
queried, err := executor.QueryReviewBatch(ctx, " P-1 ")
|
|
if err != nil {
|
|
t.Fatalf("QueryReviewBatch() error = %v", err)
|
|
}
|
|
if queried != 1 {
|
|
t.Fatalf("queried = %d, want 1", queried)
|
|
}
|
|
if upstream.submitCalls != 0 {
|
|
t.Fatalf("submitCalls = %d, want 0", upstream.submitCalls)
|
|
}
|
|
got := repo.batches[sub.OperationID]
|
|
if got.Status != "done" {
|
|
t.Fatalf("状态 = %s, want done", got.Status)
|
|
}
|
|
if got.PollCount != 4 {
|
|
t.Fatalf("pollCount = %d, want 4(手动同步不得清零自动补查计数)", got.PollCount)
|
|
}
|
|
// 子批收敛与批量入口都会尝试汇总父批,重复汇总是幂等的。
|
|
if len(repo.finalized) == 0 || repo.finalized[0] != "P-1" {
|
|
t.Fatalf("父批未汇总: %+v", repo.finalized)
|
|
}
|
|
if len(repo.outcomes[sub.OperationID]) != len(sub.DetailIDs) {
|
|
t.Fatalf("逐卡结果未写回: %+v", repo.outcomes[sub.OperationID])
|
|
}
|
|
}
|
|
|
|
// 上游仍无结论时明细保持待核查,且可以再次手动同步。
|
|
func TestManualSyncKeepsUnknownWhenUpstreamStillPending(t *testing.T) {
|
|
ctx := context.Background()
|
|
sub := fixtureSub()
|
|
repo := newReviewBatchFake(sub.OperationID)
|
|
sub.Status = "review"
|
|
repo.batches[sub.OperationID] = sub
|
|
|
|
upstream := &manualSyncUpstream{asyncFake: &asyncFake{
|
|
submit: func(*model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
|
|
t.Fatal("手动同步不得重发办理")
|
|
return nil, nil
|
|
},
|
|
query: func(in *model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
|
|
return pendingResponse(in, 14008), nil
|
|
},
|
|
}}
|
|
executor := &executor{repository: repo, upstream: upstream}
|
|
|
|
if _, err := executor.QueryReviewBatch(ctx, "P-1"); err != nil {
|
|
t.Fatalf("QueryReviewBatch() error = %v", err)
|
|
}
|
|
got := repo.batches[sub.OperationID]
|
|
if got.Status != "review" {
|
|
t.Fatalf("状态 = %s, want review(未确认不得记失败)", got.Status)
|
|
}
|
|
if got.JobID != "job-1" {
|
|
t.Fatalf("jobId = %s, want job-1", got.JobID)
|
|
}
|
|
if got.PollCount != 0 {
|
|
t.Fatalf("pollCount = %d, want 0(手动同步不得占用自动补查次数)", got.PollCount)
|
|
}
|
|
// 再次同步仍可用,说明没有被自动窗口锁死。
|
|
if _, err := executor.QueryReviewBatch(ctx, "P-1"); err != nil {
|
|
t.Fatalf("第二次 QueryReviewBatch() error = %v", err)
|
|
}
|
|
}
|
|
|
|
// 单个子批补查失败不影响其余子批,并把失败如实返回给调用方。
|
|
func TestManualSyncReportsPerIntentFailureAndContinues(t *testing.T) {
|
|
ctx := context.Background()
|
|
first := fixtureSub()
|
|
first.Status = "review"
|
|
first.JobID = "job-1"
|
|
second := fixtureSub()
|
|
second.OperationID = "intent-2"
|
|
second.Status = "review"
|
|
repo := newReviewBatchFake(first.OperationID, second.OperationID)
|
|
repo.batches[first.OperationID] = first
|
|
repo.batches[second.OperationID] = second
|
|
|
|
upstream := &manualSyncUpstream{asyncFake: &asyncFake{
|
|
submit: func(*model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
|
|
t.Fatal("手动同步不得重发办理")
|
|
return nil, nil
|
|
},
|
|
query: func(in *model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
|
|
if in.ReqData.RecoverOnly {
|
|
return nil, errors.New("上游查询不可用")
|
|
}
|
|
return pendingResponse(in, 0), nil
|
|
},
|
|
}}
|
|
executor := &executor{repository: repo, upstream: upstream}
|
|
|
|
queried, err := executor.QueryReviewBatch(ctx, "P-1")
|
|
if err == nil {
|
|
t.Fatal("QueryReviewBatch() error = nil, want error")
|
|
}
|
|
if queried != 1 {
|
|
t.Fatalf("queried = %d, want 1(成功一个,失败一个)", queried)
|
|
}
|
|
if upstream.submitCalls != 0 {
|
|
t.Fatalf("submitCalls = %d, want 0", upstream.submitCalls)
|
|
}
|
|
}
|
|
|
|
// 空批次号直接拒绝,避免误触发全量扫描。
|
|
func TestManualSyncRejectsBlankBatchNo(t *testing.T) {
|
|
executor := &executor{repository: newReviewBatchFake(), upstream: &manualSyncUpstream{asyncFake: &asyncFake{}}}
|
|
if _, err := executor.QueryReviewBatch(context.Background(), " "); err == nil {
|
|
t.Fatal("QueryReviewBatch() blank batchNo error = nil, want error")
|
|
}
|
|
}
|