Files
upstream-centrifugo/internal/proxy/subscribe_handler_test.go
upstream-mirror b352a9a30d
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s
snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:17:21 +05:00

369 lines
13 KiB
Go

package proxy
import (
"context"
"encoding/base64"
"fmt"
"log"
"net/http"
"testing"
"time"
"github.com/centrifugal/centrifugo/v6/internal/clientcontext"
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
"github.com/centrifugal/centrifugo/v6/internal/middleware"
"github.com/centrifugal/centrifugo/v6/internal/subsource"
"github.com/centrifugal/centrifugo/v6/internal/tools"
"github.com/centrifugal/centrifuge"
"github.com/stretchr/testify/require"
)
type grpcSubscribeHandleTestCase struct {
*tools.CommonGRPCProxyTestCase
subscribeProxyHandler *SubscribeHandler
channelOpts configtypes.ChannelOptions
}
func newSubscribeHandleGRPCTestCase(ctx context.Context, proxyGRPCServer proxyGRPCTestServer, opts configtypes.ChannelOptions) grpcSubscribeHandleTestCase {
commonProxyTestCase := tools.NewCommonGRPCProxyTestCase(ctx, proxyGRPCServer)
subscribeProxy, err := NewGRPCSubscribeProxy("default", getTestGrpcProxy(commonProxyTestCase))
if err != nil {
log.Fatalln("could not create grpc subscribe proxy: ", err)
}
subscribeProxyHandler := NewSubscribeHandler(SubscribeHandlerConfig{
Proxies: map[string]SubscribeProxy{
"test": subscribeProxy,
},
})
return grpcSubscribeHandleTestCase{commonProxyTestCase, subscribeProxyHandler, opts}
}
type httpSubscribeHandleTestCase struct {
*tools.CommonHTTPProxyTestCase
subscribeProxyHandler *SubscribeHandler
channelOpts configtypes.ChannelOptions
}
func newSubscribeHandleHTTPTestCase(ctx context.Context, endpoint string, opts configtypes.ChannelOptions) httpSubscribeHandleTestCase {
commonProxyTestCase := tools.NewCommonHTTPProxyTestCase(ctx)
subscribeProxy, err := NewHTTPSubscribeProxy(getTestHttpProxy(commonProxyTestCase, endpoint))
if err != nil {
log.Fatalln("could not create http subscribe proxy: ", err)
}
subscribeProxyHandler := NewSubscribeHandler(SubscribeHandlerConfig{
Proxies: map[string]SubscribeProxy{
"test": subscribeProxy,
},
})
return httpSubscribeHandleTestCase{commonProxyTestCase, subscribeProxyHandler, opts}
}
type subscribeHandleTestCase struct {
subscribeProxyHandler *SubscribeHandler
protocol string
node *centrifuge.Node
client *centrifuge.Client
channelOpts configtypes.ChannelOptions
}
func (c subscribeHandleTestCase) invokeHandle() (reply centrifuge.SubscribeReply, err error) {
subscribeHandler := c.subscribeProxyHandler.Handle()
reply, _, err = subscribeHandler(c.client, centrifuge.SubscribeEvent{}, c.channelOpts, PerCallData{})
return reply, err
}
func newSubscribeHandleTestCases(httpTestCase httpSubscribeHandleTestCase, grpcTestCase grpcSubscribeHandleTestCase) []subscribeHandleTestCase {
return []subscribeHandleTestCase{
{
subscribeProxyHandler: grpcTestCase.subscribeProxyHandler,
node: grpcTestCase.Node,
client: grpcTestCase.Client,
channelOpts: grpcTestCase.channelOpts,
protocol: "grpc",
},
{
subscribeProxyHandler: httpTestCase.subscribeProxyHandler,
node: httpTestCase.Node,
client: httpTestCase.Client,
channelOpts: httpTestCase.channelOpts,
protocol: "http",
},
}
}
// TestHandleSubscribeForwardsHeaders is an end-to-end test that the split header
// model works for a channel (subscribe) proxy, not just connect: the emulated and
// real transport headers captured at connect (here placed in the client context,
// exactly as OnClientConnecting sets them) are forwarded to the subscribe backend
// filtered by the subscribe proxy's own http_headers / emulated_headers lists.
func TestHandleSubscribeForwardsHeaders(t *testing.T) {
// Client context as it would look after connect: a real transport header, plus
// an emulated map that includes a legit token and a forged value for a name that
// is only in http_headers (must not win via emulation).
ctx := middleware.SetHeadersToContext(context.Background(), http.Header{"X-Real-Ip": []string{"1.2.3.4"}})
ctx = clientcontext.SetEmulatedHeadersToContext(ctx, map[string]string{
"x-app-token": "from-client",
"x-real-ip": "forged-by-client",
})
commonProxyTestCase := tools.NewCommonHTTPProxyTestCase(ctx)
defer commonProxyTestCase.Teardown()
var received http.Header
commonProxyTestCase.Mux.HandleFunc("/subscribe", func(w http.ResponseWriter, req *http.Request) {
received = req.Header.Clone()
_, _ = w.Write([]byte(`{"result": {}}`))
})
proxyCfg := configtypes.Proxy{
Endpoint: commonProxyTestCase.Server.URL + "/subscribe",
Timeout: configtypes.Duration(5 * time.Second),
ProxyCommon: configtypes.ProxyCommon{
HttpHeaders: []string{"X-Real-Ip"}, // transport-only
EmulatedHeaders: []string{"X-App-Token"}, // client-supplied
},
}
subscribeProxy, err := GetSubscribeProxy("test", proxyCfg)
require.NoError(t, err)
handler := NewSubscribeHandler(SubscribeHandlerConfig{
Proxies: map[string]SubscribeProxy{"test": subscribeProxy},
}).Handle()
chOpts := configtypes.ChannelOptions{
SubscribeProxyEnabled: true,
SubscribeProxyName: "test",
}
_, _, err = handler(commonProxyTestCase.Client, centrifuge.SubscribeEvent{}, chOpts, PerCallData{})
require.NoError(t, err)
require.NotNil(t, received, "subscribe backend should have been called")
require.Equal(t, "from-client", received.Get("X-App-Token"), "emulated header must be forwarded via the subscribe proxy's emulated_headers")
require.Equal(t, "1.2.3.4", received.Get("X-Real-Ip"), "real transport value must win; forged emulated value must NOT be forwarded via http_headers")
}
func TestHandleSubscribeWithResult(t *testing.T) {
customData := "test"
customDataB64 := base64.StdEncoding.EncodeToString([]byte(customData))
chOpts := configtypes.ChannelOptions{
Presence: true,
JoinLeave: true,
ForcePushJoinLeave: true,
ForceRecovery: true,
ForcePositioning: true,
SubscribeProxyEnabled: true,
SubscribeProxyName: "test",
}
opts := proxyGRPCTestServerOptions{
B64Data: customDataB64,
}
grpcTestCase := newSubscribeHandleGRPCTestCase(context.Background(), newProxyGRPCTestServer("result", opts), chOpts)
defer grpcTestCase.Teardown()
httpTestCase := newSubscribeHandleHTTPTestCase(context.Background(), "/subscribe", chOpts)
httpTestCase.Mux.HandleFunc("/subscribe", func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte(fmt.Sprintf(`{"result": {"b64info": "%s", "b64data": "%s"}}`, customDataB64, customDataB64)))
})
defer httpTestCase.Teardown()
expectedSubscribeOpts := centrifuge.SubscribeOptions{
ChannelInfo: []byte(customData),
Data: []byte(customData),
EmitPresence: true,
EmitJoinLeave: true,
PushJoinLeave: true,
EnableRecovery: true,
EnablePositioning: true,
Source: subsource.SubscribeProxy,
}
cases := newSubscribeHandleTestCases(httpTestCase, grpcTestCase)
for _, c := range cases {
reply, err := c.invokeHandle()
require.NoError(t, err, c.protocol)
require.Equal(t, expectedSubscribeOpts, reply.Options, c.protocol)
require.True(t, reply.ClientSideRefresh, c.protocol)
}
}
func TestHandleSubscribeWithOverride(t *testing.T) {
customData := "test"
customDataB64 := base64.StdEncoding.EncodeToString([]byte(customData))
chOpts := configtypes.ChannelOptions{
Presence: false,
JoinLeave: true,
ForceRecovery: false,
ForcePositioning: false,
SubscribeProxyEnabled: true,
SubscribeProxyName: "test",
}
opts := proxyGRPCTestServerOptions{
B64Data: customDataB64,
}
grpcTestCase := newSubscribeHandleGRPCTestCase(context.Background(), newProxyGRPCTestServer("override", opts), chOpts)
defer grpcTestCase.Teardown()
httpTestCase := newSubscribeHandleHTTPTestCase(context.Background(), "/subscribe", chOpts)
httpTestCase.Mux.HandleFunc("/subscribe", func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte(fmt.Sprintf(`{"result": {"b64info": "%s", "override": {"join_leave": {"value": false}, "presence": {"value": true}, "force_positioning": {"value": true}, "force_recovery": {"value": true}}}}`, customDataB64)))
})
defer httpTestCase.Teardown()
expectedSubscribeOpts := centrifuge.SubscribeOptions{
ChannelInfo: []byte(customData),
EmitPresence: true,
EmitJoinLeave: false,
PushJoinLeave: false,
EnablePositioning: true,
EnableRecovery: true,
Source: subsource.SubscribeProxy,
}
cases := newSubscribeHandleTestCases(httpTestCase, grpcTestCase)
for _, c := range cases {
reply, err := c.invokeHandle()
require.NoError(t, err, c.protocol)
require.Equal(t, expectedSubscribeOpts, reply.Options, c.protocol)
require.True(t, reply.ClientSideRefresh, c.protocol)
}
}
func TestHandleSubscribeWithContextCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
grpcTestCase := newSubscribeHandleGRPCTestCase(ctx, proxyGRPCTestServer{}, configtypes.ChannelOptions{
SubscribeProxyEnabled: true,
SubscribeProxyName: "test",
})
defer grpcTestCase.Teardown()
httpTestCase := newSubscribeHandleHTTPTestCase(ctx, "/subscribe", configtypes.ChannelOptions{
SubscribeProxyEnabled: true,
SubscribeProxyName: "test",
})
httpTestCase.Mux.HandleFunc("/subscribe", func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte(`{}`))
})
defer httpTestCase.Teardown()
cases := newSubscribeHandleTestCases(httpTestCase, grpcTestCase)
for _, c := range cases {
reply, err := c.invokeHandle()
require.ErrorIs(t, centrifuge.DisconnectConnectionClosed, err, c.protocol)
require.Equal(t, centrifuge.SubscribeReply{}, reply, c.protocol)
}
}
func TestHandleSubscribeWithoutProxyServerStart(t *testing.T) {
grpcTestCase := newSubscribeHandleGRPCTestCase(context.Background(), proxyGRPCTestServer{}, configtypes.ChannelOptions{
SubscribeProxyEnabled: true,
SubscribeProxyName: "test",
})
grpcTestCase.Teardown()
httpTestCase := newSubscribeHandleHTTPTestCase(context.Background(), "/subscribe", configtypes.ChannelOptions{
SubscribeProxyEnabled: true,
SubscribeProxyName: "test",
})
httpTestCase.Teardown()
cases := newSubscribeHandleTestCases(httpTestCase, grpcTestCase)
for _, c := range cases {
reply, err := c.invokeHandle()
require.Error(t, err, c.protocol)
require.Equal(t, centrifuge.SubscribeReply{}, reply, c.protocol)
}
}
func TestHandleSubscribeWithProxyServerCustomDisconnect(t *testing.T) {
chOpts := configtypes.ChannelOptions{
SubscribeProxyEnabled: true,
SubscribeProxyName: "test",
}
grpcTestCase := newSubscribeHandleGRPCTestCase(context.Background(), newProxyGRPCTestServer("custom disconnect", proxyGRPCTestServerOptions{}), chOpts)
defer grpcTestCase.Teardown()
httpTestCase := newSubscribeHandleHTTPTestCase(context.Background(), "/subscribe", chOpts)
httpTestCase.Mux.HandleFunc("/subscribe", func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte(`{"disconnect": {"code": 4000, "reconnect": false, "reason": "custom disconnect"}}`))
})
defer httpTestCase.Teardown()
expectedErr := centrifuge.Disconnect{
Code: 4000,
Reason: "custom disconnect",
}
cases := newSubscribeHandleTestCases(httpTestCase, grpcTestCase)
for _, c := range cases {
reply, err := c.invokeHandle()
require.NotNil(t, err, c.protocol)
require.Equal(t, expectedErr.Error(), err.Error(), c.protocol)
require.Equal(t, centrifuge.SubscribeReply{}, reply, c.protocol)
}
}
func TestHandleSubscribeWithProxyServerCustomError(t *testing.T) {
chOpts := configtypes.ChannelOptions{
SubscribeProxyEnabled: true,
SubscribeProxyName: "test",
}
grpcTestCase := newSubscribeHandleGRPCTestCase(context.Background(), newProxyGRPCTestServer("custom error", proxyGRPCTestServerOptions{}), chOpts)
defer grpcTestCase.Teardown()
httpTestCase := newSubscribeHandleHTTPTestCase(context.Background(), "/subscribe", chOpts)
httpTestCase.Mux.HandleFunc("/subscribe", func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte(`{"error": {"code": 1000, "message": "custom error"}}`))
})
defer httpTestCase.Teardown()
expectedErr := centrifuge.Error{
Code: 1000,
Message: "custom error",
}
cases := newSubscribeHandleTestCases(httpTestCase, grpcTestCase)
for _, c := range cases {
reply, err := c.invokeHandle()
require.NotNil(t, err, c.protocol)
require.Equal(t, expectedErr.Error(), err.Error(), c.protocol)
require.Equal(t, centrifuge.SubscribeReply{}, reply, c.protocol)
}
}
func TestHandleSubscribeWithInvalidCustomData(t *testing.T) {
chOpts := configtypes.ChannelOptions{
SubscribeProxyEnabled: true,
SubscribeProxyName: "test",
}
opts := proxyGRPCTestServerOptions{
B64Data: "invalid data",
}
grpcTestCase := newSubscribeHandleGRPCTestCase(context.Background(), newProxyGRPCTestServer("result", opts), chOpts)
defer grpcTestCase.Teardown()
httpTestCase := newSubscribeHandleHTTPTestCase(context.Background(), "/subscribe", chOpts)
httpTestCase.Mux.HandleFunc("/subscribe", func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte(`{"result": {"b64info": "invalid data"}}`))
})
defer httpTestCase.Teardown()
cases := newSubscribeHandleTestCases(httpTestCase, grpcTestCase)
for _, c := range cases {
reply, err := c.invokeHandle()
require.ErrorIs(t, err, centrifuge.ErrorInternal, c.protocol)
require.Equal(t, centrifuge.SubscribeReply{}, reply, c.protocol)
}
}