diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 3fe51a0c..fb07a8e7 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -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: @@ -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 @@ -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 diff --git a/NetSdrClientAppTests/UdpClientWrapperTests.cs b/NetSdrClientAppTests/UdpClientWrapperTests.cs index 9d9b8e18..f6f01b0d 100644 --- a/NetSdrClientAppTests/UdpClientWrapperTests.cs +++ b/NetSdrClientAppTests/UdpClientWrapperTests.cs @@ -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 _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() + { + 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(); - _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(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("_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()), 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."); + + // 2. Verify that UdpClient and CTS fields are set to null (Cleanup logic) + Assert.That(GetPrivateField("_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); + Assert.That(GetPrivateField("_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() { - // 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(); } } } \ No newline at end of file