5100* tests strengthtenen work
This commit is contained in:
@@ -0,0 +1,756 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// RoutingDecisionPropertyTests.cs
|
||||
// Sprint: SPRINT_5100_0007_0001_testing_strategy_2026
|
||||
// Task: TEST-STRAT-5100-004 - Property-based tests for routing/decision logic
|
||||
// Description: FsCheck property tests for DefaultRoutingPlugin routing invariants
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Xunit;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using StellaOps.Router.Gateway.Configuration;
|
||||
using StellaOps.Router.Gateway.Routing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Router.Gateway.Tests.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// Property-based tests for routing decision logic using FsCheck.
|
||||
/// Tests verify invariants of the DefaultRoutingPlugin routing algorithm.
|
||||
/// </summary>
|
||||
public sealed class RoutingDecisionPropertyTests
|
||||
{
|
||||
#region Generators
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random ConnectionState with valid values.
|
||||
/// </summary>
|
||||
private static Gen<ConnectionState> GenerateConnection(
|
||||
string? forcedRegion = null,
|
||||
InstanceHealthStatus? forcedStatus = null,
|
||||
string? forcedVersion = null)
|
||||
{
|
||||
return from connectionId in Gen.Elements("conn-1", "conn-2", "conn-3", "conn-4", "conn-5")
|
||||
from serviceName in Gen.Constant("test-service")
|
||||
from version in forcedVersion != null
|
||||
? Gen.Constant(forcedVersion)
|
||||
: Gen.Elements("1.0.0", "1.1.0", "2.0.0")
|
||||
from region in forcedRegion != null
|
||||
? Gen.Constant(forcedRegion)
|
||||
: Gen.Elements("eu1", "eu2", "us1", "us2", "ap1")
|
||||
from status in forcedStatus.HasValue
|
||||
? Gen.Constant(forcedStatus.Value)
|
||||
: Gen.Elements(InstanceHealthStatus.Healthy, InstanceHealthStatus.Degraded, InstanceHealthStatus.Unhealthy)
|
||||
from pingMs in Gen.Choose(1, 500)
|
||||
select new ConnectionState
|
||||
{
|
||||
ConnectionId = $"{connectionId}-{region}",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = $"{connectionId}-{region}",
|
||||
ServiceName = serviceName,
|
||||
Version = version,
|
||||
Region = region
|
||||
},
|
||||
Status = status,
|
||||
AveragePingMs = pingMs,
|
||||
LastHeartbeatUtc = DateTimeOffset.UtcNow.AddSeconds(-pingMs % 60)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a list of connection candidates.
|
||||
/// </summary>
|
||||
private static Gen<List<ConnectionState>> GenerateCandidates(
|
||||
int minCount = 1,
|
||||
int maxCount = 10,
|
||||
string? forcedRegion = null,
|
||||
InstanceHealthStatus? forcedStatus = null)
|
||||
{
|
||||
return from count in Gen.Choose(minCount, maxCount)
|
||||
from connections in Gen.ListOf(count, GenerateConnection(forcedRegion, forcedStatus))
|
||||
select connections.DistinctBy(c => c.ConnectionId).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates RoutingOptions with valid combinations.
|
||||
/// </summary>
|
||||
private static Gen<RoutingOptions> GenerateRoutingOptions()
|
||||
{
|
||||
return from preferLocal in Arb.Generate<bool>()
|
||||
from allowDegraded in Arb.Generate<bool>()
|
||||
from strictVersion in Arb.Generate<bool>()
|
||||
from tieBreaker in Gen.Elements(TieBreakerMode.Random, TieBreakerMode.RoundRobin, TieBreakerMode.LowestLatency)
|
||||
select new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = preferLocal,
|
||||
AllowDegradedInstances = allowDegraded,
|
||||
StrictVersionMatching = strictVersion,
|
||||
TieBreaker = tieBreaker,
|
||||
RoutingTimeoutMs = 5000,
|
||||
DefaultVersion = null
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Tests - Determinism
|
||||
|
||||
[Property(MaxTest = 100, Arbitrary = new[] { typeof(ConnectionArbitrary) })]
|
||||
public void SameInputs_ProduceDeterministicDecisions()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = true,
|
||||
AllowDegradedInstances = true,
|
||||
StrictVersionMatching = true,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var plugin = CreatePlugin("eu1", options);
|
||||
var candidates = CreateFixedCandidates();
|
||||
|
||||
// Act - Run routing multiple times
|
||||
var decisions = new List<string?>();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext("1.0.0", candidates),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
decisions.Add(decision?.Connection?.ConnectionId);
|
||||
}
|
||||
|
||||
// Assert - All decisions should be identical
|
||||
decisions.All(d => d == decisions[0]).Should().BeTrue(
|
||||
"same inputs with deterministic tie-breaker should produce same routing decision");
|
||||
}
|
||||
|
||||
[Property(MaxTest = 100)]
|
||||
public void EmptyCandidates_AlwaysReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var optionsGen = GenerateRoutingOptions();
|
||||
var options = optionsGen.Sample(1, 1).First();
|
||||
var plugin = CreatePlugin("eu1", options);
|
||||
|
||||
// Act
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext("1.0.0", []),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
decision.Should().BeNull("empty candidates should always return null");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Tests - Health Preference
|
||||
|
||||
[Property(MaxTest = 100)]
|
||||
public void HealthyPreferred_WhenHealthyExists_NeverChoosesDegraded()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = false,
|
||||
AllowDegradedInstances = true,
|
||||
StrictVersionMatching = false,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var plugin = CreatePlugin("eu1", options);
|
||||
|
||||
// Create mixed candidates with both healthy and degraded
|
||||
var healthy = new ConnectionState
|
||||
{
|
||||
ConnectionId = "healthy-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "healthy-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
AveragePingMs = 100 // Higher latency but healthy
|
||||
};
|
||||
|
||||
var degraded = new ConnectionState
|
||||
{
|
||||
ConnectionId = "degraded-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "degraded-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Degraded,
|
||||
AveragePingMs = 1 // Lower latency but degraded
|
||||
};
|
||||
|
||||
var candidates = new List<ConnectionState> { degraded, healthy };
|
||||
|
||||
// Act
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext(null, candidates),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
decision.Should().NotBeNull();
|
||||
decision!.Connection.Status.Should().Be(InstanceHealthStatus.Healthy,
|
||||
"healthy instances should always be preferred over degraded");
|
||||
}
|
||||
|
||||
[Property(MaxTest = 100)]
|
||||
public void WhenOnlyDegraded_AndAllowDegradedTrue_SelectsDegraded()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = false,
|
||||
AllowDegradedInstances = true,
|
||||
StrictVersionMatching = false,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var plugin = CreatePlugin("eu1", options);
|
||||
|
||||
var degraded1 = new ConnectionState
|
||||
{
|
||||
ConnectionId = "degraded-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "degraded-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Degraded,
|
||||
AveragePingMs = 10
|
||||
};
|
||||
|
||||
var degraded2 = new ConnectionState
|
||||
{
|
||||
ConnectionId = "degraded-2",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "degraded-2",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Degraded,
|
||||
AveragePingMs = 20
|
||||
};
|
||||
|
||||
var candidates = new List<ConnectionState> { degraded1, degraded2 };
|
||||
|
||||
// Act
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext(null, candidates),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
decision.Should().NotBeNull("degraded instances should be selected when no healthy available and AllowDegradedInstances=true");
|
||||
decision!.Connection.Status.Should().Be(InstanceHealthStatus.Degraded);
|
||||
}
|
||||
|
||||
[Property(MaxTest = 100)]
|
||||
public void WhenOnlyDegraded_AndAllowDegradedFalse_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = false,
|
||||
AllowDegradedInstances = false,
|
||||
StrictVersionMatching = false,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var plugin = CreatePlugin("eu1", options);
|
||||
|
||||
var degraded = new ConnectionState
|
||||
{
|
||||
ConnectionId = "degraded-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "degraded-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Degraded,
|
||||
AveragePingMs = 10
|
||||
};
|
||||
|
||||
var candidates = new List<ConnectionState> { degraded };
|
||||
|
||||
// Act
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext(null, candidates),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
decision.Should().BeNull("degraded instances should not be selected when AllowDegradedInstances=false");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Tests - Region Tier Preference
|
||||
|
||||
[Property(MaxTest = 100)]
|
||||
public void LocalRegion_AlwaysPreferred_WhenAvailable()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = true,
|
||||
AllowDegradedInstances = false,
|
||||
StrictVersionMatching = false,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var gatewayRegion = "eu1";
|
||||
var plugin = CreatePlugin(gatewayRegion, options);
|
||||
|
||||
var localInstance = new ConnectionState
|
||||
{
|
||||
ConnectionId = "local-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "local-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1" // Same as gateway
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
AveragePingMs = 100 // Higher latency
|
||||
};
|
||||
|
||||
var remoteInstance = new ConnectionState
|
||||
{
|
||||
ConnectionId = "remote-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "remote-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "us1" // Different region
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
AveragePingMs = 1 // Lower latency
|
||||
};
|
||||
|
||||
var candidates = new List<ConnectionState> { remoteInstance, localInstance };
|
||||
|
||||
// Act
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext(null, candidates, gatewayRegion),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
decision.Should().NotBeNull();
|
||||
decision!.Connection.Instance.Region.Should().Be(gatewayRegion,
|
||||
"local region should always be preferred when PreferLocalRegion=true");
|
||||
}
|
||||
|
||||
[Property(MaxTest = 100)]
|
||||
public void WhenNoLocalRegion_FallsBackToRemote()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = true,
|
||||
AllowDegradedInstances = false,
|
||||
StrictVersionMatching = false,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var gatewayRegion = "eu1";
|
||||
var plugin = CreatePlugin(gatewayRegion, options);
|
||||
|
||||
var remoteInstance = new ConnectionState
|
||||
{
|
||||
ConnectionId = "remote-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "remote-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "us1" // Different region
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
AveragePingMs = 10
|
||||
};
|
||||
|
||||
var candidates = new List<ConnectionState> { remoteInstance };
|
||||
|
||||
// Act
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext(null, candidates, gatewayRegion),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
decision.Should().NotBeNull("should fallback to remote region when no local available");
|
||||
decision!.Connection.Instance.Region.Should().Be("us1");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Tests - Version Matching
|
||||
|
||||
[Property(MaxTest = 100)]
|
||||
public void StrictVersionMatching_RejectsNonMatchingVersions()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = false,
|
||||
AllowDegradedInstances = true,
|
||||
StrictVersionMatching = true,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var plugin = CreatePlugin("eu1", options);
|
||||
|
||||
var v1Instance = new ConnectionState
|
||||
{
|
||||
ConnectionId = "v1-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "v1-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
AveragePingMs = 10
|
||||
};
|
||||
|
||||
var v2Instance = new ConnectionState
|
||||
{
|
||||
ConnectionId = "v2-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "v2-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "2.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
AveragePingMs = 10
|
||||
};
|
||||
|
||||
var candidates = new List<ConnectionState> { v1Instance, v2Instance };
|
||||
|
||||
// Act
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext("2.0.0", candidates),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
decision.Should().NotBeNull();
|
||||
decision!.Connection.Instance.Version.Should().Be("2.0.0",
|
||||
"strict version matching should only select matching version");
|
||||
}
|
||||
|
||||
[Property(MaxTest = 100)]
|
||||
public void RequestedVersion_NotAvailable_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = false,
|
||||
AllowDegradedInstances = true,
|
||||
StrictVersionMatching = true,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var plugin = CreatePlugin("eu1", options);
|
||||
|
||||
var v1Instance = new ConnectionState
|
||||
{
|
||||
ConnectionId = "v1-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "v1-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
AveragePingMs = 10
|
||||
};
|
||||
|
||||
var candidates = new List<ConnectionState> { v1Instance };
|
||||
|
||||
// Act
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext("3.0.0", candidates),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
decision.Should().BeNull("requested version not available should return null");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Tests - Tie-Breaker Behavior
|
||||
|
||||
[Property(MaxTest = 100)]
|
||||
public void LowestLatency_TieBreaker_SelectsLowestPing()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = false,
|
||||
AllowDegradedInstances = false,
|
||||
StrictVersionMatching = false,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var plugin = CreatePlugin("eu1", options);
|
||||
|
||||
var highLatency = new ConnectionState
|
||||
{
|
||||
ConnectionId = "high-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "high-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
AveragePingMs = 100
|
||||
};
|
||||
|
||||
var lowLatency = new ConnectionState
|
||||
{
|
||||
ConnectionId = "low-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "low-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
AveragePingMs = 10
|
||||
};
|
||||
|
||||
var candidates = new List<ConnectionState> { highLatency, lowLatency };
|
||||
|
||||
// Act
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext(null, candidates),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
decision.Should().NotBeNull();
|
||||
decision!.Connection.ConnectionId.Should().Be("low-1",
|
||||
"lowest latency tie-breaker should select instance with lowest ping");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Tests - Invariants
|
||||
|
||||
[Property(MaxTest = 100)]
|
||||
public void DecisionAlwaysIncludesEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = false,
|
||||
AllowDegradedInstances = true,
|
||||
StrictVersionMatching = false,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var plugin = CreatePlugin("eu1", options);
|
||||
var candidates = CreateFixedCandidates();
|
||||
|
||||
// Act
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext(null, candidates),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
decision.Should().NotBeNull();
|
||||
decision!.Endpoint.Should().NotBeNull("decision should always include endpoint");
|
||||
decision.Connection.Should().NotBeNull("decision should always include connection");
|
||||
}
|
||||
|
||||
[Property(MaxTest = 100)]
|
||||
public void UnhealthyInstances_NeverSelected()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = false,
|
||||
AllowDegradedInstances = true,
|
||||
StrictVersionMatching = false,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var plugin = CreatePlugin("eu1", options);
|
||||
|
||||
var unhealthy = new ConnectionState
|
||||
{
|
||||
ConnectionId = "unhealthy-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "unhealthy-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Unhealthy,
|
||||
AveragePingMs = 1 // Even with lowest latency
|
||||
};
|
||||
|
||||
var candidates = new List<ConnectionState> { unhealthy };
|
||||
|
||||
// Act
|
||||
var decision = plugin.ChooseInstanceAsync(
|
||||
CreateContext(null, candidates),
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
decision.Should().BeNull("unhealthy instances should never be selected");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
private static DefaultRoutingPlugin CreatePlugin(string gatewayRegion, RoutingOptions? options = null)
|
||||
{
|
||||
options ??= new RoutingOptions
|
||||
{
|
||||
PreferLocalRegion = true,
|
||||
AllowDegradedInstances = true,
|
||||
StrictVersionMatching = false,
|
||||
TieBreaker = TieBreakerMode.LowestLatency,
|
||||
RoutingTimeoutMs = 5000
|
||||
};
|
||||
|
||||
var gatewayConfig = new RouterNodeConfig
|
||||
{
|
||||
Region = gatewayRegion,
|
||||
NeighborRegions = ["eu2", "eu3"]
|
||||
};
|
||||
|
||||
return new DefaultRoutingPlugin(
|
||||
Options.Create(options),
|
||||
Options.Create(gatewayConfig));
|
||||
}
|
||||
|
||||
private static RoutingContext CreateContext(
|
||||
string? requestedVersion,
|
||||
List<ConnectionState> candidates,
|
||||
string gatewayRegion = "eu1")
|
||||
{
|
||||
return new RoutingContext
|
||||
{
|
||||
Method = "GET",
|
||||
Path = "/test",
|
||||
Headers = new Dictionary<string, string>(),
|
||||
Endpoint = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/test"
|
||||
},
|
||||
AvailableConnections = candidates,
|
||||
GatewayRegion = gatewayRegion,
|
||||
RequestedVersion = requestedVersion,
|
||||
CancellationToken = CancellationToken.None
|
||||
};
|
||||
}
|
||||
|
||||
private static List<ConnectionState> CreateFixedCandidates()
|
||||
{
|
||||
return
|
||||
[
|
||||
new ConnectionState
|
||||
{
|
||||
ConnectionId = "conn-1",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "conn-1",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
AveragePingMs = 10
|
||||
},
|
||||
new ConnectionState
|
||||
{
|
||||
ConnectionId = "conn-2",
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = "conn-2",
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = "eu1"
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
AveragePingMs = 20
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom Arbitrary for generating ConnectionState instances.
|
||||
/// </summary>
|
||||
public class ConnectionArbitrary
|
||||
{
|
||||
public static Arbitrary<ConnectionState> ConnectionState()
|
||||
{
|
||||
return Arb.From(Gen.Elements(
|
||||
CreateConn("c1", "eu1", InstanceHealthStatus.Healthy, 10),
|
||||
CreateConn("c2", "eu1", InstanceHealthStatus.Healthy, 20),
|
||||
CreateConn("c3", "eu2", InstanceHealthStatus.Healthy, 30),
|
||||
CreateConn("c4", "us1", InstanceHealthStatus.Degraded, 5)));
|
||||
}
|
||||
|
||||
private static ConnectionState CreateConn(string id, string region, InstanceHealthStatus status, int pingMs)
|
||||
{
|
||||
return new ConnectionState
|
||||
{
|
||||
ConnectionId = id,
|
||||
Instance = new ServiceInstance
|
||||
{
|
||||
InstanceId = id,
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Region = region
|
||||
},
|
||||
Status = status,
|
||||
AveragePingMs = pingMs
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="FsCheck" Version="2.16.6" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Testcontainers" Version="4.4.0" />
|
||||
|
||||
Reference in New Issue
Block a user