267 lines
8.8 KiB
C#
267 lines
8.8 KiB
C#
using FluentAssertions;
|
|
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;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Router.Gateway.Tests;
|
|
|
|
public sealed class DefaultRoutingPluginTests
|
|
{
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ChooseInstanceAsync_WhenNoCandidates_ReturnsNull()
|
|
{
|
|
var plugin = CreatePlugin(gatewayRegion: "eu1");
|
|
|
|
var decision = await plugin.ChooseInstanceAsync(
|
|
new RoutingContext
|
|
{
|
|
Method = "GET",
|
|
Path = "/items",
|
|
Headers = new Dictionary<string, string>(),
|
|
Endpoint = new EndpointDescriptor
|
|
{
|
|
ServiceName = "inventory",
|
|
Version = "1.0.0",
|
|
Method = "GET",
|
|
Path = "/items"
|
|
},
|
|
AvailableConnections = [],
|
|
GatewayRegion = "eu1",
|
|
RequestedVersion = null,
|
|
CancellationToken = CancellationToken.None
|
|
},
|
|
CancellationToken.None);
|
|
|
|
decision.Should().BeNull();
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ChooseInstanceAsync_WhenRequestedVersionDoesNotMatch_ReturnsNull()
|
|
{
|
|
var plugin = CreatePlugin(
|
|
gatewayRegion: "eu1",
|
|
options: new RoutingOptions
|
|
{
|
|
StrictVersionMatching = true,
|
|
DefaultVersion = null
|
|
});
|
|
|
|
var candidates = new List<ConnectionState>
|
|
{
|
|
CreateConnection("inv-1", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Healthy)
|
|
};
|
|
|
|
var decision = await plugin.ChooseInstanceAsync(
|
|
CreateContext(
|
|
requestedVersion: "2.0.0",
|
|
candidates: candidates),
|
|
CancellationToken.None);
|
|
|
|
decision.Should().BeNull();
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ChooseInstanceAsync_PrefersHealthyOverDegraded()
|
|
{
|
|
var plugin = CreatePlugin(
|
|
gatewayRegion: "eu1",
|
|
options: new RoutingOptions
|
|
{
|
|
StrictVersionMatching = true,
|
|
DefaultVersion = null,
|
|
PreferLocalRegion = true,
|
|
AllowDegradedInstances = true,
|
|
TieBreaker = TieBreakerMode.RoundRobin
|
|
});
|
|
|
|
var degraded = CreateConnection("inv-degraded", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Degraded);
|
|
degraded.AveragePingMs = 1;
|
|
|
|
var healthy = CreateConnection("inv-healthy", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Healthy);
|
|
healthy.AveragePingMs = 50;
|
|
|
|
var decision = await plugin.ChooseInstanceAsync(
|
|
CreateContext(
|
|
requestedVersion: "1.0.0",
|
|
candidates: [degraded, healthy]),
|
|
CancellationToken.None);
|
|
|
|
decision.Should().NotBeNull();
|
|
decision!.Connection.ConnectionId.Should().Be("inv-healthy");
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ChooseInstanceAsync_PrefersLocalRegionOverRemote()
|
|
{
|
|
var plugin = CreatePlugin(
|
|
gatewayRegion: "eu1",
|
|
options: new RoutingOptions
|
|
{
|
|
StrictVersionMatching = true,
|
|
DefaultVersion = null,
|
|
PreferLocalRegion = true,
|
|
AllowDegradedInstances = true,
|
|
TieBreaker = TieBreakerMode.RoundRobin
|
|
});
|
|
|
|
var remote = CreateConnection("inv-us1", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
|
|
var local = CreateConnection("inv-eu1", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Healthy);
|
|
|
|
var decision = await plugin.ChooseInstanceAsync(
|
|
CreateContext(
|
|
requestedVersion: "1.0.0",
|
|
candidates: [remote, local]),
|
|
CancellationToken.None);
|
|
|
|
decision.Should().NotBeNull();
|
|
decision!.Connection.ConnectionId.Should().Be("inv-eu1");
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ChooseInstanceAsync_WhenNoLocal_UsesNeighborRegion()
|
|
{
|
|
var plugin = CreatePlugin(
|
|
gatewayRegion: "eu1",
|
|
gatewayNeighbors: ["eu2"],
|
|
options: new RoutingOptions
|
|
{
|
|
StrictVersionMatching = true,
|
|
DefaultVersion = null,
|
|
PreferLocalRegion = true,
|
|
AllowDegradedInstances = true,
|
|
TieBreaker = TieBreakerMode.RoundRobin
|
|
});
|
|
|
|
var neighbor = CreateConnection("inv-eu2", "inventory", "1.0.0", region: "eu2", status: InstanceHealthStatus.Healthy);
|
|
var remote = CreateConnection("inv-us1", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
|
|
|
|
var decision = await plugin.ChooseInstanceAsync(
|
|
CreateContext(
|
|
requestedVersion: "1.0.0",
|
|
candidates: [remote, neighbor]),
|
|
CancellationToken.None);
|
|
|
|
decision.Should().NotBeNull();
|
|
decision!.Connection.ConnectionId.Should().Be("inv-eu2");
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ChooseInstanceAsync_WhenTied_UsesRoundRobin()
|
|
{
|
|
var plugin = CreatePlugin(
|
|
gatewayRegion: "eu1",
|
|
options: new RoutingOptions
|
|
{
|
|
StrictVersionMatching = true,
|
|
DefaultVersion = null,
|
|
PreferLocalRegion = false,
|
|
AllowDegradedInstances = true,
|
|
TieBreaker = TieBreakerMode.RoundRobin,
|
|
PingToleranceMs = 1_000
|
|
});
|
|
|
|
var heartbeat = DateTime.UtcNow;
|
|
|
|
var a = CreateConnection("inv-a", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
|
|
a.AveragePingMs = 10;
|
|
a.LastHeartbeatUtc = heartbeat;
|
|
|
|
var b = CreateConnection("inv-b", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
|
|
b.AveragePingMs = 10;
|
|
b.LastHeartbeatUtc = heartbeat;
|
|
|
|
var ctx = CreateContext(requestedVersion: "1.0.0", candidates: [a, b]);
|
|
|
|
var decision1 = await plugin.ChooseInstanceAsync(ctx, CancellationToken.None);
|
|
var decision2 = await plugin.ChooseInstanceAsync(ctx, CancellationToken.None);
|
|
|
|
decision1.Should().NotBeNull();
|
|
decision2.Should().NotBeNull();
|
|
|
|
decision1!.Connection.ConnectionId.Should().Be("inv-a");
|
|
decision2!.Connection.ConnectionId.Should().Be("inv-b");
|
|
}
|
|
|
|
private static DefaultRoutingPlugin CreatePlugin(
|
|
string gatewayRegion,
|
|
RoutingOptions? options = null,
|
|
IReadOnlyList<string>? gatewayNeighbors = null)
|
|
{
|
|
options ??= new RoutingOptions
|
|
{
|
|
StrictVersionMatching = true,
|
|
DefaultVersion = null,
|
|
PreferLocalRegion = true,
|
|
AllowDegradedInstances = true,
|
|
TieBreaker = TieBreakerMode.RoundRobin
|
|
};
|
|
|
|
var node = new RouterNodeConfig
|
|
{
|
|
Region = gatewayRegion,
|
|
NeighborRegions = gatewayNeighbors?.ToList() ?? []
|
|
};
|
|
|
|
return new DefaultRoutingPlugin(
|
|
Options.Create(options),
|
|
Options.Create(node));
|
|
}
|
|
|
|
private static RoutingContext CreateContext(
|
|
string? requestedVersion,
|
|
IReadOnlyList<ConnectionState> candidates)
|
|
{
|
|
return new RoutingContext
|
|
{
|
|
Method = "GET",
|
|
Path = "/items",
|
|
Headers = new Dictionary<string, string>(),
|
|
Endpoint = new EndpointDescriptor
|
|
{
|
|
ServiceName = "inventory",
|
|
Version = "1.0.0",
|
|
Method = "GET",
|
|
Path = "/items"
|
|
},
|
|
AvailableConnections = candidates,
|
|
GatewayRegion = "eu1",
|
|
RequestedVersion = requestedVersion,
|
|
CancellationToken = CancellationToken.None
|
|
};
|
|
}
|
|
|
|
private static ConnectionState CreateConnection(
|
|
string connectionId,
|
|
string serviceName,
|
|
string version,
|
|
string region,
|
|
InstanceHealthStatus status)
|
|
{
|
|
return new ConnectionState
|
|
{
|
|
ConnectionId = connectionId,
|
|
Instance = new InstanceDescriptor
|
|
{
|
|
InstanceId = connectionId,
|
|
ServiceName = serviceName,
|
|
Version = version,
|
|
Region = region
|
|
},
|
|
Status = status,
|
|
LastHeartbeatUtc = DateTime.UtcNow,
|
|
TransportType = TransportType.InMemory
|
|
};
|
|
}
|
|
}
|
|
|