package allocate import ( "context" "errors" "strings" "testing" "time" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/os/gtime" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity" ) func TestAllocateProcessingRetryAfterHasLargeBatchDefault(t *testing.T) { if got := allocateProcessingRetryAfter(context.Background()); got < 30*time.Minute { t.Fatalf("allocateProcessingRetryAfter() = %s, want at least 30m", got) } } func TestIsAllocateParentRequiresTypeAndRole(t *testing.T) { parent := &entity.BatchMain{ BatchType: consts.BatchTypeAllocate, BatchInfo: gjson.New(`{"jobRole":"allocate_parent"}`), } if !isAllocateParent(parent) { t.Fatal("expected allocate parent metadata to be recognized") } parent.SourceBatchNo = "ALC-CHILD" if isAllocateParent(parent) { t.Fatal("child batch must not be recognized as allocate parent") } } func TestIsAllocateDeadlock(t *testing.T) { if !isAllocateDeadlock(errors.New("Error 1213 (40001): Deadlock found when trying to get lock")) { t.Fatal("expected MySQL deadlock to be retryable") } if isAllocateDeadlock(errors.New("validation failed")) { t.Fatal("validation errors must not be treated as deadlocks") } } func TestResolveAllocateParentStartTime(t *testing.T) { parentStart := gtime.New("2026-09-29 12:59:00") childStart := gtime.New("2026-09-29 12:59:32") fallback := gtime.New("2026-09-29 13:00:21") if got := resolveAllocateParentStartTime(parentStart, childStart, fallback); got != parentStart { t.Fatalf("parent start time = %v, want existing parent time %v", got, parentStart) } if got := resolveAllocateParentStartTime(nil, childStart, fallback); got != childStart { t.Fatalf("parent start time = %v, want earliest child time %v", got, childStart) } if got := resolveAllocateParentStartTime(nil, nil, fallback); got != fallback { t.Fatalf("parent start time = %v, want fallback time %v", got, fallback) } } func TestAllocateBatchSizesRemainBounded(t *testing.T) { if allocateCardQueryBatchSize <= 0 || allocateCardQueryBatchSize > 1000 { t.Fatalf("allocateCardQueryBatchSize = %d", allocateCardQueryBatchSize) } if allocateCardUpdateBatchSize <= 0 || allocateCardUpdateBatchSize > 1000 { t.Fatalf("allocateCardUpdateBatchSize = %d", allocateCardUpdateBatchSize) } if allocateDetailUpdateBatchSize <= 0 || allocateDetailUpdateBatchSize > 1000 { t.Fatalf("allocateDetailUpdateBatchSize = %d", allocateDetailUpdateBatchSize) } } func TestBuildDetailUpdateStatementUsesBoundParameters(t *testing.T) { task := &Task{Batch: Batch{BatchNo: "ALC-1", CreatedBy: 7}} details := []*Detail{ {ID: 11, DetailStatus: consts.BatchDetailStatusPending, Reason: "unsafe ' reason"}, {ID: 12, DetailStatus: consts.BatchDetailStatusRunning, Reason: "second"}, } statement, args := buildDetailUpdateStatement(task, details, gtime.Now()) if strings.Contains(statement, details[0].Reason) { t.Fatal("detail reason was interpolated into SQL") } if !strings.Contains(statement, "CASE") || !strings.Contains(statement, "IN (?,?)") { t.Fatalf("unexpected statement: %s", statement) } if len(args) != len(details)*7+9 { t.Fatalf("argument count = %d, want %d", len(args), len(details)*7+9) } } func TestNormalizeAtomicParentResultRejectsPartialSuccess(t *testing.T) { task := &Task{Details: []*Detail{ {ICCID: "8986000000000000001"}, {ICCID: "8986000000000000002", Reason: "目标客户不存在"}, }} successCount, failCount := normalizeAtomicParentResult(task) if successCount != 0 || failCount != len(task.Details) { t.Fatalf("normalizeAtomicParentResult() = %d, %d; want 0, %d", successCount, failCount, len(task.Details)) } for _, detail := range task.Details { if detail.Success() { t.Fatalf("detail %s remained successful", detail.ICCID) } } status, result, _ := parentBatchResult(successCount, failCount) if status != consts.BatchAsyncStatusFailed || result != consts.BatchRunResultFailed { t.Fatalf("parentBatchResult() = %d, %d; want failed, failed", status, result) } } func TestValidateDetailAllowsCurrentHolderReallocation(t *testing.T) { task := &Task{ Batch: Batch{ CreatedDept: 100, }, TargetsByID: map[int64]*Target{ 200: {DeptID: 200, ParentID: 100}, }, } detail := &Detail{ ICCID: "8986000000000000000", TargetDeptID: 200, Card: &Card{ ICCID: "8986000000000000000", DeptID: 100, AllocationBatchNo: "ALC202608050001", PackageStatus: consts.PackageStatusEffective, }, } validateDetail(task, detail) if detail.Reason != "" { t.Fatalf("expected current holder reallocation to pass, got %q", detail.Reason) } } func TestValidateDetailRejectsAncestorReallocationAfterCardMoved(t *testing.T) { task := &Task{ Batch: Batch{ CreatedDept: 100, }, TargetsByID: map[int64]*Target{ 300: {DeptID: 300, ParentID: 100}, }, } detail := &Detail{ ICCID: "8986000000000000000", TargetDeptID: 300, Card: &Card{ ICCID: "8986000000000000000", DeptID: 200, AllocationBatchNo: "ALC202608050001", PackageStatus: consts.PackageStatusEffective, }, } validateDetail(task, detail) if detail.Reason == "" { t.Fatal("expected ancestor reallocation to be rejected after card moved") } } func TestValidateTaskMarksWholeBatchFailed(t *testing.T) { task := &Task{ Details: []*Detail{ {ICCID: "8986000000000000001"}, {ICCID: "8986000000000000002", Reason: "目标客户不存在"}, }, } validateTask(task) for _, detail := range task.Details { if detail.Success() { t.Fatalf("detail %s remained successful after atomic failure", detail.ICCID) } } if task.FailureReason == "" { t.Fatal("expected batch failure reason") } status, result := batchResult(countResult(task)) if status != consts.BatchAsyncStatusFailed || result != consts.BatchRunResultFailed { t.Fatalf("batchResult() = %d, %d; want failed, failed", status, result) } } func TestValidateDetailRejectsChangedCardICCID(t *testing.T) { task := &Task{Batch: Batch{CreatedDept: 100}, TargetsByID: map[int64]*Target{200: {DeptID: 200, ParentID: 100}}} detail := &Detail{ICCID: "8986000000000000001", TargetDeptID: 200, Card: &Card{ICCID: "8986000000000000002", DeptID: 100, PackageStatus: consts.PackageStatusEffective}} validateDetail(task, detail) if detail.Reason == "" { t.Fatal("expected changed card ICCID to be rejected") } }