350 lines
9.5 KiB
C#
350 lines
9.5 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using Moq;
|
|
using StellaOps.Router.Common.Abstractions;
|
|
using StellaOps.Router.Common.Enums;
|
|
using StellaOps.Router.Common.Models;
|
|
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Microservice.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="RouterConnectionManager"/>.
|
|
/// </summary>
|
|
public sealed class RouterConnectionManagerTests : IDisposable
|
|
{
|
|
private readonly Mock<IEndpointDiscoveryProvider> _discoveryProviderMock;
|
|
private readonly Mock<IMicroserviceTransport> _transportMock;
|
|
private readonly StellaMicroserviceOptions _options;
|
|
|
|
public RouterConnectionManagerTests()
|
|
{
|
|
_discoveryProviderMock = new Mock<IEndpointDiscoveryProvider>();
|
|
_transportMock = new Mock<IMicroserviceTransport>();
|
|
_options = new StellaMicroserviceOptions
|
|
{
|
|
ServiceName = "test-service",
|
|
Version = "1.0.0",
|
|
Region = "test",
|
|
InstanceId = "test-instance-1",
|
|
HeartbeatInterval = TimeSpan.FromMilliseconds(50),
|
|
ReconnectBackoffInitial = TimeSpan.FromMilliseconds(10),
|
|
ReconnectBackoffMax = TimeSpan.FromMilliseconds(100)
|
|
};
|
|
|
|
_discoveryProviderMock.Setup(d => d.DiscoverEndpoints())
|
|
.Returns(new List<EndpointDescriptor>());
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Cleanup if needed
|
|
}
|
|
|
|
private RouterConnectionManager CreateManager()
|
|
{
|
|
return new RouterConnectionManager(
|
|
Options.Create(_options),
|
|
_discoveryProviderMock.Object,
|
|
_transportMock.Object,
|
|
NullLogger<RouterConnectionManager>.Instance);
|
|
}
|
|
|
|
#region Constructor Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Constructor_InitializesCorrectly()
|
|
{
|
|
// Act
|
|
using var manager = CreateManager();
|
|
|
|
// Assert
|
|
manager.Connections.Should().BeEmpty();
|
|
manager.CurrentStatus.Should().Be(InstanceHealthStatus.Healthy);
|
|
manager.InFlightRequestCount.Should().Be(0);
|
|
manager.ErrorRate.Should().Be(0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CurrentStatus Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void CurrentStatus_CanBeSet()
|
|
{
|
|
// Arrange
|
|
using var manager = CreateManager();
|
|
|
|
// Act
|
|
manager.CurrentStatus = InstanceHealthStatus.Draining;
|
|
|
|
// Assert
|
|
manager.CurrentStatus.Should().Be(InstanceHealthStatus.Draining);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Theory]
|
|
[InlineData(InstanceHealthStatus.Healthy)]
|
|
[InlineData(InstanceHealthStatus.Degraded)]
|
|
[InlineData(InstanceHealthStatus.Draining)]
|
|
[InlineData(InstanceHealthStatus.Unhealthy)]
|
|
public void CurrentStatus_AcceptsAllStatusValues(InstanceHealthStatus status)
|
|
{
|
|
// Arrange
|
|
using var manager = CreateManager();
|
|
|
|
// Act
|
|
manager.CurrentStatus = status;
|
|
|
|
// Assert
|
|
manager.CurrentStatus.Should().Be(status);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region InFlightRequestCount Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void InFlightRequestCount_CanBeSet()
|
|
{
|
|
// Arrange
|
|
using var manager = CreateManager();
|
|
|
|
// Act
|
|
manager.InFlightRequestCount = 42;
|
|
|
|
// Assert
|
|
manager.InFlightRequestCount.Should().Be(42);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ErrorRate Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void ErrorRate_CanBeSet()
|
|
{
|
|
// Arrange
|
|
using var manager = CreateManager();
|
|
|
|
// Act
|
|
manager.ErrorRate = 0.25;
|
|
|
|
// Assert
|
|
manager.ErrorRate.Should().Be(0.25);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region StartAsync Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task StartAsync_DiscoversEndpoints()
|
|
{
|
|
// Arrange
|
|
_options.Routers.Add(new RouterEndpointConfig
|
|
{
|
|
Host = "localhost",
|
|
Port = 5000,
|
|
TransportType = TransportType.InMemory
|
|
});
|
|
using var manager = CreateManager();
|
|
|
|
// Act
|
|
await manager.StartAsync(CancellationToken.None);
|
|
await Task.Delay(10);
|
|
await manager.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
_discoveryProviderMock.Verify(d => d.DiscoverEndpoints(), Times.Once);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task StartAsync_WithRouters_CreatesConnections()
|
|
{
|
|
// Arrange
|
|
_options.Routers.Add(new RouterEndpointConfig
|
|
{
|
|
Host = "localhost",
|
|
Port = 5000,
|
|
TransportType = TransportType.InMemory
|
|
});
|
|
using var manager = CreateManager();
|
|
|
|
// Act
|
|
await manager.StartAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
manager.Connections.Should().HaveCount(1);
|
|
manager.Connections[0].Instance.ServiceName.Should().Be("test-service");
|
|
|
|
// Cleanup
|
|
await manager.StopAsync(CancellationToken.None);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task StartAsync_RegistersEndpointsInConnection()
|
|
{
|
|
// Arrange
|
|
_options.Routers.Add(new RouterEndpointConfig
|
|
{
|
|
Host = "localhost",
|
|
Port = 5000,
|
|
TransportType = TransportType.InMemory
|
|
});
|
|
|
|
var endpoints = new List<EndpointDescriptor>
|
|
{
|
|
new() { ServiceName = "test", Version = "1.0", Method = "GET", Path = "/api/users" },
|
|
new() { ServiceName = "test", Version = "1.0", Method = "POST", Path = "/api/users" }
|
|
};
|
|
_discoveryProviderMock.Setup(d => d.DiscoverEndpoints()).Returns(endpoints);
|
|
|
|
using var manager = CreateManager();
|
|
|
|
// Act
|
|
await manager.StartAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
manager.Connections[0].Endpoints.Should().HaveCount(2);
|
|
|
|
// Cleanup
|
|
await manager.StopAsync(CancellationToken.None);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task StartAsync_AfterDispose_ThrowsObjectDisposedException()
|
|
{
|
|
// Arrange
|
|
var manager = CreateManager();
|
|
manager.Dispose();
|
|
|
|
// Act
|
|
var action = () => manager.StartAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
await action.Should().ThrowAsync<ObjectDisposedException>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region StopAsync Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task StopAsync_ClearsConnections()
|
|
{
|
|
// Arrange
|
|
_options.Routers.Add(new RouterEndpointConfig
|
|
{
|
|
Host = "localhost",
|
|
Port = 5000,
|
|
TransportType = TransportType.InMemory
|
|
});
|
|
using var manager = CreateManager();
|
|
await manager.StartAsync(CancellationToken.None);
|
|
|
|
// Act
|
|
await manager.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
manager.Connections.Should().BeEmpty();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Heartbeat Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Heartbeat_SendsViaTransport()
|
|
{
|
|
// Arrange
|
|
_options.Routers.Add(new RouterEndpointConfig
|
|
{
|
|
Host = "localhost",
|
|
Port = 5000,
|
|
TransportType = TransportType.InMemory
|
|
});
|
|
using var manager = CreateManager();
|
|
|
|
// Act
|
|
await manager.StartAsync(CancellationToken.None);
|
|
await Task.Delay(150); // Wait for heartbeat to run
|
|
await manager.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
_transportMock.Verify(
|
|
t => t.SendHeartbeatAsync(It.IsAny<HeartbeatPayload>(), It.IsAny<CancellationToken>()),
|
|
Times.AtLeastOnce);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Heartbeat_IncludesCurrentMetrics()
|
|
{
|
|
// Arrange
|
|
_options.Routers.Add(new RouterEndpointConfig
|
|
{
|
|
Host = "localhost",
|
|
Port = 5000,
|
|
TransportType = TransportType.InMemory
|
|
});
|
|
using var manager = CreateManager();
|
|
using StellaOps.TestKit;
|
|
manager.CurrentStatus = InstanceHealthStatus.Degraded;
|
|
manager.InFlightRequestCount = 10;
|
|
manager.ErrorRate = 0.05;
|
|
|
|
HeartbeatPayload? capturedHeartbeat = null;
|
|
_transportMock.Setup(t => t.SendHeartbeatAsync(It.IsAny<HeartbeatPayload>(), It.IsAny<CancellationToken>()))
|
|
.Callback<HeartbeatPayload, CancellationToken>((h, _) => capturedHeartbeat = h)
|
|
.Returns(Task.CompletedTask);
|
|
|
|
// Act
|
|
await manager.StartAsync(CancellationToken.None);
|
|
await Task.Delay(150); // Wait for heartbeat
|
|
await manager.StopAsync(CancellationToken.None);
|
|
|
|
// Assert
|
|
capturedHeartbeat.Should().NotBeNull();
|
|
capturedHeartbeat!.Status.Should().Be(InstanceHealthStatus.Degraded);
|
|
capturedHeartbeat.InFlightRequestCount.Should().Be(10);
|
|
capturedHeartbeat.ErrorRate.Should().Be(0.05);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Dispose Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Dispose_CanBeCalledMultipleTimes()
|
|
{
|
|
// Arrange
|
|
var manager = CreateManager();
|
|
|
|
// Act
|
|
var action = () =>
|
|
{
|
|
manager.Dispose();
|
|
manager.Dispose();
|
|
manager.Dispose();
|
|
};
|
|
|
|
// Assert
|
|
action.Should().NotThrow();
|
|
}
|
|
|
|
#endregion
|
|
}
|