feat: add report material workflows and gateway safeguards
This commit is contained in:
@@ -9,10 +9,12 @@ import (
|
||||
|
||||
"cmpp-platform/gateway/internal/inbound"
|
||||
"cmpp-platform/gateway/internal/queue"
|
||||
"cmpp-platform/gateway/internal/ratelimit"
|
||||
"cmpp-platform/gateway/internal/upstream"
|
||||
)
|
||||
|
||||
type ConnectFunc func(context.Context, ConnectChannelCommand) (ConnectionStateCallback, error)
|
||||
type SubmitFunc func(context.Context, queue.SubmitCommand) (queue.SubmitResult, error)
|
||||
|
||||
type ConnectChannelCommand struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
@@ -54,7 +56,9 @@ type Server struct {
|
||||
APIBaseURL string
|
||||
HTTPClient *http.Client
|
||||
Connect ConnectFunc
|
||||
Submit SubmitFunc
|
||||
Upstream *upstream.Manager
|
||||
Limiter ratelimit.Limiter
|
||||
RecoveryCandidates func(context.Context) ([]inbound.DownstreamPresence, error)
|
||||
RecoveryStatuses func(context.Context) ([]inbound.DownstreamRecoveryStatus, error)
|
||||
}
|
||||
@@ -74,6 +78,9 @@ func Register(mux *http.ServeMux, server Server) {
|
||||
if server.Connect == nil {
|
||||
server.Connect = server.connectChannel
|
||||
}
|
||||
if server.Submit == nil {
|
||||
server.Submit = server.Upstream.Submit
|
||||
}
|
||||
mux.HandleFunc("/connections/connect", server.handleConnectChannel)
|
||||
mux.HandleFunc("/upstream/submit", server.handleUpstreamSubmit)
|
||||
mux.HandleFunc("/downstream/receipt", server.handleDownstreamReceipt)
|
||||
@@ -98,6 +105,12 @@ func (s Server) handleConnectChannel(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if configurer, ok := s.Limiter.(ratelimit.Configurer); ok {
|
||||
if err := configurer.Configure(r.Context(), command.ChannelID, command.Channel.RateLimitPerSecond); err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to configure gateway channel rate limit: %v", err), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
status, err := s.Connect(r.Context(), command)
|
||||
if err != nil {
|
||||
@@ -119,7 +132,13 @@ func (s Server) handleUpstreamSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, fmt.Sprintf("invalid submit command: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
result, err := s.Upstream.Submit(r.Context(), command)
|
||||
if s.Limiter != nil {
|
||||
if _, err := s.Limiter.Wait(r.Context(), command.ChannelID, command.Route.RateLimitPerSecond); err != nil {
|
||||
http.Error(w, fmt.Sprintf("gateway channel rate limit unavailable: %v", err), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
}
|
||||
result, err := s.Submit(r.Context(), command)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
|
||||
@@ -10,10 +10,57 @@ import (
|
||||
"time"
|
||||
|
||||
"cmpp-platform/gateway/internal/inbound"
|
||||
"cmpp-platform/gateway/internal/queue"
|
||||
)
|
||||
|
||||
type controlRecordingLimiter struct {
|
||||
channelID string
|
||||
rate int
|
||||
configuredChannelID string
|
||||
configuredRate int
|
||||
}
|
||||
|
||||
func (l *controlRecordingLimiter) Wait(_ context.Context, channelID string, rate int) (time.Duration, error) {
|
||||
l.channelID = channelID
|
||||
l.rate = rate
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (l *controlRecordingLimiter) Configure(_ context.Context, channelID string, rate int) error {
|
||||
l.configuredChannelID = channelID
|
||||
l.configuredRate = rate
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestUpstreamSubmitUsesGatewayChannelLimiter(t *testing.T) {
|
||||
limiter := &controlRecordingLimiter{}
|
||||
handler := handlerWithServer(Server{
|
||||
Limiter: limiter,
|
||||
Submit: func(_ context.Context, command queue.SubmitCommand) (queue.SubmitResult, error) {
|
||||
return queue.SubmitResult{Envelope: command.Envelope, SubmitStatus: "accepted"}, nil
|
||||
},
|
||||
})
|
||||
resp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/upstream/submit", strings.NewReader(`{
|
||||
"schemaVersion":"v1","messageType":"SubmitCommand","messageId":"msg-1","channelId":"channel-1",
|
||||
"submitId":"submit-1","tenantId":"tenant-1","applicationId":"app-1","phoneNumber":"13800138000","content":"hello",
|
||||
"route":{"channelCode":"CMPP-A","cmppAccountCode":"sp","rateLimitPerSecond":100},
|
||||
"cmpp":{"serviceId":"SMS","srcId":"10690000","registeredDelivery":1,"msgFmt":8},
|
||||
"upstream":{"gatewayHost":"127.0.0.1","gatewayPort":17890,"account":"sp","passwordCipher":"secret","cmppVersion":"3.0"},
|
||||
"retry":{"attempt":0,"maxAttempts":1}
|
||||
}`))
|
||||
handler.ServeHTTP(resp, req)
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected response status: %d body=%s", resp.Code, resp.Body.String())
|
||||
}
|
||||
if limiter.channelID != "channel-1" || limiter.rate != 100 {
|
||||
t.Fatalf("unexpected limiter call: %+v", limiter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectChannelCallbacksConnectedState(t *testing.T) {
|
||||
handler := handlerWithConnect(func(context.Context, ConnectChannelCommand) (ConnectionStateCallback, error) {
|
||||
limiter := &controlRecordingLimiter{}
|
||||
handler := handlerWithServer(Server{Limiter: limiter, Connect: func(context.Context, ConnectChannelCommand) (ConnectionStateCallback, error) {
|
||||
return ConnectionStateCallback{
|
||||
ChannelID: "channel-1",
|
||||
ConnectionID: "channel-1:primary",
|
||||
@@ -23,7 +70,7 @@ func TestConnectChannelCallbacksConnectedState(t *testing.T) {
|
||||
LastConnectedAt: time.Now().UTC().Format(time.RFC3339Nano),
|
||||
LastHeartbeatAt: time.Now().UTC().Format(time.RFC3339Nano),
|
||||
}, nil
|
||||
})
|
||||
}})
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/connections/connect", strings.NewReader(validConnectCommand()))
|
||||
@@ -45,6 +92,9 @@ func TestConnectChannelCallbacksConnectedState(t *testing.T) {
|
||||
if callback.LastConnectedAt == "" || callback.LastHeartbeatAt == "" {
|
||||
t.Fatalf("expected connection timestamps: %+v", callback)
|
||||
}
|
||||
if limiter.configuredChannelID != "channel-1" || limiter.configuredRate != 100 {
|
||||
t.Fatalf("unexpected configured limiter: %+v", limiter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectChannelCallbacksFailedState(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user