fix: correlate downstream receipts with submit responses

This commit is contained in:
hectorzhao
2026-07-14 16:22:41 +08:00
parent 135b4fd24e
commit 1ce02ef206
12 changed files with 487 additions and 91 deletions
+110
View File
@@ -226,6 +226,82 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
}
}
func TestSubmitResponsePrecedesQueuedFailureReceipt(t *testing.T) {
resetDownstreamRegistry()
defer resetDownstreamRegistry()
account := "100001"
password := "secret-hash"
var mu sync.Mutex
var submit submitRequest
pendingReturned := false
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/gateway/events/inbound/authenticate":
_ = json.NewEncoder(w).Encode(authResponse{PasswordCipher: password, Account: account, EnterpriseCode: account})
case "/api/gateway/events/inbound/submit":
mu.Lock()
defer mu.Unlock()
if err := json.NewDecoder(r.Body).Decode(&submit); err != nil {
t.Fatalf("decode submit: %v", err)
}
_ = json.NewEncoder(w).Encode(submitResponse{Accepted: true, MessageID: "MSG-ORDER"})
case "/api/gateway/events/downstream/pending":
mu.Lock()
defer mu.Unlock()
if submit.SequenceID == 0 || pendingReturned {
_ = json.NewEncoder(w).Encode([]pendingDelivery{})
return
}
payload, _ := json.Marshal(DownstreamReceipt{
Account: account, MessageID: "MSG-ORDER", PhoneNumber: "13500002696",
ReceiptStatus: "undelivered", RawStatus: "REJECTD", SubmitSequenceID: submit.SequenceID,
})
pendingReturned = true
_ = json.NewEncoder(w).Encode([]pendingDelivery{{
ID: "delivery-order", DeliveryType: "receipt", Payload: payload, CreatedAt: time.Now().UTC(),
}})
case "/api/gateway/events/inbound/connection", "/api/gateway/events/downstream/sent":
w.WriteHeader(http.StatusOK)
default:
t.Fatalf("unexpected api path: %s", r.URL.Path)
}
}))
defer api.Close()
addr := reserveTCPAddr(t)
go func() { _ = (Server{Addr: addr, APIBaseURL: api.URL + "/api"}).ListenAndServe() }()
time.Sleep(300 * time.Millisecond)
client := cmpp.NewClient(cmpp.V20)
defer client.Disconnect()
if err := client.Connect(addr, account, password, 2*time.Second); err != nil {
t.Fatalf("connect CMPP2 inbound: %v", err)
}
content, _ := cmpputils.Utf8ToUcs2("测试回执顺序")
if _, err := client.SendReqPkt(&cmpp.Cmpp2SubmitReqPkt{
PkTotal: 1, PkNumber: 1, RegisteredDelivery: 1, MsgLevel: 1,
ServiceId: "cmpp", FeeUserType: 2, FeeTerminalId: "13500002696",
MsgFmt: 8, MsgSrc: account, FeeType: "02", FeeCode: "0", SrcId: "10690000",
DestUsrTl: 1, DestTerminalId: []string{"13500002696"}, MsgLength: uint8(len(content)), MsgContent: content,
}); err != nil {
t.Fatalf("send submit: %v", err)
}
first, err := client.RecvAndUnpackPkt(2 * time.Second)
if err != nil {
t.Fatalf("receive first packet: %v", err)
}
submitResponse, ok := first.(*cmpp.Cmpp2SubmitRspPkt)
if !ok || submitResponse.Result != 0 || submitResponse.MsgId == 0 {
t.Fatalf("first packet must be successful SUBMIT_RESP, got %T %+v", first, first)
}
deliver := recvDeliver20(t, client)
if deliver.MsgId != submitResponse.MsgId {
t.Fatalf("receipt Msg_Id=%d does not match SUBMIT_RESP Msg_Id=%d", deliver.MsgId, submitResponse.MsgId)
}
}
func TestInboundServerNegotiatesCMPP2AndUsesAuthenticatedAccountForSubmit(t *testing.T) {
resetDownstreamRegistry()
defer resetDownstreamRegistry()
@@ -582,6 +658,40 @@ func TestDownstreamDeliveryRequiresAcknowledgement(t *testing.T) {
}
}
func TestReceiptLookupDoesNotFallbackToAccountBeforeSubmitMappingExists(t *testing.T) {
resetDownstreamRegistry()
defer resetDownstreamRegistry()
conn := &cmpp.Conn{}
rememberAccount(downstreamSession{
account: "100001", protocol: "cmpp20", conn: conn, mu: &sync.Mutex{}, connectionID: "conn-1",
})
if session := findReceiptSession("MSG-NOT-REMEMBERED", "100001"); session != nil {
t.Fatalf("receipt unexpectedly fell back to account session: %+v", session)
}
recovered := recoverReceiptSession(DownstreamReceipt{
MessageID: "MSG-NOT-REMEMBERED", Account: "100001", SubmitSequenceID: 1216579149,
})
if recovered == nil {
t.Fatal("expected persisted submit sequence to recover receipt session")
}
if recovered.gatewayMsgID != messageIDFrom("MSG-NOT-REMEMBERED", 1216579149) || recovered.gatewayMsgID == 0 {
t.Fatalf("unexpected recovered Msg_Id: %d", recovered.gatewayMsgID)
}
}
func TestSendDownstreamRejectsZeroMessageID(t *testing.T) {
_, err := sendDownstream(
&downstreamSession{mu: &sync.Mutex{}},
&cmpp.Cmpp2DeliverReqPkt{MsgId: 0},
"delivery-zero",
)
if err == nil || !strings.Contains(err.Error(), "Msg_Id=0") {
t.Fatalf("expected zero Msg_Id rejection, got %v", err)
}
}
func TestDownstreamDeliveryReportsAckTimeout(t *testing.T) {
resetDownstreamRegistry()
defer resetDownstreamRegistry()