snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
707
internal/config/validate_test.go
Normal file
707
internal/config/validate_test.go
Normal file
@@ -0,0 +1,707 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidatePublicationDataFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
format string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty string is valid",
|
||||
format: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json is valid",
|
||||
format: configtypes.PublicationDataFormatJSON,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json_object is valid",
|
||||
format: configtypes.PublicationDataFormatJSONObject,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "binary is valid",
|
||||
format: configtypes.PublicationDataFormatBinary,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "unknown format xml is invalid",
|
||||
format: "xml",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unknown format text is invalid",
|
||||
format: "text",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "random string is invalid",
|
||||
format: "foobar",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Channel.WithoutNamespace.PublicationDataFormat = tt.format
|
||||
err := cfg.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown publication_data_format")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePublicationDataFormatInNamespace(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
format string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "json in namespace",
|
||||
format: "json",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json_object in namespace",
|
||||
format: "json_object",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "binary in namespace",
|
||||
format: "binary",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid format in namespace",
|
||||
format: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{
|
||||
{
|
||||
Name: "test",
|
||||
ChannelOptions: configtypes.ChannelOptions{
|
||||
PublicationDataFormat: tt.format,
|
||||
},
|
||||
},
|
||||
}
|
||||
err := cfg.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown publication_data_format")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGlobalPublicationDataFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
format string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty global format is valid",
|
||||
format: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json global format is valid",
|
||||
format: "json",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "binary global format is valid",
|
||||
format: "binary",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid global format",
|
||||
format: "xml",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Channel.PublicationDataFormat = tt.format
|
||||
err := cfg.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown channel.publication_data_format")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mapDefaultConfig returns a Config suitable as a base for map namespace validation tests.
|
||||
func mapDefaultConfig() Config {
|
||||
cfg := DefaultConfig()
|
||||
return cfg
|
||||
}
|
||||
|
||||
func mapNamespace(name, mode string) configtypes.ChannelNamespace {
|
||||
return configtypes.ChannelNamespace{
|
||||
Name: name,
|
||||
ChannelOptions: configtypes.ChannelOptions{
|
||||
SubscriptionType: "map",
|
||||
Map: configtypes.MapConfig{Mode: mode},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMapNamespace_Ephemeral(t *testing.T) {
|
||||
t.Run("valid_minimal", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ee", "ephemeral")
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("missing_key_ttl", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ee", "ephemeral")
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map.key_ttl is required")
|
||||
})
|
||||
|
||||
t.Run("stream_size_rejected", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ee", "ephemeral")
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
ns.Map.StreamSize = 100
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map.stream_size must be 0")
|
||||
})
|
||||
|
||||
t.Run("stream_ttl_rejected", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ee", "ephemeral")
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
ns.Map.StreamTTL = configtypes.Duration(time.Minute)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map.stream_ttl must be 0")
|
||||
})
|
||||
|
||||
t.Run("meta_ttl_rejected", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ee", "ephemeral")
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
ns.Map.MetaTTL = configtypes.Duration(10 * time.Minute)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map.meta_ttl must be 0")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateAutoCacheRecover(t *testing.T) {
|
||||
t.Run("valid_with_cache_recovery", func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
opts := &cfg.Channel.WithoutNamespace
|
||||
opts.HistorySize = 1
|
||||
opts.HistoryTTL = configtypes.Duration(time.Hour)
|
||||
opts.ForceRecovery = true
|
||||
opts.ForceRecoveryMode = "cache"
|
||||
opts.AutoCacheRecover = true
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("requires_force_recovery", func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
opts := &cfg.Channel.WithoutNamespace
|
||||
opts.HistorySize = 1
|
||||
opts.HistoryTTL = configtypes.Duration(time.Hour)
|
||||
opts.ForceRecoveryMode = "cache"
|
||||
opts.AutoCacheRecover = true
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "auto_cache_recover requires force_recovery")
|
||||
})
|
||||
|
||||
t.Run("requires_cache_mode", func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
opts := &cfg.Channel.WithoutNamespace
|
||||
opts.HistorySize = 1
|
||||
opts.HistoryTTL = configtypes.Duration(time.Hour)
|
||||
opts.ForceRecovery = true
|
||||
opts.AutoCacheRecover = true
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "force_recovery_mode set to cache")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateMapNamespace_Recoverable(t *testing.T) {
|
||||
t.Run("valid_minimal_defaults_zero", func(t *testing.T) {
|
||||
// Recoverable with all stream options at zero is valid
|
||||
// because centrifuge auto-derives defaults at runtime.
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ce", "recoverable")
|
||||
ns.Map.KeyTTL = configtypes.Duration(30 * time.Second)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("valid_explicit_stream_options", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ce", "recoverable")
|
||||
ns.Map.KeyTTL = configtypes.Duration(30 * time.Second)
|
||||
ns.Map.StreamSize = 200
|
||||
ns.Map.StreamTTL = configtypes.Duration(2 * time.Minute)
|
||||
ns.Map.MetaTTL = configtypes.Duration(20 * time.Minute)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("missing_key_ttl", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ce", "recoverable")
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map.key_ttl is required")
|
||||
})
|
||||
|
||||
t.Run("meta_ttl_less_than_stream_ttl", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ce", "recoverable")
|
||||
ns.Map.KeyTTL = configtypes.Duration(30 * time.Second)
|
||||
ns.Map.StreamTTL = configtypes.Duration(10 * time.Minute)
|
||||
ns.Map.MetaTTL = configtypes.Duration(5 * time.Minute) // less than stream_ttl
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "metadata must outlive stream")
|
||||
})
|
||||
|
||||
t.Run("meta_ttl_equals_stream_ttl_ok", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ce", "recoverable")
|
||||
ns.Map.KeyTTL = configtypes.Duration(30 * time.Second)
|
||||
ns.Map.StreamTTL = configtypes.Duration(5 * time.Minute)
|
||||
ns.Map.MetaTTL = configtypes.Duration(5 * time.Minute) // equals stream_ttl
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("only_stream_ttl_set_meta_ttl_zero_ok", func(t *testing.T) {
|
||||
// When only stream_ttl is set, meta_ttl=0 means auto-derived by centrifuge.
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ce", "recoverable")
|
||||
ns.Map.KeyTTL = configtypes.Duration(30 * time.Second)
|
||||
ns.Map.StreamTTL = configtypes.Duration(5 * time.Minute)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("only_meta_ttl_set_stream_ttl_zero_ok", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ce", "recoverable")
|
||||
ns.Map.KeyTTL = configtypes.Duration(30 * time.Second)
|
||||
ns.Map.MetaTTL = configtypes.Duration(10 * time.Minute)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("meta_ttl_less_than_auto_derived_stream_ttl", func(t *testing.T) {
|
||||
// StreamTTL=0 will be auto-derived to 1min by centrifuge.
|
||||
// MetaTTL=30s is less than that — must be caught at config time.
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ce", "recoverable")
|
||||
ns.Map.KeyTTL = configtypes.Duration(30 * time.Second)
|
||||
ns.Map.MetaTTL = configtypes.Duration(30 * time.Second) // < auto-derived 1min
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "metadata must outlive stream")
|
||||
})
|
||||
|
||||
t.Run("meta_ttl_equals_auto_derived_stream_ttl_ok", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ce", "recoverable")
|
||||
ns.Map.KeyTTL = configtypes.Duration(30 * time.Second)
|
||||
ns.Map.MetaTTL = configtypes.Duration(1 * time.Minute) // == auto-derived 1min
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateMapNamespace_Persistent(t *testing.T) {
|
||||
t.Run("valid_minimal", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("cp", "persistent")
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("valid_with_stream_options", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("cp", "persistent")
|
||||
ns.Map.StreamSize = 500
|
||||
ns.Map.StreamTTL = configtypes.Duration(5 * time.Minute)
|
||||
ns.Map.MetaTTL = configtypes.Duration(50 * time.Minute)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("key_ttl_rejected", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("cp", "persistent")
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map.key_ttl must not be set")
|
||||
})
|
||||
|
||||
t.Run("meta_ttl_less_than_stream_ttl", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("cp", "persistent")
|
||||
ns.Map.StreamTTL = configtypes.Duration(10 * time.Minute)
|
||||
ns.Map.MetaTTL = configtypes.Duration(1 * time.Minute)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "metadata must outlive stream")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateMapNamespace_RequiredFields(t *testing.T) {
|
||||
t.Run("missing_mode", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{{
|
||||
Name: "ns",
|
||||
ChannelOptions: configtypes.ChannelOptions{
|
||||
SubscriptionType: "map",
|
||||
Map: configtypes.MapConfig{},
|
||||
},
|
||||
}}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map.mode is required")
|
||||
})
|
||||
|
||||
t.Run("invalid_mode", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ns", "unknown_mode")
|
||||
ns.Map.KeyTTL = configtypes.Duration(30 * time.Second)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown map.mode")
|
||||
})
|
||||
|
||||
t.Run("invalid_subscription_type", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{{
|
||||
Name: "ns",
|
||||
ChannelOptions: configtypes.ChannelOptions{
|
||||
SubscriptionType: "invalid_type",
|
||||
},
|
||||
}}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown subscription_type")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateMapNamespace_SubscriptionType(t *testing.T) {
|
||||
t.Run("map_clients", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ns", "ephemeral")
|
||||
ns.SubscriptionType = "map_clients"
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("map_users", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ns", "ephemeral")
|
||||
ns.SubscriptionType = "map_users"
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("stream_only_no_map_config_needed", func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{{
|
||||
Name: "ns",
|
||||
ChannelOptions: configtypes.ChannelOptions{
|
||||
SubscriptionType: "stream",
|
||||
},
|
||||
}}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("map_clients_persistent_rejected", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ns", "persistent")
|
||||
ns.SubscriptionType = "map_clients"
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), `map.mode "persistent" is not allowed with subscription_type "map_clients"`)
|
||||
})
|
||||
|
||||
t.Run("map_users_persistent_rejected", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ns", "persistent")
|
||||
ns.SubscriptionType = "map_users"
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), `map.mode "persistent" is not allowed with subscription_type "map_users"`)
|
||||
})
|
||||
|
||||
t.Run("client_key_with_publish_proxy_rejected", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ns", "ephemeral")
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
ns.Map.ClientKey = "client_id"
|
||||
ns.Map.PublishProxyEnabled = true
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map.client_key is not compatible with map.publish_proxy_enabled")
|
||||
})
|
||||
|
||||
t.Run("client_key_with_remove_proxy_rejected", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ns", "ephemeral")
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
ns.Map.ClientKey = "user_id"
|
||||
ns.Map.RemoveProxyEnabled = true
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map.client_key is not compatible with")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateMapNamespace_PresenceAndRemoveOptions(t *testing.T) {
|
||||
t.Run("remove_on_unsubscribe_valid", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("ns", "ephemeral")
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
ns.Map.RemoveClientOnUnsubscribe = true
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("remove_on_unsubscribe_requires_map_type", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{{
|
||||
Name: "ns",
|
||||
ChannelOptions: configtypes.ChannelOptions{
|
||||
SubscriptionType: "stream",
|
||||
Map: configtypes.MapConfig{RemoveClientOnUnsubscribe: true},
|
||||
},
|
||||
}}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map.remove_client_on_unsubscribe requires subscription_type to be a map type")
|
||||
})
|
||||
|
||||
t.Run("map_clients_presence_channel_prefix_valid", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("games", "ephemeral")
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
ns.MapClientsPresenceChannelPrefix = "clients:"
|
||||
clients := mapNamespace("clients", "ephemeral")
|
||||
clients.SubscriptionType = "map_clients"
|
||||
clients.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns, clients}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("map_clients_presence_channel_prefix_not_found", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("games", "ephemeral")
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
ns.MapClientsPresenceChannelPrefix = "nonexistent:"
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map_clients_presence_channel_prefix")
|
||||
require.Contains(t, err.Error(), "does not exist")
|
||||
})
|
||||
|
||||
t.Run("map_users_presence_channel_prefix_not_found", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
ns := mapNamespace("games", "ephemeral")
|
||||
ns.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
ns.MapUsersPresenceChannelPrefix = "nonexistent:"
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{ns}
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "map_users_presence_channel_prefix")
|
||||
require.Contains(t, err.Error(), "does not exist")
|
||||
})
|
||||
|
||||
t.Run("presence_channel_prefix_works_with_stream_type", func(t *testing.T) {
|
||||
cfg := mapDefaultConfig()
|
||||
clients := mapNamespace("clients", "ephemeral")
|
||||
clients.SubscriptionType = "map_clients"
|
||||
clients.Map.KeyTTL = configtypes.Duration(60 * time.Second)
|
||||
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{{
|
||||
Name: "ns",
|
||||
ChannelOptions: configtypes.ChannelOptions{
|
||||
SubscriptionType: "stream",
|
||||
MapClientsPresenceChannelPrefix: "clients:",
|
||||
},
|
||||
}, clients}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateMapNamespace_MapBrokerType(t *testing.T) {
|
||||
t.Run("unknown_type", func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.MapBroker.Type = "unknown"
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown map broker type")
|
||||
})
|
||||
|
||||
for _, brokerType := range []string{"memory", "redis", "postgres"} {
|
||||
t.Run("valid_"+brokerType, func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.MapBroker.Type = brokerType
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateConnectCodeToHTTPResponseTransforms(t *testing.T) {
|
||||
validTransform := configtypes.ConnectCodeToHTTPResponseTransform{
|
||||
Code: 3500,
|
||||
To: configtypes.TransformedConnectErrorHttpResponse{
|
||||
StatusCode: 403,
|
||||
Body: "forbidden",
|
||||
},
|
||||
}
|
||||
noCode := validTransform
|
||||
noCode.Code = 0
|
||||
noStatusCode := validTransform
|
||||
noStatusCode.To.StatusCode = 0
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
transforms configtypes.ConnectCodeToHTTPResponseTransforms
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid transform",
|
||||
transforms: configtypes.ConnectCodeToHTTPResponseTransforms{validTransform},
|
||||
},
|
||||
{
|
||||
name: "missing code",
|
||||
transforms: configtypes.ConnectCodeToHTTPResponseTransforms{noCode},
|
||||
wantErr: "code should be set in connect_code_to_http_response.transforms[0]",
|
||||
},
|
||||
{
|
||||
name: "missing status code",
|
||||
transforms: configtypes.ConnectCodeToHTTPResponseTransforms{validTransform, noStatusCode},
|
||||
wantErr: "status_code should be set in connect_code_to_http_response.transforms[1].to",
|
||||
},
|
||||
}
|
||||
|
||||
// The same transforms are validated for both unidirectional HTTP transports,
|
||||
// each reporting under its own config path.
|
||||
transports := []struct {
|
||||
name string
|
||||
prefix string
|
||||
set func(cfg *Config, transforms configtypes.ConnectCodeToHTTPResponseTransforms)
|
||||
}{
|
||||
{
|
||||
name: "uni_sse",
|
||||
prefix: "in uni_sse.connect_code_to_http_response.transforms: ",
|
||||
set: func(cfg *Config, transforms configtypes.ConnectCodeToHTTPResponseTransforms) {
|
||||
cfg.UniSSE.ConnectCodeToHTTPResponse.Transforms = transforms
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uni_http_stream",
|
||||
prefix: "in uni_http_stream.connect_code_to_http_response.transforms: ",
|
||||
set: func(cfg *Config, transforms configtypes.ConnectCodeToHTTPResponseTransforms) {
|
||||
cfg.UniHTTPStream.ConnectCodeToHTTPResponse.Transforms = transforms
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, transport := range transports {
|
||||
for _, tt := range tests {
|
||||
t.Run(transport.name+"/"+tt.name, func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
transport.set(&cfg, tt.transforms)
|
||||
err := cfg.Validate()
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
require.EqualError(t, err, transport.prefix+tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDuplicateProxyName(t *testing.T) {
|
||||
proxy := configtypes.Proxy{
|
||||
Endpoint: "http://localhost:3000/proxy",
|
||||
Timeout: configtypes.Duration(time.Second),
|
||||
}
|
||||
|
||||
t.Run("unique names are valid", func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Proxies = configtypes.NamedProxies{
|
||||
{Name: "first", Proxy: proxy},
|
||||
{Name: "second", Proxy: proxy},
|
||||
}
|
||||
require.NoError(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("duplicate name is invalid", func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Proxies = configtypes.NamedProxies{
|
||||
{Name: "first", Proxy: proxy},
|
||||
{Name: "first", Proxy: proxy},
|
||||
}
|
||||
require.EqualError(t, cfg.Validate(), "duplicate proxy name in proxies: first")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user