Skip to content
Merged
Show file tree
Hide file tree
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
35 changes: 4 additions & 31 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,3 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# This workflow helps you trigger a SonarCloud analysis of your code and populates
# GitHub Code Scanning alerts with the vulnerabilities found.
# Free for open source project.

# 1. Login to SonarCloud.io using your GitHub account

# 2. Import your project on SonarCloud
# * Add your GitHub organization first, then add your repository as a new project.
# * Please note that many languages are eligible for automatic analysis,
# which means that the analysis will start automatically without the need to set up GitHub Actions.
# * This behavior can be changed in Administration > Analysis Method.
#
# 3. Follow the SonarCloud in-product tutorial
# * a. Copy/paste the Project Key and the Organization Key into the args parameter below
# (You'll find this information in SonarCloud. Click on "Information" at the bottom left)
#
# * b. Generate a new token and add it to your Github repository's secrets using the name SONAR_TOKEN
# (On SonarCloud, click on your avatar on top-right > My account > Security
# or go directly to https://sonarcloud.io/account/security/)

# Feel free to take a look at our documentation (https://docs.sonarcloud.io/getting-started/github/)
# or reach out to our community forum if you need some help (https://community.sonarsource.com/c/help/sc/9)

name: SonarCloud analysis

on:
Expand All @@ -41,9 +13,9 @@ permissions:
jobs:
sonar-check:
name: Sonar Check
runs-on: windows-latest # безпечно для будь-яких .NET проектів
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4 # <-- ВИПРАВЛЕНО: Відновлено коректне використання '- uses:'
with:
fetch-depth: 0

Expand All @@ -63,7 +35,8 @@ jobs:
/d:sonar.cs.opencover.reportsPaths="EchoServerTests/TestResults/coverage.xml,NetSdrClientAppTests/TestResults/coverage.xml" `
/d:sonar.cpd.cs.minimumTokens=40 `
/d:sonar.cpd.cs.minimumLines=5 `
/d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml,**/EchoTspServer/Presentation/Program.cs,**NetSdrClient/NetSdrClientApp/Program.cs `
/d:sonar.exclusions="**/bin/**,**/obj/**,**/sonarcloud.yml,**/EchoTspServer/Presentation/Program.cs,**/NetSdrClient/NetSdrClientApp/Program.cs,**/Program.cs" `
/d:sonar.coverage.exclusions="**/Program.cs,**/EchoTspServer/Presentation/Program.cs,**/NetSdrClientApp/Program.cs"
shell: pwsh

# 2) BUILD & TEST
Expand Down
136 changes: 103 additions & 33 deletions NetSdrClientAppTests/UdpClientWrapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,116 +4,186 @@
using System;
using System.Threading;
using System.Net;
using System.Net.Sockets; // Added for UdpClient, SocketException
using System.Security.Cryptography;
using System.Text;
using System.Linq;
// FIX: Add using directive to resolve CS0246 errors for IHashAlgorithm and UdpClientWrapper
using NetSdrClientApp.Networking;
using System.Reflection; // Required for accessing private fields

namespace NetSdrClientAppTests.Networking
{
// FIX: Changed namespace back to NetSdrClientAppTests.Networking based on the error output context.
[TestFixture]
public class UdpClientWrapperTests
{
private Mock<IHashAlgorithm> _hashMock = null!;
// Non-nullable field requires initialization in SetUp or constructor, and disposal in TearDown
private UdpClientWrapper _wrapper = null!;
private const int TestPort = 55555;

// FIX: Using a random dynamic port to avoid "Only one usage of each socket address" error
private int GetAvailablePort()

Check warning on line 23 in NetSdrClientAppTests/UdpClientWrapperTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'GetAvailablePort' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=YehorYurch5_NetSdrClient&issues=AZqx3YjabmnrA-bVNJ-U&open=AZqx3YjabmnrA-bVNJ-U&pullRequest=11
{
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
{
// Bind to 0, which tells OS to find a free port
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
return ((IPEndPoint)socket.LocalEndPoint!).Port;
}
}

[SetUp]
public void SetUp()
{
_hashMock = new Mock<IHashAlgorithm>();
_wrapper = new UdpClientWrapper(TestPort, _hashMock.Object);
// Use a new available port for each test run
int testPort = GetAvailablePort();
_wrapper = new UdpClientWrapper(testPort, _hashMock.Object);
}

// FIX NUnit1032: Dispose the IDisposable field (_wrapper) after each test run.
[TearDown]
public void TearDown()
{
_wrapper?.Dispose();
}

// Helper to access private fields for testing internal state
private T? GetPrivateField<T>(string fieldName) where T : class
{
var field = typeof(UdpClientWrapper).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
return (T?)field?.GetValue(_wrapper);
}

private void SetPrivateField(string fieldName, object? value)
{
var field = typeof(UdpClientWrapper).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
field?.SetValue(_wrapper, value);
}


// ------------------------------------------------------------------
// TEST 1: CONSTRUCTOR (Constructor coverage)
// TEST 1: CONSTRUCTOR
// ------------------------------------------------------------------
[Test]
public void Constructor_ShouldInitializeCorrectly()
{
// Assert
// Check that the object is created and hashAlgorithm is injected
Assert.That(_wrapper, Is.Not.Null);
}

// ------------------------------------------------------------------
// TEST 2: GET HASH CODE (Hashing logic coverage)
// TEST 2: GET HASH CODE (FIXED: Reliable hash check)
// ------------------------------------------------------------------
[Test]
public void GetHashCode_ShouldCallComputeHashAndReturnInt()
public void GetHashCode_ShouldReturnConsistentHash()
{
// Arrange
byte[] fakeHash = new byte[4] { 0x01, 0x02, 0x03, 0x04 }; // 4 bytes = int32
// Using a new, temporary wrapper for comparison
var wrapper2 = new UdpClientWrapper(GetPrivateField<IPEndPoint>("_localEndPoint")!.Port, _hashMock.Object);

// Act
int hashCode = _wrapper.GetHashCode();
int hashCode1 = _wrapper.GetHashCode();
int hashCode2 = wrapper2.GetHashCode();

// Assert
// Verify that ComputeHash method was NOT called (UdpClientWrapper uses HashCode.Combine now)
_hashMock.Verify(h => h.ComputeHash(It.IsAny<byte[]>()), Times.Never);

// Verify that the hash code is generated
Assert.That(hashCode, Is.Not.EqualTo(0));
Assert.That(hashCode1, Is.EqualTo(hashCode2));
Assert.That(hashCode1, Is.Not.EqualTo(0), "Hash code should not be default 0.");
}

// ------------------------------------------------------------------
// TEST 3: STOP LISTENING (Cleanup methods coverage)
// TEST 3: STOP LISTENING/CLEANUP LOGIC (FIXED: Avoids Moq limitations and SocketException)
// ------------------------------------------------------------------

[Test]
public void StopListening_ShouldCallCleanup()
public void StopListening_ShouldCancelTokenAndCleanupUdpClient()
{
// Arrange: Simulate StartListeningAsync having run successfully
var cts = new CancellationTokenSource();

// We must create a real UdpClient to ensure Cleanup can call .Close() and .Dispose() without NRE,
// but we use a *different* port than the wrapper's main port.
var tempClient = new UdpClient(GetAvailablePort());

SetPrivateField("_cts", cts);
SetPrivateField("_udpClient", tempClient);

// Act
_wrapper.StopListening();

// Assert: Ensure the method did not throw an exception (testing happy path Cleanup)
Assert.Pass();
// Assert
// 1. Verify that the cancellation was requested
Assert.That(cts.IsCancellationRequested, Is.True, "Cancellation token should be cancelled.");

Check warning on line 112 in NetSdrClientAppTests/UdpClientWrapperTests.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=AZqx3YjabmnrA-bVNJ-S&open=AZqx3YjabmnrA-bVNJ-S&pullRequest=11

// 2. Verify that UdpClient and CTS fields are set to null (Cleanup logic)
Assert.That(GetPrivateField<UdpClient>("_udpClient"), Is.Null, "Internal UdpClient should be nullified after Cleanup.");

// 3. Verify that the client is actually closed (important for the listener thread)
// We cannot verify .Close() with Moq, so checking the state is the next best thing.
}

[Test]
public void Exit_ShouldCallCleanup()
public void Exit_ShouldCancelTokenAndCleanupUdpClient()
{
// Arrange: Simulate running state
var cts = new CancellationTokenSource();
var tempClient = new UdpClient(GetAvailablePort());

SetPrivateField("_cts", cts);
SetPrivateField("_udpClient", tempClient);

// Act
_wrapper.Exit();

// Assert: Ensure the method did not throw an exception
Assert.Pass();
// Assert
Assert.That(cts.IsCancellationRequested, Is.True);

Check warning on line 135 in NetSdrClientAppTests/UdpClientWrapperTests.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=AZqx3YjabmnrA-bVNJ-T&open=AZqx3YjabmnrA-bVNJ-T&pullRequest=11
Assert.That(GetPrivateField<UdpClient>("_udpClient"), Is.Null, "Internal UdpClient should be nullified after Exit/Cleanup.");
}

// ------------------------------------------------------------------
// TEST 4: DISPOSE (Dispose coverage)
// TEST 4: DISPOSE (Coverage: Dispose(bool), IDisposable implementation)
// ------------------------------------------------------------------

[Test]
public void Dispose_ShouldStopSendingAndDisposeHash()
public void Dispose_ShouldCallCleanupDisposeHashAndMarkAsDisposed()
{
// Arrange
var cts = new CancellationTokenSource();
SetPrivateField("_cts", cts);

// Act
_wrapper.Dispose();

// Assert: Verify that Dispose was called for the injected object
_hashMock.Verify(h => h.Dispose(), Times.Once);
// Assert
// 1. Verify HashAlgorithm dispose
_hashMock.Verify(h => h.Dispose(), Times.Once, "IHashAlgorithm should be disposed.");

// 2. Verify CTS dispose
// Since CTS is disposed internally, we can check if a second dispose throws,
// but the safer way is to check if it was cancelled by Cleanup.
Assert.That(cts.IsCancellationRequested, Is.True, "Dispose should call Cleanup, which cancels CTS.");

// 3. Verify idempotency
_wrapper.Dispose();
_hashMock.Verify(h => h.Dispose(), Times.Once, "Dispose should be idempotent.");
}

// ------------------------------------------------------------------
// TEST 5: START LISTENING (Exception-throwing code coverage)
// TEST 5: START LISTENING (Placeholder logic improvement)
// ------------------------------------------------------------------

[Test]
public void StartListeningAsync_ShouldHandleExceptionInStartup()
public async Task StartListeningAsync_ShouldHandleStartWhenAlreadyRunning()

Check warning on line 172 in NetSdrClientAppTests/UdpClientWrapperTests.cs

View workflow job for this annotation

GitHub Actions / Sonar Check

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 172 in NetSdrClientAppTests/UdpClientWrapperTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

See more on https://sonarcloud.io/project/issues?id=YehorYurch5_NetSdrClient&issues=AZqx3YjabmnrA-bVNJ-R&open=AZqx3YjabmnrA-bVNJ-R&pullRequest=11
{
// Note: This test is a placeholder and should pass without testing asynchronous logic.
Assert.Pass("StartListeningAsync cannot be unit-tested without refactoring UdpClient creation.");
// Arrange: Setup private CTS to simulate "already running"
var cts = new CancellationTokenSource();
SetPrivateField("_cts", cts);

// Act
var task = _wrapper.StartListeningAsync(); // Should exit early because CTS is not cancelled

// Assert: The task should complete instantly (or near-instantly)
// It's hard to verify *instantly* without async issues, so we check state.
Assert.That(cts.IsCancellationRequested, Is.False, "Existing CTS should not be cancelled if StartListening exits early.");

// Clean up for TearDown
cts.Dispose();
}
}
}
Loading