chore: 并入 iotlcm-upstream-chan 历史到 upstream-chan/(原提交 1e74497)
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user