Add unit tests for RabbitMq and Udp transport servers and clients
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling. - Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options. - Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation. - Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios. - Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling. - Included tests for UdpTransportOptions to verify default values and modification capabilities. - Enhanced service registration tests for Udp transport services in the dependency injection container.
This commit is contained in:
@@ -16,6 +16,9 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Router.Transport.Tcp\StellaOps.Router.Transport.Tcp.csproj" />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Buffers.Binary;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
@@ -7,24 +9,88 @@ using Xunit;
|
||||
|
||||
namespace StellaOps.Router.Transport.Tcp.Tests;
|
||||
|
||||
#region TcpTransportOptions Tests
|
||||
|
||||
public class TcpTransportOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultOptions_HaveCorrectValues()
|
||||
{
|
||||
// Act
|
||||
var options = new TcpTransportOptions();
|
||||
|
||||
Assert.Equal(5100, options.Port);
|
||||
Assert.Equal(64 * 1024, options.ReceiveBufferSize);
|
||||
Assert.Equal(64 * 1024, options.SendBufferSize);
|
||||
Assert.Equal(TimeSpan.FromSeconds(30), options.KeepAliveInterval);
|
||||
Assert.Equal(TimeSpan.FromSeconds(10), options.ConnectTimeout);
|
||||
Assert.Equal(10, options.MaxReconnectAttempts);
|
||||
Assert.Equal(TimeSpan.FromMinutes(1), options.MaxReconnectBackoff);
|
||||
Assert.Equal(16 * 1024 * 1024, options.MaxFrameSize);
|
||||
// Assert
|
||||
options.Port.Should().Be(5100);
|
||||
options.ReceiveBufferSize.Should().Be(64 * 1024);
|
||||
options.SendBufferSize.Should().Be(64 * 1024);
|
||||
options.KeepAliveInterval.Should().Be(TimeSpan.FromSeconds(30));
|
||||
options.ConnectTimeout.Should().Be(TimeSpan.FromSeconds(10));
|
||||
options.MaxReconnectAttempts.Should().Be(10);
|
||||
options.MaxReconnectBackoff.Should().Be(TimeSpan.FromMinutes(1));
|
||||
options.MaxFrameSize.Should().Be(16 * 1024 * 1024);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Host_CanBeSet()
|
||||
{
|
||||
// Act
|
||||
var options = new TcpTransportOptions { Host = "192.168.1.100" };
|
||||
|
||||
// Assert
|
||||
options.Host.Should().Be("192.168.1.100");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Port_CanBeSet()
|
||||
{
|
||||
// Act
|
||||
var options = new TcpTransportOptions { Port = 9999 };
|
||||
|
||||
// Assert
|
||||
options.Port.Should().Be(9999);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1024)]
|
||||
[InlineData(128 * 1024)]
|
||||
[InlineData(1024 * 1024)]
|
||||
public void ReceiveBufferSize_CanBeSet(int bufferSize)
|
||||
{
|
||||
// Act
|
||||
var options = new TcpTransportOptions { ReceiveBufferSize = bufferSize };
|
||||
|
||||
// Assert
|
||||
options.ReceiveBufferSize.Should().Be(bufferSize);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1024)]
|
||||
[InlineData(128 * 1024)]
|
||||
[InlineData(1024 * 1024)]
|
||||
public void SendBufferSize_CanBeSet(int bufferSize)
|
||||
{
|
||||
// Act
|
||||
var options = new TcpTransportOptions { SendBufferSize = bufferSize };
|
||||
|
||||
// Assert
|
||||
options.SendBufferSize.Should().Be(bufferSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxReconnectAttempts_CanBeSetToZero()
|
||||
{
|
||||
// Act
|
||||
var options = new TcpTransportOptions { MaxReconnectAttempts = 0 };
|
||||
|
||||
// Assert
|
||||
options.MaxReconnectAttempts.Should().Be(0);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FrameProtocol Tests
|
||||
|
||||
public class FrameProtocolTests
|
||||
{
|
||||
[Fact]
|
||||
@@ -47,15 +113,16 @@ public class FrameProtocolTests
|
||||
var readFrame = await FrameProtocol.ReadFrameAsync(stream, 1024 * 1024, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(readFrame);
|
||||
Assert.Equal(originalFrame.Type, readFrame.Type);
|
||||
Assert.Equal(originalFrame.CorrelationId, readFrame.CorrelationId);
|
||||
Assert.Equal(originalFrame.Payload.ToArray(), readFrame.Payload.ToArray());
|
||||
readFrame.Should().NotBeNull();
|
||||
readFrame!.Type.Should().Be(originalFrame.Type);
|
||||
readFrame.CorrelationId.Should().Be(originalFrame.CorrelationId);
|
||||
readFrame.Payload.ToArray().Should().Equal(originalFrame.Payload.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAndReadFrame_EmptyPayload()
|
||||
{
|
||||
// Arrange
|
||||
using var stream = new MemoryStream();
|
||||
var originalFrame = new Frame
|
||||
{
|
||||
@@ -64,29 +131,34 @@ public class FrameProtocolTests
|
||||
Payload = ReadOnlyMemory<byte>.Empty
|
||||
};
|
||||
|
||||
// Act
|
||||
await FrameProtocol.WriteFrameAsync(stream, originalFrame, CancellationToken.None);
|
||||
|
||||
stream.Position = 0;
|
||||
var readFrame = await FrameProtocol.ReadFrameAsync(stream, 1024 * 1024, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(readFrame);
|
||||
Assert.Equal(FrameType.Cancel, readFrame.Type);
|
||||
Assert.Empty(readFrame.Payload.ToArray());
|
||||
// Assert
|
||||
readFrame.Should().NotBeNull();
|
||||
readFrame!.Type.Should().Be(FrameType.Cancel);
|
||||
readFrame.Payload.ToArray().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFrame_ReturnsNullOnEmptyStream()
|
||||
{
|
||||
// Arrange
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
// Act
|
||||
var result = await FrameProtocol.ReadFrameAsync(stream, 1024 * 1024, CancellationToken.None);
|
||||
|
||||
Assert.Null(result);
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFrame_ThrowsOnOversizedFrame()
|
||||
{
|
||||
// Arrange
|
||||
using var stream = new MemoryStream();
|
||||
var largeFrame = new Frame
|
||||
{
|
||||
@@ -96,20 +168,190 @@ public class FrameProtocolTests
|
||||
};
|
||||
|
||||
await FrameProtocol.WriteFrameAsync(stream, largeFrame, CancellationToken.None);
|
||||
|
||||
stream.Position = 0;
|
||||
|
||||
// Max frame size is smaller than the written frame
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => FrameProtocol.ReadFrameAsync(stream, 100, CancellationToken.None));
|
||||
// Act & Assert - Max frame size is smaller than the written frame
|
||||
var action = () => FrameProtocol.ReadFrameAsync(stream, 100, CancellationToken.None);
|
||||
await action.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*exceeds maximum*");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(FrameType.Request)]
|
||||
[InlineData(FrameType.Response)]
|
||||
[InlineData(FrameType.Cancel)]
|
||||
[InlineData(FrameType.Hello)]
|
||||
[InlineData(FrameType.Heartbeat)]
|
||||
public async Task WriteAndReadFrame_AllFrameTypes(FrameType frameType)
|
||||
{
|
||||
// Arrange
|
||||
using var stream = new MemoryStream();
|
||||
var frame = new Frame
|
||||
{
|
||||
Type = frameType,
|
||||
CorrelationId = Guid.NewGuid().ToString("N"),
|
||||
Payload = "test data"u8.ToArray()
|
||||
};
|
||||
|
||||
// Act
|
||||
await FrameProtocol.WriteFrameAsync(stream, frame, CancellationToken.None);
|
||||
stream.Position = 0;
|
||||
var readFrame = await FrameProtocol.ReadFrameAsync(stream, 1024 * 1024, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
readFrame.Should().NotBeNull();
|
||||
readFrame!.Type.Should().Be(frameType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFrame_WithNullCorrelationId_GeneratesNewGuid()
|
||||
{
|
||||
// Arrange
|
||||
using var stream = new MemoryStream();
|
||||
var frame = new Frame
|
||||
{
|
||||
Type = FrameType.Request,
|
||||
CorrelationId = null,
|
||||
Payload = new byte[] { 1 }
|
||||
};
|
||||
|
||||
// Act
|
||||
await FrameProtocol.WriteFrameAsync(stream, frame, CancellationToken.None);
|
||||
stream.Position = 0;
|
||||
var readFrame = await FrameProtocol.ReadFrameAsync(stream, 1024 * 1024, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
readFrame.Should().NotBeNull();
|
||||
readFrame!.CorrelationId.Should().NotBeNullOrEmpty();
|
||||
Guid.TryParse(readFrame.CorrelationId, out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFrame_BigEndianLength_CorrectByteOrder()
|
||||
{
|
||||
// Arrange
|
||||
using var stream = new MemoryStream();
|
||||
var payload = new byte[256]; // 256 bytes of data
|
||||
var frame = new Frame
|
||||
{
|
||||
Type = FrameType.Request,
|
||||
CorrelationId = Guid.NewGuid().ToString("N"),
|
||||
Payload = payload
|
||||
};
|
||||
|
||||
// Act
|
||||
await FrameProtocol.WriteFrameAsync(stream, frame, CancellationToken.None);
|
||||
|
||||
// Assert - Check the first 4 bytes (big-endian length)
|
||||
stream.Position = 0;
|
||||
var lengthBuffer = new byte[4];
|
||||
await stream.ReadAsync(lengthBuffer, CancellationToken.None);
|
||||
|
||||
var expectedLength = 1 + 16 + payload.Length; // frame type + correlation ID + payload
|
||||
var actualLength = BinaryPrimitives.ReadInt32BigEndian(lengthBuffer);
|
||||
actualLength.Should().Be(expectedLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFrame_IncompleteLengthPrefix_ThrowsException()
|
||||
{
|
||||
// Arrange - Only 2 bytes instead of 4 for length prefix
|
||||
using var stream = new MemoryStream(new byte[] { 0, 1 });
|
||||
|
||||
// Act & Assert
|
||||
var action = () => FrameProtocol.ReadFrameAsync(stream, 1024 * 1024, CancellationToken.None);
|
||||
await action.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*Incomplete length prefix*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFrame_InvalidPayloadLength_TooSmall_ThrowsException()
|
||||
{
|
||||
// Arrange - Length of 5 is too small (header is 17 bytes minimum)
|
||||
using var stream = new MemoryStream();
|
||||
var lengthBuffer = new byte[4];
|
||||
BinaryPrimitives.WriteInt32BigEndian(lengthBuffer, 5);
|
||||
stream.Write(lengthBuffer);
|
||||
stream.Position = 0;
|
||||
|
||||
// Act & Assert
|
||||
var action = () => FrameProtocol.ReadFrameAsync(stream, 1024 * 1024, CancellationToken.None);
|
||||
await action.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*Invalid payload length*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFrame_IncompletePayload_ThrowsException()
|
||||
{
|
||||
// Arrange - Claim to have 100 bytes but only provide 10
|
||||
using var stream = new MemoryStream();
|
||||
var lengthBuffer = new byte[4];
|
||||
BinaryPrimitives.WriteInt32BigEndian(lengthBuffer, 100);
|
||||
stream.Write(lengthBuffer);
|
||||
stream.Write(new byte[10]); // Only 10 bytes instead of 100
|
||||
stream.Position = 0;
|
||||
|
||||
// Act & Assert
|
||||
var action = () => FrameProtocol.ReadFrameAsync(stream, 1024 * 1024, CancellationToken.None);
|
||||
await action.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*Incomplete payload*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFrame_WithLargePayload_ReadsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
using var stream = new MemoryStream();
|
||||
var largePayload = new byte[64 * 1024]; // 64KB
|
||||
Random.Shared.NextBytes(largePayload);
|
||||
|
||||
var frame = new Frame
|
||||
{
|
||||
Type = FrameType.Request,
|
||||
CorrelationId = Guid.NewGuid().ToString("N"),
|
||||
Payload = largePayload
|
||||
};
|
||||
|
||||
// Act
|
||||
await FrameProtocol.WriteFrameAsync(stream, frame, CancellationToken.None);
|
||||
stream.Position = 0;
|
||||
var readFrame = await FrameProtocol.ReadFrameAsync(stream, 100 * 1024, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
readFrame.Should().NotBeNull();
|
||||
readFrame!.Payload.ToArray().Should().Equal(largePayload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteFrame_CancellationRequested_ThrowsOperationCanceled()
|
||||
{
|
||||
// Arrange
|
||||
using var stream = new MemoryStream();
|
||||
var frame = new Frame
|
||||
{
|
||||
Type = FrameType.Request,
|
||||
CorrelationId = Guid.NewGuid().ToString("N"),
|
||||
Payload = new byte[100]
|
||||
};
|
||||
using var cts = new CancellationTokenSource();
|
||||
await cts.CancelAsync();
|
||||
|
||||
// Act & Assert
|
||||
var action = () => FrameProtocol.WriteFrameAsync(stream, frame, cts.Token);
|
||||
await action.Should().ThrowAsync<OperationCanceledException>();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PendingRequestTracker Tests
|
||||
|
||||
public class PendingRequestTrackerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task TrackRequest_CompletesWithResponse()
|
||||
{
|
||||
// Arrange
|
||||
using var tracker = new PendingRequestTracker();
|
||||
var correlationId = Guid.NewGuid();
|
||||
var expectedResponse = new Frame
|
||||
@@ -119,81 +361,385 @@ public class PendingRequestTrackerTests
|
||||
Payload = ReadOnlyMemory<byte>.Empty
|
||||
};
|
||||
|
||||
// Act
|
||||
var responseTask = tracker.TrackRequest(correlationId, CancellationToken.None);
|
||||
Assert.False(responseTask.IsCompleted);
|
||||
|
||||
responseTask.IsCompleted.Should().BeFalse();
|
||||
tracker.CompleteRequest(correlationId, expectedResponse);
|
||||
|
||||
var response = await responseTask;
|
||||
Assert.Equal(expectedResponse.Type, response.Type);
|
||||
|
||||
// Assert
|
||||
response.Type.Should().Be(expectedResponse.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TrackRequest_CancelsOnTokenCancellation()
|
||||
{
|
||||
// Arrange
|
||||
using var tracker = new PendingRequestTracker();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var correlationId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var responseTask = tracker.TrackRequest(correlationId, cts.Token);
|
||||
await cts.CancelAsync();
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => responseTask);
|
||||
// Assert
|
||||
var action = () => responseTask;
|
||||
await action.Should().ThrowAsync<TaskCanceledException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Count_ReturnsCorrectValue()
|
||||
{
|
||||
// Arrange
|
||||
using var tracker = new PendingRequestTracker();
|
||||
|
||||
Assert.Equal(0, tracker.Count);
|
||||
|
||||
// Act & Assert
|
||||
tracker.Count.Should().Be(0);
|
||||
_ = tracker.TrackRequest(Guid.NewGuid(), CancellationToken.None);
|
||||
_ = tracker.TrackRequest(Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, tracker.Count);
|
||||
tracker.Count.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelAll_CancelsAllPendingRequests()
|
||||
{
|
||||
// Arrange
|
||||
using var tracker = new PendingRequestTracker();
|
||||
var task1 = tracker.TrackRequest(Guid.NewGuid(), CancellationToken.None);
|
||||
var task2 = tracker.TrackRequest(Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
// Act
|
||||
tracker.CancelAll();
|
||||
|
||||
Assert.True(task1.IsCanceled || task1.IsFaulted);
|
||||
Assert.True(task2.IsCanceled || task2.IsFaulted);
|
||||
// Assert
|
||||
task1.IsCanceled.Should().BeTrue();
|
||||
task2.IsCanceled.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailRequest_SetsException()
|
||||
{
|
||||
// Arrange
|
||||
using var tracker = new PendingRequestTracker();
|
||||
var correlationId = Guid.NewGuid();
|
||||
var task = tracker.TrackRequest(correlationId, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
tracker.FailRequest(correlationId, new InvalidOperationException("Test error"));
|
||||
|
||||
Assert.True(task.IsFaulted);
|
||||
Assert.IsType<InvalidOperationException>(task.Exception?.InnerException);
|
||||
// Assert
|
||||
task.IsFaulted.Should().BeTrue();
|
||||
task.Exception?.InnerException.Should().BeOfType<InvalidOperationException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelRequest_CancelsSpecificRequest()
|
||||
{
|
||||
// Arrange
|
||||
using var tracker = new PendingRequestTracker();
|
||||
var correlationId1 = Guid.NewGuid();
|
||||
var correlationId2 = Guid.NewGuid();
|
||||
var task1 = tracker.TrackRequest(correlationId1, CancellationToken.None);
|
||||
var task2 = tracker.TrackRequest(correlationId2, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
tracker.CancelRequest(correlationId1);
|
||||
|
||||
// Assert
|
||||
task1.IsCanceled.Should().BeTrue();
|
||||
task2.IsCanceled.Should().BeFalse();
|
||||
task2.IsCompleted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteRequest_WithUnknownId_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
using var tracker = new PendingRequestTracker();
|
||||
var unknownId = Guid.NewGuid();
|
||||
var frame = new Frame { Type = FrameType.Response };
|
||||
|
||||
// Act
|
||||
var action = () => tracker.CompleteRequest(unknownId, frame);
|
||||
|
||||
// Assert
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelRequest_WithUnknownId_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
using var tracker = new PendingRequestTracker();
|
||||
var unknownId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var action = () => tracker.CancelRequest(unknownId);
|
||||
|
||||
// Assert
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailRequest_WithUnknownId_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
using var tracker = new PendingRequestTracker();
|
||||
var unknownId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var action = () => tracker.FailRequest(unknownId, new Exception());
|
||||
|
||||
// Assert
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_CancelsAllPendingRequests()
|
||||
{
|
||||
// Arrange
|
||||
var tracker = new PendingRequestTracker();
|
||||
var task = tracker.TrackRequest(Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
// Act
|
||||
tracker.Dispose();
|
||||
|
||||
// Assert - Task may be canceled or faulted depending on implementation
|
||||
(task.IsCanceled || task.IsFaulted).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
var tracker = new PendingRequestTracker();
|
||||
|
||||
// Act
|
||||
var action = () =>
|
||||
{
|
||||
tracker.Dispose();
|
||||
tracker.Dispose();
|
||||
tracker.Dispose();
|
||||
};
|
||||
|
||||
// Assert
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompleteRequest_DecreasesCount()
|
||||
{
|
||||
// Arrange
|
||||
using var tracker = new PendingRequestTracker();
|
||||
var correlationId = Guid.NewGuid();
|
||||
var frame = new Frame { Type = FrameType.Response };
|
||||
|
||||
_ = tracker.TrackRequest(correlationId, CancellationToken.None);
|
||||
tracker.Count.Should().Be(1);
|
||||
|
||||
// Act
|
||||
tracker.CompleteRequest(correlationId, frame);
|
||||
await Task.Delay(10); // Allow task completion to propagate
|
||||
|
||||
// Assert
|
||||
tracker.Count.Should().Be(0);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TcpTransportServer Tests
|
||||
|
||||
public class TcpTransportServerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StartAsync_StartsListening()
|
||||
{
|
||||
// Arrange
|
||||
var options = Options.Create(new TcpTransportOptions { Port = 0 }); // Port 0 = auto-assign
|
||||
await using var server = new TcpTransportServer(options, NullLogger<TcpTransportServer>.Instance);
|
||||
|
||||
// Act
|
||||
await server.StartAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, server.ConnectionCount);
|
||||
// Assert
|
||||
server.ConnectionCount.Should().Be(0);
|
||||
await server.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopAsync_CanBeCalledWithoutStart()
|
||||
{
|
||||
// Arrange
|
||||
var options = Options.Create(new TcpTransportOptions { Port = 0 });
|
||||
await using var server = new TcpTransportServer(options, NullLogger<TcpTransportServer>.Instance);
|
||||
|
||||
// Act
|
||||
var action = () => server.StopAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await action.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionCount_InitiallyZero()
|
||||
{
|
||||
// Arrange
|
||||
var options = Options.Create(new TcpTransportOptions { Port = 0 });
|
||||
await using var server = new TcpTransportServer(options, NullLogger<TcpTransportServer>.Instance);
|
||||
|
||||
// Assert
|
||||
server.ConnectionCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsync_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
var options = Options.Create(new TcpTransportOptions { Port = 0 });
|
||||
var server = new TcpTransportServer(options, NullLogger<TcpTransportServer>.Instance);
|
||||
|
||||
// Act
|
||||
var action = async () =>
|
||||
{
|
||||
await server.DisposeAsync();
|
||||
await server.DisposeAsync();
|
||||
await server.DisposeAsync();
|
||||
};
|
||||
|
||||
// Assert
|
||||
await action.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_TwiceDoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
var options = Options.Create(new TcpTransportOptions { Port = 0 });
|
||||
await using var server = new TcpTransportServer(options, NullLogger<TcpTransportServer>.Instance);
|
||||
|
||||
// Act
|
||||
await server.StartAsync(CancellationToken.None);
|
||||
var action = () => server.StartAsync(CancellationToken.None);
|
||||
|
||||
// Assert - Starting twice should not throw (idempotent)
|
||||
await action.Should().NotThrowAsync();
|
||||
await server.StopAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TcpTransportClient Tests
|
||||
|
||||
public class TcpTransportClientTests
|
||||
{
|
||||
private TcpTransportClient CreateClient(TcpTransportOptions? options = null)
|
||||
{
|
||||
var opts = options ?? new TcpTransportOptions { Host = "localhost", Port = 5100 };
|
||||
return new TcpTransportClient(
|
||||
Options.Create(opts),
|
||||
NullLogger<TcpTransportClient>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Constructor_InitializesCorrectly()
|
||||
{
|
||||
// Act
|
||||
await using var client = CreateClient();
|
||||
|
||||
// Assert - No exception means it initialized correctly
|
||||
client.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAsync_WithoutHost_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TcpTransportOptions { Host = null, Port = 5100 };
|
||||
await using var client = CreateClient(options);
|
||||
var instance = new InstanceDescriptor
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
InstanceId = "test-1",
|
||||
Region = "local"
|
||||
};
|
||||
|
||||
// Act
|
||||
var action = () => client.ConnectAsync(instance, [], CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await action.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*Host is not configured*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAsync_WithEmptyHost_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TcpTransportOptions { Host = "", Port = 5100 };
|
||||
await using var client = CreateClient(options);
|
||||
var instance = new InstanceDescriptor
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
InstanceId = "test-1",
|
||||
Region = "local"
|
||||
};
|
||||
|
||||
// Act
|
||||
var action = () => client.ConnectAsync(instance, [], CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await action.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*Host is not configured*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsync_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
var client = CreateClient();
|
||||
|
||||
// Act
|
||||
var action = async () =>
|
||||
{
|
||||
await client.DisposeAsync();
|
||||
await client.DisposeAsync();
|
||||
await client.DisposeAsync();
|
||||
};
|
||||
|
||||
// Assert
|
||||
await action.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisconnectAsync_WithoutConnect_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
await using var client = CreateClient();
|
||||
|
||||
// Act
|
||||
var action = () => client.DisconnectAsync();
|
||||
|
||||
// Assert
|
||||
await action.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancelAllInflight_WithNoInflight_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
await using var client = CreateClient();
|
||||
|
||||
// Act
|
||||
var action = () => client.CancelAllInflight("test shutdown");
|
||||
|
||||
// Assert
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
Reference in New Issue
Block a user