From c06d818b02e329ddea8376ff8e5cea5e516120b9 Mon Sep 17 00:00:00 2001 From: Kahbazi Date: Fri, 18 Sep 2020 04:25:00 +0430 Subject: [PATCH] Implement Equals for ProxyRoute & Cluster (#366) * Implement Equals for ProxyRoute & Cluster * Add copyright license * Address review points * Fix test merge conflicts Co-authored-by: Chris Ross --- .../Contract/CircuitBreakerOptions.cs | 16 +++ .../ClusterDiscovery/Contract/Cluster.cs | 26 +++- .../Contract/ClusterPartitioningOptions.cs | 19 +++ .../Contract/HealthCheckOptions.cs | 21 ++- .../Contract/LoadBalancingOptions.cs | 15 ++ .../Contract/ProxyHttpClientOptions.cs | 34 +++++ .../ClusterDiscovery/Contract/QuotaOptions.cs | 18 ++- .../Contract/SessionAffinityOptions.cs | 19 +++ .../Contract/Destination.cs | 17 +++ .../RouteDiscovery/Contract/ProxyMatch.cs | 25 +++- .../RouteDiscovery/Contract/ProxyRoute.cs | 71 ++------- .../Service/Config/RuntimeRouteBuilder.cs | 3 +- .../Service/Management/ProxyConfigManager.cs | 7 +- .../Service/RuntimeModel/ClusterConfig.cs | 11 +- .../Service/RuntimeModel/RouteConfig.cs | 13 +- .../Utilities/CaseInsensitiveEqualHelper.cs | 127 ++++++++++++++++ .../Contract/CircuitBreakerOptionsTests.cs | 92 ++++++++++++ .../ClusterPartitioningOptionsTests.cs | 98 +++++++++++++ .../Contract/HealthCheckOptionsTests.cs | 110 ++++++++++++++ .../Contract/LoadBalancingOptionsTests.cs | 86 +++++++++++ .../Contract/ProxyHttpClientOptionsTests.cs | 106 +++++++++++++- .../Contract/QuotaOptionsTests.cs | 92 ++++++++++++ .../Contract/SessionAffinityOptionsTests.cs | 135 ++++++++++++++++++ .../Middleware/AffinityMiddlewareTestBase.cs | 6 +- .../DestinationInitializerMiddlewareTests.cs | 12 +- .../Middleware/LoadBalancerMiddlewareTests.cs | 10 +- .../Middleware/ProxyInvokerMiddlewareTests.cs | 11 +- .../Config/RuntimeRouteBuilderTests.cs | 10 +- .../Service/RuntimeModel/ClusterInfoTests.cs | 4 +- .../CaseInsensitiveEqualHelperTests.cs | 106 ++++++++++++++ 30 files changed, 1215 insertions(+), 105 deletions(-) create mode 100644 src/ReverseProxy/Utilities/CaseInsensitiveEqualHelper.cs create mode 100644 test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/SessionAffinityOptionsTests.cs create mode 100644 test/ReverseProxy.Tests/Utilities/CaseInsensitiveEqualHelperTests.cs diff --git a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/CircuitBreakerOptions.cs b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/CircuitBreakerOptions.cs index 58de8c9ab..3278b4b14 100644 --- a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/CircuitBreakerOptions.cs +++ b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/CircuitBreakerOptions.cs @@ -30,5 +30,21 @@ internal CircuitBreakerOptions DeepClone() MaxConcurrentRetries = MaxConcurrentRetries, }; } + + internal static bool Equals(CircuitBreakerOptions options1, CircuitBreakerOptions options2) + { + if (options1 == null && options2 == null) + { + return true; + } + + if (options1 == null || options2 == null) + { + return false; + } + + return options1.MaxConcurrentRequests == options2.MaxConcurrentRequests + && options1.MaxConcurrentRetries == options2.MaxConcurrentRetries; + } } } diff --git a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/Cluster.cs b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/Cluster.cs index 9f03ef577..42d8e743e 100644 --- a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/Cluster.cs +++ b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/Cluster.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using Microsoft.ReverseProxy.Abstractions.ClusterDiscovery.Contract; +using Microsoft.ReverseProxy.Utilities; namespace Microsoft.ReverseProxy.Abstractions { @@ -82,5 +82,29 @@ Cluster IDeepCloneable.DeepClone() Metadata = Metadata?.DeepClone(StringComparer.OrdinalIgnoreCase), }; } + + internal static bool Equals(Cluster cluster1, Cluster cluster2) + { + if (cluster1 == null && cluster2 == null) + { + return true; + } + + if (cluster1 == null || cluster2 == null) + { + return false; + } + + return string.Equals(cluster1.Id, cluster2.Id, StringComparison.OrdinalIgnoreCase) + && CircuitBreakerOptions.Equals(cluster1.CircuitBreaker, cluster2.CircuitBreaker) + && QuotaOptions.Equals(cluster1.Quota, cluster2.Quota) + && ClusterPartitioningOptions.Equals(cluster1.Partitioning, cluster2.Partitioning) + && LoadBalancingOptions.Equals(cluster1.LoadBalancing, cluster2.LoadBalancing) + && SessionAffinityOptions.Equals(cluster1.SessionAffinity, cluster2.SessionAffinity) + && HealthCheckOptions.Equals(cluster1.HealthCheck, cluster2.HealthCheck) + && ProxyHttpClientOptions.Equals(cluster1.HttpClient, cluster2.HttpClient) + && CaseInsensitiveEqualHelper.Equals(cluster1.Destinations, cluster2.Destinations, Destination.Equals) + && CaseInsensitiveEqualHelper.Equals(cluster1.Metadata, cluster2.Metadata); + } } } diff --git a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ClusterPartitioningOptions.cs b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ClusterPartitioningOptions.cs index ebab1f9fe..691cbb773 100644 --- a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ClusterPartitioningOptions.cs +++ b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ClusterPartitioningOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; + namespace Microsoft.ReverseProxy.Abstractions { /// @@ -33,5 +35,22 @@ internal ClusterPartitioningOptions DeepClone() PartitioningAlgorithm = PartitioningAlgorithm, }; } + + internal static bool Equals(ClusterPartitioningOptions options1, ClusterPartitioningOptions options2) + { + if (options1 == null && options2 == null) + { + return true; + } + + if (options1 == null || options2 == null) + { + return false; + } + + return options1.PartitionCount == options2.PartitionCount + && string.Equals(options1.PartitionKeyExtractor, options2.PartitionKeyExtractor, StringComparison.OrdinalIgnoreCase) + && string.Equals(options1.PartitioningAlgorithm, options2.PartitioningAlgorithm, StringComparison.OrdinalIgnoreCase); + } } } diff --git a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/HealthCheckOptions.cs b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/HealthCheckOptions.cs index 85206d10b..8c6a9fba8 100644 --- a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/HealthCheckOptions.cs +++ b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/HealthCheckOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -47,5 +47,24 @@ internal HealthCheckOptions DeepClone() Path = Path, }; } + + internal static bool Equals(HealthCheckOptions options1, HealthCheckOptions options2) + { + if (options1 == null && options2 == null) + { + return true; + } + + if (options1 == null || options2 == null) + { + return false; + } + + return options1.Enabled == options2.Enabled + && options1.Interval == options2.Interval + && options1.Timeout == options2.Timeout + && options1.Port == options2.Port + && string.Equals(options1.Path, options2.Path, StringComparison.OrdinalIgnoreCase); + } } } diff --git a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/LoadBalancingOptions.cs b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/LoadBalancingOptions.cs index fe7fd921e..9c34e86ae 100644 --- a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/LoadBalancingOptions.cs +++ b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/LoadBalancingOptions.cs @@ -17,5 +17,20 @@ internal LoadBalancingOptions DeepClone() Mode = Mode, }; } + + internal static bool Equals(LoadBalancingOptions options1, LoadBalancingOptions options2) + { + if (options1 == null && options2 == null) + { + return true; + } + + if (options1 == null || options2 == null) + { + return false; + } + + return options1.Mode == options2.Mode; + } } } diff --git a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptions.cs b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptions.cs index cd087a6aa..3432be7d2 100644 --- a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptions.cs +++ b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; @@ -30,5 +31,38 @@ internal ProxyHttpClientOptions DeepClone() MaxConnectionsPerServer = MaxConnectionsPerServer }; } + + internal static bool Equals(ProxyHttpClientOptions options1, ProxyHttpClientOptions options2) + { + if (options1 == null && options2 == null) + { + return true; + } + + if (options1 == null || options2 == null) + { + return false; + } + + return options1.SslProtocols == options2.SslProtocols + && Equals(options1.ClientCertificate, options2.ClientCertificate) + && options1.DangerousAcceptAnyServerCertificate == options2.DangerousAcceptAnyServerCertificate + && options1.MaxConnectionsPerServer == options2.MaxConnectionsPerServer; + } + + private static bool Equals(X509Certificate2 certificate1, X509Certificate2 certificate2) + { + if (certificate1 == null && certificate2 == null) + { + return true; + } + + if (certificate1 == null || certificate2 == null) + { + return false; + } + + return string.Equals(certificate1.Thumbprint, certificate2.Thumbprint, StringComparison.OrdinalIgnoreCase); + } } } diff --git a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/QuotaOptions.cs b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/QuotaOptions.cs index 105f88b56..1be4b4550 100644 --- a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/QuotaOptions.cs +++ b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/QuotaOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. namespace Microsoft.ReverseProxy.Abstractions @@ -27,5 +27,21 @@ internal QuotaOptions DeepClone() Burst = Burst, }; } + + internal static bool Equals(QuotaOptions options1, QuotaOptions options2) + { + if (options1 == null && options2 == null) + { + return true; + } + + if (options1 == null || options2 == null) + { + return false; + } + + return options1.Average == options2.Average + && options1.Burst == options2.Burst; + } } } diff --git a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/SessionAffinityOptions.cs b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/SessionAffinityOptions.cs index 38565b3ae..46628cd5c 100644 --- a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/SessionAffinityOptions.cs +++ b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/SessionAffinityOptions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.ReverseProxy.Utilities; namespace Microsoft.ReverseProxy.Abstractions { @@ -41,5 +42,23 @@ internal SessionAffinityOptions DeepClone() Settings = Settings?.DeepClone(StringComparer.OrdinalIgnoreCase) }; } + + internal static bool Equals(SessionAffinityOptions options1, SessionAffinityOptions options2) + { + if (options1 == null && options2 == null) + { + return true; + } + + if (options1 == null || options2 == null) + { + return false; + } + + return options1.Enabled == options2.Enabled + && string.Equals(options1.Mode, options2.Mode, StringComparison.OrdinalIgnoreCase) + && string.Equals(options1.FailurePolicy, options2.FailurePolicy, StringComparison.OrdinalIgnoreCase) + && CaseInsensitiveEqualHelper.Equals(options1.Settings, options2.Settings); + } } } diff --git a/src/ReverseProxy/Abstractions/DestinationDiscovery/Contract/Destination.cs b/src/ReverseProxy/Abstractions/DestinationDiscovery/Contract/Destination.cs index cbad444c3..a55c4d124 100644 --- a/src/ReverseProxy/Abstractions/DestinationDiscovery/Contract/Destination.cs +++ b/src/ReverseProxy/Abstractions/DestinationDiscovery/Contract/Destination.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.ReverseProxy.Utilities; namespace Microsoft.ReverseProxy.Abstractions { @@ -30,5 +31,21 @@ Destination IDeepCloneable.DeepClone() Metadata = Metadata?.DeepClone(StringComparer.OrdinalIgnoreCase), }; } + + internal static bool Equals(Destination destination1, Destination destination2) + { + if (destination1 == null && destination2 == null) + { + return true; + } + + if (destination1 == null || destination2 == null) + { + return false; + } + + return string.Equals(destination1.Address, destination2.Address, StringComparison.OrdinalIgnoreCase) + && CaseInsensitiveEqualHelper.Equals(destination1.Metadata, destination2.Metadata); + } } } diff --git a/src/ReverseProxy/Abstractions/RouteDiscovery/Contract/ProxyMatch.cs b/src/ReverseProxy/Abstractions/RouteDiscovery/Contract/ProxyMatch.cs index 04d5b0edc..2fbd6e44d 100644 --- a/src/ReverseProxy/Abstractions/RouteDiscovery/Contract/ProxyMatch.cs +++ b/src/ReverseProxy/Abstractions/RouteDiscovery/Contract/ProxyMatch.cs @@ -1,14 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Linq; +using Microsoft.ReverseProxy.Utilities; namespace Microsoft.ReverseProxy.Abstractions { - /// - /// Describes the matching criteria for a route. - /// + /// + /// Describes the matching criteria for a route. + /// public class ProxyMatch : IDeepCloneable { /// @@ -48,5 +50,22 @@ ProxyMatch IDeepCloneable.DeepClone() // Headers = Headers.DeepClone(); // TODO: }; } + + internal static bool Equals(ProxyMatch proxyMatch1, ProxyMatch proxyMatch2) + { + if (proxyMatch1 == null && proxyMatch2 == null) + { + return true; + } + + if (proxyMatch1 == null || proxyMatch2 == null) + { + return false; + } + + return string.Equals(proxyMatch1.Path, proxyMatch2.Path, StringComparison.OrdinalIgnoreCase) + && CaseInsensitiveEqualHelper.Equals(proxyMatch1.Hosts, proxyMatch2.Hosts) + && CaseInsensitiveEqualHelper.Equals(proxyMatch1.Methods, proxyMatch2.Methods); + } } } diff --git a/src/ReverseProxy/Abstractions/RouteDiscovery/Contract/ProxyRoute.cs b/src/ReverseProxy/Abstractions/RouteDiscovery/Contract/ProxyRoute.cs index 4d533e293..69c371ec3 100644 --- a/src/ReverseProxy/Abstractions/RouteDiscovery/Contract/ProxyRoute.cs +++ b/src/ReverseProxy/Abstractions/RouteDiscovery/Contract/ProxyRoute.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.ReverseProxy.Utilities; namespace Microsoft.ReverseProxy.Abstractions { @@ -72,70 +73,26 @@ ProxyRoute IDeepCloneable.DeepClone() }; } - // Used to diff for config changes - internal int GetConfigHash() + internal static bool Equals(ProxyRoute proxyRoute1, ProxyRoute proxyRoute2) { - var hash = 0; - - if (!string.IsNullOrEmpty(RouteId)) - { - hash ^= RouteId.GetHashCode(); - } - - if (Match.Methods != null && Match.Methods.Count > 0) - { - // Assumes un-ordered - hash ^= Match.Methods.Select(item => item.GetHashCode()) - .Aggregate((total, nextCode) => total ^ nextCode); - } - - if (Match.Hosts != null && Match.Hosts.Count > 0) - { - // Assumes un-ordered - hash ^= Match.Hosts.Select(item => item.GetHashCode()) - .Aggregate((total, nextCode) => total ^ nextCode); - } - - if (!string.IsNullOrEmpty(Match.Path)) - { - hash ^= Match.Path.GetHashCode(); - } - - if (Order.HasValue) - { - hash ^= Order.GetHashCode(); - } - - if (!string.IsNullOrEmpty(ClusterId)) - { - hash ^= ClusterId.GetHashCode(); - } - - if (!string.IsNullOrEmpty(AuthorizationPolicy)) - { - hash ^= AuthorizationPolicy.GetHashCode(); - } - - if (!string.IsNullOrEmpty(CorsPolicy)) - { - hash ^= CorsPolicy.GetHashCode(); - } - - if (Metadata != null) + if (proxyRoute1 == null && proxyRoute2 == null) { - hash ^= Metadata.Select(item => HashCode.Combine(item.Key.GetHashCode(), item.Value.GetHashCode())) - .Aggregate((total, nextCode) => total ^ nextCode); + return true; } - if (Transforms != null) + if (proxyRoute1 == null || proxyRoute2 == null) { - hash ^= Transforms.Select(transform => - transform.Select(item => HashCode.Combine(item.Key.GetHashCode(), item.Value.GetHashCode())) - .Aggregate((total, nextCode) => total ^ nextCode)) // Unordered Dictionary - .Aggregate(seed: 397, (total, nextCode) => total * 31 ^ nextCode); // Ordered List + return false; } - return hash; + return proxyRoute1.Order == proxyRoute2.Order + && string.Equals(proxyRoute1.RouteId, proxyRoute2.RouteId, StringComparison.OrdinalIgnoreCase) + && string.Equals(proxyRoute1.ClusterId, proxyRoute2.ClusterId, StringComparison.OrdinalIgnoreCase) + && string.Equals(proxyRoute1.AuthorizationPolicy, proxyRoute2.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase) + && string.Equals(proxyRoute1.CorsPolicy, proxyRoute2.CorsPolicy, StringComparison.OrdinalIgnoreCase) + && ProxyMatch.Equals(proxyRoute1.Match, proxyRoute2.Match) + && CaseInsensitiveEqualHelper.Equals(proxyRoute1.Metadata, proxyRoute2.Metadata) + && CaseInsensitiveEqualHelper.Equals(proxyRoute1.Transforms, proxyRoute2.Transforms); } } } diff --git a/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs b/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs index 9a7b47bf6..22a051b36 100644 --- a/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs +++ b/src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs @@ -55,8 +55,7 @@ public RouteConfig Build(ProxyRoute source, ClusterInfo cluster, RouteInfo runti var aspNetCoreEndpoints = new List(1); var newRouteConfig = new RouteConfig( runtimeRoute, - source.GetConfigHash(), - source.Order, + source, cluster, aspNetCoreEndpoints.AsReadOnly(), transforms); diff --git a/src/ReverseProxy/Service/Management/ProxyConfigManager.cs b/src/ReverseProxy/Service/Management/ProxyConfigManager.cs index be220bc5d..79631db27 100644 --- a/src/ReverseProxy/Service/Management/ProxyConfigManager.cs +++ b/src/ReverseProxy/Service/Management/ProxyConfigManager.cs @@ -286,6 +286,7 @@ private void UpdateRuntimeClusters(IList newClusters) }); var newClusterConfig = new ClusterConfig( + newCluster, new ClusterConfig.ClusterHealthCheckOptions( enabled: newCluster.HealthCheck?.Enabled ?? false, interval: newCluster.HealthCheck?.Interval ?? TimeSpan.FromSeconds(0), @@ -304,11 +305,7 @@ private void UpdateRuntimeClusters(IList newClusters) (IReadOnlyDictionary)newCluster.Metadata); if (currentClusterConfig == null || - currentClusterConfig.HealthCheckOptions.Enabled != newClusterConfig.HealthCheckOptions.Enabled || - currentClusterConfig.HealthCheckOptions.Interval != newClusterConfig.HealthCheckOptions.Interval || - currentClusterConfig.HealthCheckOptions.Timeout != newClusterConfig.HealthCheckOptions.Timeout || - currentClusterConfig.HealthCheckOptions.Port != newClusterConfig.HealthCheckOptions.Port || - currentClusterConfig.HealthCheckOptions.Path != newClusterConfig.HealthCheckOptions.Path) + currentClusterConfig.HasConfigChanged(newClusterConfig)) { if (currentClusterConfig == null) { diff --git a/src/ReverseProxy/Service/RuntimeModel/ClusterConfig.cs b/src/ReverseProxy/Service/RuntimeModel/ClusterConfig.cs index 86a707acc..8cfde3098 100644 --- a/src/ReverseProxy/Service/RuntimeModel/ClusterConfig.cs +++ b/src/ReverseProxy/Service/RuntimeModel/ClusterConfig.cs @@ -24,7 +24,10 @@ namespace Microsoft.ReverseProxy.RuntimeModel /// public sealed class ClusterConfig { + private readonly Cluster _cluster; + public ClusterConfig( + Cluster cluster, ClusterHealthCheckOptions healthCheckOptions, ClusterLoadBalancingOptions loadBalancingOptions, ClusterSessionAffinityOptions sessionAffinityOptions, @@ -32,6 +35,7 @@ public ClusterConfig( ClusterProxyHttpClientOptions httpClientOptions, IReadOnlyDictionary metadata) { + _cluster = cluster; HealthCheckOptions = healthCheckOptions; LoadBalancingOptions = loadBalancingOptions; SessionAffinityOptions = sessionAffinityOptions; @@ -58,6 +62,11 @@ public ClusterConfig( /// public IReadOnlyDictionary Metadata { get; } + internal bool HasConfigChanged(ClusterConfig newClusterConfig) + { + return !Cluster.Equals(_cluster, newClusterConfig._cluster); + } + /// /// Active health probing options for a cluster. /// @@ -132,7 +141,7 @@ public ClusterSessionAffinityOptions(bool enabled, string mode, string failurePo public string FailurePolicy { get; } - public IReadOnlyDictionary Settings { get; } + public IReadOnlyDictionary Settings { get; } } public readonly struct ClusterProxyHttpClientOptions : IEquatable diff --git a/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs b/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs index 142dd18a3..3c48b0dee 100644 --- a/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs +++ b/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs @@ -20,10 +20,11 @@ namespace Microsoft.ReverseProxy.RuntimeModel /// internal sealed class RouteConfig { + private readonly ProxyRoute _proxyRoute; + public RouteConfig( RouteInfo route, - int configHash, - int? order, + ProxyRoute proxyRoute, ClusterInfo cluster, IReadOnlyList aspNetCoreEndpoints, Transforms transforms) @@ -31,16 +32,14 @@ public RouteConfig( Route = route ?? throw new ArgumentNullException(nameof(route)); Endpoints = aspNetCoreEndpoints ?? throw new ArgumentNullException(nameof(aspNetCoreEndpoints)); - ConfigHash = configHash; - Order = order; + _proxyRoute = proxyRoute; + Order = proxyRoute.Order; Cluster = cluster; Transforms = transforms; } public RouteInfo Route { get; } - internal int ConfigHash { get; } - public int? Order { get; } // May not be populated if the cluster config is missing. @@ -53,7 +52,7 @@ public RouteConfig( public bool HasConfigChanged(ProxyRoute newConfig, ClusterInfo cluster) { return Cluster != cluster - || !ConfigHash.Equals(newConfig.GetConfigHash()); + || !ProxyRoute.Equals(_proxyRoute, newConfig); } } } diff --git a/src/ReverseProxy/Utilities/CaseInsensitiveEqualHelper.cs b/src/ReverseProxy/Utilities/CaseInsensitiveEqualHelper.cs new file mode 100644 index 000000000..5ca9095c7 --- /dev/null +++ b/src/ReverseProxy/Utilities/CaseInsensitiveEqualHelper.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace Microsoft.ReverseProxy.Utilities +{ + internal class CaseInsensitiveEqualHelper + { + internal static bool Equals(IReadOnlyList list1, IReadOnlyList list2) + { + if (ReferenceEquals(list1, list2)) + { + return true; + } + + if ((list1?.Count ?? 0) == 0 && (list2?.Count ?? 0) == 0) + { + return true; + } + + if (list1 != null && list2 == null || list1 == null && list2 != null) + { + return false; + } + + if (list1.Count != list2.Count) + { + return false; + } + + for (var i = 0; i < list1.Count; i++) + { + if (!string.Equals(list1[i], list2[i], StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return true; + } + + internal static bool Equals(IList> dictionaryList1, IList> dictionaryList2) + { + if (ReferenceEquals(dictionaryList1, dictionaryList2)) + { + return true; + } + + if ((dictionaryList1?.Count ?? 0) == 0 && (dictionaryList2?.Count ?? 0) == 0) + { + return true; + } + + if (dictionaryList1 != null && dictionaryList2 == null || dictionaryList1 == null && dictionaryList2 != null) + { + return false; + } + + if (dictionaryList1.Count != dictionaryList2.Count) + { + return false; + } + + for (var i = 0; i < dictionaryList1.Count; i++) + { + if (!Equals(dictionaryList1[i], dictionaryList2[i])) + { + return false; + } + } + + return true; + } + + internal static bool Equals(IDictionary dictionary1, IDictionary dictionary2) + { + return Equals(dictionary1, dictionary2, StringEquals); + } + + private static bool StringEquals(string value1, string value2) + { + return string.Equals(value1, value2, StringComparison.OrdinalIgnoreCase); + } + + internal static bool Equals(IDictionary dictionary1, IDictionary dictionary2, Func comparer) + { + if (ReferenceEquals(dictionary1, dictionary2)) + { + return true; + } + + if ((dictionary1?.Count ?? 0) == 0 && (dictionary2?.Count ?? 0) == 0) + { + return true; + } + + if (dictionary1 != null && dictionary2 == null || dictionary1 == null && dictionary2 != null) + { + return false; + } + + if (dictionary1.Count != dictionary2.Count) + { + return false; + } + + foreach (var (key, value1) in dictionary1) + { + if (dictionary2.TryGetValue(key, out var value2)) + { + if (!comparer(value1, value2)) + { + return false; + } + } + else + { + return false; + } + } + + return true; + } + } +} diff --git a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/CircuitBreakerOptionsTests.cs b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/CircuitBreakerOptionsTests.cs index 9d3b65228..b6e4e9e77 100644 --- a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/CircuitBreakerOptionsTests.cs +++ b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/CircuitBreakerOptionsTests.cs @@ -31,6 +31,98 @@ public void DeepClone_Works() Assert.Equal(sut.MaxConcurrentRequests, clone.MaxConcurrentRequests); Assert.Equal(sut.MaxConcurrentRetries, clone.MaxConcurrentRetries); } + + [Fact] + public void Equals_Same_Value_Returns_True() + { + // Arrange + var options1 = new CircuitBreakerOptions + { + MaxConcurrentRequests = 10, + MaxConcurrentRetries = 5, + }; + + var options2 = new CircuitBreakerOptions + { + MaxConcurrentRequests = 10, + MaxConcurrentRetries = 5, + }; + + // Act + var equals = CircuitBreakerOptions.Equals(options1, options2); + + // Assert + Assert.True(equals); + } + + [Fact] + public void Equals_Different_Value_Returns_False() + { + // Arrange + var options1 = new CircuitBreakerOptions + { + MaxConcurrentRequests = 10, + MaxConcurrentRetries = 5, + }; + + var options2 = new CircuitBreakerOptions + { + MaxConcurrentRequests = 20, + MaxConcurrentRetries = 10, + }; + + // Act + var equals = CircuitBreakerOptions.Equals(options1, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_First_Null_Returns_False() + { + // Arrange + var options2 = new CircuitBreakerOptions + { + MaxConcurrentRequests = 20, + MaxConcurrentRetries = 10, + }; + + // Act + var equals = CircuitBreakerOptions.Equals(null, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Second_Null_Returns_False() + { + // Arrange + var options1 = new CircuitBreakerOptions + { + MaxConcurrentRequests = 10, + MaxConcurrentRetries = 5, + }; + + // Act + var equals = CircuitBreakerOptions.Equals(options1, null); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Both_Null_Returns_True() + { + // Arrange + + // Act + var equals = CircuitBreakerOptions.Equals(null, null); + + // Assert + Assert.True(equals); + } } } diff --git a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ClusterPartitioningOptionsTests.cs b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ClusterPartitioningOptionsTests.cs index a3cbbbfbf..11164352d 100644 --- a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ClusterPartitioningOptionsTests.cs +++ b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ClusterPartitioningOptionsTests.cs @@ -33,5 +33,103 @@ public void DeepClone_Works() Assert.Equal(sut.PartitionKeyExtractor, clone.PartitionKeyExtractor); Assert.Equal(sut.PartitioningAlgorithm, clone.PartitioningAlgorithm); } + + [Fact] + public void Equals_Same_Value_Returns_True() + { + // Arrange + var options1 = new ClusterPartitioningOptions + { + PartitionCount = 10, + PartitionKeyExtractor = "Header('x-ms-org-id')", + PartitioningAlgorithm = "alg1", + }; + + var options2 = new ClusterPartitioningOptions + { + PartitionCount = 10, + PartitionKeyExtractor = "Header('x-ms-org-id')", + PartitioningAlgorithm = "alg1", + }; + + // Act + var equals = ClusterPartitioningOptions.Equals(options1, options2); + + // Assert + Assert.True(equals); + } + + [Fact] + public void Equals_Different_Value_Returns_False() + { + // Arrange + var options1 = new ClusterPartitioningOptions + { + PartitionCount = 10, + PartitionKeyExtractor = "Header('x-ms-org-id')", + PartitioningAlgorithm = "alg1", + }; + + var options2 = new ClusterPartitioningOptions + { + PartitionCount = 20, + PartitionKeyExtractor = "Header('x-ms-org-code')", + PartitioningAlgorithm = "alg2", + }; + + // Act + var equals = ClusterPartitioningOptions.Equals(options1, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_First_Null_Returns_False() + { + // Arrange + var options2 = new ClusterPartitioningOptions + { + PartitionCount = 20, + PartitionKeyExtractor = "Header('x-ms-org-code')", + PartitioningAlgorithm = "alg2", + }; + + // Act + var equals = ClusterPartitioningOptions.Equals(null, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Second_Null_Returns_False() + { + // Arrange + var options1 = new ClusterPartitioningOptions + { + PartitionCount = 10, + PartitionKeyExtractor = "Header('x-ms-org-id')", + PartitioningAlgorithm = "alg1", + }; + + // Act + var equals = ClusterPartitioningOptions.Equals(options1, null); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Both_Null_Returns_True() + { + // Arrange + + // Act + var equals = ClusterPartitioningOptions.Equals(null, null); + + // Assert + Assert.True(equals); + } } } diff --git a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/HealthCheckOptionsTests.cs b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/HealthCheckOptionsTests.cs index 0a55d46db..b8e2c25dd 100644 --- a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/HealthCheckOptionsTests.cs +++ b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/HealthCheckOptionsTests.cs @@ -38,5 +38,115 @@ public void DeepClone_Works() Assert.Equal(sut.Port, clone.Port); Assert.Equal(sut.Path, clone.Path); } + + [Fact] + public void Equals_Same_Value_Returns_True() + { + // Arrange + var options1 = new HealthCheckOptions + { + Enabled = true, + Interval = TimeSpan.FromSeconds(2), + Timeout = TimeSpan.FromSeconds(1), + Port = 123, + Path = "/a", + }; + + var options2 = new HealthCheckOptions + { + Enabled = true, + Interval = TimeSpan.FromSeconds(2), + Timeout = TimeSpan.FromSeconds(1), + Port = 123, + Path = "/a", + }; + + // Act + var equals = HealthCheckOptions.Equals(options1, options2); + + // Assert + Assert.True(equals); + } + + [Fact] + public void Equals_Different_Value_Returns_False() + { + // Arrange + var options1 = new HealthCheckOptions + { + Enabled = true, + Interval = TimeSpan.FromSeconds(2), + Timeout = TimeSpan.FromSeconds(1), + Port = 123, + Path = "/a", + }; + + var options2 = new HealthCheckOptions + { + Enabled = false, + Interval = TimeSpan.FromSeconds(4), + Timeout = TimeSpan.FromSeconds(2), + Port = 246, + Path = "/b", + }; + + // Act + var equals = HealthCheckOptions.Equals(options1, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_First_Null_Returns_False() + { + // Arrange + var options2 = new HealthCheckOptions + { + Enabled = false, + Interval = TimeSpan.FromSeconds(4), + Timeout = TimeSpan.FromSeconds(2), + Port = 246, + Path = "/b", + }; + + // Act + var equals = HealthCheckOptions.Equals(null, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Second_Null_Returns_False() + { + // Arrange + var options1 = new HealthCheckOptions + { + Enabled = true, + Interval = TimeSpan.FromSeconds(2), + Timeout = TimeSpan.FromSeconds(1), + Port = 123, + Path = "/a", + }; + + // Act + var equals = HealthCheckOptions.Equals(options1, null); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Both_Null_Returns_True() + { + // Arrange + + // Act + var equals = HealthCheckOptions.Equals(null, null); + + // Assert + Assert.True(equals); + } } } diff --git a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/LoadBalancingOptionsTests.cs b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/LoadBalancingOptionsTests.cs index c19718c9b..15c321931 100644 --- a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/LoadBalancingOptionsTests.cs +++ b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/LoadBalancingOptionsTests.cs @@ -27,5 +27,91 @@ public void DeepClone_Works() // Assert Assert.NotSame(sut, clone); } + + [Fact] + public void Equals_Same_Value_Returns_True() + { + // Arrange + var options1 = new LoadBalancingOptions + { + Mode = LoadBalancingMode.First + }; + + var options2 = new LoadBalancingOptions + { + Mode = LoadBalancingMode.First + }; + + // Act + var equals = LoadBalancingOptions.Equals(options1, options2); + + // Assert + Assert.True(equals); + } + + [Fact] + public void Equals_Different_Value_Returns_False() + { + // Arrange + var options1 = new LoadBalancingOptions + { + Mode = LoadBalancingMode.First + }; + + var options2 = new LoadBalancingOptions + { + Mode = LoadBalancingMode.PowerOfTwoChoices + }; + + // Act + var equals = LoadBalancingOptions.Equals(options1, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_First_Null_Returns_False() + { + // Arrange + var options2 = new LoadBalancingOptions + { + Mode = LoadBalancingMode.PowerOfTwoChoices + }; + + // Act + var equals = LoadBalancingOptions.Equals(null, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Second_Null_Returns_False() + { + // Arrange + var options1 = new LoadBalancingOptions + { + Mode = LoadBalancingMode.First + }; + + // Act + var equals = LoadBalancingOptions.Equals(options1, null); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Both_Null_Returns_True() + { + // Arrange + + // Act + var equals = LoadBalancingOptions.Equals(null, null); + + // Assert + Assert.True(equals); + } } } diff --git a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptionsTests.cs b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptionsTests.cs index df7a682ca..a89faaee6 100644 --- a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptionsTests.cs +++ b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptionsTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Generic; using System.Security.Authentication; using Microsoft.ReverseProxy.Utilities.Tests; using Xunit; @@ -38,5 +37,110 @@ public void DeepClone_Works() Assert.Same(options.ClientCertificate, clone.ClientCertificate); Assert.Equal(options.MaxConnectionsPerServer, clone.MaxConnectionsPerServer); } + + + [Fact] + public void Equals_Same_Value_Returns_True() + { + // Arrange + var options1 = new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls11, + DangerousAcceptAnyServerCertificate = false, + ClientCertificate = TestResources.GetTestCertificate(), + MaxConnectionsPerServer = 20 + }; + + var options2 = new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls11, + DangerousAcceptAnyServerCertificate = false, + ClientCertificate = TestResources.GetTestCertificate(), + MaxConnectionsPerServer = 20 + }; + + // Act + var equals = ProxyHttpClientOptions.Equals(options1, options2); + + // Assert + Assert.True(equals); + } + + [Fact] + public void Equals_Different_Value_Returns_False() + { + // Arrange + var options1 = new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls11, + DangerousAcceptAnyServerCertificate = false, + ClientCertificate = TestResources.GetTestCertificate(), + MaxConnectionsPerServer = 20 + }; + + var options2 = new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls12, + DangerousAcceptAnyServerCertificate = true, + ClientCertificate = TestResources.GetTestCertificate(), + MaxConnectionsPerServer = 20 + }; + + // Act + var equals = ProxyHttpClientOptions.Equals(options1, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_First_Null_Returns_False() + { + // Arrange + var options2 = new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls12, + DangerousAcceptAnyServerCertificate = true, + ClientCertificate = TestResources.GetTestCertificate(), + MaxConnectionsPerServer = 20 + }; + + // Act + var equals = ProxyHttpClientOptions.Equals(null, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Second_Null_Returns_False() + { + // Arrange + var options1 = new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls11, + DangerousAcceptAnyServerCertificate = false, + ClientCertificate = TestResources.GetTestCertificate(), + MaxConnectionsPerServer = 20 + }; + + // Act + var equals = ProxyHttpClientOptions.Equals(options1, null); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Both_Null_Returns_True() + { + // Arrange + + // Act + var equals = ProxyHttpClientOptions.Equals(null, null); + + // Assert + Assert.True(equals); + } } } diff --git a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/QuotaOptionsTests.cs b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/QuotaOptionsTests.cs index b56e66fcc..34310bc48 100644 --- a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/QuotaOptionsTests.cs +++ b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/QuotaOptionsTests.cs @@ -31,5 +31,97 @@ public void DeepClone_Works() Assert.Equal(sut.Average,clone.Average); Assert.Equal(sut.Burst,clone.Burst); } + + [Fact] + public void Equals_Same_Value_Returns_True() + { + // Arrange + var options1 = new QuotaOptions + { + Average = 10, + Burst = 100, + }; + + var options2 = new QuotaOptions + { + Average = 10, + Burst = 100, + }; + + // Act + var equals = QuotaOptions.Equals(options1, options2); + + // Assert + Assert.True(equals); + } + + [Fact] + public void Equals_Different_Value_Returns_False() + { + // Arrange + var options1 = new QuotaOptions + { + Average = 10, + Burst = 100, + }; + + var options2 = new QuotaOptions + { + Average = 20, + Burst = 200, + }; + + // Act + var equals = QuotaOptions.Equals(options1, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_First_Null_Returns_False() + { + // Arrange + var options2 = new QuotaOptions + { + Average = 20, + Burst = 200, + }; + + // Act + var equals = QuotaOptions.Equals(null, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Second_Null_Returns_False() + { + // Arrange + var options1 = new QuotaOptions + { + Average = 10, + Burst = 100, + }; + + // Act + var equals = QuotaOptions.Equals(options1, null); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Both_Null_Returns_True() + { + // Arrange + + // Act + var equals = QuotaOptions.Equals(null, null); + + // Assert + Assert.True(equals); + } } } diff --git a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/SessionAffinityOptionsTests.cs b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/SessionAffinityOptionsTests.cs new file mode 100644 index 000000000..6f2924145 --- /dev/null +++ b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/SessionAffinityOptionsTests.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Xunit; + +namespace Microsoft.ReverseProxy.Abstractions.ClusterDiscovery.Contract +{ + public class SessionAffinityOptionsTests + { + [Fact] + public void Constructor_Works() + { + new SessionAffinityOptions(); + } + + [Fact] + public void DeepClone_Works() + { + // Arrange + var sut = new SessionAffinityOptions + { + Enabled = true, + FailurePolicy = "policy1", + Mode = "mode1" + }; + + // Act + var clone = sut.DeepClone(); + + // Assert + Assert.NotSame(sut, clone); + Assert.Equal(sut.Enabled, clone.Enabled); + Assert.Equal(sut.FailurePolicy, clone.FailurePolicy); + Assert.Equal(sut.Mode, clone.Mode); + } + + [Fact] + public void Equals_Same_Value_Returns_True() + { + // Arrange + var options1 = new SessionAffinityOptions + { + Enabled = true, + FailurePolicy = "policy1", + Mode = "mode1" + }; + + var options2 = new SessionAffinityOptions + { + Enabled = true, + FailurePolicy = "policy1", + Mode = "mode1" + }; + + // Act + var equals = SessionAffinityOptions.Equals(options1, options2); + + // Assert + Assert.True(equals); + } + + [Fact] + public void Equals_Different_Value_Returns_False() + { + // Arrange + var options1 = new SessionAffinityOptions + { + Enabled = true, + FailurePolicy = "policy1", + Mode = "mode1" + }; + + var options2 = new SessionAffinityOptions + { + Enabled = false, + FailurePolicy = "policy2", + Mode = "mode2" + }; + + // Act + var equals = SessionAffinityOptions.Equals(options1, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_First_Null_Returns_False() + { + // Arrange + var options2 = new SessionAffinityOptions + { + Enabled = false, + FailurePolicy = "policy2", + Mode = "mode2" + }; + + // Act + var equals = SessionAffinityOptions.Equals(null, options2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Second_Null_Returns_False() + { + // Arrange + var options1 = new SessionAffinityOptions + { + Enabled = true, + FailurePolicy = "policy1", + Mode = "mode1" + }; + + // Act + var equals = SessionAffinityOptions.Equals(options1, null); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Both_Null_Returns_True() + { + // Arrange + + // Act + var equals = SessionAffinityOptions.Equals(null, null); + + // Assert + Assert.True(equals); + } + } +} diff --git a/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs b/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs index bdcfe96a4..05ba59400 100644 --- a/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs +++ b/test/ReverseProxy.Tests/Middleware/AffinityMiddlewareTestBase.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Net.Http; using Microsoft.AspNetCore.Http; +using Microsoft.ReverseProxy.Abstractions; using Microsoft.ReverseProxy.RuntimeModel; using Microsoft.ReverseProxy.Service.Management; using Microsoft.ReverseProxy.Service.Proxy.Infrastructure; @@ -19,7 +20,7 @@ public abstract class AffinityMiddlewareTestBase { protected const string AffinitizedDestinationName = "dest-B"; protected readonly IReadOnlyList Destinations = new[] { new DestinationInfo("dest-A"), new DestinationInfo(AffinitizedDestinationName), new DestinationInfo("dest-C") }; - protected readonly ClusterConfig ClusterConfig = new ClusterConfig(default, default, new ClusterConfig.ClusterSessionAffinityOptions(true, "Mode-B", "Policy-1", null), + protected readonly ClusterConfig ClusterConfig = new ClusterConfig(default, default, default, new ClusterConfig.ClusterSessionAffinityOptions(true, "Mode-B", "Policy-1", null), new HttpMessageInvoker(new Mock().Object), default, new Dictionary()); internal ClusterInfo GetCluster() @@ -91,7 +92,8 @@ internal IReverseProxyFeature GetDestinationsFeature(IReadOnlyList(1); - var routeConfig = new RouteConfig(new RouteInfo("route-1"), 47, null, cluster, endpoints.AsReadOnly(), Transforms.Empty); + var proxyRoute = new ProxyRoute(); + var routeConfig = new RouteConfig(new RouteInfo("route-1"), proxyRoute, cluster, endpoints.AsReadOnly(), Transforms.Empty); var endpoint = new Endpoint(default, new EndpointMetadataCollection(routeConfig), string.Empty); endpoints.Add(endpoint); return endpoint; diff --git a/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs b/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs index ce3821b05..980ae15d8 100644 --- a/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs +++ b/test/ReverseProxy.Tests/Middleware/DestinationInitializerMiddlewareTests.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.ReverseProxy.Abstractions; using Microsoft.ReverseProxy.Common.Tests; using Microsoft.ReverseProxy.RuntimeModel; using Microsoft.ReverseProxy.Service.Management; @@ -28,7 +29,7 @@ public void Constructor_Works() { Create(); } - + [Fact] public async Task Invoke_SetsFeatures() { @@ -36,7 +37,7 @@ public async Task Invoke_SetsFeatures() var cluster1 = new ClusterInfo( clusterId: "cluster1", destinationManager: new DestinationManager()); - cluster1.Config.Value = new ClusterConfig(default, default, default, httpClient, default, new Dictionary()); + cluster1.Config.Value = new ClusterConfig(default, default, default, default, httpClient, default, new Dictionary()); var destination1 = cluster1.DestinationManager.GetOrCreateItem( "destination1", destination => @@ -48,8 +49,7 @@ public async Task Invoke_SetsFeatures() var aspNetCoreEndpoints = new List(); var routeConfig = new RouteConfig( new RouteInfo("route1"), - configHash: 0, - order: null, + proxyRoute: new ProxyRoute(), cluster1, aspNetCoreEndpoints.AsReadOnly(), transforms: null); @@ -80,6 +80,7 @@ public async Task Invoke_NoHealthyEndpoints_503() clusterId: "cluster1", destinationManager: new DestinationManager()); cluster1.Config.Value = new ClusterConfig( + new Cluster(), new ClusterConfig.ClusterHealthCheckOptions(enabled: true, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, 0, ""), new ClusterConfig.ClusterLoadBalancingOptions(), new ClusterConfig.ClusterSessionAffinityOptions(), @@ -96,8 +97,7 @@ public async Task Invoke_NoHealthyEndpoints_503() var aspNetCoreEndpoints = new List(); var routeConfig = new RouteConfig( route: new RouteInfo("route1"), - configHash: 0, - order: null, + proxyRoute: new ProxyRoute(), cluster: cluster1, aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(), transforms: null); diff --git a/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs b/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs index 4ec24cb61..c6721690d 100644 --- a/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs +++ b/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs @@ -40,7 +40,7 @@ public async Task Invoke_Works() var cluster1 = new ClusterInfo( clusterId: "cluster1", destinationManager: new DestinationManager()); - cluster1.Config.Value = new ClusterConfig(default, new ClusterConfig.ClusterLoadBalancingOptions(LoadBalancingMode.RoundRobin), default, httpClient, default, new Dictionary()); + cluster1.Config.Value = new ClusterConfig(default, default, new ClusterConfig.ClusterLoadBalancingOptions(LoadBalancingMode.RoundRobin), default, httpClient, default, new Dictionary()); var destination1 = cluster1.DestinationManager.GetOrCreateItem( "destination1", destination => @@ -59,8 +59,7 @@ public async Task Invoke_Works() var aspNetCoreEndpoints = new List(); var routeConfig = new RouteConfig( route: new RouteInfo("route1"), - configHash: 0, - order: null, + proxyRoute: new ProxyRoute(), cluster: cluster1, aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(), transforms: null); @@ -98,7 +97,7 @@ public async Task Invoke_ServiceReturnsNoResults_503() var cluster1 = new ClusterInfo( clusterId: "cluster1", destinationManager: new DestinationManager()); - cluster1.Config.Value = new ClusterConfig(default, new ClusterConfig.ClusterLoadBalancingOptions(LoadBalancingMode.RoundRobin), default, httpClient, default, new Dictionary()); + cluster1.Config.Value = new ClusterConfig(default, default, new ClusterConfig.ClusterLoadBalancingOptions(LoadBalancingMode.RoundRobin), default, httpClient, default, new Dictionary()); var destination1 = cluster1.DestinationManager.GetOrCreateItem( "destination1", destination => @@ -117,8 +116,7 @@ public async Task Invoke_ServiceReturnsNoResults_503() var aspNetCoreEndpoints = new List(); var routeConfig = new RouteConfig( route: new RouteInfo("route1"), - configHash: 0, - order: null, + proxyRoute: new ProxyRoute(), cluster: cluster1, aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(), transforms: null); diff --git a/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs b/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs index 0d89a5dc6..d65c0036e 100644 --- a/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs +++ b/test/ReverseProxy.Tests/Middleware/ProxyInvokerMiddlewareTests.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.ReverseProxy.Abstractions; using Microsoft.ReverseProxy.Abstractions.Telemetry; using Microsoft.ReverseProxy.Common.Tests; using Microsoft.ReverseProxy.RuntimeModel; @@ -49,7 +50,7 @@ public async Task Invoke_Works() var cluster1 = new ClusterInfo( clusterId: "cluster1", destinationManager: new DestinationManager()); - var clusterConfig = new ClusterConfig(default, default, default, httpClient, default, new Dictionary()); + var clusterConfig = new ClusterConfig(default, default, default, default, httpClient, default, new Dictionary()); var destination1 = cluster1.DestinationManager.GetOrCreateItem( "destination1", destination => @@ -64,8 +65,7 @@ public async Task Invoke_Works() var aspNetCoreEndpoints = new List(); var routeConfig = new RouteConfig( route: new RouteInfo("route1"), - configHash: 0, - order: null, + proxyRoute: new ProxyRoute(), cluster: cluster1, aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(), transforms: null); @@ -127,7 +127,7 @@ public async Task NoDestinations_503() var cluster1 = new ClusterInfo( clusterId: "cluster1", destinationManager: new DestinationManager()); - var clusterConfig = new ClusterConfig(default, default, default, httpClient, default, new Dictionary()); + var clusterConfig = new ClusterConfig(default, default, default, default, httpClient, default, new Dictionary()); httpContext.Features.Set( new ReverseProxyFeature() { AvailableDestinations = Array.Empty(), ClusterConfig = clusterConfig }); httpContext.Features.Set(cluster1); @@ -135,8 +135,7 @@ public async Task NoDestinations_503() var aspNetCoreEndpoints = new List(); var routeConfig = new RouteConfig( route: new RouteInfo("route1"), - configHash: 0, - order: null, + proxyRoute: new ProxyRoute(), cluster: cluster1, aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly(), transforms: null); diff --git a/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs b/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs index 0c09a509d..e334efde2 100644 --- a/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs +++ b/test/ReverseProxy.Tests/Service/Config/RuntimeRouteBuilderTests.cs @@ -60,13 +60,13 @@ public void BuildEndpoints_HostAndPath_Works() Assert.Same(cluster, config.Cluster); Assert.Equal(12, config.Order); - Assert.Equal(route.GetConfigHash(), config.ConfigHash); Assert.Single(config.Endpoints); var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; Assert.Equal("route1", routeEndpoint.DisplayName); Assert.Same(config, routeEndpoint.Metadata.GetMetadata()); Assert.Equal("/a", routeEndpoint.RoutePattern.RawText); Assert.Equal(12, routeEndpoint.Order); + Assert.False(config.HasConfigChanged(route, cluster)); var hostMetadata = routeEndpoint.Metadata.GetMetadata(); Assert.NotNull(hostMetadata); @@ -97,13 +97,13 @@ public void BuildEndpoints_JustHost_Works() Assert.Same(cluster, config.Cluster); Assert.Equal(12, config.Order); - Assert.Equal(route.GetConfigHash(), config.ConfigHash); Assert.Single(config.Endpoints); var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; Assert.Equal("route1", routeEndpoint.DisplayName); Assert.Same(config, routeEndpoint.Metadata.GetMetadata()); Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); Assert.Equal(12, routeEndpoint.Order); + Assert.False(config.HasConfigChanged(route, cluster)); var hostMetadata = routeEndpoint.Metadata.GetMetadata(); Assert.NotNull(hostMetadata); @@ -134,13 +134,13 @@ public void BuildEndpoints_JustHostWithWildcard_Works() Assert.Same(cluster, config.Cluster); Assert.Equal(12, config.Order); - Assert.Equal(route.GetConfigHash(), config.ConfigHash); Assert.Single(config.Endpoints); var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; Assert.Equal("route1", routeEndpoint.DisplayName); Assert.Same(config, routeEndpoint.Metadata.GetMetadata()); Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); Assert.Equal(12, routeEndpoint.Order); + Assert.False(config.HasConfigChanged(route, cluster)); var hostMetadata = routeEndpoint.Metadata.GetMetadata(); Assert.NotNull(hostMetadata); @@ -171,13 +171,13 @@ public void BuildEndpoints_JustPath_Works() Assert.Same(cluster, config.Cluster); Assert.Equal(12, config.Order); - Assert.Equal(route.GetConfigHash(), config.ConfigHash); Assert.Single(config.Endpoints); var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; Assert.Equal("route1", routeEndpoint.DisplayName); Assert.Same(config, routeEndpoint.Metadata.GetMetadata()); Assert.Equal("/a", routeEndpoint.RoutePattern.RawText); Assert.Equal(12, routeEndpoint.Order); + Assert.False(config.HasConfigChanged(route, cluster)); var hostMetadata = routeEndpoint.Metadata.GetMetadata(); Assert.Null(hostMetadata); @@ -202,13 +202,13 @@ public void BuildEndpoints_NullMatchers_Works() Assert.Same(cluster, config.Cluster); Assert.Equal(12, config.Order); - Assert.NotEqual(0, config.ConfigHash); Assert.Single(config.Endpoints); var routeEndpoint = config.Endpoints[0] as AspNetCore.Routing.RouteEndpoint; Assert.Equal("route1", routeEndpoint.DisplayName); Assert.Same(config, routeEndpoint.Metadata.GetMetadata()); Assert.Equal("/{**catchall}", routeEndpoint.RoutePattern.RawText); Assert.Equal(12, routeEndpoint.Order); + Assert.False(config.HasConfigChanged(route, cluster)); var hostMetadata = routeEndpoint.Metadata.GetMetadata(); Assert.Null(hostMetadata); diff --git a/test/ReverseProxy.Tests/Service/RuntimeModel/ClusterInfoTests.cs b/test/ReverseProxy.Tests/Service/RuntimeModel/ClusterInfoTests.cs index c6b97d215..19666b4c0 100644 --- a/test/ReverseProxy.Tests/Service/RuntimeModel/ClusterInfoTests.cs +++ b/test/ReverseProxy.Tests/Service/RuntimeModel/ClusterInfoTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Net.Http; +using Microsoft.ReverseProxy.Abstractions; using Microsoft.ReverseProxy.Common.Tests; using Microsoft.ReverseProxy.Service.Management; using Moq; @@ -77,7 +78,7 @@ public void DynamicState_ReactsToClusterConfigChanges() Assert.NotNull(state1); Assert.Empty(state1.AllDestinations); - cluster.Config.Value = new ClusterConfig(healthCheckOptions: default, loadBalancingOptions: default, sessionAffinityOptions: default, + cluster.Config.Value = new ClusterConfig(cluster: default, healthCheckOptions: default, loadBalancingOptions: default, sessionAffinityOptions: default, httpClient: new HttpMessageInvoker(new Mock().Object), httpClientOptions: default, metadata: new Dictionary()); Assert.NotSame(state1, cluster.DynamicState.Value); Assert.Empty(cluster.DynamicState.Value.AllDestinations); @@ -141,6 +142,7 @@ private static void EnableHealthChecks(ClusterInfo cluster) { // Pretend that health checks are enabled so that destination health states are honored cluster.Config.Value = new ClusterConfig( + new Cluster(), healthCheckOptions: new ClusterConfig.ClusterHealthCheckOptions( enabled: true, interval: TimeSpan.FromSeconds(5), diff --git a/test/ReverseProxy.Tests/Utilities/CaseInsensitiveEqualHelperTests.cs b/test/ReverseProxy.Tests/Utilities/CaseInsensitiveEqualHelperTests.cs new file mode 100644 index 000000000..50053a9e8 --- /dev/null +++ b/test/ReverseProxy.Tests/Utilities/CaseInsensitiveEqualHelperTests.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Xunit; + +namespace Microsoft.ReverseProxy.Utilities +{ + public class CaseInsensitiveEqualHelperTests + { + [Fact] + public void Equals_Same_Instance_Returns_True() + { + // Arrange + var list1 = new string[] { "item1", "item2" }; + + // Act + var equals = CaseInsensitiveEqualHelper.Equals(list1, list1); + + // Assert + Assert.True(equals); + } + + [Fact] + public void Equals_Empty_List_Returns_True() + { + // Arrange + var list1 = new string[] { }; + + var list2 = new string[] { }; + + // Act + var equals = CaseInsensitiveEqualHelper.Equals(list1, list2); + + // Assert + Assert.True(equals); + } + + [Fact] + public void Equals_List_Same_Value_Returns_True() + { + // Arrange + var list1 = new string[] { "item1", "item2" }; + + var list2 = new string[] { "item1", "item2" }; + + // Act + var equals = CaseInsensitiveEqualHelper.Equals(list1, list2); + + // Assert + Assert.True(equals); + } + + [Fact] + public void Equals_List_Different_Value_Returns_False() + { + // Arrange + var list1 = new string[] { "item1", "item2" }; + + var list2 = new string[] { "item3", "item4" }; + + // Act + var equals = CaseInsensitiveEqualHelper.Equals(list1, list2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_First_List_Null_Returns_False() + { + // Arrange + var list2 = new string[] { "item1", "item2" }; + + // Act + var equals = CaseInsensitiveEqualHelper.Equals(null, list2); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Second_List_Null_Returns_False() + { + // Arrange + var list1 = new string[] { "item1", "item2" }; + + // Act + var equals = CaseInsensitiveEqualHelper.Equals(list1, null); + + // Assert + Assert.False(equals); + } + + [Fact] + public void Equals_Null_List_Returns_True() + { + // Arrange + + // Act + var equals = CaseInsensitiveEqualHelper.Equals(list1: null, list2: null); + + // Assert + Assert.True(equals); + } + } +}