From f5c093eea9ea1a501211373555606046c548e608 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Thu, 19 Mar 2026 14:11:40 -0700 Subject: [PATCH 1/3] Filter allowed stream & broadcast channel predicates --- ...DefaultStreamNamespacePredicateProvider.cs | 45 ++++- .../ImplicitChannelSubscriptionAttribute.cs | 11 ++ .../StreamSubscriptionAttributes.cs | 11 ++ ...DefaultStreamNamespacePredicateProvider.cs | 41 ++++- .../ConstructorPredicateProviderTests.cs | 168 ++++++++++++++++++ 5 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 test/Orleans.Core.Tests/General/ConstructorPredicateProviderTests.cs diff --git a/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/DefaultStreamNamespacePredicateProvider.cs b/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/DefaultStreamNamespacePredicateProvider.cs index 9afd51e5088..bb1223310b4 100644 --- a/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/DefaultStreamNamespacePredicateProvider.cs +++ b/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/DefaultStreamNamespacePredicateProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Orleans.Serialization.TypeSystem; namespace Orleans.BroadcastChannel @@ -7,7 +8,7 @@ namespace Orleans.BroadcastChannel /// Default implementation of for internally supported stream predicates. /// public class DefaultChannelNamespacePredicateProvider : IChannelNamespacePredicateProvider - { + { /// public bool TryGetPredicate(string predicatePattern, out IChannelNamespacePredicate predicate) { @@ -30,17 +31,38 @@ public bool TryGetPredicate(string predicatePattern, out IChannelNamespacePredic } /// - /// Stream namespace predicate provider which supports objects which can be constructed and optionally accept a string as a constructor argument. + /// Channel namespace predicate provider which supports objects which can be constructed and optionally accept a string as a constructor argument. /// public class ConstructorChannelNamespacePredicateProvider : IChannelNamespacePredicateProvider { +#if NET9_0_OR_GREATER + private readonly Lock _lock = new(); +#else + private readonly object _lock = new(); +#endif + private readonly Dictionary _allowedPredicateTypes = new(StringComparer.Ordinal); + /// /// The prefix used to identify this predicate provider. /// public const string Prefix = "ctor"; /// - /// Formats a stream namespace predicate which indicates a concrete type to be constructed, along with an optional argument. + /// Registers a predicate type as allowed for construction. + /// + /// The predicate type to register. + public void RegisterPredicateType(Type predicateType) + { + ArgumentNullException.ThrowIfNull(predicateType); + var typeName = RuntimeTypeNameFormatter.Format(predicateType); + lock (_lock) + { + _allowedPredicateTypes[typeName] = true; + } + } + + /// + /// Formats a channel namespace predicate which indicates a concrete type to be constructed, along with an optional argument. /// public static string FormatPattern(Type predicateType, string constructorArgument) { @@ -76,7 +98,24 @@ public bool TryGetPredicate(string predicatePattern, out IChannelNamespacePredic arg = predicatePattern[(index + 1)..]; } + bool allowed; + lock (_lock) + { + allowed = _allowedPredicateTypes.ContainsKey(typeName); + } + + if (!allowed) + { + throw new InvalidOperationException($"Type \"{typeName}\" is not a registered channel namespace predicate. Ensure the grain interface assembly is loaded and the predicate type is used in an [{nameof(ImplicitChannelSubscriptionAttribute)}]."); + } + var type = Type.GetType(typeName, throwOnError: true); + + if (!typeof(IChannelNamespacePredicate).IsAssignableFrom(type)) + { + throw new InvalidOperationException($"Type \"{type}\" is not a valid channel namespace predicate because it does not implement {nameof(IChannelNamespacePredicate)}."); + } + if (string.IsNullOrEmpty(arg)) { predicate = (IChannelNamespacePredicate)Activator.CreateInstance(type); diff --git a/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/ImplicitChannelSubscriptionAttribute.cs b/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/ImplicitChannelSubscriptionAttribute.cs index 0932743273a..3783dadb786 100644 --- a/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/ImplicitChannelSubscriptionAttribute.cs +++ b/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/ImplicitChannelSubscriptionAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; using Orleans.BroadcastChannel; using Orleans.Metadata; using Orleans.Runtime; @@ -70,6 +71,16 @@ public ImplicitChannelSubscriptionAttribute(IChannelNamespacePredicate predicate /// public IEnumerable> GetBindings(IServiceProvider services, Type grainClass, GrainType grainType) { + // Register the predicate type so the constructor provider will accept it. + foreach (var provider in services.GetServices()) + { + if (provider is ConstructorChannelNamespacePredicateProvider ctorProvider) + { + ctorProvider.RegisterPredicateType(Predicate.GetType()); + break; + } + } + var binding = new Dictionary { [WellKnownGrainTypeProperties.BindingTypeKey] = WellKnownGrainTypeProperties.BroadcastChannelBindingTypeValue, diff --git a/src/Orleans.Streaming/Predicates/StreamSubscriptionAttributes.cs b/src/Orleans.Streaming/Predicates/StreamSubscriptionAttributes.cs index bd1b9845c90..acd010ab771 100644 --- a/src/Orleans.Streaming/Predicates/StreamSubscriptionAttributes.cs +++ b/src/Orleans.Streaming/Predicates/StreamSubscriptionAttributes.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; using Orleans.Metadata; using Orleans.Runtime; using Orleans.Streams; @@ -73,6 +74,16 @@ public ImplicitStreamSubscriptionAttribute(IStreamNamespacePredicate predicate, /// public IEnumerable> GetBindings(IServiceProvider services, Type grainClass, GrainType grainType) { + // Register the predicate type so the constructor provider will accept it. + foreach (var provider in services.GetServices()) + { + if (provider is ConstructorStreamNamespacePredicateProvider ctorProvider) + { + ctorProvider.RegisterPredicateType(Predicate.GetType()); + break; + } + } + var binding = new Dictionary { [WellKnownGrainTypeProperties.BindingTypeKey] = WellKnownGrainTypeProperties.StreamBindingTypeValue, diff --git a/src/Orleans.Streaming/PubSub/DefaultStreamNamespacePredicateProvider.cs b/src/Orleans.Streaming/PubSub/DefaultStreamNamespacePredicateProvider.cs index 6ee3f479f04..e7b8ca4b97a 100644 --- a/src/Orleans.Streaming/PubSub/DefaultStreamNamespacePredicateProvider.cs +++ b/src/Orleans.Streaming/PubSub/DefaultStreamNamespacePredicateProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Orleans.Serialization.TypeSystem; namespace Orleans.Streams @@ -7,7 +8,7 @@ namespace Orleans.Streams /// Default implementation of for internally supported stream predicates. /// public class DefaultStreamNamespacePredicateProvider : IStreamNamespacePredicateProvider - { + { /// public bool TryGetPredicate(string predicatePattern, out IStreamNamespacePredicate predicate) { @@ -34,11 +35,32 @@ public bool TryGetPredicate(string predicatePattern, out IStreamNamespacePredica /// public class ConstructorStreamNamespacePredicateProvider : IStreamNamespacePredicateProvider { +#if NET9_0_OR_GREATER + private readonly Lock _lock = new(); +#else + private readonly object _lock = new(); +#endif + private readonly Dictionary _allowedPredicateTypes = new(StringComparer.Ordinal); + /// /// The prefix used to identify this predicate provider. /// public const string Prefix = "ctor"; + /// + /// Registers a predicate type as allowed for construction. + /// + /// The predicate type to register. + public void RegisterPredicateType(Type predicateType) + { + ArgumentNullException.ThrowIfNull(predicateType); + var typeName = RuntimeTypeNameFormatter.Format(predicateType); + lock (_lock) + { + _allowedPredicateTypes[typeName] = true; + } + } + /// /// Formats a stream namespace predicate which indicates a concrete type to be constructed, along with an optional argument. /// @@ -76,7 +98,24 @@ public bool TryGetPredicate(string predicatePattern, out IStreamNamespacePredica arg = predicatePattern[(index + 1)..]; } + bool allowed; + lock (_lock) + { + allowed = _allowedPredicateTypes.ContainsKey(typeName); + } + + if (!allowed) + { + throw new InvalidOperationException($"Type \"{typeName}\" is not a registered stream namespace predicate. Ensure the grain interface assembly is loaded and the predicate type is used in an [{nameof(ImplicitStreamSubscriptionAttribute)}]."); + } + var type = Type.GetType(typeName, throwOnError: true); + + if (!typeof(IStreamNamespacePredicate).IsAssignableFrom(type)) + { + throw new InvalidOperationException($"Type \"{type}\" is not a valid stream namespace predicate because it does not implement {nameof(IStreamNamespacePredicate)}."); + } + if (string.IsNullOrEmpty(arg)) { predicate = (IStreamNamespacePredicate)Activator.CreateInstance(type); diff --git a/test/Orleans.Core.Tests/General/ConstructorPredicateProviderTests.cs b/test/Orleans.Core.Tests/General/ConstructorPredicateProviderTests.cs new file mode 100644 index 00000000000..c17a09f763f --- /dev/null +++ b/test/Orleans.Core.Tests/General/ConstructorPredicateProviderTests.cs @@ -0,0 +1,168 @@ +using System; +using Orleans.BroadcastChannel; +using Orleans.Serialization.TypeSystem; +using Orleans.Streams; +using Xunit; + +namespace UnitTests; + +/// +/// Tests for and +/// predicate type registration and resolution. +/// +[TestCategory("BVT"), TestCategory("Predicates")] +public class ConstructorPredicateProviderTests +{ + [Fact] + public void StreamProvider_RegisteredPredicateType_Succeeds() + { + var provider = new ConstructorStreamNamespacePredicateProvider(); + provider.RegisterPredicateType(typeof(TestStreamPredicate)); + var pattern = ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicate), constructorArgument: null); + + var result = provider.TryGetPredicate(pattern, out var predicate); + + Assert.True(result); + Assert.NotNull(predicate); + Assert.IsType(predicate); + } + + [Fact] + public void StreamProvider_RegisteredPredicateTypeWithArg_Succeeds() + { + var provider = new ConstructorStreamNamespacePredicateProvider(); + provider.RegisterPredicateType(typeof(TestStreamPredicateWithArg)); + var pattern = ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicateWithArg), constructorArgument: "test-ns"); + + var result = provider.TryGetPredicate(pattern, out var predicate); + + Assert.True(result); + Assert.NotNull(predicate); + Assert.IsType(predicate); + Assert.True(predicate.IsMatch("test-ns")); + } + + [Fact] + public void StreamProvider_UnregisteredType_Throws() + { + var provider = new ConstructorStreamNamespacePredicateProvider(); + var maliciousPattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.IO.FileInfo))}:C:\\temp\\evil.txt"; + + Assert.Throws(() => provider.TryGetPredicate(maliciousPattern, out _)); + } + + [Fact] + public void StreamProvider_UnregisteredArbitraryType_Throws() + { + var provider = new ConstructorStreamNamespacePredicateProvider(); + var maliciousPattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.Collections.ArrayList))}"; + + Assert.Throws(() => provider.TryGetPredicate(maliciousPattern, out _)); + } + + [Fact] + public void StreamProvider_NonMatchingPrefix_ReturnsFalse() + { + var provider = new ConstructorStreamNamespacePredicateProvider(); + + var result = provider.TryGetPredicate("namespace:test", out var predicate); + + Assert.False(result); + Assert.Null(predicate); + } + + [Fact] + public void ChannelProvider_RegisteredPredicateType_Succeeds() + { + var provider = new ConstructorChannelNamespacePredicateProvider(); + provider.RegisterPredicateType(typeof(TestChannelPredicate)); + var pattern = ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicate), constructorArgument: null); + + var result = provider.TryGetPredicate(pattern, out var predicate); + + Assert.True(result); + Assert.NotNull(predicate); + Assert.IsType(predicate); + } + + [Fact] + public void ChannelProvider_RegisteredPredicateTypeWithArg_Succeeds() + { + var provider = new ConstructorChannelNamespacePredicateProvider(); + provider.RegisterPredicateType(typeof(TestChannelPredicateWithArg)); + var pattern = ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicateWithArg), constructorArgument: "ch-ns"); + + var result = provider.TryGetPredicate(pattern, out var predicate); + + Assert.True(result); + Assert.NotNull(predicate); + Assert.IsType(predicate); + Assert.True(predicate.IsMatch("ch-ns")); + } + + [Fact] + public void ChannelProvider_UnregisteredType_Throws() + { + var provider = new ConstructorChannelNamespacePredicateProvider(); + var maliciousPattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.IO.FileInfo))}:C:\\temp\\evil.txt"; + + Assert.Throws(() => provider.TryGetPredicate(maliciousPattern, out _)); + } + + [Fact] + public void ChannelProvider_UnregisteredArbitraryType_Throws() + { + var provider = new ConstructorChannelNamespacePredicateProvider(); + var maliciousPattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.Collections.ArrayList))}"; + + Assert.Throws(() => provider.TryGetPredicate(maliciousPattern, out _)); + } + + [Fact] + public void ChannelProvider_NonMatchingPrefix_ReturnsFalse() + { + var provider = new ConstructorChannelNamespacePredicateProvider(); + + var result = provider.TryGetPredicate("namespace:test", out var predicate); + + Assert.False(result); + Assert.Null(predicate); + } + + public class TestStreamPredicate: IStreamNamespacePredicate + { + public string PredicatePattern => ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicate), constructorArgument: null); + + public bool IsMatch(string streamNamespace) => true; + } + + public class TestStreamPredicateWithArg : IStreamNamespacePredicate + { + private readonly string _namespace; + + public TestStreamPredicateWithArg(string ns) => _namespace = ns; + + public string PredicatePattern => ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicateWithArg), _namespace); + + public bool IsMatch(string streamNamespace) => string.Equals(_namespace, streamNamespace, StringComparison.Ordinal); + } + + public class TestChannelPredicate : IChannelNamespacePredicate + { + public string PredicatePattern => ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicate), constructorArgument: null); + + public bool IsMatch(string streamNamespace) => true; + } + + public class TestChannelPredicateWithArg : IChannelNamespacePredicate + { + private readonly string _namespace; + + public TestChannelPredicateWithArg(string ns) => _namespace = ns; + + public string PredicatePattern => ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicateWithArg), _namespace); + + public bool IsMatch(string streamNamespace) => string.Equals(_namespace, streamNamespace, StringComparison.Ordinal); + } + +} From 3dc41da6cd67e393582c85d420c488e799d8518e Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Thu, 19 Mar 2026 14:38:52 -0700 Subject: [PATCH 2/3] Move tests into the right places --- Orleans.slnx | 1 + ...rChannelNamespacePredicateProviderTests.cs | 89 ++++++++++ .../Orleans.BroadcastChannel.Tests.csproj | 17 ++ .../ConstructorPredicateProviderTests.cs | 168 ------------------ ...orStreamNamespacePredicateProviderTests.cs | 89 ++++++++++ 5 files changed, 196 insertions(+), 168 deletions(-) create mode 100644 test/Orleans.BroadcastChannel.Tests/ConstructorChannelNamespacePredicateProviderTests.cs create mode 100644 test/Orleans.BroadcastChannel.Tests/Orleans.BroadcastChannel.Tests.csproj delete mode 100644 test/Orleans.Core.Tests/General/ConstructorPredicateProviderTests.cs create mode 100644 test/Orleans.Streaming.Tests/StreamingTests/ConstructorStreamNamespacePredicateProviderTests.cs diff --git a/Orleans.slnx b/Orleans.slnx index ec2dd21f427..dd0bac06746 100644 --- a/Orleans.slnx +++ b/Orleans.slnx @@ -122,6 +122,7 @@ + diff --git a/test/Orleans.BroadcastChannel.Tests/ConstructorChannelNamespacePredicateProviderTests.cs b/test/Orleans.BroadcastChannel.Tests/ConstructorChannelNamespacePredicateProviderTests.cs new file mode 100644 index 00000000000..df111f0c040 --- /dev/null +++ b/test/Orleans.BroadcastChannel.Tests/ConstructorChannelNamespacePredicateProviderTests.cs @@ -0,0 +1,89 @@ +using System; +using Orleans.BroadcastChannel; +using Orleans.Serialization.TypeSystem; +using Xunit; + +namespace UnitTests; + +/// +/// Tests for predicate type registration and resolution. +/// +[TestCategory("BVT"), TestCategory("Predicates")] +public class ConstructorChannelNamespacePredicateProviderTests +{ + [Fact] + public void RegisteredPredicateType_Succeeds() + { + var provider = new ConstructorChannelNamespacePredicateProvider(); + provider.RegisterPredicateType(typeof(TestChannelPredicate)); + var pattern = ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicate), constructorArgument: null); + + var result = provider.TryGetPredicate(pattern, out var predicate); + + Assert.True(result); + Assert.NotNull(predicate); + Assert.IsType(predicate); + } + + [Fact] + public void RegisteredPredicateTypeWithArg_Succeeds() + { + var provider = new ConstructorChannelNamespacePredicateProvider(); + provider.RegisterPredicateType(typeof(TestChannelPredicateWithArg)); + var pattern = ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicateWithArg), constructorArgument: "ch-ns"); + + var result = provider.TryGetPredicate(pattern, out var predicate); + + Assert.True(result); + Assert.NotNull(predicate); + Assert.IsType(predicate); + Assert.True(predicate.IsMatch("ch-ns")); + } + + [Fact] + public void UnregisteredType_Throws() + { + var provider = new ConstructorChannelNamespacePredicateProvider(); + var pattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.IO.FileInfo))}:C:\\temp\\evil.txt"; + + Assert.Throws(() => provider.TryGetPredicate(pattern, out _)); + } + + [Fact] + public void UnregisteredArbitraryType_Throws() + { + var provider = new ConstructorChannelNamespacePredicateProvider(); + var pattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.Collections.ArrayList))}"; + + Assert.Throws(() => provider.TryGetPredicate(pattern, out _)); + } + + [Fact] + public void NonMatchingPrefix_ReturnsFalse() + { + var provider = new ConstructorChannelNamespacePredicateProvider(); + + var result = provider.TryGetPredicate("namespace:test", out var predicate); + + Assert.False(result); + Assert.Null(predicate); + } + + public class TestChannelPredicate : IChannelNamespacePredicate + { + public string PredicatePattern => ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicate), constructorArgument: null); + + public bool IsMatch(string streamNamespace) => true; + } + + public class TestChannelPredicateWithArg : IChannelNamespacePredicate + { + private readonly string _namespace; + + public TestChannelPredicateWithArg(string ns) => _namespace = ns; + + public string PredicatePattern => ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicateWithArg), _namespace); + + public bool IsMatch(string streamNamespace) => string.Equals(_namespace, streamNamespace, StringComparison.Ordinal); + } +} diff --git a/test/Orleans.BroadcastChannel.Tests/Orleans.BroadcastChannel.Tests.csproj b/test/Orleans.BroadcastChannel.Tests/Orleans.BroadcastChannel.Tests.csproj new file mode 100644 index 00000000000..d815e2fa191 --- /dev/null +++ b/test/Orleans.BroadcastChannel.Tests/Orleans.BroadcastChannel.Tests.csproj @@ -0,0 +1,17 @@ + + + UnitTests + $(TestTargetFrameworks) + + + + + + + + + + + + + diff --git a/test/Orleans.Core.Tests/General/ConstructorPredicateProviderTests.cs b/test/Orleans.Core.Tests/General/ConstructorPredicateProviderTests.cs deleted file mode 100644 index c17a09f763f..00000000000 --- a/test/Orleans.Core.Tests/General/ConstructorPredicateProviderTests.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using Orleans.BroadcastChannel; -using Orleans.Serialization.TypeSystem; -using Orleans.Streams; -using Xunit; - -namespace UnitTests; - -/// -/// Tests for and -/// predicate type registration and resolution. -/// -[TestCategory("BVT"), TestCategory("Predicates")] -public class ConstructorPredicateProviderTests -{ - [Fact] - public void StreamProvider_RegisteredPredicateType_Succeeds() - { - var provider = new ConstructorStreamNamespacePredicateProvider(); - provider.RegisterPredicateType(typeof(TestStreamPredicate)); - var pattern = ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicate), constructorArgument: null); - - var result = provider.TryGetPredicate(pattern, out var predicate); - - Assert.True(result); - Assert.NotNull(predicate); - Assert.IsType(predicate); - } - - [Fact] - public void StreamProvider_RegisteredPredicateTypeWithArg_Succeeds() - { - var provider = new ConstructorStreamNamespacePredicateProvider(); - provider.RegisterPredicateType(typeof(TestStreamPredicateWithArg)); - var pattern = ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicateWithArg), constructorArgument: "test-ns"); - - var result = provider.TryGetPredicate(pattern, out var predicate); - - Assert.True(result); - Assert.NotNull(predicate); - Assert.IsType(predicate); - Assert.True(predicate.IsMatch("test-ns")); - } - - [Fact] - public void StreamProvider_UnregisteredType_Throws() - { - var provider = new ConstructorStreamNamespacePredicateProvider(); - var maliciousPattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.IO.FileInfo))}:C:\\temp\\evil.txt"; - - Assert.Throws(() => provider.TryGetPredicate(maliciousPattern, out _)); - } - - [Fact] - public void StreamProvider_UnregisteredArbitraryType_Throws() - { - var provider = new ConstructorStreamNamespacePredicateProvider(); - var maliciousPattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.Collections.ArrayList))}"; - - Assert.Throws(() => provider.TryGetPredicate(maliciousPattern, out _)); - } - - [Fact] - public void StreamProvider_NonMatchingPrefix_ReturnsFalse() - { - var provider = new ConstructorStreamNamespacePredicateProvider(); - - var result = provider.TryGetPredicate("namespace:test", out var predicate); - - Assert.False(result); - Assert.Null(predicate); - } - - [Fact] - public void ChannelProvider_RegisteredPredicateType_Succeeds() - { - var provider = new ConstructorChannelNamespacePredicateProvider(); - provider.RegisterPredicateType(typeof(TestChannelPredicate)); - var pattern = ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicate), constructorArgument: null); - - var result = provider.TryGetPredicate(pattern, out var predicate); - - Assert.True(result); - Assert.NotNull(predicate); - Assert.IsType(predicate); - } - - [Fact] - public void ChannelProvider_RegisteredPredicateTypeWithArg_Succeeds() - { - var provider = new ConstructorChannelNamespacePredicateProvider(); - provider.RegisterPredicateType(typeof(TestChannelPredicateWithArg)); - var pattern = ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicateWithArg), constructorArgument: "ch-ns"); - - var result = provider.TryGetPredicate(pattern, out var predicate); - - Assert.True(result); - Assert.NotNull(predicate); - Assert.IsType(predicate); - Assert.True(predicate.IsMatch("ch-ns")); - } - - [Fact] - public void ChannelProvider_UnregisteredType_Throws() - { - var provider = new ConstructorChannelNamespacePredicateProvider(); - var maliciousPattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.IO.FileInfo))}:C:\\temp\\evil.txt"; - - Assert.Throws(() => provider.TryGetPredicate(maliciousPattern, out _)); - } - - [Fact] - public void ChannelProvider_UnregisteredArbitraryType_Throws() - { - var provider = new ConstructorChannelNamespacePredicateProvider(); - var maliciousPattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.Collections.ArrayList))}"; - - Assert.Throws(() => provider.TryGetPredicate(maliciousPattern, out _)); - } - - [Fact] - public void ChannelProvider_NonMatchingPrefix_ReturnsFalse() - { - var provider = new ConstructorChannelNamespacePredicateProvider(); - - var result = provider.TryGetPredicate("namespace:test", out var predicate); - - Assert.False(result); - Assert.Null(predicate); - } - - public class TestStreamPredicate: IStreamNamespacePredicate - { - public string PredicatePattern => ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicate), constructorArgument: null); - - public bool IsMatch(string streamNamespace) => true; - } - - public class TestStreamPredicateWithArg : IStreamNamespacePredicate - { - private readonly string _namespace; - - public TestStreamPredicateWithArg(string ns) => _namespace = ns; - - public string PredicatePattern => ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicateWithArg), _namespace); - - public bool IsMatch(string streamNamespace) => string.Equals(_namespace, streamNamespace, StringComparison.Ordinal); - } - - public class TestChannelPredicate : IChannelNamespacePredicate - { - public string PredicatePattern => ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicate), constructorArgument: null); - - public bool IsMatch(string streamNamespace) => true; - } - - public class TestChannelPredicateWithArg : IChannelNamespacePredicate - { - private readonly string _namespace; - - public TestChannelPredicateWithArg(string ns) => _namespace = ns; - - public string PredicatePattern => ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicateWithArg), _namespace); - - public bool IsMatch(string streamNamespace) => string.Equals(_namespace, streamNamespace, StringComparison.Ordinal); - } - -} diff --git a/test/Orleans.Streaming.Tests/StreamingTests/ConstructorStreamNamespacePredicateProviderTests.cs b/test/Orleans.Streaming.Tests/StreamingTests/ConstructorStreamNamespacePredicateProviderTests.cs new file mode 100644 index 00000000000..c117f534b4f --- /dev/null +++ b/test/Orleans.Streaming.Tests/StreamingTests/ConstructorStreamNamespacePredicateProviderTests.cs @@ -0,0 +1,89 @@ +using System; +using Orleans.Serialization.TypeSystem; +using Orleans.Streams; +using Xunit; + +namespace UnitTests; + +/// +/// Tests for predicate type registration and resolution. +/// +[TestCategory("BVT"), TestCategory("Predicates")] +public class ConstructorStreamNamespacePredicateProviderTests +{ + [Fact] + public void RegisteredPredicateType_Succeeds() + { + var provider = new ConstructorStreamNamespacePredicateProvider(); + provider.RegisterPredicateType(typeof(TestStreamPredicate)); + var pattern = ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicate), constructorArgument: null); + + var result = provider.TryGetPredicate(pattern, out var predicate); + + Assert.True(result); + Assert.NotNull(predicate); + Assert.IsType(predicate); + } + + [Fact] + public void RegisteredPredicateTypeWithArg_Succeeds() + { + var provider = new ConstructorStreamNamespacePredicateProvider(); + provider.RegisterPredicateType(typeof(TestStreamPredicateWithArg)); + var pattern = ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicateWithArg), constructorArgument: "test-ns"); + + var result = provider.TryGetPredicate(pattern, out var predicate); + + Assert.True(result); + Assert.NotNull(predicate); + Assert.IsType(predicate); + Assert.True(predicate.IsMatch("test-ns")); + } + + [Fact] + public void UnregisteredType_Throws() + { + var provider = new ConstructorStreamNamespacePredicateProvider(); + var pattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.IO.FileInfo))}:C:\\temp\\evil.txt"; + + Assert.Throws(() => provider.TryGetPredicate(pattern, out _)); + } + + [Fact] + public void UnregisteredArbitraryType_Throws() + { + var provider = new ConstructorStreamNamespacePredicateProvider(); + var pattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.Collections.ArrayList))}"; + + Assert.Throws(() => provider.TryGetPredicate(pattern, out _)); + } + + [Fact] + public void NonMatchingPrefix_ReturnsFalse() + { + var provider = new ConstructorStreamNamespacePredicateProvider(); + + var result = provider.TryGetPredicate("namespace:test", out var predicate); + + Assert.False(result); + Assert.Null(predicate); + } + + public class TestStreamPredicate : IStreamNamespacePredicate + { + public string PredicatePattern => ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicate), constructorArgument: null); + + public bool IsMatch(string streamNamespace) => true; + } + + public class TestStreamPredicateWithArg : IStreamNamespacePredicate + { + private readonly string _namespace; + + public TestStreamPredicateWithArg(string ns) => _namespace = ns; + + public string PredicatePattern => ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicateWithArg), _namespace); + + public bool IsMatch(string streamNamespace) => string.Equals(_namespace, streamNamespace, StringComparison.Ordinal); + } +} From 88b65de150799f135fff1dccc9f3149e6a9a4b97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:40:04 +0000 Subject: [PATCH 3/3] chore(deps-dev): bump flatted in /src/Dashboard/Orleans.Dashboard.App Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2. - [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2) --- updated-dependencies: - dependency-name: flatted dependency-version: 3.4.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- .../Orleans.Dashboard.App/package-lock.json | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/Dashboard/Orleans.Dashboard.App/package-lock.json b/src/Dashboard/Orleans.Dashboard.App/package-lock.json index 5de02c58ce5..1428e6dd114 100644 --- a/src/Dashboard/Orleans.Dashboard.App/package-lock.json +++ b/src/Dashboard/Orleans.Dashboard.App/package-lock.json @@ -61,7 +61,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1367,7 +1366,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "dev": true, - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1407,7 +1405,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1543,7 +1540,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -1607,7 +1603,6 @@ "version": "4.5.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -1772,7 +1767,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2078,10 +2072,11 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -2613,7 +2608,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2676,7 +2670,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "dev": true, - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -2890,7 +2883,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43",