package upstreamapi import ( "context" "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" "errors" "strings" "sync" "testing" "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/database/gdb" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model" ) type memoryApnOperationStore struct { mu sync.Mutex rows map[string][2]string failSave bool } func (s *memoryApnOperationStore) Claim(_ context.Context, id, ch, hash string) (bool, error) { s.mu.Lock() defer s.mu.Unlock() k := id + ch if _, ok := s.rows[k]; ok { return false, nil } s.rows[k] = [2]string{hash, ""} return true, nil } func (s *memoryApnOperationStore) Load(_ context.Context, id, ch string) (string, string, error) { s.mu.Lock() defer s.mu.Unlock() r := s.rows[id+ch] return r[0], r[1], nil } func (s *memoryApnOperationStore) Save(ctx context.Context, id, ch, hash, body string) error { if err := ctx.Err(); err != nil { return err } s.mu.Lock() defer s.mu.Unlock() if s.failSave { return errors.New("模拟磁盘失败") } r, exists := s.rows[id+ch] if !exists || r[0] != hash { return errors.New("意图不存在或摘要不符") } merged, err := mergeApnOperationJSON(r[1], body) if err != nil { return err } r[1] = merged s.rows[id+ch] = r return nil } func operationTestInput() *model.SimCardOperateChangeApnInput { return &model.SimCardOperateChangeApnInput{SimKeys: []*model.SimKey{{ICCID: "test", ChanKey: "channel"}}, ReqData: model.SimCardOperateChangeApnInputItem{OperationID: "stable-operation", SubmitOnly: true, OperType: "1"}} } func acceptedOperation(input *model.SimCardOperateChangeApnInput) model.SimCardOperateChangeApnOutput { return model.SimCardOperateChangeApnOutput{&model.SimBaseRes[model.SimCardOperateChangeApnItem]{SimKey: input.SimKeys[0], ResData: model.SimCardOperateChangeApnItem{OrderNum: "saved-job", Pending: true}}} } func TestApnCapabilityExistingJobNeedsNoIntentDatabase(t *testing.T) { input := operationTestInput() input.ReqData.SubmitOnly = false input.ReqData.QueryJobID = "existing-job" // 此包未配置数据库:已有 jobId 的只读协议探测应仍然返回能力。 output, err := (&sUpstreamAPI{}).QuerySimApnCapability(context.Background(), input) if err != nil || len(output) != 1 || output[0].ResData.ProtocolVersion != 1 || *output[0].SimKey != *input.SimKeys[0] { t.Fatalf("已有任务的只读协议探测依赖了提交意图表: output=%+v err=%v", output, err) } input.SimKeys = []*model.SimKey{nil} if _, err := (&sUpstreamAPI{}).QuerySimApnCapability(context.Background(), input); err == nil { t.Fatal("已有jobId也必须校验冻结卡标识") } } func TestApnOperationResponseLossAndRestartRecovery(t *testing.T) { store := &memoryApnOperationStore{rows: map[string][2]string{}} input := operationTestInput() calls := 0 submit := func(context.Context) (model.SimCardOperateChangeApnOutput, error) { calls++ return acceptedOperation(input), nil } if _, err := durableApnOperation(context.Background(), store, "channel", input, submit); err != nil { t.Fatal(err) } // 丢弃首次响应后,另一次调用只读受理映射,不访问运营商。 input.ReqData.RecoverOnly = true recovered, err := durableApnOperation(context.Background(), store, "channel", input, submit) if err != nil || calls != 1 || recovered[0].ResData.OrderNum != "saved-job" { t.Fatalf("恢复失败 calls=%d result=%+v err=%v", calls, recovered, err) } input.ReqData.RecoverOnly = false if _, err = durableApnOperation(context.Background(), store, "channel", input, submit); err != nil || calls != 1 { t.Fatalf("重复投递重新办理 calls=%d err=%v", calls, err) } input.ReqData.OperType = "0" if _, err = durableApnOperation(context.Background(), store, "channel", input, submit); err == nil { t.Fatal("相同ID不得绑定不同请求") } } func TestApnOperationConcurrentSubmitOnlyCallsCarrierOnce(t *testing.T) { store := &memoryApnOperationStore{rows: map[string][2]string{}} input := operationTestInput() started, release, done := make(chan struct{}), make(chan struct{}), make(chan error, 1) go func() { _, err := durableApnOperation(context.Background(), store, "channel", input, func(context.Context) (model.SimCardOperateChangeApnOutput, error) { close(started) <-release return acceptedOperation(input), nil }) done <- err }() <-started duplicate, err := durableApnOperation(context.Background(), store, "channel", input, func(context.Context) (model.SimCardOperateChangeApnOutput, error) { t.Error("并发重复operationId不得重新办理") return nil, nil }) close(release) if err != nil || len(duplicate) != 1 || !duplicate[0].ResData.Uncertain { t.Fatalf("并发处理中应标待核对: %v %+v", err, duplicate) } if err := <-done; err != nil { t.Fatal(err) } } func TestApnOperationRecoveryRejectsDifferentPayload(t *testing.T) { store := &memoryApnOperationStore{rows: map[string][2]string{}} input := operationTestInput() _, err := durableApnOperation(context.Background(), store, "channel", input, func(context.Context) (model.SimCardOperateChangeApnOutput, error) { return acceptedOperation(input), nil }) if err != nil { t.Fatal(err) } input.ReqData.RecoverOnly = true input.ReqData.OperType = "0" if _, err = durableApnOperation(context.Background(), store, "channel", input, nil); err == nil { t.Fatal("恢复请求不得读取其他操作的受理映射") } } func TestApnOperationIntentWithoutResponseNeverResubmits(t *testing.T) { store := &memoryApnOperationStore{rows: map[string][2]string{}} input := operationTestInput() payload, _ := json.Marshal(input) digest := sha256.Sum256(payload) _, _ = store.Claim(context.Background(), input.ReqData.OperationID, "channel", hex.EncodeToString(digest[:])) input.ReqData.RecoverOnly = true calls := 0 output, err := durableApnOperation(context.Background(), store, "channel", input, func(context.Context) (model.SimCardOperateChangeApnOutput, error) { calls++; return nil, nil }) if err != nil || calls != 0 || !output[0].ResData.Uncertain { t.Fatalf("未确认意图被重新提交:calls=%d err=%v", calls, err) } } func TestApnOperationPersistenceFailureNeverResubmits(t *testing.T) { store := &memoryApnOperationStore{rows: map[string][2]string{}, failSave: true} input := operationTestInput() calls := 0 submit := func(context.Context) (model.SimCardOperateChangeApnOutput, error) { calls++ return acceptedOperation(input), nil } if _, err := durableApnOperation(context.Background(), store, "channel", input, submit); err == nil { t.Fatal("持久化失败不得返回已可靠保存") } store.failSave = false input.ReqData.RecoverOnly = true output, err := durableApnOperation(context.Background(), store, "channel", input, submit) if err != nil || calls != 1 || !output[0].ResData.Uncertain { t.Fatalf("受理映射失败后不得重复办理:calls=%d err=%v", calls, err) } } func TestApnOperationClientCancellationDoesNotLoseJob(t *testing.T) { store := &memoryApnOperationStore{rows: map[string][2]string{}} input := operationTestInput() ctx, cancel := context.WithCancel(context.Background()) output, err := durableApnOperation(ctx, store, "channel", input, func(context.Context) (model.SimCardOperateChangeApnOutput, error) { cancel() return acceptedOperation(input), nil }) if err != nil || output[0].ResData.OrderNum != "saved-job" { t.Fatalf("客户端取消导致jobId丢失:%v", err) } _, saved, _ := store.Load(context.Background(), input.ReqData.OperationID, "channel") if saved == "" { t.Fatal("jobId未落库") } } type affectedApnTestDB struct { gdb.DB tx gdb.TX } func (d affectedApnTestDB) Transaction(ctx context.Context, f func(context.Context, gdb.TX) error) error { return f(ctx, d.tx) } type affectedApnTestTX struct { gdb.TX hash string rows int64 } func (tx affectedApnTestTX) GetOne(string, ...any) (gdb.Record, error) { return gdb.Record{"request_hash": gvar.New(tx.hash), "response_json": gvar.New("")}, nil } func (tx affectedApnTestTX) Exec(string, ...any) (sql.Result, error) { return affectedApnTestResult(tx.rows), nil } type affectedApnTestResult int64 func (r affectedApnTestResult) LastInsertId() (int64, error) { return 0, nil } func (r affectedApnTestResult) RowsAffected() (int64, error) { return int64(r), nil } func TestApnOperationSQLSaveRejectsAffectedZeroAndHashMismatch(t *testing.T) { hash := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" for _, tc := range []struct { rows int64 hash string wantError bool }{{0, hash, true}, {1, "wrong-hash", true}, {1, hash, false}} { store := sqlApnOperationStore{db: affectedApnTestDB{tx: affectedApnTestTX{hash: tc.hash, rows: tc.rows}}} err := store.Save(context.Background(), "test-operation", "test-channel", hash, "[]") if (err != nil) != tc.wantError { t.Fatalf("rows=%d hash=%s err=%v", tc.rows, tc.hash, err) } } } type retryCheckpointApnStore struct { memoryApnOperationStore failed bool } func (s *retryCheckpointApnStore) Save(ctx context.Context, id, channel, hash, body string) error { if !s.failed { s.failed = true return errors.New("模拟首次checkpoint磁盘故障") } return s.memoryApnOperationStore.Save(ctx, id, channel, hash, body) } func TestApnOperationFailedCheckpointRetriesKnownJobNotUnknown(t *testing.T) { store := &retryCheckpointApnStore{memoryApnOperationStore: memoryApnOperationStore{rows: map[string][2]string{}}} input := operationTestInput() output, err := durableApnOperation(context.Background(), store, "channel", input, func(ctx context.Context) (model.SimCardOperateChangeApnOutput, error) { checkpoint := ctx.Value(model.ApnOperationCheckpointKey{}).(func(model.SimCardOperateChangeApnOutput) error) if err := checkpoint(acceptedOperation(input)); err == nil { t.Fatal("应模拟checkpoint失败") } return nil, errors.New("受理后响应断开") }) if err != nil || output[0].ResData.OrderNum != "saved-job" || !output[0].ResData.Pending { t.Fatalf("保存重试丢掉已取得jobId:%+v %v", output, err) } _, saved, _ := store.Load(context.Background(), input.ReqData.OperationID, "channel") if !strings.Contains(saved, "saved-job") { t.Fatalf("重试未可靠保存jobId:%s", saved) } } func TestApnOperationCheckpointCannotBeErased(t *testing.T) { for _, failed := range []bool{false, true} { store := &memoryApnOperationStore{rows: map[string][2]string{}} input := operationTestInput() output, err := durableApnOperation(context.Background(), store, "channel", input, func(ctx context.Context) (model.SimCardOperateChangeApnOutput, error) { checkpoint := ctx.Value(model.ApnOperationCheckpointKey{}).(func(model.SimCardOperateChangeApnOutput) error) if err := checkpoint(acceptedOperation(input)); err != nil { t.Fatal(err) } if failed { return nil, errors.New("模拟受理后请求失败") } return nil, nil }) if err != nil || len(output) != 1 || output[0].ResData.OrderNum != "saved-job" || !output[0].ResData.Pending { t.Fatalf("后续空响应或错误抹掉jobId:%+v %v", output, err) } input.ReqData.RecoverOnly = true output, err = durableApnOperation(context.Background(), store, "channel", input, nil) if err != nil || output[0].ResData.OrderNum != "saved-job" { t.Fatalf("持久化映射已被回退:%+v %v", output, err) } } } func TestApnOperationMergePreservesTerminalAndCardAssociation(t *testing.T) { input := operationTestInput() terminal := acceptedOperation(input) terminal[0].ResData.Pending = false terminal[0].Message = "明确成功" body, _ := json.Marshal(terminal) unknown, _ := json.Marshal(unknownApnOperation(input, "后来的未知结果")) merged, err := mergeApnOperationJSON(string(body), string(unknown)) if err != nil || merged != string(body) { t.Fatalf("逐卡终态被未知覆盖:%s %v", merged, err) } second := *input.SimKeys[0] second.ICCID = "second" other, _ := json.Marshal(model.SimCardOperateChangeApnOutput{&model.SimBaseRes[model.SimCardOperateChangeApnItem]{SimKey: &second, ResData: model.SimCardOperateChangeApnItem{Pending: true, OrderNum: "second-job"}}}) merged, err = mergeApnOperationJSON(merged, string(other)) var results model.SimCardOperateChangeApnOutput _ = json.Unmarshal([]byte(merged), &results) if err != nil || len(results) != 2 || results[0].SimKey.ICCID != "test" || results[1].SimKey.ICCID != "second" { t.Fatalf("逐卡合并丢失卡关联:%s %v", merged, err) } } type failingClaimApnStore struct{ memoryApnOperationStore } func (s *failingClaimApnStore) Claim(context.Context, string, string, string) (bool, error) { return false, errors.New("模拟INSERT失败") } func TestApnOperationClaimFailureNeverCallsCarrier(t *testing.T) { called := false _, err := durableApnOperation(context.Background(), &failingClaimApnStore{}, "channel", operationTestInput(), func(context.Context) (model.SimCardOperateChangeApnOutput, error) { called = true return nil, nil }) if err == nil || called { t.Fatalf("意图INSERT失败仍调用运营商:called=%v err=%v", called, err) } } func TestApnOperationExplicitFailureWinsPendingAndUnknown(t *testing.T) { input := operationTestInput() pending, _ := json.Marshal(acceptedOperation(input)) failure := model.SimCardOperateChangeApnOutput{&model.SimBaseRes[model.SimCardOperateChangeApnItem]{SimKey: input.SimKeys[0], Code: 9999, Message: "运营商明确拒绝"}} failed, _ := json.Marshal(failure) merged, err := mergeApnOperationJSON(string(pending), string(failed)) if err != nil { t.Fatal(err) } unknown, _ := json.Marshal(unknownApnOperation(input, "后到的通信错误")) merged, err = mergeApnOperationJSON(merged, string(unknown)) var output model.SimCardOperateChangeApnOutput if err != nil || json.Unmarshal([]byte(merged), &output) != nil || len(output) != 1 || output[0].Code != 9999 || output[0].Message != "运营商明确拒绝" || output[0].ResData.Pending || output[0].ResData.Uncertain || output[0].ResData.OrderNum != "saved-job" { t.Fatalf("明确失败被pending或未知覆盖:%s %v", merged, err) } } func TestApnOperationCheckpointSurvivesCrashBeforeReturn(t *testing.T) { store := &memoryApnOperationStore{rows: map[string][2]string{}} input := operationTestInput() func() { defer func() { _ = recover() }() _, _ = durableApnOperation(context.Background(), store, "channel", input, func(ctx context.Context) (model.SimCardOperateChangeApnOutput, error) { checkpoint := ctx.Value(model.ApnOperationCheckpointKey{}).(func(model.SimCardOperateChangeApnOutput) error) if err := checkpoint(acceptedOperation(input)); err != nil { t.Fatal(err) } panic("模拟拿jobId后进程中断") }) }() input.ReqData.RecoverOnly = true output, err := durableApnOperation(context.Background(), store, "channel", input, func(context.Context) (model.SimCardOperateChangeApnOutput, error) { t.Fatal("恢复不得重新办理") return nil, nil }) if err != nil || output[0].ResData.OrderNum != "saved-job" { t.Fatalf("checkpoint未恢复:%+v %v", output, err) } }