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 } }` }