sync: 同步 v1.0.4 并脱敏配置与镜像

This commit is contained in:
MiFaZhan
2026-10-29 14:36:49 +08:00
parent 743e8e6e6e
commit 2206a393f7
516 changed files with 18119 additions and 2253 deletions
@@ -0,0 +1,290 @@
package lifecycle
import (
"context"
"sync"
"testing"
"time"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
)
type fakeExecutorRepository struct {
mu sync.Mutex
claims []bool
claimCalls int
loadedBatchNos []string
task *Task
cards map[string]*entity.TSimCardInfo
validationFailureCalls int
outcomes []Outcome
finalizeCalls int
markFailedCalls int
}
func (r *fakeExecutorRepository) ClaimBatch(context.Context, string) (bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
index := r.claimCalls
r.claimCalls++
if index >= len(r.claims) {
return false, nil
}
return r.claims[index], nil
}
func (r *fakeExecutorRepository) LoadTask(_ context.Context, batchNo string) (*Task, error) {
r.loadedBatchNos = append(r.loadedBatchNos, batchNo)
return r.task, nil
}
func (r *fakeExecutorRepository) LoadCards(context.Context, *Task) (map[string]*entity.TSimCardInfo, error) {
return r.cards, nil
}
func (r *fakeExecutorRepository) SaveValidationFailure(context.Context, *Task) error {
r.validationFailureCalls++
return nil
}
func (r *fakeExecutorRepository) SaveOutcome(_ context.Context, _ *Task, _ *Row, _ *entity.TSimCardInfo, outcome Outcome) error {
r.outcomes = append(r.outcomes, outcome)
return nil
}
func (r *fakeExecutorRepository) SaveDetailFailure(context.Context, *Task, *Row) error {
return nil
}
func (r *fakeExecutorRepository) FinalizeBatch(context.Context, *Task) error {
r.finalizeCalls++
return nil
}
func (r *fakeExecutorRepository) MarkBatchFailed(context.Context, string, string) error {
r.markFailedCalls++
return nil
}
type fakeStatusChanger struct {
calls int
requests []*model.SimCardStatusChangeInput
response []model.SimBaseRes[model.SimCardStatusChangeItem]
err error
}
func (c *fakeStatusChanger) ChangeSimStatus(_ context.Context, input *model.SimCardStatusChangeInput) ([]model.SimBaseRes[model.SimCardStatusChangeItem], error) {
c.calls++
c.requests = append(c.requests, input)
return c.response, c.err
}
func TestActionOperTypeMapping(t *testing.T) {
stop, err := OperationSuspend.Config()
if err != nil {
t.Fatal(err)
}
start, err := OperationResume.Config()
if err != nil {
t.Fatal(err)
}
if stop.OperType != "0" || stop.BatchType != consts.BatchTypeStop {
t.Fatalf("unexpected stop mapping: %+v", stop)
}
if start.OperType != "1" || start.BatchType != consts.BatchTypeStart {
t.Fatalf("unexpected start mapping: %+v", start)
}
}
func TestExecuteValidationFailureSkipsUpstream(t *testing.T) {
task := newTestTask(t, OperationSuspend, []*Row{
{RowNo: 1, Iccid: "missing", Authorized: true},
{RowNo: 2, Iccid: "valid", Authorized: true},
})
repo := &fakeExecutorRepository{
claims: []bool{true},
task: task,
cards: map[string]*entity.TSimCardInfo{
"valid": {Id: 2, Iccid: "valid", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusActivated},
},
}
upstream := &fakeStatusChanger{}
if err := newExecutor(repo, upstream).Execute(context.Background(), "STP-test"); err != nil {
t.Fatal(err)
}
if upstream.calls != 0 {
t.Fatalf("upstream called %d times for invalid batch", upstream.calls)
}
if repo.validationFailureCalls != 1 || repo.finalizeCalls != 1 {
t.Fatalf("validation/finalize calls = %d/%d", repo.validationFailureCalls, repo.finalizeCalls)
}
if task.Rows[0].Reason == "" || task.Rows[1].Reason == "" {
t.Fatalf("expected all rows to retain failure reasons: %+v", task.Rows)
}
}
func TestValidateStopStatus(t *testing.T) {
t.Skip("TODO: 恢复卡状态限制后启用此测试")
tests := []struct {
name string
status int
valid bool
}{
{name: "activated", status: consts.V3ApiSimBossCardStatusActivated, valid: true},
{name: "pending", status: consts.V3ApiSimBossCardStatusPendingActivate},
{name: "testable", status: consts.V3ApiSimBossCardStatusTestable},
{name: "stock", status: consts.V3ApiSimBossCardStatusStock},
{name: "stopped", status: consts.V3ApiSimBossCardStatusStopped},
{name: "pre-cancel", status: consts.V3ApiSimBossCardStatusPreCancel},
{name: "canceled", status: consts.V3ApiSimBossCardStatusCanceled},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
row := &Row{Iccid: "card-1"}
validateCardStatus(OperationSuspend, row, &entity.TSimCardInfo{BossCardStatus: tt.status})
if got := row.Reason == ""; got != tt.valid {
t.Fatalf("status %d valid=%v, reason=%q", tt.status, got, row.Reason)
}
})
}
}
func TestExecuteMixedStopStatusSkipsUpstream(t *testing.T) {
t.Skip("TODO: 恢复卡状态限制后启用此测试")
task := newTestTask(t, OperationSuspend, []*Row{
{RowNo: 1, Iccid: "activated", Authorized: true},
{RowNo: 2, Iccid: "stock", Authorized: true},
})
repo := &fakeExecutorRepository{
claims: []bool{true},
task: task,
cards: map[string]*entity.TSimCardInfo{
"activated": {Id: 1, Iccid: "activated", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusActivated},
"stock": {Id: 2, Iccid: "stock", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusStock},
},
}
upstream := &fakeStatusChanger{}
if err := newExecutor(repo, upstream).Execute(context.Background(), "STP-mixed"); err != nil {
t.Fatal(err)
}
if upstream.calls != 0 {
t.Fatalf("upstream called %d times for mixed status batch", upstream.calls)
}
if repo.validationFailureCalls != 1 || repo.finalizeCalls != 1 {
t.Fatalf("validation/finalize calls = %d/%d", repo.validationFailureCalls, repo.finalizeCalls)
}
if task.Rows[0].Reason == "" || task.Rows[1].Reason == "" {
t.Fatalf("expected both rows to be failed without upstream call: %+v", task.Rows)
}
}
func TestExecuteRestoresByBatchNoAndMapsPerCardResults(t *testing.T) {
task := newTestTask(t, OperationSuspend, []*Row{
{RowNo: 1, Iccid: "card-1", Authorized: true},
{RowNo: 2, Iccid: "card-2", Authorized: true},
})
repo := &fakeExecutorRepository{
claims: []bool{true},
task: task,
cards: map[string]*entity.TSimCardInfo{
"card-1": {Id: 1, Iccid: "card-1", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusActivated},
"card-2": {Id: 2, Iccid: "card-2", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusActivated},
},
}
upstream := &fakeStatusChanger{response: []model.SimBaseRes[model.SimCardStatusChangeItem]{
{Code: 0, SimKey: &model.SimKey{ICCID: "card-1"}, ResData: model.SimCardStatusChangeItem{OrderNum: "order-1"}},
{Code: 500, Message: "operator rejected", SimKey: &model.SimKey{ICCID: "card-2"}},
}}
if err := newExecutor(repo, upstream).Execute(context.Background(), "STP-restore"); err != nil {
t.Fatal(err)
}
if len(repo.loadedBatchNos) != 1 || repo.loadedBatchNos[0] != "STP-restore" {
t.Fatalf("executor did not restore by batchNo: %+v", repo.loadedBatchNos)
}
if upstream.calls != 1 || len(upstream.requests) != 1 {
t.Fatalf("unexpected upstream calls: %d", upstream.calls)
}
if upstream.requests[0].ReqData.OperType != "0" {
t.Fatalf("unexpected operType: %s", upstream.requests[0].ReqData.OperType)
}
if len(repo.outcomes) != 2 || !repo.outcomes[0].Success || repo.outcomes[1].Success {
t.Fatalf("unexpected per-card outcomes: %+v", repo.outcomes)
}
if repo.outcomes[0].OrderNum != "order-1" || repo.outcomes[1].Message != "operator rejected" {
t.Fatalf("upstream details were not preserved: %+v", repo.outcomes)
}
}
func TestExecuteDuplicateClaimRunsOnlyOnce(t *testing.T) {
task := newTestTask(t, OperationResume, []*Row{{RowNo: 1, Iccid: "card-1", Authorized: true}})
repo := &fakeExecutorRepository{
claims: []bool{true, false},
task: task,
cards: map[string]*entity.TSimCardInfo{
"card-1": {Id: 1, Iccid: "card-1", ChanKey: "channel-a", BossCardStatus: consts.V3ApiSimBossCardStatusStopped},
},
}
upstream := &fakeStatusChanger{response: []model.SimBaseRes[model.SimCardStatusChangeItem]{
{Code: 0, SimKey: &model.SimKey{ICCID: "card-1"}},
}}
executor := newExecutor(repo, upstream)
if err := executor.Execute(context.Background(), "STR-once"); err != nil {
t.Fatal(err)
}
if err := executor.Execute(context.Background(), "STR-once"); err != nil {
t.Fatal(err)
}
if upstream.calls != 1 || len(repo.loadedBatchNos) != 1 || repo.finalizeCalls != 1 {
t.Fatalf("duplicate claim executed twice: upstream=%d load=%d finalize=%d", upstream.calls, len(repo.loadedBatchNos), repo.finalizeCalls)
}
if upstream.requests[0].ReqData.OperType != "1" {
t.Fatalf("unexpected start operType: %s", upstream.requests[0].ReqData.OperType)
}
}
type blockingExecutor struct {
started chan struct{}
release chan struct{}
}
func (e *blockingExecutor) Execute(context.Context, string) error {
close(e.started)
<-e.release
return nil
}
func (e *blockingExecutor) MarkFailed(context.Context, string, string) error {
return nil
}
func TestDispatchReturnsBeforeExecutorCompletes(t *testing.T) {
executor := &blockingExecutor{started: make(chan struct{}), release: make(chan struct{})}
Dispatch(executor, "STP-async")
select {
case <-executor.started:
case <-time.After(time.Second):
t.Fatal("background executor did not start")
}
select {
case <-executor.release:
t.Fatal("release channel should still be blocked")
default:
}
close(executor.release)
}
func newTestTask(t *testing.T, operation Operation, rows []*Row) *Task {
t.Helper()
config, err := operation.Config()
if err != nil {
t.Fatal(err)
}
return &Task{
Batch: &entity.BatchMain{BatchNo: "batch-test", BatchType: config.BatchType, TenantId: "tenant-1"},
Operation: operation,
Config: config,
Rows: rows,
}
}