product advisories, stella router improval, tests streghthening

This commit is contained in:
StellaOps Bot
2025-12-24 14:20:26 +02:00
parent 5540ce9430
commit 2c2bbf1005
171 changed files with 58943 additions and 135 deletions

View File

@@ -0,0 +1,537 @@
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Common.Models;
namespace StellaOps.Router.Common.Tests;
/// <summary>
/// Property-based tests ensuring routing determinism: same message + same configuration = same route.
/// </summary>
public sealed class RoutingDeterminismTests
{
#region Core Determinism Property Tests
[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]);
}
[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);
}
[Fact]
public void SamePathAndMethod_WithSameHeaders_ProducesSameRouteKey()
{
// Arrange
var context1 = new RoutingContext
{
Method = "GET",
Path = "/api/users/123",
Headers = new Dictionary<string, string>
{
["X-Correlation-Id"] = "corr-456",
["Accept"] = "application/json"
},
GatewayRegion = "us-east"
};
var context2 = new RoutingContext
{
Method = "GET",
Path = "/api/users/123",
Headers = new Dictionary<string, string>
{
["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
[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"));
}
[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
[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"));
}
[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
[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"));
}
[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);
}
[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
[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<string, string>()
};
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);
});
}
[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
[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");
}
}
[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<string, string>
{
["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<string, string>()
};
}
private static RoutingContext CreateContextWithVersion(string? version)
{
return new RoutingContext
{
Method = "GET",
Path = "/api/test",
GatewayRegion = "us-east",
RequestedVersion = version,
Headers = new Dictionary<string, string>()
};
}
private static List<ConnectionState> 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
/// <summary>
/// Deterministic route selector for testing.
/// Implements the same algorithm that production code should use.
/// </summary>
private sealed class DeterministicRouteSelector
{
public ConnectionState SelectConnection(RoutingContext context, IReadOnlyList<ConnectionState> 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();
}
}
/// <summary>
/// Endpoint matcher for testing deterministic endpoint selection.
/// </summary>
private sealed class EndpointMatcher
{
private readonly IReadOnlyList<(PathMatcher Matcher, EndpointDescriptor Endpoint)> _endpoints;
public EndpointMatcher(IEnumerable<EndpointDescriptor> 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
}