feat: complete cmpp gateway delivery recovery workflows
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +15,63 @@ import (
|
||||
cmpputils "github.com/bigwhite/gocmpp/utils"
|
||||
)
|
||||
|
||||
type memoryPresenceStore struct {
|
||||
snapshots map[string]DownstreamPresence
|
||||
removed []string
|
||||
}
|
||||
|
||||
type memoryRecoveryStore struct {
|
||||
decisions map[string]RecoveryStartDecision
|
||||
completed []DownstreamRecoveryStatus
|
||||
}
|
||||
|
||||
func (m *memoryPresenceStore) TouchAccount(_ context.Context, snapshot DownstreamPresence) error {
|
||||
if m.snapshots == nil {
|
||||
m.snapshots = map[string]DownstreamPresence{}
|
||||
}
|
||||
m.snapshots[snapshot.Account] = snapshot
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryPresenceStore) RemoveAccount(_ context.Context, account string) error {
|
||||
delete(m.snapshots, account)
|
||||
m.removed = append(m.removed, account)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryPresenceStore) ListAccounts(_ context.Context) ([]DownstreamPresence, error) {
|
||||
result := make([]DownstreamPresence, 0, len(m.snapshots))
|
||||
for _, snapshot := range m.snapshots {
|
||||
result = append(result, snapshot)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *memoryRecoveryStore) StartAccountRecovery(_ context.Context, account string, _ string) (RecoveryStartDecision, error) {
|
||||
if decision, ok := m.decisions[account]; ok {
|
||||
return decision, nil
|
||||
}
|
||||
return RecoveryStartDecision{Allowed: true}, nil
|
||||
}
|
||||
|
||||
func (m *memoryRecoveryStore) CompleteAccountRecovery(_ context.Context, status DownstreamRecoveryStatus) error {
|
||||
m.completed = append(m.completed, status)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryRecoveryStore) ListRecoveryStatuses(_ context.Context) ([]DownstreamRecoveryStatus, error) {
|
||||
return append([]DownstreamRecoveryStatus(nil), m.completed...), nil
|
||||
}
|
||||
|
||||
func (m *memoryRecoveryStore) GetAccountRecoveryStatus(_ context.Context, account string) (DownstreamRecoveryStatus, error) {
|
||||
for _, item := range m.completed {
|
||||
if item.Account == account {
|
||||
return item, nil
|
||||
}
|
||||
}
|
||||
return DownstreamRecoveryStatus{Account: account}, nil
|
||||
}
|
||||
|
||||
func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
|
||||
account := "100001"
|
||||
password := "secret-hash"
|
||||
@@ -29,6 +89,8 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
|
||||
t.Fatalf("decode submit: %v", err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(submitResponse{Accepted: true, MessageID: "MSG-1"})
|
||||
case "/api/gateway/events/downstream/pending":
|
||||
_ = json.NewEncoder(w).Encode([]pendingDelivery{})
|
||||
default:
|
||||
t.Fatalf("unexpected api path: %s", r.URL.Path)
|
||||
}
|
||||
@@ -76,6 +138,26 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
|
||||
if rsp.Result != 0 || rsp.MsgId == 0 {
|
||||
t.Fatalf("unexpected submit response: %+v", rsp)
|
||||
}
|
||||
delivered, err := PushReceipt(DownstreamReceipt{
|
||||
MessageID: "MSG-1",
|
||||
PhoneNumber: "13500002696",
|
||||
ReceiptStatus: "delivered",
|
||||
DeliveredAt: time.Now().UTC().Format(time.RFC3339Nano),
|
||||
})
|
||||
if err != nil || !delivered {
|
||||
t.Fatalf("push receipt delivered=%v err=%v", delivered, err)
|
||||
}
|
||||
deliver := recvDeliver(t, client)
|
||||
if deliver.RegisterDelivery != 1 {
|
||||
t.Fatalf("expected receipt deliver, got %+v", deliver)
|
||||
}
|
||||
var receipt cmpp.CmppReceiptPkt
|
||||
if err := receipt.Unpack([]byte(deliver.MsgContent)); err != nil {
|
||||
t.Fatalf("unpack pushed receipt: %v", err)
|
||||
}
|
||||
if receipt.Stat != "DELIVRD" || receipt.DestTerminalId != "13500002696" {
|
||||
t.Fatalf("unexpected pushed receipt: %+v", receipt)
|
||||
}
|
||||
if gotAuth.Account != account || gotAuth.AuthSource == "" || gotAuth.RemoteIP == "" {
|
||||
t.Fatalf("unexpected auth payload: %+v", gotAuth)
|
||||
}
|
||||
@@ -84,6 +166,183 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlushOnlineAccountsFetchesPendingDeliveries(t *testing.T) {
|
||||
resetDownstreamRegistry()
|
||||
defer resetDownstreamRegistry()
|
||||
|
||||
account := "100001"
|
||||
calls := 0
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/gateway/events/downstream/pending":
|
||||
calls++
|
||||
var payload pendingDeliveryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode pending request: %v", err)
|
||||
}
|
||||
if payload.Account != account {
|
||||
t.Fatalf("unexpected account: %+v", payload)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode([]pendingDelivery{})
|
||||
default:
|
||||
t.Fatalf("unexpected api path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
downstreamRegistry.Lock()
|
||||
downstreamRegistry.byAccount[account] = &downstreamSession{account: account}
|
||||
downstreamRegistry.Unlock()
|
||||
|
||||
server := Server{APIBaseURL: api.URL + "/api"}
|
||||
server.flushOnlineAccounts(log.Default())
|
||||
|
||||
if calls != 1 {
|
||||
t.Fatalf("pending fetch calls = %d, want 1", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverPendingCandidatesFetchesPresenceAccounts(t *testing.T) {
|
||||
resetDownstreamRegistry()
|
||||
defer resetDownstreamRegistry()
|
||||
|
||||
account := "100009"
|
||||
calls := 0
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/gateway/events/downstream/pending":
|
||||
calls++
|
||||
var payload pendingDeliveryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode pending request: %v", err)
|
||||
}
|
||||
if payload.Account != account {
|
||||
t.Fatalf("unexpected account: %+v", payload)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode([]pendingDelivery{})
|
||||
default:
|
||||
t.Fatalf("unexpected api path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
store := &memoryPresenceStore{
|
||||
snapshots: map[string]DownstreamPresence{
|
||||
account: {
|
||||
Account: account,
|
||||
GatewayInstanceID: "gateway-a",
|
||||
State: "connected",
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
server := Server{APIBaseURL: api.URL + "/api", PresenceStore: store}
|
||||
server.recoverPendingCandidates(log.Default())
|
||||
|
||||
if calls != 1 {
|
||||
t.Fatalf("recovery pending fetch calls = %d, want 1", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverPendingCandidatesWritesWaitingConnectionStatus(t *testing.T) {
|
||||
resetDownstreamRegistry()
|
||||
defer resetDownstreamRegistry()
|
||||
|
||||
account := "100010"
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/gateway/events/downstream/pending":
|
||||
_ = json.NewEncoder(w).Encode([]pendingDelivery{{
|
||||
ID: "delivery-1",
|
||||
DeliveryType: "receipt",
|
||||
Payload: json.RawMessage(`{"messageId":"MSG-404","phoneNumber":"13800000001","receiptStatus":"delivered"}`),
|
||||
}})
|
||||
case "/api/gateway/events/downstream/recovery-status":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
t.Fatalf("unexpected api path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
recovery := &memoryRecoveryStore{}
|
||||
store := &memoryPresenceStore{
|
||||
snapshots: map[string]DownstreamPresence{
|
||||
account: {
|
||||
Account: account,
|
||||
GatewayInstanceID: "gateway-a",
|
||||
State: "connected",
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
server := Server{APIBaseURL: api.URL + "/api", PresenceStore: store, RecoveryStore: recovery}
|
||||
server.recoverPendingCandidates(log.Default())
|
||||
|
||||
if len(recovery.completed) != 1 {
|
||||
t.Fatalf("completed recovery statuses = %d, want 1", len(recovery.completed))
|
||||
}
|
||||
if recovery.completed[0].State != "waiting_connection" {
|
||||
t.Fatalf("unexpected recovery status: %+v", recovery.completed[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRememberAndForgetAccountUpdatesPresenceStore(t *testing.T) {
|
||||
resetDownstreamRegistry()
|
||||
defer resetDownstreamRegistry()
|
||||
|
||||
store := &memoryPresenceStore{}
|
||||
session := downstreamSession{
|
||||
account: "100001",
|
||||
srcID: "10690000",
|
||||
remoteIP: "127.0.0.1",
|
||||
connectedAt: time.Now().UTC(),
|
||||
conn: &cmpp.Conn{},
|
||||
mu: &sync.Mutex{},
|
||||
presence: store,
|
||||
instanceID: "gateway-a",
|
||||
}
|
||||
|
||||
rememberAccount(session)
|
||||
snapshot, ok := store.snapshots["100001"]
|
||||
if !ok {
|
||||
t.Fatal("expected presence snapshot to be stored")
|
||||
}
|
||||
if snapshot.Account != "100001" || snapshot.GatewayInstanceID != "gateway-a" || snapshot.State != "connected" {
|
||||
t.Fatalf("unexpected snapshot: %+v", snapshot)
|
||||
}
|
||||
|
||||
forgetDownstream(downstreamRegistry.byAccount["100001"])
|
||||
if len(store.removed) != 1 || store.removed[0] != "100001" {
|
||||
t.Fatalf("unexpected removed accounts: %+v", store.removed)
|
||||
}
|
||||
}
|
||||
|
||||
func recvDeliver(t *testing.T, client *cmpp.Client) *cmpp.Cmpp3DeliverReqPkt {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
packet, err := client.RecvAndUnpackPkt(200 * time.Millisecond)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if deliver, ok := packet.(*cmpp.Cmpp3DeliverReqPkt); ok {
|
||||
return deliver
|
||||
}
|
||||
}
|
||||
t.Fatal("timed out waiting deliver request")
|
||||
return nil
|
||||
}
|
||||
|
||||
func resetDownstreamRegistry() {
|
||||
downstreamRegistry.Lock()
|
||||
defer downstreamRegistry.Unlock()
|
||||
downstreamRegistry.byAccount = make(map[string]*downstreamSession)
|
||||
downstreamRegistry.byMessageID = make(map[string]*downstreamSession)
|
||||
}
|
||||
|
||||
func reserveTCPAddr(t *testing.T) string {
|
||||
t.Helper()
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
|
||||
Reference in New Issue
Block a user