Added UnitTests
This commit is contained in:
@@ -90,6 +90,9 @@ namespace AMWD.Net.Api.Fritz.CallMonitor
|
||||
args.CalleeNumber = columns[4];
|
||||
args.CallerNumber = columns[5];
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return args;
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
<None Include="README.md" Pack="true" PackagePath="/" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="FritzCallMonitor.Tests" PublicKey="$(PublicKey)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace AMWD.Net.Api.Fritz.CallMonitor.Utils
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public static async void Forget(this Task task, ILogger? logger = null)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -24,10 +24,10 @@ namespace AMWD.Net.Api.Fritz.CallMonitor.Utils
|
||||
public ReconnectTcpClient(string host, int port)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
throw new ArgumentNullException(nameof(host), "The host is required.");
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
|
||||
if (port <= ushort.MinValue || ushort.MaxValue < port)
|
||||
throw new ArgumentOutOfRangeException(nameof(port), $"The port must be between {ushort.MinValue + 1} and {ushort.MaxValue}.");
|
||||
if (port < 1 || 65535 < port)
|
||||
throw new ArgumentOutOfRangeException(nameof(port));
|
||||
|
||||
_host = host;
|
||||
_port = port;
|
||||
@@ -98,7 +98,7 @@ namespace AMWD.Net.Api.Fritz.CallMonitor.Utils
|
||||
|
||||
_tcpClient?.Dispose();
|
||||
_tcpClient = null;
|
||||
});
|
||||
}, cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -127,13 +127,13 @@ namespace AMWD.Net.Api.Fritz.CallMonitor.Utils
|
||||
_tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
var connectTask = _tcpClient.ConnectAsync(_host, _port, cancellationToken);
|
||||
await _tcpClient.ConnectAsync(_host, _port, cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
var connectTask = _tcpClient.ConnectAsync(_host, _port);
|
||||
#endif
|
||||
var completedTask = await Task.WhenAny(connectTask, Task.Delay(1000, cancellationToken)).ConfigureAwait(false);
|
||||
var completedTask = await Task.WhenAny(connectTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(false);
|
||||
if (completedTask != connectTask)
|
||||
throw new TimeoutException("Connection attempt timed out.");
|
||||
throw new OperationCanceledException("Connection attempt was canceled.", cancellationToken);
|
||||
#endif
|
||||
|
||||
if (OnConnected != null)
|
||||
await OnConnected(this).ConfigureAwait(false);
|
||||
@@ -176,34 +176,26 @@ namespace AMWD.Net.Api.Fritz.CallMonitor.Utils
|
||||
byte[] buffer = new byte[1];
|
||||
while (!cancellationToken.IsCancellationRequested && !_isDisposed)
|
||||
{
|
||||
if (!IsConnected)
|
||||
if (_tcpClient == null || !IsConnected)
|
||||
{
|
||||
await ConnectWithRetryAsync(cancellationToken).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var stream = _tcpClient?.GetStream();
|
||||
if (stream != null && stream.CanRead)
|
||||
var stream = _tcpClient.GetStream();
|
||||
bool disconnected = false;
|
||||
try
|
||||
{
|
||||
bool disconnected = false;
|
||||
try
|
||||
{
|
||||
// Attempt to read zero bytes to check if the connection is still alive.
|
||||
// Should return immediately if the connection is still active.
|
||||
// So the timeout of 1sec is more than enough.
|
||||
var readTask = stream.ReadAsync(buffer, 0, 0, cancellationToken);
|
||||
var completedTask = await Task.WhenAny(readTask, Task.Delay(1000, cancellationToken)).ConfigureAwait(false);
|
||||
if (completedTask != readTask)
|
||||
continue; // Timeout
|
||||
}
|
||||
catch
|
||||
{
|
||||
disconnected = true;
|
||||
}
|
||||
|
||||
if (disconnected || !IsConnected)
|
||||
await ConnectWithRetryAsync(cancellationToken).ConfigureAwait(false);
|
||||
// Attempt to read zero bytes to check if the connection is still alive.
|
||||
await stream.ReadAsync(buffer, 0, 0, cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
disconnected = true;
|
||||
}
|
||||
|
||||
if (disconnected)
|
||||
await ConnectWithRetryAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check for an active connection every 5 seconds.
|
||||
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -21,10 +21,6 @@ namespace AMWD.Net.Api.Fritz.CallMonitor.Wrappers
|
||||
public virtual void Dispose()
|
||||
=> _networkStream.Dispose();
|
||||
|
||||
/// <inheritdoc cref="NetworkStream.CanRead"/>
|
||||
public virtual bool CanRead =>
|
||||
_networkStream.CanRead;
|
||||
|
||||
/// <inheritdoc cref="Stream.ReadAsync(byte[], int, int, CancellationToken)"/>
|
||||
public virtual Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
=> _networkStream.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
|
||||
154
test/FritzCallMonitor.Tests/CallMonitorEventArgsTest.cs
Normal file
154
test/FritzCallMonitor.Tests/CallMonitorEventArgsTest.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using AMWD.Net.Api.Fritz.CallMonitor;
|
||||
|
||||
namespace FritzCallMonitor.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class CallMonitorEventArgsTest
|
||||
{
|
||||
private string _dateOffset;
|
||||
|
||||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
var offset = TimeZoneInfo.Local.GetUtcOffset(DateTime.Now);
|
||||
_dateOffset = offset < TimeSpan.Zero
|
||||
? "-" + offset.ToString("hh\\:mm")
|
||||
: "+" + offset.ToString("hh\\:mm");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldParseRingEvent()
|
||||
{
|
||||
// Arrange
|
||||
string line = "25.08.25 20:15:30;RING;2;012345678901;9876543;SIP0;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual($"2025-08-25 20:15:30 {_dateOffset}", result.Timestamp?.ToString("yyyy-MM-dd HH:mm:ss K"));
|
||||
Assert.AreEqual(EventType.Ring, result.Event);
|
||||
Assert.AreEqual(2, result.ConnectionId);
|
||||
Assert.IsNull(result.LinePort);
|
||||
Assert.AreEqual("012345678901", result.CallerNumber);
|
||||
Assert.AreEqual("9876543", result.CalleeNumber);
|
||||
Assert.IsNull(result.Duration);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldParseConnectEvent()
|
||||
{
|
||||
string line = "25.08.25 20:15:30;CONNECT;1;3;012345678901;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual($"2025-08-25 20:15:30 {_dateOffset}", result.Timestamp?.ToString("yyyy-MM-dd HH:mm:ss K"));
|
||||
Assert.AreEqual(EventType.Connect, result.Event);
|
||||
Assert.AreEqual(1, result.ConnectionId);
|
||||
Assert.AreEqual(3, result.LinePort);
|
||||
Assert.AreEqual("012345678901", result.CallerNumber);
|
||||
Assert.IsNull(result.CalleeNumber);
|
||||
Assert.IsNull(result.Duration);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldParseDisconnectEvent()
|
||||
{
|
||||
string line = "25.08.25 20:15:30;DISCONNECT;2;42;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual($"2025-08-25 20:15:30 {_dateOffset}", result.Timestamp?.ToString("yyyy-MM-dd HH:mm:ss K"));
|
||||
Assert.AreEqual(EventType.Disconnect, result.Event);
|
||||
Assert.AreEqual(2, result.ConnectionId);
|
||||
Assert.IsNull(result.LinePort);
|
||||
Assert.IsNull(result.CallerNumber);
|
||||
Assert.IsNull(result.CalleeNumber);
|
||||
Assert.AreEqual(TimeSpan.FromSeconds(42), result.Duration);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldParseCallEvent()
|
||||
{
|
||||
string line = "25.08.25 20:15:30;CALL;4;7;9876543;012345678901;SIP0;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual($"2025-08-25 20:15:30 {_dateOffset}", result.Timestamp?.ToString("yyyy-MM-dd HH:mm:ss K"));
|
||||
Assert.AreEqual(EventType.Call, result.Event);
|
||||
Assert.AreEqual(4, result.ConnectionId);
|
||||
Assert.AreEqual(7, result.LinePort);
|
||||
Assert.AreEqual("012345678901", result.CallerNumber);
|
||||
Assert.AreEqual("9876543", result.CalleeNumber);
|
||||
Assert.IsNull(result.Duration);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldReturnNullOnInvalidDate()
|
||||
{
|
||||
string line = "99.99.99 99:99:99;RING;2;012345678901;9876543;SIP0;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldReturnNullOnUnknownEventType()
|
||||
{
|
||||
string line = "25.08.25 20:15:30;UNKNOWN;2;012345678901;9876543;SIP0;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldReturnNullOnInvalidConnectionId()
|
||||
{
|
||||
string line = "25.08.25 20:15:30;RING;abc;012345678901;9876543;SIP0;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldHandleInvalidLinePortInConnect()
|
||||
{
|
||||
string line = "25.08.25 20:15:30;CONNECT;1;abc;012345678901;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsNull(result.LinePort);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldHandleInvalidLinePortInCall()
|
||||
{
|
||||
string line = "25.08.25 20:15:30;CALL;4;abc;9876543;012345678901;SIP0;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsNull(result.LinePort);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldHandleInvalidDurationInDisconnect()
|
||||
{
|
||||
string line = "25.08.25 20:15:30;DISCONNECT;2;abc;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsNull(result.Duration);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldReturnNullOnTooFewColumns()
|
||||
{
|
||||
string line = "25.08.25 20:15:30;RING;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldParseWithExtraColumns()
|
||||
{
|
||||
string line = "25.08.25 20:15:30;RING;2;012345678901;9876543;SIP0;EXTRA;COLUMN;";
|
||||
var result = CallMonitorEventArgs.Parse(line);
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual("012345678901", result.CallerNumber);
|
||||
Assert.AreEqual("9876543", result.CalleeNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
453
test/FritzCallMonitor.Tests/ReconnectTcpClientTest.cs
Normal file
453
test/FritzCallMonitor.Tests/ReconnectTcpClientTest.cs
Normal file
@@ -0,0 +1,453 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Net.Api.Fritz.CallMonitor.Utils;
|
||||
using AMWD.Net.Api.Fritz.CallMonitor.Wrappers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
namespace FritzCallMonitor.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class ReconnectTcpClientTest
|
||||
{
|
||||
private const int ASYNC_DELAY = 10;
|
||||
|
||||
public TestContext TestContext { get; set; }
|
||||
|
||||
private const int PORT = 4711;
|
||||
private const string HOST = "localhost";
|
||||
|
||||
private Mock<SocketWrapper> _socketMock;
|
||||
private Mock<TcpClientWrapper> _tcpClientMock;
|
||||
private Mock<NetworkStreamWrapper> _networkStreamMock;
|
||||
private Mock<TcpClientWrapperFactory> _tcpClientFactoryMock;
|
||||
|
||||
private bool _tcpClientConnected;
|
||||
private Queue<int> _tcpClientConnectTaskDelays;
|
||||
|
||||
private Queue<int> _networkStreamReadDelays;
|
||||
|
||||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
_tcpClientConnected = true;
|
||||
|
||||
_tcpClientConnectTaskDelays = new Queue<int>();
|
||||
_networkStreamReadDelays = new Queue<int>();
|
||||
|
||||
_tcpClientConnectTaskDelays.Enqueue(0);
|
||||
_networkStreamReadDelays.Enqueue(0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldCreateInstance()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act & Assert
|
||||
using var client = new ReconnectTcpClient(HOST, PORT);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow("")]
|
||||
[DataRow(" ")]
|
||||
public void ShouldThrowArgumentNullExceptionOnMissingHost(string host)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act & Assert
|
||||
Assert.ThrowsExactly<ArgumentNullException>(() => new ReconnectTcpClient(host, PORT));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(65536)]
|
||||
public void ShouldThrowArgumentOutOfRangeExceptionOnInvalidPort(int port)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act & Assert
|
||||
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => new ReconnectTcpClient(HOST, port));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldDispose()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
await client.StartAsync(TestContext.CancellationTokenSource.Token);
|
||||
|
||||
// Act
|
||||
client.Dispose();
|
||||
|
||||
// Assert
|
||||
_tcpClientMock.Verify(m => m.Dispose(), Times.Once);
|
||||
_tcpClientMock.VerifyGet(m => m.Client, Times.Once);
|
||||
_tcpClientMock.Verify(m => m.ConnectAsync(HOST, PORT, It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_socketMock.Verify(m => m.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true), Times.Once);
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldDisposeOnlyOnce()
|
||||
{
|
||||
// Arrange
|
||||
using var client = GetClient();
|
||||
await client.StartAsync(TestContext.CancellationTokenSource.Token);
|
||||
|
||||
// Act
|
||||
client.Dispose();
|
||||
client.Dispose();
|
||||
|
||||
// Assert
|
||||
_tcpClientMock.Verify(m => m.Dispose(), Times.Once);
|
||||
_tcpClientMock.VerifyGet(m => m.Client, Times.Once);
|
||||
_tcpClientMock.Verify(m => m.ConnectAsync(HOST, PORT, It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
_socketMock.Verify(m => m.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true), Times.Once);
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldThrowObjectDisposedExceptionOnStart()
|
||||
{
|
||||
// Arrange
|
||||
using var client = GetClient();
|
||||
client.Dispose();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsExactlyAsync<ObjectDisposedException>(async () => await client.StartAsync(TestContext.CancellationTokenSource.Token));
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldThrowObjectDisposedExceptionOnStop()
|
||||
{
|
||||
// Arrange
|
||||
using var client = GetClient();
|
||||
client.Dispose();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsExactlyAsync<ObjectDisposedException>(async () => await client.StopAsync(TestContext.CancellationTokenSource.Token));
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldThrowObjectDisposedExceptionOnGetStream()
|
||||
{
|
||||
// Arrange
|
||||
using var client = GetClient();
|
||||
client.Dispose();
|
||||
|
||||
// Act & Assert
|
||||
Assert.ThrowsExactly<ObjectDisposedException>(() => client.GetStream());
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldReturnIsConnected()
|
||||
{
|
||||
// Arrange
|
||||
_tcpClientConnectTaskDelays.Enqueue(Timeout.Infinite);
|
||||
|
||||
var client = GetClient();
|
||||
await client.StartAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||
|
||||
// Act & Assert
|
||||
_tcpClientConnected = true;
|
||||
Assert.IsTrue(client.IsConnected);
|
||||
|
||||
// Act & Assert
|
||||
_tcpClientConnected = false;
|
||||
Assert.IsFalse(client.IsConnected);
|
||||
|
||||
_socketMock.Verify(m => m.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true), Times.AtLeastOnce);
|
||||
|
||||
_tcpClientMock.VerifyGet(m => m.Client, Times.AtLeastOnce);
|
||||
_tcpClientMock.VerifyGet(m => m.Connected, Times.AtLeast(2));
|
||||
_tcpClientMock.Verify(m => m.ConnectAsync(HOST, PORT, It.IsAny<CancellationToken>()), Times.AtLeastOnce);
|
||||
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
||||
|
||||
_networkStreamMock.Verify(m => m.ReadAsync(It.IsAny<byte[]>(), 0, It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldCallOnConnectedCallback()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
bool callbackCalled = false;
|
||||
client.OnConnected = c =>
|
||||
{
|
||||
callbackCalled = true;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
// Act
|
||||
await client.StartAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||
await client.StopAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(callbackCalled);
|
||||
|
||||
_socketMock.Verify(m => m.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true), Times.Once);
|
||||
|
||||
_tcpClientMock.VerifyGet(m => m.Client, Times.Once);
|
||||
_tcpClientMock.VerifyGet(m => m.Connected, Times.Once);
|
||||
_tcpClientMock.Verify(m => m.ConnectAsync(HOST, PORT, It.IsAny<CancellationToken>()), Times.Once);
|
||||
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
||||
_tcpClientMock.Verify(m => m.Dispose(), Times.Once);
|
||||
|
||||
_networkStreamMock.Verify(m => m.ReadAsync(It.IsAny<byte[]>(), 0, It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldStartAndStopWithoutException()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
|
||||
// Act & Assert
|
||||
await client.StartAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||
await client.StopAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||
|
||||
_socketMock.Verify(m => m.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true), Times.Once);
|
||||
|
||||
_tcpClientMock.VerifyGet(m => m.Client, Times.Once);
|
||||
_tcpClientMock.VerifyGet(m => m.Connected, Times.Once);
|
||||
_tcpClientMock.Verify(m => m.ConnectAsync("localhost", 4711, It.IsAny<CancellationToken>()), Times.Once);
|
||||
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
||||
_tcpClientMock.Verify(m => m.Dispose(), Times.Once);
|
||||
|
||||
_networkStreamMock.Verify(m => m.ReadAsync(It.IsAny<byte[]>(), 0, It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldStopWithoutStart()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
|
||||
// Act & Assert
|
||||
await client.StopAsync(TestContext.CancellationTokenSource.Token);
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldReturnStream()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
await client.StartAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||
|
||||
// Act
|
||||
var stream = client.GetStream();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(stream);
|
||||
Assert.AreEqual(_networkStreamMock.Object, stream);
|
||||
|
||||
_socketMock.Verify(m => m.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true), Times.Once);
|
||||
|
||||
_tcpClientMock.VerifyGet(m => m.Client, Times.Once);
|
||||
_tcpClientMock.VerifyGet(m => m.Connected, Times.Once);
|
||||
_tcpClientMock.Verify(m => m.ConnectAsync("localhost", 4711, It.IsAny<CancellationToken>()), Times.Once);
|
||||
_tcpClientMock.Verify(m => m.GetStream(), Times.Exactly(2));
|
||||
|
||||
_networkStreamMock.Verify(m => m.ReadAsync(It.IsAny<byte[]>(), 0, It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldReturnNullStreamWhenNotConnected()
|
||||
{
|
||||
// Arrange
|
||||
var client = GetClient();
|
||||
|
||||
// Act
|
||||
var stream = client.GetStream();
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(stream);
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldTerminateCleanly()
|
||||
{
|
||||
// Arrange
|
||||
_tcpClientConnectTaskDelays.Clear();
|
||||
_tcpClientConnectTaskDelays.Enqueue(Timeout.Infinite);
|
||||
using var client = GetClient();
|
||||
|
||||
// Act
|
||||
var startTask = client.StartAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||
await client.StopAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||
await startTask;
|
||||
|
||||
// Assert
|
||||
_socketMock.Verify(m => m.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true), Times.Once);
|
||||
|
||||
_tcpClientMock.VerifyGet(m => m.Client, Times.Once);
|
||||
_tcpClientMock.Verify(m => m.ConnectAsync(HOST, PORT, It.IsAny<CancellationToken>()), Times.Once);
|
||||
_tcpClientMock.Verify(m => m.Dispose(), Times.Once);
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(true)]
|
||||
[DataRow(false)]
|
||||
public async Task ShouldLogConnectError(bool useLogger)
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger>();
|
||||
|
||||
using var client = GetClient();
|
||||
if (useLogger)
|
||||
client.Logger = loggerMock.Object;
|
||||
|
||||
_tcpClientMock
|
||||
.Setup(m => m.ConnectAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new SocketException());
|
||||
|
||||
// Act
|
||||
var startTask = client.StartAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(1000, TestContext.CancellationTokenSource.Token); // Should try to connect two times.
|
||||
await client.StopAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||
await startTask;
|
||||
|
||||
// Assert
|
||||
_socketMock.Verify(m => m.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true), Times.Exactly(2));
|
||||
|
||||
_tcpClientMock.VerifyGet(m => m.Client, Times.Exactly(2));
|
||||
_tcpClientMock.Verify(m => m.ConnectAsync(HOST, PORT, It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||
_tcpClientMock.Verify(m => m.Dispose(), Times.Exactly(2));
|
||||
|
||||
if (useLogger)
|
||||
{
|
||||
loggerMock.Verify(
|
||||
m => m.Log(LogLevel.Warning, It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString().Equals($"Failed to connect to {HOST}:{PORT}. Retrying in 500ms...")),
|
||||
It.IsAny<SocketException>(), It.IsAny<Func<It.IsAnyType, Exception, string>>()),
|
||||
Times.Once
|
||||
);
|
||||
loggerMock.Verify(
|
||||
m => m.Log(LogLevel.Warning, It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString().Equals($"Failed to connect to {HOST}:{PORT}. Retrying in 1000ms...")),
|
||||
It.IsAny<SocketException>(), It.IsAny<Func<It.IsAnyType, Exception, string>>()),
|
||||
Times.Once
|
||||
);
|
||||
loggerMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldSkipReconnect()
|
||||
{
|
||||
// Arrange
|
||||
using var client = GetClient();
|
||||
|
||||
_networkStreamMock
|
||||
.Setup(m => m.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new ObjectDisposedException("Test"));
|
||||
|
||||
// Act
|
||||
var startTask = client.StartAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(1000, TestContext.CancellationTokenSource.Token); // Should try to connect two times.
|
||||
await client.StopAsync(TestContext.CancellationTokenSource.Token);
|
||||
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||
await startTask;
|
||||
|
||||
// Assert
|
||||
_socketMock.Verify(m => m.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true), Times.AtLeastOnce);
|
||||
|
||||
_tcpClientMock.VerifyGet(m => m.Client, Times.AtLeastOnce);
|
||||
_tcpClientMock.VerifyGet(m => m.Connected, Times.AtLeast(2));
|
||||
_tcpClientMock.Verify(m => m.ConnectAsync(HOST, PORT, It.IsAny<CancellationToken>()), Times.AtLeastOnce);
|
||||
_tcpClientMock.Verify(m => m.GetStream(), Times.AtLeastOnce);
|
||||
_tcpClientMock.Verify(m => m.Dispose(), Times.AtLeastOnce);
|
||||
|
||||
_networkStreamMock.Verify(m => m.ReadAsync(It.IsAny<byte[]>(), 0, It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
private void VerifyNoOtherCalls()
|
||||
{
|
||||
_socketMock.VerifyNoOtherCalls();
|
||||
_tcpClientMock.VerifyNoOtherCalls();
|
||||
_networkStreamMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
private ReconnectTcpClient GetClient()
|
||||
{
|
||||
_socketMock = new Mock<SocketWrapper>(null);
|
||||
_tcpClientMock = new Mock<TcpClientWrapper>();
|
||||
_networkStreamMock = new Mock<NetworkStreamWrapper>(null);
|
||||
_tcpClientFactoryMock = new Mock<TcpClientWrapperFactory>();
|
||||
|
||||
_tcpClientMock
|
||||
.Setup(m => m.Connected)
|
||||
.Returns(() => _tcpClientConnected);
|
||||
|
||||
_tcpClientMock
|
||||
.Setup(m => m.Client)
|
||||
.Returns(() => _socketMock.Object);
|
||||
|
||||
_tcpClientMock
|
||||
.Setup(m => m.ConnectAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.Returns<string, int, CancellationToken>((_, __, ct) => Task.Delay(_tcpClientConnectTaskDelays.Dequeue(), ct));
|
||||
|
||||
_tcpClientMock
|
||||
.Setup(m => m.GetStream())
|
||||
.Returns(() => _networkStreamMock.Object);
|
||||
|
||||
_networkStreamMock
|
||||
.Setup(m => m.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.Returns<byte[], int, int, CancellationToken>((_, __, count, ct) => Task.Delay(_networkStreamReadDelays.Dequeue(), ct).ContinueWith(t => count, ct));
|
||||
|
||||
_tcpClientFactoryMock
|
||||
.Setup(m => m.Create())
|
||||
.Returns(() => _tcpClientMock.Object);
|
||||
|
||||
var client = new ReconnectTcpClient(HOST, PORT);
|
||||
|
||||
var factoryFieldInfo = client.GetType().GetField("_tcpClientFactory", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
factoryFieldInfo.SetValue(client, _tcpClientFactoryMock.Object);
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user