Files
iotlcm/internal/service/renewchange/prepare_test.go
T
MiFaZhan 2bec257289 chore: 初始开源发布
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),属既有缺陷,本仓未引入亦未修复。
2026-10-22 16:37:56 +08:00

496 lines
15 KiB
Go

package renewchange
import (
"context"
"testing"
"time"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
rc "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/renewchange"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gtime"
)
func TestPrepareExecutorCanReclaimStaleProcessingBatch(t *testing.T) {
now := time.Date(2026, 8, 26, 14, 0, 0, 0, time.Local)
executor := &PrepareExecutor{nowFunc: func() time.Time { return now }}
tests := []struct {
name string
main *entity.BatchMain
canRun bool
}{
{
name: "fresh processing batch",
main: &entity.BatchMain{
BatchType: consts.BatchTypePrepare,
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
StartTime: gtime.NewFromTime(now.Add(-prepareProcessingRetryAfter + time.Second)),
},
},
{
name: "stale processing batch",
main: &entity.BatchMain{
BatchType: consts.BatchTypePrepare,
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
StartTime: gtime.NewFromTime(now.Add(-prepareProcessingRetryAfter)),
},
canRun: true,
},
{
name: "missing start time",
main: &entity.BatchMain{
BatchType: consts.BatchTypePrepare,
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
},
canRun: true,
},
{
name: "terminal batch",
main: &entity.BatchMain{
BatchType: consts.BatchTypePrepare,
BatchAsyncStatus: consts.BatchAsyncStatusSuccess,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := executor.CanExecuteProcessing(context.Background(), test.main)
if err != nil {
t.Fatalf("CanExecuteProcessing() error = %v", err)
}
if got != test.canRun {
t.Fatalf("CanExecuteProcessing() = %v, want %v", got, test.canRun)
}
})
}
}
func TestLoadPrepareOptionsDurationIsExplicitOnly(t *testing.T) {
opts := loadPrepareOptions(gjson.New(map[string]any{
"targetBatchType": 101,
}))
if opts.durationSet {
t.Fatalf("durationSet = true, want false")
}
if opts.durationValue != 0 {
t.Fatalf("durationValue = %d, want 0", opts.durationValue)
}
opts = loadPrepareOptions(gjson.New(map[string]any{
"durationMonths": 6,
}))
if !opts.durationSet {
t.Fatalf("durationSet = false, want true")
}
if opts.durationValue != 6 {
t.Fatalf("durationValue = %d, want 6", opts.durationValue)
}
opts = loadPrepareOptions(gjson.New(map[string]any{
"durationMonths": 12,
}))
if !opts.durationSet {
t.Fatalf("durationSet = false, want true")
}
if opts.durationValue != 1 {
t.Fatalf("durationValue = %d, want 1", opts.durationValue)
}
}
func TestChangeDurationOptionsUseMonthsAtExecutionBoundary(t *testing.T) {
got, err := changeDurationOptions(gjson.New([]int{1, 2, 3}))
if err != nil {
t.Fatalf("changeDurationOptions(years) error = %v", err)
}
if want := []int{12, 24, 36}; len(got) != len(want) {
t.Fatalf("changeDurationOptions(years) = %#v, want %#v", got, want)
} else {
for i := range want {
if got[i] != want[i] {
t.Fatalf("changeDurationOptions(years) = %#v, want %#v", got, want)
}
}
}
got, err = changeDurationOptions(gjson.New([]int{12, 24, 36}))
if err != nil {
t.Fatalf("changeDurationOptions(legacy months) error = %v", err)
}
if want := []int{12, 24, 36}; len(got) != len(want) {
t.Fatalf("changeDurationOptions(legacy months) = %#v, want %#v", got, want)
} else {
for i := range want {
if got[i] != want[i] {
t.Fatalf("changeDurationOptions(legacy months) = %#v, want %#v", got, want)
}
}
}
}
func TestMergeRowErrorsKeepsValidationReasons(t *testing.T) {
got := mergeRowErrors(
map[int]string{2: "parse failed"},
map[int]string{1: "duration not allowed", 2: "validation failed", 0: "batch failed"},
)
if got[1] != "duration not allowed" {
t.Fatalf("row 1 reason = %q", got[1])
}
if got[2] != "parse failed" {
t.Fatalf("row 2 reason = %q", got[2])
}
if got[0] != "batch failed" {
t.Fatalf("row 0 reason = %q", got[0])
}
}
func TestResolvePrepareIdentityRowsFromCardsCanonicalizesMSISDN(t *testing.T) {
rows := []*RenewRow{
{RowNo: 1, ICCID: "13800008000"},
{RowNo: 2, ICCID: "8986002", MSISDN: "bad-msisdn"},
}
rowErrors := map[int]string{}
resolvePrepareIdentityRowsFromCards(rows, rowErrors, []*rc.SimCardInfo{
{Id: 1, Iccid: "8986001", Msisdn: "13800008000", Imsi: "460001"},
{Id: 2, Iccid: "8986002", Msisdn: "13800008002", Imsi: "460002"},
})
if rows[0].ICCID != "8986001" || rows[0].MSISDN != "13800008000" || rows[0].IMSI != "460001" {
t.Fatalf("row 1 was not canonicalized: %#v", rows[0])
}
if rowErrors[1] != "" {
t.Fatalf("row 1 error = %q, want empty", rowErrors[1])
}
if rowErrors[2] == "" {
t.Fatal("row 2 should fail when ICCID and MSISDN point to different identities")
}
}
func TestCollectPrepareIccidsMarksDuplicateAfterCanonicalization(t *testing.T) {
rows := []*RenewRow{
{RowNo: 1, ICCID: "8986001"},
{RowNo: 2, ICCID: "8986001"},
}
rowErrors := map[int]string{}
got := collectPrepareIccids(rows, rowErrors)
if len(got) != 1 || got[0] != "8986001" {
t.Fatalf("iccids = %#v, want single canonical ICCID", got)
}
if rowErrors[1] == "" || rowErrors[2] == "" {
t.Fatalf("duplicate errors not marked: %#v", rowErrors)
}
}
func TestBuildPrepareInfoWritesRenewFlatContract(t *testing.T) {
got := buildPrepareInfo(nil, 101, &prepareOutcome{
customer: &customerInfo{
AgentID: 9,
DeptID: 117,
Name: "customer",
},
priceSnap: &RenewPriceSnapshot{
CustomerAgentId: 117,
PackageCode: "PKG-1",
PackageName: "package 1",
PriceSource: PriceSourceSystem,
AgentDefaultPriceId: 11,
TargetPriceId: 22,
RenewUnitPrice: 3.5,
RenewableDurationOptions: []int{1, 3, 12},
RenewBeginDate: "2026-08-31",
RenewBeginDateMin: "2026-08-31",
RenewBeginDateMax: "2026-08-31",
SameRenewBeginDate: true,
},
}, 2)
if got["customerAgentId"] != int64(117) {
t.Fatalf("customerAgentId = %v, want 117", got["customerAgentId"])
}
if got["packageCode"] != "PKG-1" {
t.Fatalf("packageCode = %v, want PKG-1", got["packageCode"])
}
if got["quantity"] != 2 {
t.Fatalf("quantity = %v, want 2", got["quantity"])
}
if got["renewUnitPrice"] != 3.5 {
t.Fatalf("renewUnitPrice = %v, want 3.5", got["renewUnitPrice"])
}
if got["priceSnapshot"] == nil {
t.Fatalf("priceSnapshot is nil")
}
if got["renewBeginDate"] != "2026-08-31" {
t.Fatalf("renewBeginDate = %v, want 2026-08-31", got["renewBeginDate"])
}
if got["sameRenewBeginDate"] != true {
t.Fatalf("sameRenewBeginDate = %v, want true", got["sameRenewBeginDate"])
}
}
func TestFillRenewBeginSnapshotSummarizesRenewBeginDate(t *testing.T) {
snapshot := &RenewPriceSnapshot{}
fillRenewBeginSnapshot(snapshot, []*RenewRow{
{RowNo: 1, OldExpireTime: gtime.NewFromStr("2026-09-10 00:00:00")},
{RowNo: 2, OldExpireTime: gtime.NewFromStr("2026-08-31 23:59:59")},
}, nil)
if snapshot.RenewBeginDate != "" {
t.Fatalf("renewBeginDate = %q, want empty for mixed dates", snapshot.RenewBeginDate)
}
if snapshot.RenewBeginDateMin != "2026-08-31" {
t.Fatalf("renewBeginDateMin = %q, want 2026-08-31", snapshot.RenewBeginDateMin)
}
if snapshot.RenewBeginDateMax != "2026-09-10" {
t.Fatalf("renewBeginDateMax = %q, want 2026-09-10", snapshot.RenewBeginDateMax)
}
if snapshot.SameRenewBeginDate {
t.Fatalf("sameRenewBeginDate = true, want false")
}
fillRenewBeginSnapshot(snapshot, []*RenewRow{
{RowNo: 1, OldExpireTime: gtime.NewFromStr("2026-10-01 00:00:00")},
{RowNo: 2, OldExpireTime: gtime.NewFromStr("2026-10-01 23:59:59")},
}, nil)
if snapshot.RenewBeginDate != "2026-10-01" {
t.Fatalf("renewBeginDate = %q, want 2026-10-01", snapshot.RenewBeginDate)
}
if !snapshot.SameRenewBeginDate {
t.Fatalf("sameRenewBeginDate = false, want true")
}
}
func TestRenewBeginDateFromExpireUsesExpireDate(t *testing.T) {
beginAt := renewBeginDateFromExpire(gtime.NewFromStr("2026-08-15 00:00:00"))
if beginAt == nil {
t.Fatal("renew begin date is nil")
}
if got := beginAt.Format("Y-m-d H:i:s"); got != "2026-08-15 23:59:59" {
t.Fatalf("renew begin date = %s, want 2026-08-15 23:59:59", got)
}
if got := addMonths(beginAt.Time, 12).Format("2006-01-02 15:04:05"); got != "2027-08-15 23:59:59" {
t.Fatalf("renew end date = %s, want 2027-08-15 23:59:59", got)
}
nextBeginAt := renewBeginDateFromExpire(gtime.NewFromStr("2027-07-31 00:00:00"))
if nextBeginAt == nil {
t.Fatal("next renew begin date is nil")
}
if got := nextBeginAt.Format("Y-m-d H:i:s"); got != "2027-07-31 23:59:59" {
t.Fatalf("next renew begin date = %s, want 2027-07-31 23:59:59", got)
}
giftBeginAt := renewBeginDateFromExpire(gtime.NewFromStr("2026-08-31 00:00:00"))
if giftBeginAt == nil {
t.Fatal("gift renew begin date is nil")
}
if got := giftBeginAt.Format("Y-m-d H:i:s"); got != "2026-08-31 23:59:59" {
t.Fatalf("gift renew begin date = %s, want 2026-08-31 23:59:59", got)
}
if got := addMonths(giftBeginAt.Time, 13).Format("2006-01-02 15:04:05"); got != "2027-09-30 23:59:59" {
t.Fatalf("renew end date with gift month = %s, want 2027-09-30 23:59:59", got)
}
}
func TestValidateRenewSameCurrentPeriod(t *testing.T) {
sameRows := []*RenewRow{
{
RowNo: 1,
OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"),
OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"),
},
{
RowNo: 2,
OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"),
OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"),
},
}
if err := validateRenewSameCurrentPeriod(sameRows, nil); err != nil {
t.Fatalf("validateRenewSameCurrentPeriod() error = %v", err)
}
diffBeginRows := []*RenewRow{
{
RowNo: 1,
OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"),
OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"),
},
{
RowNo: 2,
OldEffTime: gtime.NewFromStr("2026-02-01 00:00:00"),
OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"),
},
}
if err := validateRenewSameCurrentPeriod(diffBeginRows, nil); err == nil {
t.Fatalf("validateRenewSameCurrentPeriod() error = nil, want begin mismatch")
}
diffExpireRows := []*RenewRow{
{
RowNo: 1,
OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"),
OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"),
},
{
RowNo: 2,
OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"),
OldExpireTime: gtime.NewFromStr("2027-01-31 23:59:59"),
},
}
if err := validateRenewSameCurrentPeriod(diffExpireRows, nil); err == nil {
t.Fatalf("validateRenewSameCurrentPeriod() error = nil, want expire mismatch")
}
missingTimeRows := []*RenewRow{
{
RowNo: 1,
OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"),
OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"),
},
{RowNo: 2, OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00")},
}
if err := validateRenewSameCurrentPeriod(missingTimeRows, nil); err == nil {
t.Fatalf("validateRenewSameCurrentPeriod() error = nil, want missing time")
}
rowErrors := map[int]string{2: "existing validation error"}
if err := validateRenewSameCurrentPeriod(missingTimeRows, rowErrors); err != nil {
t.Fatalf("validateRenewSameCurrentPeriod() with rowErrors error = %v", err)
}
}
func TestRenewPriceOwnerUsesCurrentCustomerParent(t *testing.T) {
if got := renewPriceOwnerDeptID(&rc.SysDeptRow{DeptId: 117, ParentId: 100}); got != 100 {
t.Fatalf("price owner dept = %d, want 100", got)
}
if got := renewPriceOwnerDeptID(&rc.SysDeptRow{DeptId: 100, ParentId: 0}); got != 0 {
t.Fatalf("top-level price owner dept = %d, want 0", got)
}
}
func TestRenewExpireAfterAllowedWindow(t *testing.T) {
now := time.Date(2026, 8, 7, 16, 50, 39, 0, time.Local)
if renewExpireAfterAllowedWindow(nil, now) {
t.Fatal("nil expire time should not be rejected")
}
if renewExpireAfterAllowedWindow(gtime.NewFromTime(now.AddDate(0, 6, 0)), now) {
t.Fatal("exactly 6 months ahead should be allowed")
}
if !renewExpireAfterAllowedWindow(gtime.NewFromTime(now.AddDate(0, 6, 0).Add(time.Second)), now) {
t.Fatal("beyond 6 months should be rejected")
}
}
func TestBuildPrepareInfoWritesChangeFlatContract(t *testing.T) {
got := buildPrepareInfo(nil, 102, &prepareOutcome{
customer: &customerInfo{
DeptID: 117,
Name: "customer",
},
changeSnap: &ChangePrepareSnapshot{
SourceBatchNo: "CHG-1",
CustomerAgentId: 117,
CustomerName: "customer",
OldPackageCode: "OLD",
OldPackageName: "old package",
OldPackageType: 101,
OldPackageFlowMb: 1024,
Quantity: 1,
},
changePrice: &ChangePriceSnapshot{
CustomerAgentId: 117,
NewPackageCode: "NEW",
NewPackageName: "new package",
PriceSource: PriceSourceAgentDefault,
AgentDefaultPriceId: 33,
TargetPriceId: 44,
ChangeUnitPrice: 6.5,
RenewableDurationOptions: []int{1, 2},
},
}, 1)
if got["customerAgentId"] != int64(117) {
t.Fatalf("customerAgentId = %v, want 117", got["customerAgentId"])
}
if got["sourceBatchNo"] != "CHG-1" {
t.Fatalf("sourceBatchNo = %v, want CHG-1", got["sourceBatchNo"])
}
if got["oldPackageCode"] != "OLD" {
t.Fatalf("oldPackageCode = %v, want OLD", got["oldPackageCode"])
}
if got["newPackageCode"] != "NEW" {
t.Fatalf("newPackageCode = %v, want NEW", got["newPackageCode"])
}
if got["changeUnitPrice"] != 6.5 {
t.Fatalf("changeUnitPrice = %v, want 6.5", got["changeUnitPrice"])
}
if got["changePrepareSnapshot"] == nil || got["changePriceSnapshot"] == nil {
t.Fatalf("change snapshots are missing")
}
}
func TestBuildPrepareDetailSnapshotIncludesChangeSubscriptionContract(t *testing.T) {
out := &prepareOutcome{
rows: map[int]*RenewRow{
1: {
RowNo: 1,
ICCID: "8986001",
MSISDN: "13800000000",
IMSI: "460001",
OldPkgCode: "OLD",
OldPkgName: "old package",
OldBindSubNo: "SUB-OLD",
OldBindOrdNo: "SO-OLD",
SourceBatchNo: "OUT-1",
},
},
changePrice: &ChangePriceSnapshot{
NewPackageCode: "NEW",
NewPackageName: "new package",
},
}
snapshot := buildPrepareDetailSnapshot(&rc.BatchDetailRow{RowNo: 1}, out, 102)
want := map[string]string{
"oldPackageCode": "OLD",
"oldPackageName": "old package",
"oldSubscriptionNo": "SUB-OLD",
"oldOrderSn": "SO-OLD",
"sourceBatchNo": "OUT-1",
"newPackageCode": "NEW",
"newPackageName": "new package",
}
for key, expected := range want {
if got := snapshot.Get(key).String(); got != expected {
t.Fatalf("%s = %q, want %q", key, got, expected)
}
}
}
func TestPriceByPackageUsesAdminContractFormula(t *testing.T) {
pkg := &rc.FlowPackageInfo{
CostPrice: 10,
SysDefaultRatio: 120,
SysDefaultProfit: 1.23,
}
if got := priceByPackage(pkg, nil); got != 13.23 {
t.Fatalf("system price = %v, want 13.23", got)
}
got := priceByPackage(pkg, &rc.FlowPackagePrice{
AgentCostRatio: 80,
AgentCostProfit: 2.5,
})
if got != 10.5 {
t.Fatalf("override price = %v, want 10.5", got)
}
}