Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 165 additions & 50 deletions NetSdrClientAppTests/TcpClientWrapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Text; // Added for string sending tests

namespace NetSdrClientAppTests.Networking
{
Expand All @@ -16,22 +17,23 @@
private Mock<INetworkStream> _streamMock = null!;
private TcpClientWrapper _wrapper = null!;

// TaskCompletionSource äëÿ êîíòðîëþ ReadAsync â á³ëüøîñò³ òåñò³â
private TaskCompletionSource<int> _readTcs = null!;

[SetUp]
public void SetUp()
{
_readTcs = new TaskCompletionSource<int>();
_streamMock = new Mock<INetworkStream>();
_clientMock = new Mock<ISystemTcpClient>();

// Setup basic successful behavior
_clientMock.Setup(c => c.GetStream()).Returns(_streamMock.Object);
// Default setup for Connected: it's true after Connect() is called
_clientMock.SetupGet(c => c.Connected).Returns(true);
_streamMock.SetupGet(s => s.CanRead).Returns(true);
_streamMock.SetupGet(s => s.CanWrite).Returns(true);

// Setup ReadAsync to block indefinitely unless cancelled,
// simulating an active connection that doesn't immediately close.
// This prevents the background listener task from immediately ending in most tests.
// Default setup for ReadAsync: blocks indefinitely unless cancelled (via token) or set manually (via _readTcs)
_streamMock
.Setup(s => s.ReadAsync(
It.IsAny<byte[]>(),
Expand All @@ -40,9 +42,9 @@
It.IsAny<CancellationToken>()))
.Returns<byte[], int, int, CancellationToken>((buffer, offset, size, token) =>
{
var tcs = new TaskCompletionSource<int>();
token.Register(() => tcs.TrySetCanceled());
return tcs.Task;
// Ðåºñòðóºìî òîêåí ñêàñóâàííÿ, ùîá ReadAsync çàâåðøèâñÿ, êîëè âèêëèêàºòüñÿ Disconnect
token.Register(() => _readTcs.TrySetCanceled());
return _readTcs.Task;
});

// Factory returning our mock object for testing
Expand All @@ -52,58 +54,77 @@
_wrapper = new TcpClientWrapper("127.0.0.1", 5000, factory);
}

// FIX: Äîäàºìî TearDown äëÿ áåçïå÷íîãî î÷èùåííÿ
[TearDown]
public void TearDown()
{
// Óòèë³çóºìî TCS, ùîá çàïîá³ãòè ïîïåðåäæåííÿì/âèòîêàì
if (_readTcs.Task.Status == TaskStatus.Running)
{
_readTcs.TrySetCanceled();
}
// Öå âèêëèêຠDisconnect() ó á³ëüøîñò³ âèïàäê³â
_wrapper.Disconnect();
}


// ------------------------------------------------------------------
// SCENARIO 1: SUCCESSFUL CONNECTION (Happy Path Coverage)
// SCENARIO 1: SUCCESSFUL CONNECTION
// ------------------------------------------------------------------

[Test]
public void Connect_WhenNotConnected_ShouldConnectAndStartListening()
{
// Arrange: Initial state relies on _tcpClient being null, so _wrapper.Connected is false.

// Act
_wrapper.Connect();

// Assert
// 1. Verify that the Connect() method was called
_clientMock.Verify(c => c.Connect("127.0.0.1", 5000), Times.Once);
// 2. Verify that the stream was retrieved
_clientMock.Verify(c => c.GetStream(), Times.Once);
// FIX: Assert must pass if Connect() was successful and set internal fields
Assert.That(_wrapper.Connected, Is.True);
}

// --- NEW TEST 1: Connect when already connected ---
[Test]
public void Connect_WhenAlreadyConnected_ShouldDoNothing()
{
// Arrange
_wrapper.Connect();
_clientMock.Invocations.Clear(); // Clear first connect invocation

// Act
_wrapper.Connect();

// Assert
// Verify Connect() was NOT called again
_clientMock.Verify(c => c.Connect(It.IsAny<string>(), It.IsAny<int>()), Times.Never);
Assert.That(_wrapper.Connected, Is.True);
}

// ------------------------------------------------------------------
// SCENARIO 2: DISCONNECTION (Disconnect Coverage)
// SCENARIO 2: DISCONNECTION
// ------------------------------------------------------------------

[Test]
public async Task Disconnect_WhenConnected_ShouldCloseResources()
{
// Arrange:
_wrapper.Connect();
// Allow a small delay for the background listener task to start.
await Task.Delay(50);
_clientMock.Invocations.Clear(); // Clear Connect invocations
await Task.Delay(50); // Allow listener task to start

// Act
_wrapper.Disconnect();
// Allow a small delay for the cancellation/closure logic to complete.
await Task.Delay(50);

// Assert: Verify all Close/Cancel were called
_streamMock.Verify(s => s.Close(), Times.Once);
_clientMock.Verify(c => c.Close(), Times.Once);

// Verify that Connected is now false
Assert.That(_wrapper.Connected, Is.False);
}

[Test]
public void Disconnect_WhenNotConnected_ShouldDoNothing()
{
// Arrange: The wrapper starts disconnected

// Act
_wrapper.Disconnect();

Expand All @@ -113,93 +134,187 @@
}

// ------------------------------------------------------------------
// SCENARIO 3: CONNECTION ERROR HANDLING (Connect Error Coverage)
// SCENARIO 3: CONNECTION ERROR HANDLING
// ------------------------------------------------------------------

[Test]
public void Connect_WhenFails_ShouldCatchException()
public void Connect_WhenFails_ShouldCatchExceptionAndCleanUp()
{
// Arrange: Set up mock so Connect throws an exception
_clientMock.Setup(c => c.Connect(It.IsAny<string>(), It.IsAny<int>()))
.Throws(new SocketException(10061));
.Throws(new SocketException(10061));

// Act
_wrapper.Connect();

// Assert: Ensure Connected = false after error
Assert.That(_wrapper.Connected, Is.False);
// Verify that Close was NOT called (no resources to close yet)
_clientMock.Verify(c => c.Close(), Times.Never);
}

// ------------------------------------------------------------------
// SCENARIO 4: DATA SENDING (Send Message Coverage)
// SCENARIO 4: DATA SENDING
// ------------------------------------------------------------------

[Test]
public async Task SendMessageAsync_WhenConnected_ShouldWriteToStream()
{
// Arrange: Ensure Connect was successful
_wrapper.Connect();

// Allow time for listener to start/stabilize
await Task.Delay(50);

byte[] testData = { 0x01, 0x02, 0x03 };

// Act
await _wrapper.SendMessageAsync(testData);

// Assert: Verify WriteAsync was called on the stream with correct data
// Assert
_streamMock.Verify(s => s.WriteAsync(
It.Is<byte[]>(arr => arr == testData),
It.Is<byte[]>(arr => arr == testData), 0, testData.Length, It.IsAny<CancellationToken>()),
Times.Once);
}

// --- NEW TEST 2: Send string message ---
[Test]
public async Task SendMessageAsyncString_WhenConnected_ShouldWriteConvertedBytes()
{
// Arrange
_wrapper.Connect();
await Task.Delay(50);
string testString = "Hello";
byte[] expectedData = Encoding.UTF8.GetBytes(testString);

// Act
await _wrapper.SendMessageAsync(testString);

// Assert: Verify WriteAsync was called with the UTF8-encoded bytes
_streamMock.Verify(s => s.WriteAsync(
It.Is<byte[]>(arr => arr.SequenceEqual(expectedData)),
0,
testData.Length,
expectedData.Length,
It.IsAny<CancellationToken>()),
Times.Once);
}

[Test]
public void SendMessageAsync_WhenNotConnected_ShouldThrowException()
{
// Arrange: The wrapper starts in a disconnected state (Connected = false)

// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(
() => _wrapper.SendMessageAsync(new byte[] { 0x01 }));
}

// ------------------------------------------------------------------
// SCENARIO 5: LISTENING (Listening Coverage - Partial)
// SCENARIO 5: LISTENING LOGIC (Advanced Coverage)
// ------------------------------------------------------------------

[Test]
public async Task StartListeningAsync_WhenCancelled_ShouldStopListeningAndDisconnect()
{
// Arrange
_wrapper.Connect();

// Allow a small delay for the background listener task to start.
await Task.Delay(50);

// Simulate ReadAsync concluding with cancellation (OperationCanceledException)
_streamMock
.Setup(s => s.ReadAsync(
It.IsAny<byte[]>(),
It.IsAny<int>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new OperationCanceledException());

// Act: We explicitly call Disconnect to trigger cancellation/cleanup
// Act: We explicitly call Disconnect, which sets up cancellation
_wrapper.Disconnect();

// Wait for the listening task to catch the cancellation and complete its finally block.
await Task.Delay(100);

// Assert: Verify that stream closure was called
// Assert
_streamMock.Verify(s => s.Close(), Times.AtLeastOnce);

// Verify that client closure was called (part of Disconnect cleanup)
_clientMock.Verify(c => c.Close(), Times.AtLeastOnce);
Assert.That(_wrapper.Connected, Is.False);
}

// --- NEW TEST 3: Connection closed by remote host (bytesRead == 0) ---
[Test]
public async Task StartListeningAsync_WhenRemoteCloses_ShouldExitLoopAndDisconnect()
{
// Arrange: Setup ReadAsync to return 0 on the first call
_streamMock.Setup(s => s.ReadAsync(
It.IsAny<byte[]>(),
It.IsAny<int>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(0); // Simulates remote closure

_wrapper.Connect();

// Allow listening loop to run once and hit the ReadAsync=0 line
await Task.Delay(100);

// Assert: Should have called Disconnect internally
_streamMock.Verify(s => s.Close(), Times.Once);
_clientMock.Verify(c => c.Close(), Times.Once);
Assert.That(_wrapper.Connected, Is.False);
}

// --- NEW TEST 4: Message received and event invoked (bytesRead > 0) ---
[Test]
public async Task StartListeningAsync_WhenDataReceived_ShouldRaiseEvent()
{
// Arrange
byte[] testData = { 0xAA, 0xBB, 0xCC };
byte[] receivedData = Array.Empty<byte>();

// Setup ReadAsync to return data on the first call, then block indefinitely
var callCount = 0;
_streamMock.Setup(s => s.ReadAsync(
It.IsAny<byte[]>(),
It.IsAny<int>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>()))
.Returns<byte[], int, int, CancellationToken>((buffer, offset, size, token) =>
{
if (Interlocked.Increment(ref callCount) == 1)
{
// Simulate data reception
Array.Copy(testData, buffer, testData.Length);
return Task.FromResult(testData.Length);
}
// Block subsequent calls
token.Register(() => _readTcs.TrySetCanceled());
return _readTcs.Task;
});

// Subscribe to the event
_wrapper.MessageReceived += (sender, data) => receivedData = data;

_wrapper.Connect();

// Wait long enough for the loop to execute the first ReadAsync call
await Task.Delay(100);

// Assert
Assert.That(receivedData.SequenceEqual(testData), Is.True, "Received data should match the test data.");

Check warning on line 290 in NetSdrClientAppTests/TcpClientWrapperTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Call independent Assert statements from inside an Assert.Multiple

See more on https://sonarcloud.io/project/issues?id=YehorYurch5_NetSdrClient&issues=AZqynqjVN_2oV1EDFW07&open=AZqynqjVN_2oV1EDFW07&pullRequest=13
Assert.That(_wrapper.Connected, Is.True, "Connection should still be active.");

// Cleanup: ensure the block is cancelled
_wrapper.Disconnect();
}

// --- NEW TEST 5: General exception in listening loop ---
[Test]
public async Task StartListeningAsync_WhenGeneralExceptionOccurs_ShouldExitLoopAndDisconnect()
{
// Arrange: Setup ReadAsync to throw a generic exception
_streamMock.Setup(s => s.ReadAsync(
It.IsAny<byte[]>(),
It.IsAny<int>(),
It.IsAny<int>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Simulated stream error"));

_wrapper.Connect();

// Allow listening loop to run once and hit the exception handler
await Task.Delay(100);

// Assert: Should have called Disconnect internally
_streamMock.Verify(s => s.Close(), Times.Once);
_clientMock.Verify(c => c.Close(), Times.Once);
Assert.That(_wrapper.Connected, Is.False);
}
}
}
Loading