sync: 同步 v1.0.4 并脱敏配置与镜像
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
@@ -135,7 +136,7 @@ func validateTask(task *Task, cards map[string]*entity.TSimCardInfo) bool {
|
||||
if hasFailure {
|
||||
for _, row := range task.Rows {
|
||||
if row != nil && row.Reason == "" {
|
||||
row.AddErrorReason("同批次存在校验失败卡,本卡未请求上游")
|
||||
row.AddErrorReason(batchimport.NotExecutedReasonPrefix + "本卡校验通过")
|
||||
}
|
||||
}
|
||||
task.FailureReason = "整批业务校验失败,未请求上游"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatchExecuteMessageJSONContract(t *testing.T) {
|
||||
body, err := json.Marshal(BatchExecuteMessage{
|
||||
BatchNo: "STP202607170001",
|
||||
BatchType: 104,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal batch execute message: %v", err)
|
||||
}
|
||||
const expected = `{"batchNo":"STP202607170001","batchType":104}`
|
||||
if string(body) != expected {
|
||||
t.Fatalf("unexpected batch execute message: %s", body)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
)
|
||||
|
||||
func TestValidateBasicRowsRejectsDuplicateIccid(t *testing.T) {
|
||||
rows := []batchimport.Row{
|
||||
&Row{RowNo: 2, Iccid: " 8986001 "},
|
||||
&Row{RowNo: 3, Iccid: "8986001"},
|
||||
}
|
||||
err := validateBasicRows(rows)
|
||||
if err == nil || !strings.Contains(err.Error(), "ICCID") || !strings.Contains(err.Error(), "重复") {
|
||||
t.Fatalf("expected readable duplicate ICCID error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBasicRowsRejectsEveryEmptyIccid(t *testing.T) {
|
||||
rows := []batchimport.Row{
|
||||
&Row{RowNo: 2, Iccid: ""},
|
||||
&Row{RowNo: 5, Iccid: " "},
|
||||
}
|
||||
err := validateBasicRows(rows)
|
||||
if err == nil {
|
||||
t.Fatal("expected empty ICCID validation error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "第2行") || !strings.Contains(err.Error(), "第5行") {
|
||||
t.Fatalf("expected all empty ICCID rows in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBasicRowsRejectsWrongFieldCount(t *testing.T) {
|
||||
rows, err := ParseInputRows(context.Background(), "8986001 13800008000")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = validateBasicRows(rows)
|
||||
if err == nil || !strings.Contains(err.Error(), "格式") {
|
||||
t.Fatalf("expected wrong field count error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidBasicRowsStopBeforePendingPreparation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rows []batchimport.Row
|
||||
}{
|
||||
{
|
||||
name: "duplicate",
|
||||
rows: []batchimport.Row{
|
||||
&Row{RowNo: 2, Iccid: "8986001"},
|
||||
&Row{RowNo: 3, Iccid: "8986001"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple empty iccid",
|
||||
rows: []batchimport.Row{
|
||||
&Row{RowNo: 2},
|
||||
&Row{RowNo: 3, Iccid: " "},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
processor, err := NewProcessor(OperationSuspend)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prepared := false
|
||||
result, err := batchimport.CreatePendingRows(
|
||||
context.Background(),
|
||||
batchimport.SubmitRequest{},
|
||||
processor,
|
||||
tt.rows,
|
||||
batchimport.PendingOptions{PrepareRows: func(context.Context, []batchimport.Row) error {
|
||||
prepared = true
|
||||
return nil
|
||||
}},
|
||||
)
|
||||
if err == nil || result != nil {
|
||||
t.Fatalf("expected pre-persistence validation error, result=%+v err=%v", result, err)
|
||||
}
|
||||
if prepared {
|
||||
t.Fatal("invalid rows reached pending preparation")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -155,11 +155,15 @@ func (r *Repository) SaveValidationFailure(ctx context.Context, task *Task) erro
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
status := consts.BatchDetailStatusFailed
|
||||
if batchimport.IsNotExecuted(row) {
|
||||
status = consts.BatchDetailStatusNotExecuted
|
||||
}
|
||||
if _, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Where(dao.BatchDetail.Columns().Id, row.DetailID).
|
||||
Where(dao.BatchDetail.Columns().BatchNo, task.Batch.BatchNo).
|
||||
Data(do.BatchDetail{
|
||||
DetailStatus: consts.BatchDetailStatusFailed,
|
||||
DetailStatus: status,
|
||||
ErrorReason: row.Reason,
|
||||
Snapshot: row.Snapshot(),
|
||||
UpdatedBy: task.Operator.UserId,
|
||||
@@ -255,21 +259,23 @@ func (r *Repository) FinalizeBatch(ctx context.Context, task *Task) error {
|
||||
}
|
||||
rows := make([]batchimport.Row, 0, len(task.Rows))
|
||||
successCount := 0
|
||||
failCount := 0
|
||||
for _, row := range task.Rows {
|
||||
rows = append(rows, row)
|
||||
if row != nil && row.Reason == "" {
|
||||
if row != nil && row.Reason == "" && task.ValidationOK {
|
||||
successCount++
|
||||
} else if row == nil || !batchimport.IsNotExecuted(row) {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
failCount := len(task.Rows) - successCount
|
||||
validCount := 0
|
||||
validCount := len(task.Rows) - failCount
|
||||
if task.ValidationOK {
|
||||
validCount = len(task.Rows)
|
||||
}
|
||||
importStatus := consts.ImportStatusSuccess
|
||||
batchStatus := consts.BatchAsyncStatusSuccess
|
||||
runResult := consts.BatchRunResultSuccess
|
||||
if failCount > 0 {
|
||||
if failCount > 0 || !task.ValidationOK {
|
||||
importStatus = consts.ImportStatusFailed
|
||||
batchStatus = consts.BatchAsyncStatusFailed
|
||||
runResult = consts.BatchRunResultFailed
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestRepositorySaveOutcomeUpdatesOnlySuccessfulCard(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if err := gdb.SetConfig(gdb.Config{
|
||||
"default": gdb.ConfigGroup{{
|
||||
Type: "sqlite",
|
||||
Link: "sqlite::@file(:memory:)",
|
||||
MaxOpenConnCount: 1,
|
||||
}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer g.DB().Close(ctx)
|
||||
|
||||
statements := []string{
|
||||
`CREATE TABLE t_sim_card_info (
|
||||
id INTEGER PRIMARY KEY,
|
||||
iccid TEXT NOT NULL,
|
||||
boss_card_status INTEGER NOT NULL,
|
||||
status_sync_time DATETIME,
|
||||
tenant_id TEXT NOT NULL,
|
||||
updated_by INTEGER,
|
||||
updated_at DATETIME
|
||||
)`,
|
||||
`CREATE TABLE batch_detail (
|
||||
id INTEGER PRIMARY KEY,
|
||||
batch_no TEXT NOT NULL,
|
||||
detail_status INTEGER NOT NULL,
|
||||
error_reason TEXT,
|
||||
snapshot TEXT,
|
||||
updated_by INTEGER,
|
||||
updated_at DATETIME
|
||||
)`,
|
||||
`INSERT INTO t_sim_card_info(id, iccid, boss_card_status, tenant_id) VALUES
|
||||
(1, 'card-success', 2, 'tenant-1'),
|
||||
(2, 'card-failed', 2, 'tenant-1')`,
|
||||
`INSERT INTO batch_detail(id, batch_no, detail_status) VALUES
|
||||
(1, 'STP-test', 1),
|
||||
(2, 'STP-test', 1)`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
config, err := OperationSuspend.Config()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
task := &Task{
|
||||
Batch: &entity.BatchMain{BatchNo: "STP-test", TenantId: "tenant-1"},
|
||||
Config: config,
|
||||
Operator: batchimport.OperatorContext{TenantId: "tenant-1", UserId: 10},
|
||||
}
|
||||
repository := NewRepository()
|
||||
successRow := &Row{DetailID: 1, Iccid: "card-success", StatusBefore: consts.V3ApiSimBossCardStatusActivated}
|
||||
failedRow := &Row{DetailID: 2, Iccid: "card-failed", StatusBefore: consts.V3ApiSimBossCardStatusActivated}
|
||||
if err = repository.SaveOutcome(ctx, task, successRow, &entity.TSimCardInfo{Id: 1}, Outcome{Success: true, OrderNum: "order-1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = repository.SaveOutcome(ctx, task, failedRow, &entity.TSimCardInfo{Id: 2}, Outcome{Code: 500, Message: "rejected"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var cards []struct {
|
||||
Iccid string `orm:"iccid"`
|
||||
BossCardStatus int `orm:"boss_card_status"`
|
||||
}
|
||||
if err = g.DB().Model("t_sim_card_info").OrderAsc("id").Scan(&cards); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(cards) != 2 || cards[0].BossCardStatus != consts.V3ApiSimBossCardStatusStopped || cards[1].BossCardStatus != consts.V3ApiSimBossCardStatusActivated {
|
||||
t.Fatalf("unexpected local card statuses: %+v", cards)
|
||||
}
|
||||
|
||||
var details []struct {
|
||||
DetailStatus int `orm:"detail_status"`
|
||||
ErrorReason string `orm:"error_reason"`
|
||||
}
|
||||
if err = g.DB().Model("batch_detail").OrderAsc("id").Scan(&details); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(details) != 2 || details[0].DetailStatus != consts.BatchDetailStatusSuccess || details[1].DetailStatus != consts.BatchDetailStatusFailed || details[1].ErrorReason != "rejected" {
|
||||
t.Fatalf("unexpected detail results: %+v", details)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user