snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:17:21 +05:00
commit b352a9a30d
472 changed files with 108375 additions and 0 deletions

747
internal/api/api_test.go Normal file
View File

@@ -0,0 +1,747 @@
package api
import (
"context"
"encoding/base64"
"fmt"
"os"
"testing"
"time"
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
"github.com/centrifugal/centrifugo/v6/internal/config"
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
"github.com/centrifugal/centrifugo/v6/internal/metrics"
"github.com/centrifugal/centrifuge"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
// Initialize metrics with a custom registry for tests to avoid conflicts
registry := prometheus.NewRegistry()
_ = metrics.Init(metrics.Config{
Registerer: registry,
})
os.Exit(m.Run())
}
func nodeWithMemoryEngine() *centrifuge.Node {
n, err := centrifuge.New(centrifuge.Config{})
if err != nil {
panic(err)
}
err = n.Run()
if err != nil {
panic(err)
}
return n
}
type testSurveyCaller struct{}
func (t testSurveyCaller) Channels(_ context.Context, _ *ChannelsRequest) (map[string]*ChannelInfo, error) {
return nil, nil
}
func (t testSurveyCaller) Connections(_ context.Context, _ *ConnectionsRequest) (map[string]*ConnectionInfo, error) {
return nil, nil
}
func TestPublishAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Publish(context.Background(), &PublishRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{Channel: "test"})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{Channel: "test", Data: []byte("test")})
require.Nil(t, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{Channel: "test:test", Data: []byte("test")})
require.Equal(t, ErrorUnknownChannel, resp.Error)
}
// TestBatchAPINilCommand guards against a crash on a nil batch command (e.g. a
// JSON `null` array element). In parallel mode each command runs in its own
// goroutine, so a nil-deref would escape net/http's recover and crash the node.
func TestBatchAPINilCommand(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
for _, parallel := range []bool{false, true} {
resp := api.Batch(context.Background(), &BatchRequest{
Parallel: parallel,
Commands: []*Command{
nil,
{Publish: &PublishRequest{Channel: "test", Data: []byte("x")}},
},
})
require.Len(t, resp.Replies, 2)
require.Equal(t, ErrorBadRequest, resp.Replies[0].Error)
require.Nil(t, resp.Replies[1].Error)
}
}
func TestBroadcastAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Broadcast(context.Background(), &BroadcastRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
// Empty data validation now happens per-channel
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test"}})
require.Nil(t, resp.Error)
require.Equal(t, ErrorBadRequest.Code, resp.Result.Responses[0].Error.Code)
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test"}, Data: []byte("test")})
require.Nil(t, resp.Error)
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test:test"}, Data: []byte("test")})
require.Nil(t, resp.Error)
require.Equal(t, ErrorUnknownChannel, resp.Result.Responses[0].Error)
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test", "test:test"}, Data: []byte("test")})
require.Nil(t, resp.Error)
require.Equal(t, ErrorUnknownChannel, resp.Result.Responses[1].Error)
}
func TestPublishAPI_DataFormat_JSON(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatJSON
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// Valid JSON should succeed
resp := api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`{"key": "value"}`),
})
require.Nil(t, resp.Error)
// Valid JSON array should succeed
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`[1, 2, 3]`),
})
require.Nil(t, resp.Error)
// Valid JSON primitives should succeed
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`"string"`),
})
require.Nil(t, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`123`),
})
require.Nil(t, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`true`),
})
require.Nil(t, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`null`),
})
require.Nil(t, resp.Error)
// Invalid JSON should fail
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`not valid json`),
})
require.NotNil(t, resp.Error)
require.Equal(t, ErrorBadRequest.Code, resp.Error.Code)
// Empty data with JSON format should fail
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte{},
})
require.NotNil(t, resp.Error)
require.Equal(t, ErrorBadRequest.Code, resp.Error.Code)
}
func TestPublishAPI_DataFormat_Binary(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatBinary
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// Binary data should succeed
resp := api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte{0x01, 0x02, 0x03},
})
require.Nil(t, resp.Error)
// Empty data with binary format should succeed
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte{},
})
require.Nil(t, resp.Error)
// Any data should succeed with binary format
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte("any text data"),
})
require.Nil(t, resp.Error)
}
func TestPublishAPI_DataFormat_Default(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
// Default format (empty string) - should reject empty data
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// Non-empty data should succeed
resp := api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte("any data"),
})
require.Nil(t, resp.Error)
// Empty data should fail (current behavior preserved)
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte{},
})
require.NotNil(t, resp.Error)
require.Equal(t, ErrorBadRequest.Code, resp.Error.Code)
}
func TestBroadcastAPI_DataFormat_JSON(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatJSON
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// Valid JSON should succeed
resp := api.Broadcast(context.Background(), &BroadcastRequest{
Channels: []string{"test", "test2"},
Data: []byte(`{"key": "value"}`),
})
require.Nil(t, resp.Error)
require.Nil(t, resp.Result.Responses[0].Error)
require.Nil(t, resp.Result.Responses[1].Error)
// Invalid JSON should fail for all channels
resp = api.Broadcast(context.Background(), &BroadcastRequest{
Channels: []string{"test", "test2"},
Data: []byte(`not valid json`),
})
require.Nil(t, resp.Error)
require.Equal(t, ErrorBadRequest.Code, resp.Result.Responses[0].Error.Code)
require.Equal(t, ErrorBadRequest.Code, resp.Result.Responses[1].Error.Code)
}
func TestBroadcastAPI_DataFormat_Binary(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatBinary
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// Empty data should succeed with binary format
resp := api.Broadcast(context.Background(), &BroadcastRequest{
Channels: []string{"test", "test2"},
Data: []byte{},
})
require.Nil(t, resp.Error)
require.Nil(t, resp.Result.Responses[0].Error)
require.Nil(t, resp.Result.Responses[1].Error)
}
func TestHistoryAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.History(context.Background(), &HistoryRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.History(context.Background(), &HistoryRequest{Channel: "test"})
require.Equal(t, ErrorNotAvailable, resp.Error)
cfg = cfgContainer.Config()
cfg.Channel.WithoutNamespace.HistorySize = 1
cfg.Channel.WithoutNamespace.HistoryTTL = configtypes.Duration(1 * time.Second)
_ = cfgContainer.Reload(cfg)
resp = api.History(context.Background(), &HistoryRequest{Channel: "test"})
require.Nil(t, resp.Error)
}
func TestHistoryRemoveAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.HistoryRemove(context.Background(), &HistoryRemoveRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.HistoryRemove(context.Background(), &HistoryRemoveRequest{Channel: "test"})
require.Equal(t, ErrorNotAvailable, resp.Error)
cfg = cfgContainer.Config()
cfg.Channel.WithoutNamespace.HistorySize = 1
cfg.Channel.WithoutNamespace.HistoryTTL = configtypes.Duration(time.Second)
_ = cfgContainer.Reload(cfg)
resp = api.HistoryRemove(context.Background(), &HistoryRemoveRequest{Channel: "test"})
require.Nil(t, resp.Error)
}
func TestPresenceAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Presence(context.Background(), &PresenceRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.Presence(context.Background(), &PresenceRequest{Channel: "test"})
require.Equal(t, ErrorNotAvailable, resp.Error)
cfg = cfgContainer.Config()
cfg.Channel.WithoutNamespace.Presence = true
_ = cfgContainer.Reload(cfg)
resp = api.Presence(context.Background(), &PresenceRequest{Channel: "test"})
require.Nil(t, resp.Error)
}
func TestPresenceStatsAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.PresenceStats(context.Background(), &PresenceStatsRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.PresenceStats(context.Background(), &PresenceStatsRequest{Channel: "test"})
require.Equal(t, ErrorNotAvailable, resp.Error)
cfg = cfgContainer.Config()
cfg.Channel.WithoutNamespace.Presence = true
_ = cfgContainer.Reload(cfg)
resp = api.PresenceStats(context.Background(), &PresenceStatsRequest{Channel: "test"})
require.Nil(t, resp.Error)
}
func TestDisconnectAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Disconnect(context.Background(), &DisconnectRequest{
User: "test",
})
require.Nil(t, resp.Error)
}
func TestUnsubscribeAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Unsubscribe(context.Background(), &UnsubscribeRequest{
User: "test",
Channel: "test",
})
require.Nil(t, resp.Error)
}
func TestRefreshAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Refresh(context.Background(), &RefreshRequest{
User: "test",
})
require.Nil(t, resp.Error)
}
func TestSubscribeAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Subscribe(context.Background(), &SubscribeRequest{
User: "test",
Channel: "test",
})
require.Nil(t, resp.Error)
}
func TestInfoAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Info(context.Background(), &InfoRequest{})
require.Nil(t, resp.Error)
}
// configWithNamespace returns a DefaultConfig with a single namespace having the
// given name and subscription_type. Used to test the subscription_type gates on
// publish/map_*/shared_poll_* API endpoints. Fills the minimum mandatory fields
// (map.mode for map types, global shared_poll.hmac_secret_key for shared_poll)
// so the config passes Validate().
func configWithNamespace(name, subscriptionType string) config.Config {
cfg := config.DefaultConfig()
chOpts := configtypes.ChannelOptions{
SubscriptionType: subscriptionType,
}
switch subscriptionType {
case "map":
// Persistent mode requires no key_ttl and is the simplest minimum.
chOpts.Map = configtypes.MapConfig{Mode: "persistent"}
case "map_clients", "map_users":
// Persistent mode is forbidden here (presence entries need TTL-based
// cleanup), so use ephemeral with a key_ttl.
chOpts.Map = configtypes.MapConfig{
Mode: "ephemeral",
KeyTTL: configtypes.Duration(time.Minute),
}
case "shared_poll":
// Global HMAC secret required for any shared_poll namespace.
cfg.SharedPoll.HMACSecretKey = "test-secret"
// Default refresh proxy must have an endpoint when no explicit
// proxy_name is set on the namespace.
cfg.Channel.Proxy.SharedPollRefresh = configtypes.Proxy{
Endpoint: "http://127.0.0.1:0/test",
Timeout: configtypes.Duration(time.Second),
}
chOpts.SharedPoll = configtypes.SharedPollConfig{Mode: "versioned"}
}
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{
{
Name: name,
ChannelOptions: chOpts,
},
}
return cfg
}
// TestPublishAPI_RejectsMapAndSharedPollNamespaces ensures the stream Publish
// endpoint rejects calls aimed at namespaces configured for map or shared poll
// subscriptions — those have their own dedicated APIs.
func TestPublishAPI_RejectsMapAndSharedPollNamespaces(t *testing.T) {
cases := []struct {
subscriptionType string
}{
{"map"},
{"map_clients"},
{"map_users"},
{"shared_poll"},
}
for _, c := range cases {
t.Run(c.subscriptionType, func(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := configWithNamespace("ns", c.subscriptionType)
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Publish(context.Background(), &PublishRequest{Channel: "ns:test", Data: []byte("data")})
require.Equal(t, ErrorBadRequest, resp.Error)
})
}
}
// TestMapPublishAPI_RejectsNonMapNamespaces ensures map_publish rejects calls
// against namespaces that aren't configured for map subscriptions.
func TestMapPublishAPI_RejectsNonMapNamespaces(t *testing.T) {
cases := []struct {
subscriptionType string
}{
{""},
{"stream"},
{"shared_poll"},
}
for _, c := range cases {
t.Run("type="+c.subscriptionType, func(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := configWithNamespace("ns", c.subscriptionType)
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.MapPublish(context.Background(), &MapPublishRequest{
Channel: "ns:test",
Key: "k",
Data: []byte(`{"v":1}`),
})
require.Equal(t, ErrorBadRequest, resp.Error)
})
}
}
// TestMapPublishAPI_RejectsInvalidKeyMode ensures key_mode is validated against
// the allowed set ("", "if_new", "if_exists").
func TestMapPublishAPI_RejectsInvalidKeyMode(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := configWithNamespace("ns", "map")
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.MapPublish(context.Background(), &MapPublishRequest{
Channel: "ns:test",
Key: "k",
Data: []byte(`{"v":1}`),
KeyMode: "bogus",
})
require.Equal(t, ErrorBadRequest, resp.Error)
}
// TestMapAPIs_RejectNonMapNamespaces ensures the other map API endpoints
// (remove / read_state / read_stream / stats / clear) reject calls against
// namespaces that aren't configured for map subscriptions, instead of leaking
// internal errors from the broker.
func TestMapAPIs_RejectNonMapNamespaces(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := configWithNamespace("ns", "") // default = stream
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
rmResp := api.MapRemove(context.Background(), &MapRemoveRequest{Channel: "ns:test", Key: "k"})
require.Equal(t, ErrorBadRequest, rmResp.Error)
rsResp := api.MapReadState(context.Background(), &MapReadStateRequest{Channel: "ns:test"})
require.Equal(t, ErrorBadRequest, rsResp.Error)
rstrResp := api.MapReadStream(context.Background(), &MapReadStreamRequest{Channel: "ns:test"})
require.Equal(t, ErrorBadRequest, rstrResp.Error)
stResp := api.MapStats(context.Background(), &MapStatsRequest{Channel: "ns:test"})
require.Equal(t, ErrorBadRequest, stResp.Error)
clResp := api.MapClear(context.Background(), &MapClearRequest{Channel: "ns:test"})
require.Equal(t, ErrorBadRequest, clResp.Error)
}
// TestMapAPIs_RejectUnknownChannel ensures the map API endpoints return
// ErrorUnknownChannel instead of leaking internal errors when the channel's
// namespace doesn't exist.
func TestMapAPIs_RejectUnknownChannel(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// "missing:test" — namespace "missing" not configured.
pubResp := api.MapPublish(context.Background(), &MapPublishRequest{Channel: "missing:test", Key: "k", Data: []byte(`{}`)})
require.Equal(t, ErrorUnknownChannel, pubResp.Error)
rmResp := api.MapRemove(context.Background(), &MapRemoveRequest{Channel: "missing:test", Key: "k"})
require.Equal(t, ErrorUnknownChannel, rmResp.Error)
rsResp := api.MapReadState(context.Background(), &MapReadStateRequest{Channel: "missing:test"})
require.Equal(t, ErrorUnknownChannel, rsResp.Error)
rstrResp := api.MapReadStream(context.Background(), &MapReadStreamRequest{Channel: "missing:test"})
require.Equal(t, ErrorUnknownChannel, rstrResp.Error)
stResp := api.MapStats(context.Background(), &MapStatsRequest{Channel: "missing:test"})
require.Equal(t, ErrorUnknownChannel, stResp.Error)
clResp := api.MapClear(context.Background(), &MapClearRequest{Channel: "missing:test"})
require.Equal(t, ErrorUnknownChannel, clResp.Error)
}
// nodeWithMapSupport builds a node able to actually serve map operations for
// channels of the given config container, so that valid publishes really reach
// the map broker instead of failing on options resolution.
func nodeWithMapSupport(cfgContainer *config.Container) *centrifuge.Node {
n, err := centrifuge.New(centrifuge.Config{
Map: centrifuge.MapConfig{
GetMapChannelOptions: func(channel string) centrifuge.MapChannelOptions {
_, _, chOpts, ok, err := cfgContainer.ChannelOptions(channel)
if err != nil || !ok {
return centrifuge.MapChannelOptions{}
}
var mode centrifuge.MapMode
switch chOpts.Map.Mode {
case "ephemeral":
mode = centrifuge.MapModeEphemeral
case "recoverable":
mode = centrifuge.MapModeRecoverable
case "persistent":
mode = centrifuge.MapModePersistent
}
return centrifuge.MapChannelOptions{Mode: mode, KeyTTL: chOpts.Map.KeyTTL.ToDuration()}
},
},
})
if err != nil {
panic(err)
}
if err = n.Run(); err != nil {
panic(err)
}
return n
}
// TestMapPublishAPI_DataFormat ensures map_publish applies the namespace
// publication_data_format policy, same as the stream Publish endpoint does.
func TestMapPublishAPI_DataFormat(t *testing.T) {
cases := []struct {
format string
data []byte
b64Data string
wantReject bool
}{
{configtypes.PublicationDataFormatJSONObject, []byte(`{"v":1}`), "", false},
{configtypes.PublicationDataFormatJSONObject, []byte(`[1,2]`), "", true},
{configtypes.PublicationDataFormatJSONObject, []byte(`not-json-object`), "", true},
{configtypes.PublicationDataFormatJSON, []byte(`[1,2]`), "", false},
{configtypes.PublicationDataFormatJSON, []byte(`nope`), "", true},
{configtypes.PublicationDataFormatBinary, nil, "", false},
{"", []byte(`whatever`), "", false},
{"", nil, "", true}, // default format rejects empty data
// b64data must be validated after decoding, not before.
{configtypes.PublicationDataFormatJSONObject, nil, base64.StdEncoding.EncodeToString([]byte(`{"v":1}`)), false},
{configtypes.PublicationDataFormatJSONObject, nil, base64.StdEncoding.EncodeToString([]byte(`nope`)), true},
}
for i, c := range cases {
t.Run(fmt.Sprintf("%d_format=%q", i, c.format), func(t *testing.T) {
cfg := configWithNamespace("ns", "map")
cfg.Channel.Namespaces[0].PublicationDataFormat = c.format
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
node := nodeWithMapSupport(cfgContainer)
defer func() { _ = node.Shutdown(context.Background()) }()
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test"})
resp := api.MapPublish(context.Background(), &MapPublishRequest{
Channel: "ns:test", Key: "k", Data: c.data, B64Data: c.b64Data,
})
if c.wantReject {
require.Equal(t, ErrorBadRequest, resp.Error)
} else {
require.Nil(t, resp.Error)
}
})
}
}
// TestSharedPollPublishAPI_DataFormat ensures shared_poll_publish applies the
// namespace publication_data_format policy.
func TestSharedPollPublishAPI_DataFormat(t *testing.T) {
cases := []struct {
format string
data []byte
wantReject bool
}{
{configtypes.PublicationDataFormatJSONObject, []byte(`{"v":1}`), false},
{configtypes.PublicationDataFormatJSONObject, []byte(`nope`), true},
{configtypes.PublicationDataFormatJSON, []byte(`nope`), true},
{configtypes.PublicationDataFormatBinary, nil, false},
{"", nil, true}, // default format rejects empty data
}
for i, c := range cases {
t.Run(fmt.Sprintf("%d_format=%q", i, c.format), func(t *testing.T) {
cfg := configWithNamespace("ns", "shared_poll")
cfg.Channel.Namespaces[0].PublicationDataFormat = c.format
cfg.Channel.Namespaces[0].SharedPoll.PublishEnabled = true
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test"})
resp := api.SharedPollPublish(context.Background(), &SharedPollPublishRequest{
Channel: "ns:test", Key: "k", Version: 1, Data: c.data,
})
if c.wantReject {
require.Equal(t, ErrorBadRequest, resp.Error)
} else {
// The test node has no shared poll manager, so a valid payload
// gets past validation and fails later — the point is that it is
// not rejected as a bad request.
require.NotEqual(t, ErrorBadRequest, resp.Error)
}
})
}
}