package control import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "cmpp-platform/gateway/internal/inbound" ) func TestConnectChannelCallbacksConnectedState(t *testing.T) { var callback ConnectionStateCallback api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/admin/gateway/connections" { t.Fatalf("unexpected callback path: %s", r.URL.Path) } if err := json.NewDecoder(r.Body).Decode(&callback); err != nil { t.Fatalf("decode callback: %v", err) } w.WriteHeader(http.StatusOK) })) defer api.Close() handler := handlerWithDial(api.URL+"/api", func(context.Context, ConnectChannelCommand) error { return 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()) } 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) { var callback ConnectionStateCallback api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&callback); err != nil { t.Fatalf("decode callback: %v", err) } w.WriteHeader(http.StatusOK) })) defer api.Close() handler := handlerWithDial(api.URL+"/api", func(context.Context, ConnectChannelCommand) error { return errTestDial }) 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()) } if callback.Status != "failed" || callback.CurrentConnections != 0 || callback.LastError == "" { t.Fatalf("unexpected callback state: %+v", callback) } } func TestConnectChannelRejectsInvalidCommand(t *testing.T) { handler := handlerWithDial("", func(context.Context, ConnectChannelCommand) error { return 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 testDialError struct{} func (testDialError) Error() string { return "dial failed" } var errTestDial testDialError func handlerWithDial(apiBaseURL string, dial DialFunc) http.Handler { return handlerWithServer(Server{APIBaseURL: apiBaseURL, Dial: dial}) } 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 } }` }