using StellaOps.Router.Common.Abstractions; using StellaOps.Router.Common.Enums; using StellaOps.Router.Common.Models; using StellaOps.TestKit; namespace StellaOps.Router.Common.Tests; /// /// Property-based tests ensuring routing determinism: same message + same configuration = same route. /// public sealed class RoutingDeterminismTests { #region Core Determinism Property Tests [Trait("Category", TestCategories.Unit)] [Fact] public void SameContextAndConnections_AlwaysSelectsSameRoute() { // Arrange var context = CreateDeterministicContext(); var connections = CreateConnectionSet( ("conn-1", "instance-1", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Healthy), ("conn-2", "instance-2", "service-a", "1.0.0", "us-west", InstanceHealthStatus.Healthy), ("conn-3", "instance-3", "service-a", "1.0.0", "eu-west", InstanceHealthStatus.Healthy)); var selector = new DeterministicRouteSelector(); // Act - Run selection multiple times var results = Enumerable.Range(0, 100) .Select(_ => selector.SelectConnection(context, connections)) .ToList(); // Assert - All results should be identical results.Should().AllBeEquivalentTo(results[0]); } [Trait("Category", TestCategories.Unit)] [Fact] public void DifferentConnectionOrder_ProducesSameResult() { // Arrange var context = CreateDeterministicContext(); var connections1 = CreateConnectionSet( ("conn-1", "instance-1", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Healthy), ("conn-2", "instance-2", "service-a", "1.0.0", "us-west", InstanceHealthStatus.Healthy), ("conn-3", "instance-3", "service-a", "1.0.0", "eu-west", InstanceHealthStatus.Healthy)); var connections2 = CreateConnectionSet( ("conn-3", "instance-3", "service-a", "1.0.0", "eu-west", InstanceHealthStatus.Healthy), ("conn-1", "instance-1", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Healthy), ("conn-2", "instance-2", "service-a", "1.0.0", "us-west", InstanceHealthStatus.Healthy)); var selector = new DeterministicRouteSelector(); // Act var result1 = selector.SelectConnection(context, connections1); var result2 = selector.SelectConnection(context, connections2); // Assert - Should select same connection regardless of input order result1.ConnectionId.Should().Be(result2.ConnectionId); } [Trait("Category", TestCategories.Unit)] [Fact] public void SamePathAndMethod_WithSameHeaders_ProducesSameRouteKey() { // Arrange var context1 = new RoutingContext { Method = "GET", Path = "/api/users/123", Headers = new Dictionary { ["X-Correlation-Id"] = "corr-456", ["Accept"] = "application/json" }, GatewayRegion = "us-east" }; var context2 = new RoutingContext { Method = "GET", Path = "/api/users/123", Headers = new Dictionary { ["Accept"] = "application/json", ["X-Correlation-Id"] = "corr-456" }, GatewayRegion = "us-east" }; // Act var key1 = ComputeRouteKey(context1); var key2 = ComputeRouteKey(context2); // Assert key1.Should().Be(key2); } #endregion #region Region Affinity Determinism Tests [Trait("Category", TestCategories.Unit)] [Fact] public void SameRegion_AlwaysPreferredWhenAvailable() { // Arrange var context = CreateContextWithRegion("us-east"); var connections = CreateConnectionSet( ("conn-remote", "instance-1", "service-a", "1.0.0", "eu-west", InstanceHealthStatus.Healthy), ("conn-local", "instance-2", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Healthy)); var selector = new DeterministicRouteSelector(); // Act var results = Enumerable.Range(0, 100) .Select(_ => selector.SelectConnection(context, connections)) .ToList(); // Assert - Always select local region results.Should().AllSatisfy(r => r.Instance.Region.Should().Be("us-east")); } [Trait("Category", TestCategories.Unit)] [Fact] public void NoLocalRegion_FallbackIsDeterministic() { // Arrange var context = CreateContextWithRegion("ap-southeast"); var connections = CreateConnectionSet( ("conn-1", "instance-1", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Healthy), ("conn-2", "instance-2", "service-a", "1.0.0", "eu-west", InstanceHealthStatus.Healthy), ("conn-3", "instance-3", "service-a", "1.0.0", "us-west", InstanceHealthStatus.Healthy)); var selector = new DeterministicRouteSelector(); // Act var results = Enumerable.Range(0, 100) .Select(_ => selector.SelectConnection(context, connections)) .ToList(); // Assert - All results should be identical (deterministic fallback) results.Should().AllBeEquivalentTo(results[0]); } #endregion #region Version Selection Determinism Tests [Trait("Category", TestCategories.Unit)] [Fact] public void SameRequestedVersion_AlwaysSelectsMatchingConnection() { // Arrange var context = CreateContextWithVersion("2.0.0"); var connections = CreateConnectionSet( ("conn-v1", "instance-1", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Healthy), ("conn-v2", "instance-2", "service-a", "2.0.0", "us-east", InstanceHealthStatus.Healthy), ("conn-v3", "instance-3", "service-a", "3.0.0", "us-east", InstanceHealthStatus.Healthy)); var selector = new DeterministicRouteSelector(); // Act var results = Enumerable.Range(0, 100) .Select(_ => selector.SelectConnection(context, connections)) .ToList(); // Assert - Always select version 2.0.0 results.Should().AllSatisfy(r => r.Instance.Version.Should().Be("2.0.0")); } [Trait("Category", TestCategories.Unit)] [Fact] public void NoVersionRequested_LatestStableIsSelectedDeterministically() { // Arrange var context = CreateContextWithVersion(null); var connections = CreateConnectionSet( ("conn-v1", "instance-1", "service-a", "1.2.3", "us-east", InstanceHealthStatus.Healthy), ("conn-v2", "instance-2", "service-a", "2.0.0", "us-east", InstanceHealthStatus.Healthy), ("conn-v3", "instance-3", "service-a", "1.9.0", "us-east", InstanceHealthStatus.Healthy)); var selector = new DeterministicRouteSelector(); // Act var results = Enumerable.Range(0, 100) .Select(_ => selector.SelectConnection(context, connections)) .ToList(); // Assert - All results identical (should pick highest version deterministically) results.Should().AllBeEquivalentTo(results[0]); } #endregion #region Health Status Determinism Tests [Trait("Category", TestCategories.Unit)] [Fact] public void HealthyConnectionsPreferred_Deterministically() { // Arrange var context = CreateDeterministicContext(); var connections = CreateConnectionSet( ("conn-unhealthy", "instance-1", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Unhealthy), ("conn-healthy", "instance-2", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Healthy), ("conn-degraded", "instance-3", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Degraded)); var selector = new DeterministicRouteSelector(); // Act var results = Enumerable.Range(0, 100) .Select(_ => selector.SelectConnection(context, connections)) .ToList(); // Assert - Always select healthy connection results.Should().AllSatisfy(r => r.ConnectionId.Should().Be("conn-healthy")); } [Trait("Category", TestCategories.Unit)] [Fact] public void DegradedConnectionSelected_WhenNoHealthyAvailable() { // Arrange var context = CreateDeterministicContext(); var connections = CreateConnectionSet( ("conn-unhealthy", "instance-1", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Unhealthy), ("conn-degraded-1", "instance-2", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Degraded), ("conn-degraded-2", "instance-3", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Degraded)); var selector = new DeterministicRouteSelector(); // Act var results = Enumerable.Range(0, 100) .Select(_ => selector.SelectConnection(context, connections)) .ToList(); // Assert - All results identical (deterministic selection among degraded) results.Should().AllBeEquivalentTo(results[0]); results[0].Status.Should().Be(InstanceHealthStatus.Degraded); } [Trait("Category", TestCategories.Unit)] [Fact] public void DrainingConnectionsExcluded() { // Arrange var context = CreateDeterministicContext(); var connections = CreateConnectionSet( ("conn-draining", "instance-1", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Draining), ("conn-healthy", "instance-2", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Healthy)); var selector = new DeterministicRouteSelector(); // Act var result = selector.SelectConnection(context, connections); // Assert - Never select draining connections result.ConnectionId.Should().Be("conn-healthy"); } #endregion #region Multi-Criteria Determinism Tests [Trait("Category", TestCategories.Unit)] [Fact] public void RegionThenVersionThenHealth_OrderingIsDeterministic() { // Arrange var context = new RoutingContext { Method = "GET", Path = "/api/data", GatewayRegion = "us-east", RequestedVersion = "2.0.0", Headers = new Dictionary() }; var connections = CreateConnectionSet( ("conn-1", "instance-1", "service-a", "2.0.0", "eu-west", InstanceHealthStatus.Healthy), ("conn-2", "instance-2", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Healthy), ("conn-3", "instance-3", "service-a", "2.0.0", "us-east", InstanceHealthStatus.Degraded), ("conn-4", "instance-4", "service-a", "2.0.0", "us-east", InstanceHealthStatus.Healthy)); var selector = new DeterministicRouteSelector(); // Act var results = Enumerable.Range(0, 100) .Select(_ => selector.SelectConnection(context, connections)) .ToList(); // Assert - Should select conn-4: us-east region + version 2.0.0 + healthy results.Should().AllSatisfy(r => { r.Instance.Region.Should().Be("us-east"); r.Instance.Version.Should().Be("2.0.0"); r.Status.Should().Be(InstanceHealthStatus.Healthy); }); } [Trait("Category", TestCategories.Unit)] [Fact] public void TieBreaker_UsesConnectionIdForConsistency() { // Arrange - Two identical connections except ID var context = CreateDeterministicContext(); var connections = CreateConnectionSet( ("conn-zzz", "instance-1", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Healthy), ("conn-aaa", "instance-2", "service-a", "1.0.0", "us-east", InstanceHealthStatus.Healthy)); var selector = new DeterministicRouteSelector(); // Act var results = Enumerable.Range(0, 100) .Select(_ => selector.SelectConnection(context, connections)) .ToList(); // Assert - Always select alphabetically first connection ID for tie-breaking results.Should().AllSatisfy(r => r.ConnectionId.Should().Be("conn-aaa")); } #endregion #region Endpoint Matching Determinism Tests [Trait("Category", TestCategories.Unit)] [Fact] public void PathParameterMatching_IsDeterministic() { // Arrange var matcher = new PathMatcher("/api/users/{userId}/orders/{orderId}"); var testPaths = new[] { "/api/users/123/orders/456", "/api/users/abc/orders/xyz", "/api/users/user-1/orders/order-2" }; // Act & Assert - Each path should always produce same match result foreach (var path in testPaths) { var results = Enumerable.Range(0, 100) .Select(_ => matcher.IsMatch(path)) .ToList(); results.Should().AllBeEquivalentTo(results[0], $"Path {path} should match consistently"); } } [Trait("Category", TestCategories.Unit)] [Fact] public void MultipleEndpoints_SamePath_SelectsFirstMatchDeterministically() { // Arrange var endpoints = new[] { CreateEndpoint("GET", "/api/users/{id}", "service-users", "1.0.0"), CreateEndpoint("GET", "/api/{resource}/{id}", "service-generic", "1.0.0") }; var selector = new EndpointMatcher(endpoints); var path = "/api/users/123"; // Act var results = Enumerable.Range(0, 100) .Select(_ => selector.FindBestMatch("GET", path)) .ToList(); // Assert - Always selects most specific match results.Should().AllSatisfy(r => r.ServiceName.Should().Be("service-users")); } #endregion #region Test Helpers private static RoutingContext CreateDeterministicContext() { return new RoutingContext { Method = "GET", Path = "/api/test", GatewayRegion = "us-east", Headers = new Dictionary { ["X-Request-Id"] = "deterministic-request-id" } }; } private static RoutingContext CreateContextWithRegion(string region) { return new RoutingContext { Method = "GET", Path = "/api/test", GatewayRegion = region, Headers = new Dictionary() }; } private static RoutingContext CreateContextWithVersion(string? version) { return new RoutingContext { Method = "GET", Path = "/api/test", GatewayRegion = "us-east", RequestedVersion = version, Headers = new Dictionary() }; } private static List CreateConnectionSet( params (string connId, string instId, string service, string version, string region, InstanceHealthStatus status)[] connections) { return connections.Select(c => new ConnectionState { ConnectionId = c.connId, Instance = new InstanceDescriptor { InstanceId = c.instId, ServiceName = c.service, Version = c.version, Region = c.region }, Status = c.status, TransportType = TransportType.InMemory, ConnectedAtUtc = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), LastHeartbeatUtc = new DateTime(2025, 1, 1, 0, 0, 1, DateTimeKind.Utc) }).ToList(); } private static EndpointDescriptor CreateEndpoint(string method, string path, string service, string version) { return new EndpointDescriptor { Method = method, Path = path, ServiceName = service, Version = version }; } private static string ComputeRouteKey(RoutingContext context) { // Route key computation should be deterministic regardless of header order var sortedHeaders = context.Headers .OrderBy(h => h.Key, StringComparer.Ordinal) .Select(h => $"{h.Key}={h.Value}"); return $"{context.Method}|{context.Path}|{string.Join("&", sortedHeaders)}"; } #endregion #region Test Support Classes /// /// Deterministic route selector for testing. /// Implements the same algorithm that production code should use. /// private sealed class DeterministicRouteSelector { public ConnectionState SelectConnection(RoutingContext context, IReadOnlyList connections) { // Filter out draining and unhealthy connections var candidates = connections .Where(c => c.Status is InstanceHealthStatus.Healthy or InstanceHealthStatus.Degraded) .ToList(); if (candidates.Count == 0) { throw new InvalidOperationException("No available connections"); } // Apply version filter if requested if (!string.IsNullOrEmpty(context.RequestedVersion)) { var versionMatches = candidates .Where(c => c.Instance.Version == context.RequestedVersion) .ToList(); if (versionMatches.Count > 0) { candidates = versionMatches; } } // Prefer local region var localRegion = candidates .Where(c => c.Instance.Region == context.GatewayRegion) .ToList(); if (localRegion.Count > 0) { candidates = localRegion; } // Prefer healthy over degraded var healthy = candidates .Where(c => c.Status == InstanceHealthStatus.Healthy) .ToList(); if (healthy.Count > 0) { candidates = healthy; } // Deterministic tie-breaker: sort by connection ID return candidates .OrderBy(c => c.ConnectionId, StringComparer.Ordinal) .First(); } } /// /// Endpoint matcher for testing deterministic endpoint selection. /// private sealed class EndpointMatcher { private readonly IReadOnlyList<(PathMatcher Matcher, EndpointDescriptor Endpoint)> _endpoints; public EndpointMatcher(IEnumerable endpoints) { // Sort by specificity: more specific paths first (fewer parameters) _endpoints = endpoints .OrderBy(e => e.Path.Count(c => c == '{')) .ThenBy(e => e.Path, StringComparer.Ordinal) .Select(e => (new PathMatcher(e.Path), e)) .ToList(); } public EndpointDescriptor FindBestMatch(string method, string path) { foreach (var (matcher, endpoint) in _endpoints) { if (endpoint.Method == method && matcher.IsMatch(path)) { return endpoint; } } throw new InvalidOperationException($"No endpoint found for {method} {path}"); } } #endregion }