226 lines
7.1 KiB
Go
226 lines
7.1 KiB
Go
package control
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"cmpp-platform/gateway/internal/inbound"
|
|
)
|
|
|
|
func TestConnectChannelCallbacksConnectedState(t *testing.T) {
|
|
handler := handlerWithConnect(func(context.Context, ConnectChannelCommand) (ConnectionStateCallback, error) {
|
|
return ConnectionStateCallback{
|
|
ChannelID: "channel-1",
|
|
ConnectionID: "channel-1:primary",
|
|
Status: "connected",
|
|
DesiredConnections: 2,
|
|
CurrentConnections: 2,
|
|
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()))
|
|
handler.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("unexpected response status: %d body=%s", resp.Code, resp.Body.String())
|
|
}
|
|
var callback ConnectionStateCallback
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &callback); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if callback.Status != "connected" || callback.CurrentConnections != 2 || callback.DesiredConnections != 2 {
|
|
t.Fatalf("unexpected callback state: %+v", callback)
|
|
}
|
|
if callback.ChannelID != "channel-1" || callback.ConnectionID != "channel-1:primary" {
|
|
t.Fatalf("unexpected callback identity: %+v", callback)
|
|
}
|
|
if callback.LastConnectedAt == "" || callback.LastHeartbeatAt == "" {
|
|
t.Fatalf("expected connection timestamps: %+v", callback)
|
|
}
|
|
}
|
|
|
|
func TestConnectChannelCallbacksFailedState(t *testing.T) {
|
|
handler := handlerWithConnect(func(context.Context, ConnectChannelCommand) (ConnectionStateCallback, error) {
|
|
return ConnectionStateCallback{
|
|
ChannelID: "channel-1",
|
|
ConnectionID: "channel-1:primary",
|
|
Status: "failed",
|
|
DesiredConnections: 2,
|
|
CurrentConnections: 0,
|
|
LastDisconnectedAt: time.Now().UTC().Format(time.RFC3339Nano),
|
|
LastError: errTestConnect.Error(),
|
|
}, nil
|
|
})
|
|
|
|
resp := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/connections/connect", strings.NewReader(validConnectCommand()))
|
|
handler.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("unexpected response status: %d body=%s", resp.Code, resp.Body.String())
|
|
}
|
|
var callback ConnectionStateCallback
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &callback); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if callback.Status != "failed" || callback.CurrentConnections != 0 || callback.LastError == "" {
|
|
t.Fatalf("unexpected callback state: %+v", callback)
|
|
}
|
|
}
|
|
|
|
func TestConnectChannelRejectsInvalidCommand(t *testing.T) {
|
|
handler := handlerWithConnect(func(context.Context, ConnectChannelCommand) (ConnectionStateCallback, error) {
|
|
return ConnectionStateCallback{}, nil
|
|
})
|
|
|
|
resp := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/connections/connect", strings.NewReader(`{"messageType":"SubmitCommand"}`))
|
|
handler.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusBadRequest {
|
|
t.Fatalf("unexpected response status: %d", resp.Code)
|
|
}
|
|
}
|
|
|
|
func TestRecoveryCandidatesEndpointReturnsView(t *testing.T) {
|
|
handler := handlerWithServer(Server{
|
|
RecoveryCandidates: func(context.Context) ([]inbound.DownstreamPresence, error) {
|
|
return []inbound.DownstreamPresence{{
|
|
Account: "100001",
|
|
GatewayInstanceID: "gateway-a",
|
|
State: "connected",
|
|
UpdatedAt: time.Now().UTC(),
|
|
}}, nil
|
|
},
|
|
})
|
|
|
|
resp := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/downstream/recovery-candidates", nil)
|
|
handler.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("unexpected response status: %d body=%s", resp.Code, resp.Body.String())
|
|
}
|
|
var payload []inbound.DownstreamPresence
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if len(payload) != 1 || payload[0].Account != "100001" {
|
|
t.Fatalf("unexpected payload: %+v", payload)
|
|
}
|
|
}
|
|
|
|
func TestRecoveryStatusesEndpointReturnsView(t *testing.T) {
|
|
handler := handlerWithServer(Server{
|
|
RecoveryStatuses: func(context.Context) ([]inbound.DownstreamRecoveryStatus, error) {
|
|
return []inbound.DownstreamRecoveryStatus{{
|
|
Account: "100001",
|
|
GatewayInstanceID: "gateway-a",
|
|
State: "waiting_connection",
|
|
AttemptCount: 2,
|
|
}}, nil
|
|
},
|
|
})
|
|
|
|
resp := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/downstream/recovery-statuses", nil)
|
|
handler.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("unexpected response status: %d body=%s", resp.Code, resp.Body.String())
|
|
}
|
|
var payload []inbound.DownstreamRecoveryStatus
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if len(payload) != 1 || payload[0].State != "waiting_connection" || payload[0].AttemptCount != 2 {
|
|
t.Fatalf("unexpected payload: %+v", payload)
|
|
}
|
|
}
|
|
|
|
func TestRecoveryOverviewEndpointReturnsCombinedView(t *testing.T) {
|
|
handler := handlerWithServer(Server{
|
|
RecoveryCandidates: func(context.Context) ([]inbound.DownstreamPresence, error) {
|
|
return []inbound.DownstreamPresence{{
|
|
Account: "100001",
|
|
GatewayInstanceID: "gateway-a",
|
|
State: "connected",
|
|
}}, nil
|
|
},
|
|
RecoveryStatuses: func(context.Context) ([]inbound.DownstreamRecoveryStatus, error) {
|
|
return []inbound.DownstreamRecoveryStatus{{
|
|
Account: "100001",
|
|
GatewayInstanceID: "gateway-a",
|
|
State: "success",
|
|
}}, nil
|
|
},
|
|
})
|
|
|
|
resp := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/downstream/recovery-overview", nil)
|
|
handler.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("unexpected response status: %d body=%s", resp.Code, resp.Body.String())
|
|
}
|
|
var payload DownstreamRecoveryOverview
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if len(payload.Candidates) != 1 || payload.Candidates[0].Account != "100001" {
|
|
t.Fatalf("unexpected candidate payload: %+v", payload.Candidates)
|
|
}
|
|
if len(payload.Statuses) != 1 || payload.Statuses[0].State != "success" {
|
|
t.Fatalf("unexpected status payload: %+v", payload.Statuses)
|
|
}
|
|
}
|
|
|
|
type testConnectError struct{}
|
|
|
|
func (testConnectError) Error() string {
|
|
return "connect failed"
|
|
}
|
|
|
|
var errTestConnect testConnectError
|
|
|
|
func handlerWithConnect(connect ConnectFunc) http.Handler {
|
|
return handlerWithServer(Server{Connect: connect})
|
|
}
|
|
|
|
func handlerWithServer(server Server) http.Handler {
|
|
mux := http.NewServeMux()
|
|
Register(mux, server)
|
|
return mux
|
|
}
|
|
|
|
func validConnectCommand() string {
|
|
return `{
|
|
"schemaVersion": "v1",
|
|
"messageType": "ConnectChannel",
|
|
"traceId": "trace-1",
|
|
"channelId": "channel-1",
|
|
"connectionId": "channel-1:primary",
|
|
"reason": "channel_created",
|
|
"desiredConnections": 2,
|
|
"channel": {
|
|
"code": "CMPP-A",
|
|
"name": "主通道",
|
|
"gatewayHost": "127.0.0.1",
|
|
"gatewayPort": 17890,
|
|
"account": "sp",
|
|
"passwordCipher": "secret",
|
|
"srcId": "10690000",
|
|
"cmppVersion": "3.0",
|
|
"rateLimitPerSecond": 100
|
|
}
|
|
}`
|
|
}
|