package config import ( "strconv" "strings" "testing" "time" "github.com/centrifugal/centrifugo/v6/internal/configtypes" "github.com/stretchr/testify/require" ) func defaultConfig(tb testing.TB) Config { conf := DefaultConfig() require.NotNil(tb, conf) return conf } func TestChannelNotFound(t *testing.T) { c := defaultConfig(t) _, found, err := channelOpts(&c, "xxx") require.False(t, found) require.NoError(t, err) } func TestConfigValidateDefault(t *testing.T) { err := defaultConfig(t).Validate() require.NoError(t, err) } func TestConfigValidateInvalidNamespaceName(t *testing.T) { c := defaultConfig(t) c.Channel.Namespaces = []configtypes.ChannelNamespace{ { Name: "invalid name", ChannelOptions: configtypes.ChannelOptions{}, }, } err := c.Validate() require.Error(t, err) } func TestConfigCompiledChannelRegex(t *testing.T) { c := defaultConfig(t) c.Channel.WithoutNamespace.ChannelRegex = "^test$" c.Channel.Namespaces = []configtypes.ChannelNamespace{ { Name: "name1", ChannelOptions: configtypes.ChannelOptions{ ChannelRegex: "^test_ns$", }, }, { Name: "name2", ChannelOptions: configtypes.ChannelOptions{}, }, } ruleContainer, err := NewContainer(c) require.NoError(t, err) require.NotNil(t, ruleContainer.Config().Channel.WithoutNamespace.CompiledChannelRegex) require.NotNil(t, ruleContainer.Config().Channel.Namespaces[0].CompiledChannelRegex) require.Nil(t, ruleContainer.Config().Channel.Namespaces[1].CompiledChannelRegex) } func TestConfigValidateDuplicateNamespaceName(t *testing.T) { c := defaultConfig(t) c.Channel.Namespaces = []configtypes.ChannelNamespace{ { Name: "name", ChannelOptions: configtypes.ChannelOptions{}, }, { Name: "name", ChannelOptions: configtypes.ChannelOptions{}, }, } err := c.Validate() require.Error(t, err) } func TestConfigValidateNoPersonalNamespace(t *testing.T) { c := defaultConfig(t) c.Channel.Namespaces = []configtypes.ChannelNamespace{} c.Client.SubscribeToUserPersonalChannel.Enabled = true c.Client.SubscribeToUserPersonalChannel.PersonalChannelNamespace = "name" err := c.Validate() require.Error(t, err) } func TestConfigValidatePersonalSingleConnectionMissingPresence(t *testing.T) { c := defaultConfig(t) c.Channel.Namespaces = []configtypes.ChannelNamespace{} c.Client.SubscribeToUserPersonalChannel.Enabled = true c.Client.SubscribeToUserPersonalChannel.SingleConnection = true err := c.Validate() require.Error(t, err) } func TestConfigValidatePersonalSingleConnectionOK(t *testing.T) { c := defaultConfig(t) c.Channel.Namespaces = []configtypes.ChannelNamespace{} c.Client.SubscribeToUserPersonalChannel.Enabled = true c.Client.SubscribeToUserPersonalChannel.SingleConnection = true c.Channel.WithoutNamespace.Presence = true err := c.Validate() require.NoError(t, err) } func TestConfigValidateHistoryTTL(t *testing.T) { t.Run("in_namespace", func(t *testing.T) { c := defaultConfig(t) c.Channel.Namespaces = []configtypes.ChannelNamespace{ { Name: "name1", ChannelOptions: configtypes.ChannelOptions{ HistorySize: 10, HistoryTTL: configtypes.Duration(20 * time.Second), HistoryMetaTTL: configtypes.Duration(10 * time.Second), }, }, } err := c.Validate() require.ErrorContains(t, err, "history meta ttl") }) t.Run("on_top_level", func(t *testing.T) { c := defaultConfig(t) c.Channel.WithoutNamespace.HistorySize = 10 c.Channel.WithoutNamespace.HistoryTTL = configtypes.Duration(31 * 24 * time.Hour) err := c.Validate() require.ErrorContains(t, err, "history meta ttl") }) t.Run("top_level_non_default_global", func(t *testing.T) { c := defaultConfig(t) c.Channel.HistoryMetaTTL = configtypes.Duration(10 * time.Hour) c.Channel.WithoutNamespace.HistorySize = 10 c.Channel.WithoutNamespace.HistoryTTL = configtypes.Duration(30 * 24 * time.Hour) err := c.Validate() require.ErrorContains(t, err, "history meta ttl") }) } func TestConfigValidatePersonalSingleConnectionNamespacedFail(t *testing.T) { c := defaultConfig(t) c.Channel.Namespaces = []configtypes.ChannelNamespace{} c.Client.SubscribeToUserPersonalChannel.Enabled = true c.Client.SubscribeToUserPersonalChannel.SingleConnection = true c.Client.SubscribeToUserPersonalChannel.PersonalChannelNamespace = "public" err := c.Validate() require.Error(t, err) } func TestConfigValidatePersonalSingleConnectionNamespacedOK(t *testing.T) { c := defaultConfig(t) c.Channel.Namespaces = []configtypes.ChannelNamespace{} c.Client.SubscribeToUserPersonalChannel.Enabled = true c.Client.SubscribeToUserPersonalChannel.SingleConnection = true c.Client.SubscribeToUserPersonalChannel.PersonalChannelNamespace = "public" c.Channel.Namespaces = []configtypes.ChannelNamespace{{ Name: "public", ChannelOptions: configtypes.ChannelOptions{ Presence: true, }, }} err := c.Validate() require.NoError(t, err) } func TestConfigValidateMalformedRecoveryTopLevel(t *testing.T) { c := defaultConfig(t) c.Channel.WithoutNamespace.ForceRecovery = true err := c.Validate() require.Error(t, err) } func TestConfigValidateMalformedRecoveryInNamespace(t *testing.T) { c := defaultConfig(t) c.Channel.Namespaces = []configtypes.ChannelNamespace{ { Name: "name", ChannelOptions: configtypes.ChannelOptions{ ForceRecovery: true, }, }, } err := c.Validate() require.Error(t, err) } func TestUserAllowed(t *testing.T) { rules, err := NewContainer(defaultConfig(t)) require.NoError(t, err) require.True(t, rules.UserAllowed("channel#1", "1")) require.True(t, rules.UserAllowed("channel", "1")) require.False(t, rules.UserAllowed("channel#1", "2")) require.True(t, rules.UserAllowed("channel#1,2", "1")) require.True(t, rules.UserAllowed("channel#1,2", "2")) require.False(t, rules.UserAllowed("channel#1,2", "3")) } func TestValidChannelName(t *testing.T) { c := defaultConfig(t) c.Channel.Namespaces = []configtypes.ChannelNamespace{ { Name: "digits", ChannelOptions: configtypes.ChannelOptions{ ChannelRegex: `^\d+$`, }, }, { Name: "plain", ChannelOptions: configtypes.ChannelOptions{}, }, { Name: "any", ChannelOptions: configtypes.ChannelOptions{ ChannelRegex: `^.+$`, }, }, } container, err := NewContainer(c) require.NoError(t, err) tests := []struct { name string channel string valid bool }{ {"no regex, ascii channel", "plain:index", true}, {"no regex, non-ascii channel", "plain:индекс", false}, {"regex matches rest, not the whole channel", "digits:42", true}, {"regex does not match rest", "digits:abc", false}, {"regex replaces the ascii check", "any:индекс", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, rest, chOpts, found, err := container.ChannelOptions(tt.channel) require.NoError(t, err) require.True(t, found) valid, err := container.ValidChannelName(tt.channel, rest, chOpts) require.NoError(t, err) require.Equal(t, tt.valid, valid) }) } } func TestIsUserLimited(t *testing.T) { rules, err := NewContainer(defaultConfig(t)) require.NoError(t, err) require.True(t, rules.IsUserLimited("#12")) require.True(t, rules.IsUserLimited("test#12")) config := rules.Config() config.Channel.UserBoundary = "" err = rules.Reload(config) require.NoError(t, err) require.False(t, rules.IsUserLimited("#12")) } func BenchmarkContainer_ChannelOptions(b *testing.B) { cfg := defaultConfig(b) const numNamespaces = 128 var channels []string var namespaces []configtypes.ChannelNamespace for i := 0; i < numNamespaces; i++ { namespaces = append(namespaces, configtypes.ChannelNamespace{ Name: "test" + strconv.Itoa(i), }) channels = append(channels, "test"+strconv.Itoa(i)+":123") } cfg.Channel.Namespaces = namespaces c, _ := NewContainer(cfg) c.ChannelOptionsCacheTTL = 200 * time.Millisecond b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { i++ ch := channels[i%numNamespaces] nsName, _, _, ok, _ := c.ChannelOptions(ch) if !ok { b.Fatal("ns not found") } if !strings.HasPrefix(ch, nsName) { b.Fatal("wrong ns name: " + nsName) } } }) } func TestPublicationDataFormatInheritance(t *testing.T) { t.Run("namespace inherits global format", func(t *testing.T) { c := defaultConfig(t) c.Channel.PublicationDataFormat = "json" c.Channel.Namespaces = []configtypes.ChannelNamespace{ { Name: "test", ChannelOptions: configtypes.ChannelOptions{ // No PublicationDataFormat set }, }, } container, err := NewContainer(c) require.NoError(t, err) // Channel without namespace should use global format _, _, chOpts, ok, err := container.ChannelOptions("mychannel") require.NoError(t, err) require.True(t, ok) require.Equal(t, "json", chOpts.PublicationDataFormat) // Namespace without explicit format should inherit global _, _, chOpts, ok, err = container.ChannelOptions("test:mychannel") require.NoError(t, err) require.True(t, ok) require.Equal(t, "json", chOpts.PublicationDataFormat) }) t.Run("namespace overrides global format", func(t *testing.T) { c := defaultConfig(t) c.Channel.PublicationDataFormat = "json" c.Channel.Namespaces = []configtypes.ChannelNamespace{ { Name: "test", ChannelOptions: configtypes.ChannelOptions{ PublicationDataFormat: "binary", }, }, } container, err := NewContainer(c) require.NoError(t, err) // Namespace with explicit format should use its own _, _, chOpts, ok, err := container.ChannelOptions("test:mychannel") require.NoError(t, err) require.True(t, ok) require.Equal(t, "binary", chOpts.PublicationDataFormat) }) t.Run("no global format set", func(t *testing.T) { c := defaultConfig(t) // No global format set c.Channel.Namespaces = []configtypes.ChannelNamespace{ { Name: "test", ChannelOptions: configtypes.ChannelOptions{ // No PublicationDataFormat set }, }, } container, err := NewContainer(c) require.NoError(t, err) // Should use empty string (default behavior) _, _, chOpts, ok, err := container.ChannelOptions("test:mychannel") require.NoError(t, err) require.True(t, ok) require.Equal(t, "", chOpts.PublicationDataFormat) }) } var testConfig Config func BenchmarkContainer_Config(b *testing.B) { cfg := defaultConfig(b) var namespaces []configtypes.ChannelNamespace for i := 0; i < 100; i++ { namespaces = append(namespaces, configtypes.ChannelNamespace{ Name: "test" + strconv.Itoa(i), }) } cfg.Channel.Namespaces = namespaces c, _ := NewContainer(cfg) c.ChannelOptionsCacheTTL = 200 * time.Millisecond b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { testConfig = c.Config() if len(testConfig.Channel.Namespaces) != 100 { b.Fatal("wrong config") } } }) }