snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
368
internal/proxy/subscribe_handler_test.go
Normal file
368
internal/proxy/subscribe_handler_test.go
Normal file
@@ -0,0 +1,368 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user