diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..fc752d1c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + # Configuration for the NuGet package ecosystem (.NET / C#) + - package-ecosystem: "nuget" + # Path to the manifest files (csproj) check the root of the repository + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e7840696..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,10 +13,11 @@ permissions: jobs: sonar-check: name: Sonar Check - runs-on: windows-latest # безпечно для будь-яких .NET проектів + runs-on: windows-latest steps: - - uses: actions/checkout@v4 - with: { fetch-depth: 0 } + - uses: actions/checkout@v4 # <-- ВИПРАВЛЕНО: Відновлено коректне використання '- uses:' + with: + fetch-depth: 0 - uses: actions/setup-dotnet@v4 with: @@ -56,28 +29,33 @@ jobs: dotnet tool install --global dotnet-sonarscanner echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH dotnet sonarscanner begin ` - /k:"ppanchen_NetSdrClient" ` - /o:"ppanchen" ` + /k:"YehorYurch5_NetSdrClient" ` + /o:"yehoryurch5-kai" ` /d:sonar.token="${{ secrets.SONAR_TOKEN }}" ` - /d:sonar.cs.opencover.reportsPaths="**/coverage.xml" ` + /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 ` - /d:sonar.qualitygate.wait=true + /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 - name: Restore run: dotnet restore NetSdrClient.sln + - name: Build run: dotnet build NetSdrClient.sln -c Release --no-restore - #- name: Tests with coverage (OpenCover) - # run: | - # dotnet test NetSdrClientAppTests/NetSdrClientAppTests.csproj -c Release --no-build ` - # /p:CollectCoverage=true ` - # /p:CoverletOutput=TestResults/coverage.xml ` - # /p:CoverletOutputFormat=opencover - # shell: pwsh + + - name: Tests with coverage (OpenCover) + run: | + # 🔹 Автоматично запускає всі тестові проєкти у рішенні (.sln) + dotnet test NetSdrClient.sln -c Release --no-build ` + /p:CollectCoverage=true ` + /p:CoverletOutput=TestResults/coverage.xml ` + /p:CoverletOutputFormat=opencover + shell: pwsh + # 3) END: SonarScanner - name: SonarScanner End run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" - shell: pwsh + shell: pwsh \ No newline at end of file diff --git a/.sonarqube/conf/0/FilesToAnalyze.txt b/.sonarqube/conf/0/FilesToAnalyze.txt new file mode 100644 index 00000000..a3f8ad80 --- /dev/null +++ b/.sonarqube/conf/0/FilesToAnalyze.txt @@ -0,0 +1,9 @@ +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoTspServer\Application\Interfaces\IClientHandler.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoTspServer\Application\Interfaces\IEchoServer.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoTspServer\Application\Interfaces\ILogger.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoTspServer\Application\Interfaces\IUdpSender.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoTspServer\Application\Services\ClientHandler.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoTspServer\Application\Services\EchoServer.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoTspServer\Application\Services\UdpTimedSender.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoTspServer\Presentation\Infrastructure\ConsoleLogger.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoTspServer\Presentation\Program.cs diff --git a/.sonarqube/conf/0/ProjectOutFolderPath.txt b/.sonarqube/conf/0/ProjectOutFolderPath.txt new file mode 100644 index 00000000..7c001277 --- /dev/null +++ b/.sonarqube/conf/0/ProjectOutFolderPath.txt @@ -0,0 +1 @@ +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\out\0 diff --git a/.sonarqube/conf/0/SonarProjectConfig.xml b/.sonarqube/conf/0/SonarProjectConfig.xml new file mode 100644 index 00000000..981db8fa --- /dev/null +++ b/.sonarqube/conf/0/SonarProjectConfig.xml @@ -0,0 +1,9 @@ + + + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\SonarQubeAnalysisConfig.xml + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoTspServer\EchoServer.csproj + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\0\FilesToAnalyze.txt + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\out\0 + Product + net8.0 + \ No newline at end of file diff --git a/.sonarqube/conf/1/FilesToAnalyze.txt b/.sonarqube/conf/1/FilesToAnalyze.txt new file mode 100644 index 00000000..141d2263 --- /dev/null +++ b/.sonarqube/conf/1/FilesToAnalyze.txt @@ -0,0 +1,7 @@ +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientApp\Messages\NetSdrMessageHelper.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientApp\NetSdrClient.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientApp\Networking\ITcpClient.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientApp\Networking\IUdpClient.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientApp\Networking\TcpClientWrapper.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientApp\Networking\UdpClientWrapper.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientApp\Program.cs diff --git a/.sonarqube/conf/1/ProjectOutFolderPath.txt b/.sonarqube/conf/1/ProjectOutFolderPath.txt new file mode 100644 index 00000000..39cc5f64 --- /dev/null +++ b/.sonarqube/conf/1/ProjectOutFolderPath.txt @@ -0,0 +1 @@ +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\out\1 diff --git a/.sonarqube/conf/1/SonarProjectConfig.xml b/.sonarqube/conf/1/SonarProjectConfig.xml new file mode 100644 index 00000000..010b9035 --- /dev/null +++ b/.sonarqube/conf/1/SonarProjectConfig.xml @@ -0,0 +1,9 @@ + + + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\SonarQubeAnalysisConfig.xml + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientApp\NetSdrClientApp.csproj + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\1\FilesToAnalyze.txt + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\out\1 + Product + net8.0 + \ No newline at end of file diff --git a/.sonarqube/conf/2/FilesToAnalyze.txt b/.sonarqube/conf/2/FilesToAnalyze.txt new file mode 100644 index 00000000..3ad8ec79 --- /dev/null +++ b/.sonarqube/conf/2/FilesToAnalyze.txt @@ -0,0 +1,13 @@ +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientAppTests\ArchitectureTests.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientAppTests\NetSdrClientTests.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientAppTests\NetSdrMessageHelperTests.cs +C:\Users\compa\.nuget\packages\microsoft.net.test.sdk\18.0.0\build\net8.0\Microsoft.NET.Test.Sdk.Program.cs +C:\Users\compa\.nuget\packages\microsoft.testplatform.testhost\18.0.0\build\net8.0\x64\testhost.exe +C:\Users\compa\.nuget\packages\microsoft.testplatform.testhost\18.0.0\build\net8.0\x64\testhost.dll +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\NUnit3.TestAdapter.dll +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\NUnit3.TestAdapter.pdb +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\nunit.engine.dll +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\nunit.engine.api.dll +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\nunit.engine.core.dll +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\testcentric.engine.metadata.dll +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientAppTests\TestResults\coverage.xml diff --git a/.sonarqube/conf/2/ProjectOutFolderPath.txt b/.sonarqube/conf/2/ProjectOutFolderPath.txt new file mode 100644 index 00000000..86126c3e --- /dev/null +++ b/.sonarqube/conf/2/ProjectOutFolderPath.txt @@ -0,0 +1 @@ +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\out\2 diff --git a/.sonarqube/conf/2/SonarProjectConfig.xml b/.sonarqube/conf/2/SonarProjectConfig.xml new file mode 100644 index 00000000..9e675326 --- /dev/null +++ b/.sonarqube/conf/2/SonarProjectConfig.xml @@ -0,0 +1,9 @@ + + + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\SonarQubeAnalysisConfig.xml + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\NetSdrClientAppTests\NetSdrClientAppTests.csproj + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\2\FilesToAnalyze.txt + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\out\2 + Test + net8.0 + \ No newline at end of file diff --git a/.sonarqube/conf/3/FilesToAnalyze.txt b/.sonarqube/conf/3/FilesToAnalyze.txt new file mode 100644 index 00000000..cfecdb23 --- /dev/null +++ b/.sonarqube/conf/3/FilesToAnalyze.txt @@ -0,0 +1,13 @@ +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoServerTests\ClientHandlerTests.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoServerTests\ConsoleLoggerTests.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoServerTests\EchoServerTests.cs +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoServerTests\UdpTimedSenderTests.cs +C:\Users\compa\.nuget\packages\microsoft.net.test.sdk\18.0.0\build\net8.0\Microsoft.NET.Test.Sdk.Program.cs +C:\Users\compa\.nuget\packages\microsoft.testplatform.testhost\18.0.0\build\net8.0\x64\testhost.exe +C:\Users\compa\.nuget\packages\microsoft.testplatform.testhost\18.0.0\build\net8.0\x64\testhost.dll +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\NUnit3.TestAdapter.dll +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\NUnit3.TestAdapter.pdb +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\nunit.engine.dll +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\nunit.engine.api.dll +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\nunit.engine.core.dll +C:\Users\compa\.nuget\packages\nunit3testadapter\4.5.0\build\netcoreapp3.1\testcentric.engine.metadata.dll diff --git a/.sonarqube/conf/3/ProjectOutFolderPath.txt b/.sonarqube/conf/3/ProjectOutFolderPath.txt new file mode 100644 index 00000000..1529b87e --- /dev/null +++ b/.sonarqube/conf/3/ProjectOutFolderPath.txt @@ -0,0 +1 @@ +C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\out\3 diff --git a/.sonarqube/conf/3/SonarProjectConfig.xml b/.sonarqube/conf/3/SonarProjectConfig.xml new file mode 100644 index 00000000..568bc0fa --- /dev/null +++ b/.sonarqube/conf/3/SonarProjectConfig.xml @@ -0,0 +1,9 @@ + + + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\SonarQubeAnalysisConfig.xml + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\EchoServerTests\EchoServerTests.csproj + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\3\FilesToAnalyze.txt + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\out\3 + Test + net8.0 + \ No newline at end of file diff --git a/.sonarqube/conf/Sonar-cs-none.ruleset b/.sonarqube/conf/Sonar-cs-none.ruleset new file mode 100644 index 00000000..ab8adc2a --- /dev/null +++ b/.sonarqube/conf/Sonar-cs-none.ruleset @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.sonarqube/conf/Sonar-cs.ruleset b/.sonarqube/conf/Sonar-cs.ruleset new file mode 100644 index 00000000..cd63aeae --- /dev/null +++ b/.sonarqube/conf/Sonar-cs.ruleset @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.sonarqube/conf/Sonar-vbnet-none.ruleset b/.sonarqube/conf/Sonar-vbnet-none.ruleset new file mode 100644 index 00000000..906b8828 --- /dev/null +++ b/.sonarqube/conf/Sonar-vbnet-none.ruleset @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.sonarqube/conf/Sonar-vbnet.ruleset b/.sonarqube/conf/Sonar-vbnet.ruleset new file mode 100644 index 00000000..5fedd671 --- /dev/null +++ b/.sonarqube/conf/Sonar-vbnet.ruleset @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.sonarqube/conf/SonarQubeAnalysisConfig.xml b/.sonarqube/conf/SonarQubeAnalysisConfig.xml new file mode 100644 index 00000000..6e075787 --- /dev/null +++ b/.sonarqube/conf/SonarQubeAnalysisConfig.xml @@ -0,0 +1,274 @@ + + + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\out + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\bin + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient + C:\Users\compa\.sonar\cache\4bafe2e94439c8193fc8c68247cb0dbaf4e80265b903288f63f128304f129bbe\OpenJDK17U-jre_x64_windows_hotspot_17.0.11_9.zip_extracted\jdk-17.0.11+9-jre/bin/java.exe + C:\Users\compa\.sonar\cache\f82851d7412d499c880b607e7c196afcf401ec1de63c79e2a2759ba2fddb35d1\sonarcloud-scanner-engine-12.6.0.1208-all.jar + false + true + true + false + https://sonarcloud.io + 8.0.0.76401 + YehorYurch5_NetSdrClient + + + + + + + + + + false + 11.11.0.40669 + true + .c,.h + .ts,.tsx,.cts,.mts + true + ibm-enterprise-cobol + 11.11.0.40669 + true + SonarCloud + DISABLED + false + ipynb + oracle.jdbc.OracleDriver + true + **/vendor/** + .tf + false + 60 + .cc,.cpp,.cxx,.c++,.hh,.hpp,.hxx,.h++,.ipp,.ixx,.mxx,.cppm,.ccm,.cxxm,.c++m + .jcl + .rs + SonarAnalyzer.Enterprise.CSharp + #@$ + false + https://schema.management.azure.com/schemas/,http://schema.management.azure.com/schemas/ + **/src/main/resources/**/*app*.properties,**/src/main/resources/**/*app*.yaml,**/src/main/resources/**/*app*.yml + false + .css,.less,.scss,.sass + securityvbnetfrontend + true + 100 + Dockerfile,*.dockerfile + .html,.xhtml,.cshtml,.vbhtml,.aspx,.ascx,.rhtml,.erb,.shtm,.shtml,.cmp,.twig + .bas,.frm,.ctl + true + 12 + false + vbnetenterprise + .scala + false + true + 72 + true + SonarAnalyzer.Architecture-2.9.0.6894.zip + .cls,.trigger + SonarAnalyzer.Enterprise.VisualBasic + AWSTemplateFormatVersion + true + 30 + False + 4 + https://secure.gravatar.com/avatar/{EMAIL_MD5}.jpg?s={SIZE}&d=identicon + 11.11.0.40669 + true + fixed + 600 + .jsp,.jspf,.jspx + true + 1000 + amd,applescript,atomtest,browser,commonjs,embertest,greasemonkey,jasmine,jest,jquery,meteor,mocha,mongo,nashorn,node,phantomjs,prototypejs,protractor,qunit,serviceworker,shared-node-browser,shelljs,webextensions,worker + false + **/vendor/** + .dart + true + .vb + SonarAnalyzer.Security.CSharp-11.11.0.40669.zip + true + .rpg,.rpgle,.sqlrpgle,.RPG,.RPGLE,.SQLRPGLE + .abap,.ab4,.flow,.asprog + false + true + 10.15.0.120848 + 30 + py + .cs,.razor + sql,tab,pkb + SonarAnalyzer.Security.CSharp + 8 + SonarAnalyzer-vbnetenterprise-10.15.0.120848.zip + false + true + true + .java,.jav + .kt,.kts + php,php3,php4,php5,phtml,inc + .xml,.xsd,.xsl,.config + 260 + true + true + true + coverage-reports/*coverage-*.xml + true + false + true + .go + 30 + true + DISABLED + true + 104 + true + **/vendor/** + false + true + .swift + false + false + as + true + 20 + .rb + true + xunit-reports/xunit-result-*.xml + 2 + angular,goog,google,OpenLayers,d3,dojo,dojox,dijit,Backbone,moment,casper,_,sap + 24 + .yaml,.yml + architecturecsharpfrontend + true + true + false + Model is not configured. + 10.15.0.120848 + noreply@sonarcloud.io + 10.15.0.120848 + SonarAnalyzer.Security.CSharp + 1 + SonarAnalyzer.Enterprise.VisualBasic + [SonarCloud] + false + csharpenterprise + true + 100 + False + SonarAnalyzer-vbnetenterprise-10.15.0.120848.zip + .json + securitycsharpfrontend + true + true + SonarAnalyzer.Security.VisualBasic-11.11.0.40669.zip + (branch|release)-.* + .m + false + coverage/.resultset.json + **/*.sh,**/*.bash,**/*.zsh,**/*.ksh,**/*.ps1,**/*.properties,**/*.conf,**/*.pem,**/*.config,.env,.aws/config,**/*.key + true + true + SonarAnalyzer-csharpenterprise-10.15.0.120848.zip + true + SonarAnalyzer-csharpenterprise-10.15.0.120848.zip + false + SonarAnalyzer.Enterprise.CSharp + csharpenterprise + securitycsharpfrontend + true + 0.05,0.1,0.2,0.5 + 2.9.0.6894 + true + 30000 + true + 10.15.0.120848 + SonarAnalyzer.Security.CSharp-11.11.0.40669.zip + .bicep + .js,.jsx,.cjs,.mjs,.vue + 20 + Directives are not configured. + .sh,.bash + false + .pli + false + vbnetenterprise + true + 2000000 + .tsql + https://sonarcloud.io + 105 + 1BD809FA-AWHW8ct9-T_TB3XqouNu + 1671634414000 + brave_brave-core,simgrid_simgrid,apache_struts,microsoft_vscode-python,mediawiki-core,jhipster-sample-application,JMeter,typo3,org.xwiki.platform:xwiki-platform,apache_ofbiz-framework,org.nuxeo:nuxeo-ecm,monica,sonarlint-visualstudio + OXYGEN:* + **/build-wrapper-dump.json + SonarCloud will undergo maintenance on Thursday, September 28th. +Automatic Analysis will not be available between 07:00 CET and 09:00 CET + https://community.sonarsource.com/t/sonarcloud-autoscan-maintenance-september-28th-07-00-and-09-00-cet/101442 + 2023-09-28T09:10:00:00.000+01:00 + https://api.sonarcloud.io/analysis + 11/7/2025 4:53:43 PM + + + yehoryurch5-kai + + + Windows-ROOT + + + + cs + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\Sonar-cs.ruleset + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\Sonar-cs-none.ruleset + + + + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\0\Google.Protobuf.License.txt + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\0\SonarAnalyzer.Architecture.CSharp.dll + + + + + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\1\Google.Protobuf.License.txt + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\1\SonarAnalyzer.Security.CSharp.dll + + + + + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\2\SonarAnalyzer.CSharp.dll + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\2\SonarAnalyzer.Enterprise.CSharp.dll + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\2\THIRD-PARTY-NOTICES.txt + + + + + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\cs\SonarLint.xml + + + + vbnet + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\Sonar-vbnet.ruleset + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\Sonar-vbnet-none.ruleset + + + + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\3\Google.Protobuf.License.txt + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\3\SonarAnalyzer.Security.VisualBasic.dll + + + + + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\4\SonarAnalyzer.Enterprise.VisualBasic.dll + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\4\SonarAnalyzer.VisualBasic.dll + C:\Users\compa\AppData\Local\Temp\.sonarqube\resources\4\THIRD-PARTY-NOTICES.txt + + + + + C:\Users\compa\source\repos\YehorYurch5\NetSdrClient\.sonarqube\conf\vbnet\SonarLint.xml + + + + \ No newline at end of file diff --git a/.sonarqube/conf/cs/SonarLint.xml b/.sonarqube/conf/cs/SonarLint.xml new file mode 100644 index 00000000..65b7c2de --- /dev/null +++ b/.sonarqube/conf/cs/SonarLint.xml @@ -0,0 +1,1140 @@ + + + + + sonar.cs.ignoreHeaderComments + true + + + sonar.cs.analyzeGeneratedCode + false + + + sonar.cs.file.suffixes + .cs,.razor + + + sonar.cs.analyzeRazorCode + true + + + sonar.cs.roslyn.ignoreIssues + false + + + + + S6562 + + + S3168 + + + S5344 + + + S3885 + + + S1854 + + + S3267 + + + S4036 + + + S2245 + + + S1215 + + + S1313 + + + S2068 + + + credentialWords + password, passwd, pwd, passphrase + + + + + S6418 + + + secretWords + api[_\-]?key, auth, credential, secret, token + + + randomnessSensibility + 3 + + + + + S5122 + + + S4790 + + + S1871 + + + S3949 + + + S3626 + + + S5445 + + + S3776 + + + threshold + 15 + + + propertyThreshold + 3 + + + + + S4502 + + + S2696 + + + S3236 + + + S5547 + + + S3330 + + + S2755 + + + S2612 + + + S1699 + + + S4830 + + + S5034 + + + S6377 + + + S7039 + + + S110 + + + max + 5 + + + + + S5332 + + + S2184 + + + S6781 + + + S5443 + + + S2053 + + + S2092 + + + S1133 + + + S7130 + + + S2737 + + + S2115 + + + S2259 + + + S2479 + + + S1192 + + + threshold + 3 + + + + + S1125 + + + S1135 + + + S4426 + + + S7133 + + + S7131 + + + S4487 + + + S2589 + + + S2222 + + + S2583 + + + S1264 + + + S3329 + + + S3655 + + + S5773 + + + S3247 + + + S1155 + + + S2629 + + + S3966 + + + S1643 + + + S125 + + + S2930 + + + S3169 + + + S4347 + + + S4158 + + + S2325 + + + S2201 + + + S6932 + + + S1751 + + + S1764 + + + S3981 + + + S3433 + + + S3443 + + + S6800 + + + S6931 + + + S2699 + + + S6934 + + + S6930 + + + S3427 + + + S3237 + + + S2275 + + + S2368 + + + S6964 + + + S6967 + + + S6960 + + + S6968 + + + S1048 + + + S3464 + + + S2970 + + + S2857 + + + S3875 + + + S2306 + + + S3877 + + + S2551 + + + S2437 + + + S3889 + + + S3869 + + + S2953 + + + S6668 + + + S6667 + + + S6669 + + + format + ^_?[Ll]og(ger)?$ + + + + + S6422 + + + S6424 + + + S2187 + + + S6675 + + + S6674 + + + S6798 + + + S6670 + + + S6672 + + + S2190 + + + S2178 + + + S4159 + + + S3060 + + + S2225 + + + S2346 + + + S2223 + + + S5856 + + + S2344 + + + S2345 + + + S4524 + + + S1134 + + + S3431 + + + S2342 + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + flagsAttributeFormat + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$ + + + + + S3447 + + + S2234 + + + S3444 + + + S3445 + + + S2114 + + + S1144 + + + S3442 + + + S3440 + + + S3449 + + + S2445 + + + S3897 + + + S2688 + + + S1110 + + + S2681 + + + S2328 + + + S1118 + + + S1117 + + + S4507 + + + S2326 + + + S3415 + + + S1479 + + + maximum + 30 + + + + + S1116 + + + S4635 + + + S1123 + + + S1244 + + + S1121 + + + S2692 + + + S2219 + + + S1006 + + + S1481 + + + S3358 + + + S3598 + + + S4200 + + + S2386 + + + S3597 + + + S4201 + + + S5659 + + + S1172 + + + S4456 + + + S3249 + + + S3246 + + + S3005 + + + S4211 + + + S5542 + + + S3244 + + + S4210 + + + S1066 + + + S1186 + + + S3363 + + + S1185 + + + S3241 + + + S3457 + + + S4545 + + + S4423 + + + S3458 + + + S6965 + + + S6966 + + + S3456 + + + S3453 + + + S2123 + + + S2365 + + + S2486 + + + S6961 + + + S3451 + + + S5753 + + + S6962 + + + S4663 + + + S6609 + + + S4428 + + + S6608 + + + S3459 + + + S3217 + + + S6607 + + + S3218 + + + S927 + + + S3450 + + + S6613 + + + S5766 + + + S6612 + + + S1168 + + + S3466 + + + S2257 + + + S3346 + + + S3343 + + + S2376 + + + S4433 + + + S3220 + + + S2252 + + + S6610 + + + S1163 + + + S6617 + + + S2139 + + + S6618 + + + S818 + + + S2372 + + + S2251 + + + S2743 + + + S1656 + + + S907 + + + S2995 + + + S3600 + + + S3963 + + + S2996 + + + S2757 + + + S3604 + + + S2997 + + + S3603 + + + S1994 + + + S2971 + + + S1696 + + + S3871 + + + S1210 + + + S1694 + + + S3993 + + + S1450 + + + S3998 + + + S3878 + + + S1104 + + + S3887 + + + S2674 + + + S3400 + + + S3881 + + + S2436 + + + max + 2 + + + maxMethod + 3 + + + + + S3972 + + + S3610 + + + S3973 + + + S2761 + + + S3971 + + + S3984 + + + S1206 + + + S1940 + + + S1944 + + + S1939 + + + S1905 + + + S4061 + + + S5042 + + + S4070 + + + S2701 + + + S1862 + + + S3927 + + + S3928 + + + S3925 + + + S3926 + + + S2955 + + + S3923 + + + S2925 + + + S127 + + + S1607 + + + S1848 + + + S3903 + + + S3904 + + + S2933 + + + S2934 + + + S6664 + + + debugThreshold + 4 + + + errorThreshold + 1 + + + warningThreshold + 1 + + + informationThreshold + 2 + + + + + S3398 + + + S112 + + + S3397 + + + S6420 + + + S5693 + + + fileUploadSizeLimit + 8388608 + + + + + S2183 + + + S107 + + + max + 7 + + + + + S108 + + + S6678 + + + S4019 + + + S4015 + + + S4136 + + + S6677 + + + S6797 + + + S2198 + + + S2077 + + + S6673 + + + S1199 + + + S3376 + + + S2166 + + + S3256 + + + S3011 + + + S1075 + + + S4586 + + + S4581 + + + S3251 + + + S3010 + + + S6640 + + + S4220 + + + S4583 + + + S3264 + + + S101 + + + S3265 + + + S6419 + + + S3262 + + + S3263 + + + S2292 + + + S3260 + + + S3261 + + + S2290 + + + S2291 + + + S6588 + + + S6580 + + + S4052 + + + S4050 + + + S6444 + + + S4144 + + + S6561 + + + S4143 + + + S3172 + + + S4260 + + + S4277 + + + S6575 + + + S4035 + + + S4275 + + + S2094 + + + S3063 + + + + + diff --git a/.sonarqube/conf/vbnet/SonarLint.xml b/.sonarqube/conf/vbnet/SonarLint.xml new file mode 100644 index 00000000..d1b44ea3 --- /dev/null +++ b/.sonarqube/conf/vbnet/SonarLint.xml @@ -0,0 +1,601 @@ + + + + + sonar.vbnet.ignoreHeaderComments + true + + + sonar.vbnet.file.suffixes + .vb + + + sonar.vbnet.roslyn.ignoreIssues + false + + + sonar.vbnet.analyzeGeneratedCode + false + + + + + S4036 + + + S2068 + + + credentialWords + password, passwd, pwd, passphrase + + + + + S6418 + + + secretWords + api[_\-]?key, auth, credential, secret, token + + + randomnessSensibility + 3 + + + + + S1313 + + + S4790 + + + S3949 + + + S5445 + + + S3776 + + + threshold + 15 + + + propertyThreshold + 3 + + + + + S5547 + + + S5443 + + + S2612 + + + S2053 + + + S4830 + + + S1135 + + + S1133 + + + S7133 + + + S7131 + + + S3385 + + + S2589 + + + S2222 + + + S2583 + + + S3329 + + + S3655 + + + S5773 + + + S7130 + + + S2259 + + + S3966 + + + S4158 + + + S2077 + + + S1751 + + + S1764 + + + S3981 + + + S6931 + + + S6930 + + + S2368 + + + S1048 + + + S3464 + + + S2178 + + + S2551 + + + S2437 + + + S3889 + + + S4159 + + + S3869 + + + S2225 + + + S2346 + + + S2347 + + + format + ^(([a-z][a-z0-9]*)?([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?_)?([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S5856 + + + S2344 + + + S2345 + + + S1134 + + + S2342 + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + flagsAttributeFormat + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$ + + + + + S3431 + + + S6146 + + + S2340 + + + S2349 + + + S6145 + + + S1940 + + + S2358 + + + S2234 + + + S2355 + + + S2352 + + + S1944 + + + S2359 + + + S3449 + + + S1110 + + + S4507 + + + S1479 + + + maximum + 30 + + + + + S1125 + + + S1123 + + + S2692 + + + S1481 + + + S5042 + + + S3358 + + + S3598 + + + S4201 + + + S5659 + + + S1172 + + + S2951 + + + S1862 + + + S5542 + + + S4210 + + + S1066 + + + S1186 + + + S3363 + + + S3927 + + + S3926 + + + S3923 + + + S4545 + + + S4423 + + + S1155 + + + S3453 + + + S2365 + + + S5753 + + + S4663 + + + S6609 + + + S2925 + + + S4428 + + + S6608 + + + S6607 + + + S927 + + + S6613 + + + S6612 + + + S3466 + + + S2257 + + + S2375 + + + minimumSeriesLength + 6 + + + + + S2376 + + + S6610 + + + S1163 + + + S3903 + + + S3904 + + + S6617 + + + S2372 + + + S1654 + + + format + ^[a-z][a-z0-9]*([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S112 + + + S1656 + + + S907 + + + S5693 + + + fileUploadSizeLimit + 8388608 + + + + + S107 + + + max + 7 + + + + + S108 + + + S1542 + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S4136 + + + S2757 + + + S3603 + + + S114 + + + format + ^I([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S117 + + + format + ^[a-z][a-z0-9]*([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S1871 + + + S2166 + + + S3011 + + + S1075 + + + S4586 + + + S4581 + + + S4583 + + + S1192 + + + threshold + 3 + + + + + S1643 + + + S101 + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S2737 + + + S1645 + + + S3871 + + + S6588 + + + S2304 + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?(\.([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?)*$ + + + + + S3998 + + + S3878 + + + S6580 + + + S5944 + + + S6444 + + + S2761 + + + S4144 + + + S6561 + + + S4143 + + + S6562 + + + S4260 + + + S4277 + + + S6575 + + + S4275 + + + S2094 + + + S3063 + + + + + diff --git a/EchoServerTests/ClientHandlerTests.cs b/EchoServerTests/ClientHandlerTests.cs new file mode 100644 index 00000000..f7ca1eec --- /dev/null +++ b/EchoServerTests/ClientHandlerTests.cs @@ -0,0 +1,72 @@ +using System.Net.Sockets; +using System.Text; +using EchoTspServer.Application.Interfaces; +using EchoTspServer.Application.Services; +using Moq; +using NUnit.Framework; + +namespace EchoTspServer.Tests +{ + [TestFixture] + public class ClientHandlerTests + { + private Mock _loggerMock; + private ClientHandler _handler; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock(); + _handler = new ClientHandler(_loggerMock.Object); + } + + [Test] + public async Task HandleClientAsync_EchoesDataBack() + { + // Arrange: створимо два з'єднані TCP сокети + using var listener = new TcpListener(System.Net.IPAddress.Loopback, 0); + listener.Start(); + int port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port; + + var clientTask = new TcpClient(); + var connectTask = clientTask.ConnectAsync("127.0.0.1", port); + + var serverClient = await listener.AcceptTcpClientAsync(); + await connectTask; + listener.Stop(); + + var token = new CancellationTokenSource(TimeSpan.FromSeconds(2)).Token; + + // Act + var handleTask = _handler.HandleClientAsync(serverClient, token); + + var stream = clientTask.GetStream(); + var message = Encoding.UTF8.GetBytes("ping"); + await stream.WriteAsync(message, 0, message.Length); + + byte[] buffer = new byte[1024]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + + // Assert + Assert.That(Encoding.UTF8.GetString(buffer, 0, bytesRead), Is.EqualTo("ping")); + _loggerMock.Verify(l => l.Info(It.Is(s => s.Contains("Echoed"))), Times.AtLeastOnce); + + serverClient.Close(); + clientTask.Close(); + } + + //[Test] + //public async Task HandleClientAsync_HandlesException_LogsError() + //{ + // // Arrange + // var fakeClient = new Mock(); + // fakeClient.Setup(c => c.GetStream()).Throws(new Exception("fake fail")); + + // // Act + // await _handler.HandleClientAsync(fakeClient.Object, CancellationToken.None); + + // // Assert + // _loggerMock.Verify(l => l.Error(It.Is(s => s.Contains("fake fail"))), Times.Once); + //} + } +} diff --git a/EchoServerTests/ConsoleLoggerTests.cs b/EchoServerTests/ConsoleLoggerTests.cs new file mode 100644 index 00000000..913bae63 --- /dev/null +++ b/EchoServerTests/ConsoleLoggerTests.cs @@ -0,0 +1,23 @@ +using EchoTspServer.Infrastructure; +using NUnit.Framework; + +namespace EchoTspServer.Tests +{ + [TestFixture] + public class ConsoleLoggerTests + { + [Test] + public void Info_WritesToConsole() + { + var logger = new ConsoleLogger(); + Assert.DoesNotThrow(() => logger.Info("Test info message")); + } + + [Test] + public void Error_WritesToConsole() + { + var logger = new ConsoleLogger(); + Assert.DoesNotThrow(() => logger.Error("Test error message")); + } + } +} diff --git a/EchoServerTests/EchoServerTests.cs b/EchoServerTests/EchoServerTests.cs new file mode 100644 index 00000000..efae0cf8 --- /dev/null +++ b/EchoServerTests/EchoServerTests.cs @@ -0,0 +1,64 @@ +using System.Net.Sockets; +using EchoTspServer.Application.Interfaces; +using EchoTspServer.Application.Services; +using Moq; +using NUnit.Framework; + +namespace EchoTspServer.Tests +{ + [TestFixture] + public class EchoServerTests + { + private Mock _loggerMock; + private Mock _handlerMock; + private EchoServer _server; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock(); + _handlerMock = new Mock(); + _server = new EchoServer(6001, _loggerMock.Object, _handlerMock.Object); + } + + [Test] + public async Task StartAsync_StartsAndStopsWithoutError() + { + var task = _server.StartAsync(); + await Task.Delay(100); // + + // act + _server.Stop(); + + try + { + await task; // + } + catch (SocketException ex) + { + // , listener AcceptTcpClientAsync + Assert.That(ex.SocketErrorCode, Is.EqualTo(SocketError.OperationAborted)); + } + + _loggerMock.Verify(l => l.Info(It.Is(s => s.Contains("Server started"))), Times.Once); + _loggerMock.Verify(l => l.Info(It.Is(s => s.Contains("Server stopped"))), Times.Once); + } + + + [Test] + public void Stop_CanBeCalledMultipleTimes_SafeToCall() + { + Assert.DoesNotThrow(() => + { + _server.Stop(); + _server.Stop(); + }); + } + + [Test] + public void Constructor_SetsDependenciesProperly() + { + Assert.NotNull(_server); + } + } +} diff --git a/EchoServerTests/EchoServerTests.csproj b/EchoServerTests/EchoServerTests.csproj new file mode 100644 index 00000000..2056239f --- /dev/null +++ b/EchoServerTests/EchoServerTests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + diff --git a/EchoServerTests/UdpTimedSenderTests.cs b/EchoServerTests/UdpTimedSenderTests.cs new file mode 100644 index 00000000..2372a575 --- /dev/null +++ b/EchoServerTests/UdpTimedSenderTests.cs @@ -0,0 +1,58 @@ +using EchoTspServer.Application.Interfaces; +using EchoTspServer.Application.Services; +using Moq; +using NUnit.Framework; + +namespace EchoTspServer.Tests +{ + [TestFixture] + public class UdpTimedSenderTests + { + private Mock _loggerMock; + private UdpTimedSender _sender; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock(); + _sender = new UdpTimedSender("127.0.0.1", 9999, _loggerMock.Object); + } + + [TearDown] + public void Cleanup() + { + _sender.Dispose(); + } + + [Test] + public void StartSending_StartsAndStopsCorrectly() + { + _sender.StartSending(100); + Assert.DoesNotThrow(() => _sender.StopSending()); + } + + [Test] + public void StartSending_WhenAlreadyRunning_Throws() + { + _sender.StartSending(100); + Assert.Throws(() => _sender.StartSending(100)); + } + + [Test] + public void Dispose_StopsAndDisposesUdpClient() + { + Assert.DoesNotThrow(() => _sender.Dispose()); + } + + [Test] + public void SendMessage_LogsInfoOnSuccess() + { + // simulate one interval + _sender.StartSending(10); + Thread.Sleep(30); + _sender.StopSending(); + + _loggerMock.Verify(l => l.Info(It.Is(s => s.Contains("Sent UDP packet"))), Times.AtLeastOnce); + } + } +} diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs deleted file mode 100644 index 5966c579..00000000 --- a/EchoTcpServer/Program.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -/// -/// This program was designed for test purposes only -/// Not for a review -/// -public class EchoServer -{ - private readonly int _port; - private TcpListener _listener; - private CancellationTokenSource _cancellationTokenSource; - - - public EchoServer(int port) - { - _port = port; - _cancellationTokenSource = new CancellationTokenSource(); - } - - public async Task StartAsync() - { - _listener = new TcpListener(IPAddress.Any, _port); - _listener.Start(); - Console.WriteLine($"Server started on port {_port}."); - - while (!_cancellationTokenSource.Token.IsCancellationRequested) - { - try - { - TcpClient client = await _listener.AcceptTcpClientAsync(); - Console.WriteLine("Client connected."); - - _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); - } - catch (ObjectDisposedException) - { - // Listener has been closed - break; - } - } - - Console.WriteLine("Server shutdown."); - } - - private async Task HandleClientAsync(TcpClient client, CancellationToken token) - { - using (NetworkStream stream = client.GetStream()) - { - try - { - byte[] buffer = new byte[8192]; - int bytesRead; - - while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) - { - // Echo back the received message - await stream.WriteAsync(buffer, 0, bytesRead, token); - Console.WriteLine($"Echoed {bytesRead} bytes to the client."); - } - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - Console.WriteLine($"Error: {ex.Message}"); - } - finally - { - client.Close(); - Console.WriteLine("Client disconnected."); - } - } - } - - public void Stop() - { - _cancellationTokenSource.Cancel(); - _listener.Stop(); - _cancellationTokenSource.Dispose(); - Console.WriteLine("Server stopped."); - } - - public static async Task Main(string[] args) - { - EchoServer server = new EchoServer(5000); - - // Start the server in a separate task - _ = Task.Run(() => server.StartAsync()); - - string host = "127.0.0.1"; // Target IP - int port = 60000; // Target Port - int intervalMilliseconds = 5000; // Send every 3 seconds - - using (var sender = new UdpTimedSender(host, port)) - { - Console.WriteLine("Press any key to stop sending..."); - sender.StartSending(intervalMilliseconds); - - Console.WriteLine("Press 'q' to quit..."); - while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) - { - // Just wait until 'q' is pressed - } - - sender.StopSending(); - server.Stop(); - Console.WriteLine("Sender stopped."); - } - } -} - - -public class UdpTimedSender : IDisposable -{ - private readonly string _host; - private readonly int _port; - private readonly UdpClient _udpClient; - private Timer _timer; - - public UdpTimedSender(string host, int port) - { - _host = host; - _port = port; - _udpClient = new UdpClient(); - } - - public void StartSending(int intervalMilliseconds) - { - if (_timer != null) - throw new InvalidOperationException("Sender is already running."); - - _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); - } - - ushort i = 0; - - private void SendMessageCallback(object state) - { - try - { - //dummy data - Random rnd = new Random(); - byte[] samples = new byte[1024]; - rnd.NextBytes(samples); - i++; - - byte[] msg = (new byte[] { 0x04, 0x84 }).Concat(BitConverter.GetBytes(i)).Concat(samples).ToArray(); - var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); - - _udpClient.Send(msg, msg.Length, endpoint); - Console.WriteLine($"Message sent to {_host}:{_port} "); - } - catch (Exception ex) - { - Console.WriteLine($"Error sending message: {ex.Message}"); - } - } - - public void StopSending() - { - _timer?.Dispose(); - _timer = null; - } - - public void Dispose() - { - StopSending(); - _udpClient.Dispose(); - } -} \ No newline at end of file diff --git a/EchoTspServer/Application/Interfaces/IClientHandler.cs b/EchoTspServer/Application/Interfaces/IClientHandler.cs new file mode 100644 index 00000000..e0c2075c --- /dev/null +++ b/EchoTspServer/Application/Interfaces/IClientHandler.cs @@ -0,0 +1,11 @@ +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoTspServer.Application.Interfaces +{ + public interface IClientHandler + { + Task HandleClientAsync(TcpClient client, CancellationToken token); + } +} diff --git a/EchoTspServer/Application/Interfaces/IEchoServer.cs b/EchoTspServer/Application/Interfaces/IEchoServer.cs new file mode 100644 index 00000000..2bd23284 --- /dev/null +++ b/EchoTspServer/Application/Interfaces/IEchoServer.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace EchoTspServer.Application.Interfaces +{ + public interface IEchoServer + { + Task StartAsync(); + void Stop(); + } +} diff --git a/EchoTspServer/Application/Interfaces/ILogger.cs b/EchoTspServer/Application/Interfaces/ILogger.cs new file mode 100644 index 00000000..77c570c1 --- /dev/null +++ b/EchoTspServer/Application/Interfaces/ILogger.cs @@ -0,0 +1,8 @@ +namespace EchoTspServer.Application.Interfaces +{ + public interface ILogger + { + void Info(string message); + void Error(string message); + } +} diff --git a/EchoTspServer/Application/Interfaces/IUdpSender.cs b/EchoTspServer/Application/Interfaces/IUdpSender.cs new file mode 100644 index 00000000..28074b49 --- /dev/null +++ b/EchoTspServer/Application/Interfaces/IUdpSender.cs @@ -0,0 +1,8 @@ +namespace EchoTspServer.Application.Interfaces +{ + public interface IUdpSender : IDisposable + { + void StartSending(int intervalMilliseconds); + void StopSending(); + } +} diff --git a/EchoTspServer/Application/Services/ClientHandler.cs b/EchoTspServer/Application/Services/ClientHandler.cs new file mode 100644 index 00000000..018f2360 --- /dev/null +++ b/EchoTspServer/Application/Services/ClientHandler.cs @@ -0,0 +1,42 @@ +using EchoTspServer.Application.Interfaces; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoTspServer.Application.Services +{ + public class ClientHandler : IClientHandler + { + private readonly ILogger _logger; + + public ClientHandler(ILogger logger) + { + _logger = logger; + } + + public async Task HandleClientAsync(TcpClient client, CancellationToken token) + { + using NetworkStream stream = client.GetStream(); + try + { + byte[] buffer = new byte[8192]; + int bytesRead; + while (!token.IsCancellationRequested && + (bytesRead = await stream.ReadAsync(buffer, token)) > 0) + { + await stream.WriteAsync(buffer.AsMemory(0, bytesRead), token); + _logger.Info($"Echoed {bytesRead} bytes to client."); + } + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + _logger.Error($"Client error: {ex.Message}"); + } + finally + { + client.Close(); + _logger.Info("Client disconnected."); + } + } + } +} diff --git a/EchoTspServer/Application/Services/EchoServer.cs b/EchoTspServer/Application/Services/EchoServer.cs new file mode 100644 index 00000000..8ca46e50 --- /dev/null +++ b/EchoTspServer/Application/Services/EchoServer.cs @@ -0,0 +1,77 @@ +using EchoTspServer.Application.Interfaces; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoTspServer.Application.Services +{ + public class EchoServer : IEchoServer + { + private readonly int _port; + private readonly ILogger _logger; + private readonly IClientHandler _clientHandler; + private TcpListener? _listener; + private CancellationTokenSource? _cts; + private bool _isStopped = false; + + public EchoServer(int port, ILogger logger, IClientHandler clientHandler) + { + _port = port; + _logger = logger; + _clientHandler = clientHandler; + } + + public async Task StartAsync() + { + _cts = new CancellationTokenSource(); + _listener = new TcpListener(IPAddress.Any, _port); + _listener.Start(); + _logger.Info($"Server started on port {_port}."); + + try + { + while (!_cts.Token.IsCancellationRequested) + { + var client = await _listener.AcceptTcpClientAsync(); + _logger.Info("Client connected."); + _ = Task.Run(() => _clientHandler.HandleClientAsync(client, _cts.Token)); + } + } + catch (ObjectDisposedException) + { + // Listener has been closed normally + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted) + { + // Listener closed — expected when stopping + } + finally + { + _logger.Info("Server shutdown."); + } + } + + public void Stop() + { + if (_isStopped) return; // already stopped — ignore + _isStopped = true; + + try + { + _cts?.Cancel(); + _listener?.Stop(); + } + catch (ObjectDisposedException) + { + // Already disposed — safe to ignore + } + finally + { + _cts?.Dispose(); + _cts = null; + _logger.Info("Server stopped."); + } + } + } +} diff --git a/EchoTspServer/Application/Services/UdpTimedSender.cs b/EchoTspServer/Application/Services/UdpTimedSender.cs new file mode 100644 index 00000000..37256679 --- /dev/null +++ b/EchoTspServer/Application/Services/UdpTimedSender.cs @@ -0,0 +1,78 @@ +using EchoTspServer.Application.Interfaces; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Security.Cryptography; // <-- Додано для криптографічно стійкого генератора +using System.Linq; // Додано для Concat/ToArray +using System; // Для InvalidOperationException + +namespace EchoTspServer.Application.Services +{ + public class UdpTimedSender : IUdpSender + { + private readonly string _host; + private readonly int _port; + private readonly ILogger _logger; + private readonly UdpClient _udpClient = new(); + private Timer? _timer; + private ushort _counter = 0; + + // Примітка: Оскільки RandomNumberGenerator.Fill статичний, не потрібно зберігати екземпляр. + // Якщо потрібно ініціалізувати поле, це можна зробити, але для Fill він не потрібен. + + public UdpTimedSender(string host, int port, ILogger logger) + { + _host = host; + _port = port; + _logger = logger; + } + + public void StartSending(int intervalMilliseconds) + { + if (_timer != null) + throw new InvalidOperationException("Sender already running."); + + _timer = new Timer(SendMessage, null, 0, intervalMilliseconds); + } + + private void SendMessage(object? _) + { + try + { + // ❌ ВИДАЛЕНО: var rnd = new Random(); + + var samples = new byte[1024]; + + // ✅ ВИПРАВЛЕННЯ: Використовуємо криптографічно стійкий генератор для заповнення масиву + RandomNumberGenerator.Fill(samples); + + _counter++; + + var msg = new byte[] { 0x04, 0x84 } + .Concat(BitConverter.GetBytes(_counter)) + .Concat(samples) + .ToArray(); + + var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); + _udpClient.Send(msg, msg.Length, endpoint); + _logger.Info($"Sent UDP packet to {_host}:{_port}"); + } + catch (Exception ex) + { + _logger.Error($"UDP send error: {ex.Message}"); + } + } + + public void StopSending() + { + _timer?.Dispose(); + _timer = null; + } + + public void Dispose() + { + StopSending(); + _udpClient.Dispose(); + } + } +} \ No newline at end of file diff --git a/EchoTcpServer/EchoServer.csproj b/EchoTspServer/EchoServer.csproj similarity index 100% rename from EchoTcpServer/EchoServer.csproj rename to EchoTspServer/EchoServer.csproj diff --git a/EchoTspServer/Presentation/Infrastructure/ConsoleLogger.cs b/EchoTspServer/Presentation/Infrastructure/ConsoleLogger.cs new file mode 100644 index 00000000..cc84765a --- /dev/null +++ b/EchoTspServer/Presentation/Infrastructure/ConsoleLogger.cs @@ -0,0 +1,10 @@ +using EchoTspServer.Application.Interfaces; + +namespace EchoTspServer.Infrastructure +{ + public class ConsoleLogger : ILogger + { + public void Info(string message) => Console.WriteLine($"[INFO] {message}"); + public void Error(string message) => Console.WriteLine($"[ERROR] {message}"); + } +} diff --git a/EchoTspServer/Presentation/Program.cs b/EchoTspServer/Presentation/Program.cs new file mode 100644 index 00000000..d8e2bb01 --- /dev/null +++ b/EchoTspServer/Presentation/Program.cs @@ -0,0 +1,37 @@ +using EchoTspServer.Application.Services; +using EchoTspServer.Infrastructure; +using System; // Додано для Console, ConsoleKey +using System.Threading.Tasks; + +// ✅ ВИПРАВЛЕННЯ: Додано іменований простір імен, як вимагає SonarCloud (S3903) +namespace EchoTspServer.Presentation +{ + class Program + { + static async Task Main() + { + var logger = new ConsoleLogger(); + var handler = new ClientHandler(logger); + + // Note: Тут використовується 5000, logger, handler. + // Якщо у конструкторі EchoServer немає порту, його варто прибрати. + // Я залишаю, як у вашому коді, припускаючи, що конструктор правильний. + var server = new EchoServer(5000, logger, handler); + + // Запускаємо StartAsync у фоновому режимі, щоб не блокувати Main + // Використовуємо _ = для ігнорування повернення Task, але уникнення попередження + _ = Task.Run(() => server.StartAsync()); + + var sender = new UdpTimedSender("127.0.0.1", 60000, logger); + sender.StartSending(5000); + + Console.WriteLine("Press 'q' to quit..."); + + // Цикл очікування команди на вихід + while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) { } + + sender.StopSending(); + server.Stop(); + } + } +} \ No newline at end of file diff --git a/NetSdrClient.sln b/NetSdrClient.sln index 42431fb3..53466daf 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -7,7 +7,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientApp", "NetSdrCl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientAppTests", "NetSdrClientAppTests\NetSdrClientAppTests.csproj", "{D0155366-89B4-4BA4-90E2-2ECC8C1EB915}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServer", "EchoTcpServer\EchoServer.csproj", "{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServer", "EchoTspServer\EchoServer.csproj", "{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServerTests", "EchoServerTests\EchoServerTests.csproj", "{EAF395E5-B991-4620-BE30-790E1FD25B3C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,8 +29,15 @@ Global {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.Build.0 = Debug|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.ActiveCfg = Release|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.Build.0 = Release|Any CPU + {EAF395E5-B991-4620-BE30-790E1FD25B3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAF395E5-B991-4620-BE30-790E1FD25B3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAF395E5-B991-4620-BE30-790E1FD25B3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAF395E5-B991-4620-BE30-790E1FD25B3C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DD11AF61-3EA2-4F2F-8492-AE189B918EFF} + EndGlobalSection EndGlobal diff --git a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs index 0d69b4df..dc81ff15 100644 --- a/NetSdrClientApp/Messages/NetSdrMessageHelper.cs +++ b/NetSdrClientApp/Messages/NetSdrMessageHelper.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection.PortableExecutable; using System.Text; -using System.Threading.Tasks; namespace NetSdrClientApp.Messages { @@ -12,9 +10,9 @@ public static class NetSdrMessageHelper { private const short _maxMessageLength = 8191; private const short _maxDataItemMessageLength = 8194; - private const short _msgHeaderLength = 2; //2 byte, 16 bit - private const short _msgControlItemLength = 2; //2 byte, 16 bit - private const short _msgSequenceNumberLength = 2; //2 byte, 16 bit + private const short _msgHeaderLength = 2; // 2 byte, 16 bit + private const short _msgControlItemLength = 2; // 2 byte, 16 bit + private const short _msgSequenceNumberLength = 2; // 2 byte, 16 bit public enum MsgTypes { @@ -28,7 +26,9 @@ public enum MsgTypes DataItem3 } - public enum ControlItemCodes + // Changed base type to ushort (UInt16) to correctly handle conversion from BitConverter.ToUInt16 + // and prevent System.ArgumentException in Enum.IsDefined. + public enum ControlItemCodes : ushort { None = 0, IQOutputDataSampleRate = 0x00B8, @@ -53,10 +53,14 @@ private static byte[] GetMessage(MsgTypes type, ControlItemCodes itemCode, byte[ var itemCodeBytes = Array.Empty(); if (itemCode != ControlItemCodes.None) { + // Convert ControlItemCodes (ushort) to bytes itemCodeBytes = BitConverter.GetBytes((ushort)itemCode); } - var headerBytes = GetHeader(type, itemCodeBytes.Length + parameters.Length); + // Total length of data, including the control item code (if present) + var totalBodyLength = itemCodeBytes.Length + parameters.Length; + + var headerBytes = GetHeader(type, totalBodyLength); List msg = new List(); msg.AddRange(headerBytes); @@ -66,22 +70,47 @@ private static byte[] GetMessage(MsgTypes type, ControlItemCodes itemCode, byte[ return msg.ToArray(); } + // Refactored to use offset indexing for robust parsing and added boundary checks. public static bool TranslateMessage(byte[] msg, out MsgTypes type, out ControlItemCodes itemCode, out ushort sequenceNumber, out byte[] body) { + type = default; itemCode = ControlItemCodes.None; sequenceNumber = 0; - bool success = true; - var msgEnumarable = msg as IEnumerable; + body = Array.Empty(); - TranslateHeader(msgEnumarable.Take(_msgHeaderLength).ToArray(), out type, out int msgLength); - msgEnumarable = msgEnumarable.Skip(_msgHeaderLength); - msgLength -= _msgHeaderLength; + int offset = 0; + + // 1. Check minimum message length (header) + if (msg == null || msg.Length < _msgHeaderLength) + { + return false; + } + + // 2. Parse header + TranslateHeader(msg.Take(_msgHeaderLength).ToArray(), out type, out int expectedBodyLength); + offset += _msgHeaderLength; + + // Check if the message has the expected length based on the header value + if (msg.Length != _msgHeaderLength + expectedBodyLength) + { + // Length mismatch (Fix for TranslateMessage_ShouldFailOnInvalidBodyLength) + return false; + } - if (type < MsgTypes.DataItem0) // get item code + int remainingLength = expectedBodyLength; + + if (type < MsgTypes.DataItem0) // Process Control Item Code { - var value = BitConverter.ToUInt16(msgEnumarable.Take(_msgControlItemLength).ToArray()); - msgEnumarable = msgEnumarable.Skip(_msgControlItemLength); - msgLength -= _msgControlItemLength; + // Control Item message must contain at least the control item code + if (remainingLength < _msgControlItemLength) + { + return false; + } + + // Read Control Item Code + ushort value = BitConverter.ToUInt16(msg, offset); + offset += _msgControlItemLength; + remainingLength -= _msgControlItemLength; if (Enum.IsDefined(typeof(ControlItemCodes), value)) { @@ -89,50 +118,64 @@ public static bool TranslateMessage(byte[] msg, out MsgTypes type, out ControlIt } else { - success = false; + // Invalid control item code (Fix for TranslateMessage_ShouldFailOnInvalidControlItemCode) + return false; } } - else // get sequenceNumber + else // Process Data Item (Sequence Number) { - sequenceNumber = BitConverter.ToUInt16(msgEnumarable.Take(_msgSequenceNumberLength).ToArray()); - msgEnumarable = msgEnumarable.Skip(_msgSequenceNumberLength); - msgLength -= _msgSequenceNumberLength; + // Data Item message must contain at least the sequence number + if (remainingLength < _msgSequenceNumberLength) + { + return false; + } + + // Read Sequence Number + sequenceNumber = BitConverter.ToUInt16(msg, offset); + offset += _msgSequenceNumberLength; + remainingLength -= _msgSequenceNumberLength; } - body = msgEnumarable.ToArray(); + // 3. Extract body + if (remainingLength > 0) + { + // Create array for the body + body = new byte[remainingLength]; - success &= body.Length == msgLength; + // Copy the body bytes (Fixes issues related to improper length calculation for body) + Array.Copy(msg, offset, body, 0, remainingLength); + } - return success; + return true; } public static IEnumerable GetSamples(ushort sampleSize, byte[] body) { - sampleSize /= 8; //to bytes - if (sampleSize > 4) + ushort sampleSizeInBytes = (ushort)(sampleSize / 8); // to bytes + + if (sampleSizeInBytes == 0 || sampleSizeInBytes > 4) { - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(sampleSize), "SampleSize must be between 8 and 32 bits and a multiple of 8."); } - var bodyEnumerable = body as IEnumerable; - var prefixBytes = Enumerable.Range(0, 4 - sampleSize) - .Select(b => (byte)0); - - while (bodyEnumerable.Count() >= sampleSize) + for (int i = 0; i <= body.Length - sampleSizeInBytes; i += sampleSizeInBytes) { - yield return BitConverter.ToInt32(bodyEnumerable - .Take(sampleSize) - .Concat(prefixBytes) - .ToArray()); - bodyEnumerable = bodyEnumerable.Skip(sampleSize); + // Create a 4-byte array for ToInt32 + byte[] sampleBytes = new byte[4]; + + // Copy sample bytes (assuming Little Endian) + Array.Copy(body, i, sampleBytes, 0, sampleSizeInBytes); + + yield return BitConverter.ToInt32(sampleBytes); } } private static byte[] GetHeader(MsgTypes type, int msgLength) { - int lengthWithHeader = msgLength + 2; + // msgLength is the length of the message body (itemCode + parameters). + int lengthWithHeader = msgLength + _msgHeaderLength; - //Data Items edge case + // Data Items edge case: if length reaches max value, set header length to 0 (for DataItem) if (type >= MsgTypes.DataItem0 && lengthWithHeader == _maxDataItemMessageLength) { lengthWithHeader = 0; @@ -140,22 +183,34 @@ private static byte[] GetHeader(MsgTypes type, int msgLength) if (msgLength < 0 || lengthWithHeader > _maxMessageLength) { - throw new ArgumentException("Message length exceeds allowed value"); + throw new ArgumentException("Message length exceeds allowed value", nameof(msgLength)); } - return BitConverter.GetBytes((ushort)(lengthWithHeader + ((int)type << 13))); + // Header format: 3 bits type + 13 bits length + ushort headerValue = (ushort)(lengthWithHeader | ((ushort)type << 13)); + + return BitConverter.GetBytes(headerValue); } private static void TranslateHeader(byte[] header, out MsgTypes type, out int msgLength) { var num = BitConverter.ToUInt16(header.ToArray()); + + // Extract type (3 bits) type = (MsgTypes)(num >> 13); - msgLength = num - ((int)type << 13); + // Extract length (13 bits) using a mask for reliability: 0b0001_1111_1111_1111 + msgLength = num & 0x1FFF; + + // Data Items edge case: if DataItem has length 0, it means _maxDataItemMessageLength if (type >= MsgTypes.DataItem0 && msgLength == 0) { msgLength = _maxDataItemMessageLength; } + + // The returned msgLength is the total message length including the header. + // We subtract the header length to get the expected body length (code/sequence + parameters) + msgLength -= _msgHeaderLength; } } -} +} \ No newline at end of file diff --git a/NetSdrClientApp/NetSdrClient.cs b/NetSdrClientApp/NetSdrClient.cs index b0a7c058..64ca9ce8 100644 --- a/NetSdrClientApp/NetSdrClient.cs +++ b/NetSdrClientApp/NetSdrClient.cs @@ -116,7 +116,7 @@ public async Task ChangeFrequencyAsync(long hz, int channel) private void _udpClient_MessageReceived(object? sender, byte[] e) { - NetSdrMessageHelper.TranslateMessage(e, out MsgTypes type, out ControlItemCodes code, out ushort sequenceNum, out byte[] body); + NetSdrMessageHelper.TranslateMessage(e, out MsgTypes type, out ControlItemCodes code, out ushort _, out byte[] body); var samples = NetSdrMessageHelper.GetSamples(16, body); Console.WriteLine($"Samples recieved: " + body.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); diff --git a/NetSdrClientApp/NetSdrClientApp.csproj b/NetSdrClientApp/NetSdrClientApp.csproj index 2ac91006..b6a7b836 100644 --- a/NetSdrClientApp/NetSdrClientApp.csproj +++ b/NetSdrClientApp/NetSdrClientApp.csproj @@ -7,8 +7,8 @@ enable - - + + diff --git a/NetSdrClientApp/Networking/INetworkClient.cs b/NetSdrClientApp/Networking/INetworkClient.cs new file mode 100644 index 00000000..8ade13ef --- /dev/null +++ b/NetSdrClientApp/Networking/INetworkClient.cs @@ -0,0 +1,39 @@ +using System.Net.Sockets; +using System.Threading.Tasks; +using System.Threading; +using System.IO; +using System; + +namespace NetSdrClientApp.Networking +{ + // 1. 볺 ( DI/Mocking) + public interface ISystemTcpClient : IDisposable + { + bool Connected { get; } + INetworkStream GetStream(); + void Connect(string host, int port); + void Close(); + } + + // 2. ( Mocking Read/Write) + public interface INetworkStream : IDisposable + { + bool CanRead { get; } + bool CanWrite { get; } + // , TcpClientWrapper + Task WriteAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken); + Task ReadAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken); + void Close(); + } + + // 3. TcpClientWrapper ( ) + public interface ITcpClient + { + void Connect(); + void Disconnect(); + Task SendMessageAsync(byte[] data); + Task SendMessageAsync(string str); + event EventHandler MessageReceived; + public bool Connected { get; } + } +} \ No newline at end of file diff --git a/NetSdrClientApp/Networking/ITcpClient.cs b/NetSdrClientApp/Networking/ITcpClient.cs deleted file mode 100644 index 3470b5d7..00000000 --- a/NetSdrClientApp/Networking/ITcpClient.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static System.Runtime.InteropServices.JavaScript.JSType; - -namespace NetSdrClientApp.Networking -{ - public interface ITcpClient - { - void Connect(); - void Disconnect(); - Task SendMessageAsync(byte[] data); - - event EventHandler MessageReceived; - public bool Connected { get; } - } -} diff --git a/NetSdrClientApp/Networking/IUdpClient.cs b/NetSdrClientApp/Networking/IUdpClient.cs index 1b9f9311..e5c0e1c4 100644 --- a/NetSdrClientApp/Networking/IUdpClient.cs +++ b/NetSdrClientApp/Networking/IUdpClient.cs @@ -1,10 +1,22 @@ - -public interface IUdpClient +using System; +using System.Threading.Tasks; + +namespace NetSdrClientApp.Networking { - event EventHandler? MessageReceived; + // Interface for hash algorithm abstraction (used by Sha256Adapter) + public interface IHashAlgorithm : IDisposable + { + byte[] ComputeHash(byte[] buffer); + } + + // Interface for the UDP client wrapper. Implements IDisposable. + public interface IUdpClient : IDisposable + { + event EventHandler? MessageReceived; - Task StartListeningAsync(); + Task StartListeningAsync(); - void StopListening(); - void Exit(); + void StopListening(); + void Exit(); + } } \ No newline at end of file diff --git a/NetSdrClientApp/Networking/TcpClientWrapper.cs b/NetSdrClientApp/Networking/TcpClientWrapper.cs index 1f37e2e5..8804595e 100644 --- a/NetSdrClientApp/Networking/TcpClientWrapper.cs +++ b/NetSdrClientApp/Networking/TcpClientWrapper.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Net.Http; using System.Net.Sockets; using System.Text; using System.Threading; @@ -10,22 +7,71 @@ namespace NetSdrClientApp.Networking { + // Adapter for the real TcpClient + public class SystemTcpClientAdapter : ISystemTcpClient + { + private readonly TcpClient _client; + + public SystemTcpClientAdapter(TcpClient client) => _client = client; + public bool Connected => _client.Connected; + public void Close() => _client.Close(); + public void Connect(string host, int port) => _client.Connect(host, port); + public void Dispose() => _client.Dispose(); + + // Return adapter for NetworkStream + public INetworkStream GetStream() => new NetworkStreamAdapter(_client.GetStream()); + } + + // Adapter for NetworkStream + public class NetworkStreamAdapter : INetworkStream + { + private readonly NetworkStream _stream; + + public NetworkStreamAdapter(NetworkStream stream) => _stream = stream; + + public bool CanRead => _stream.CanRead; + public bool CanWrite => _stream.CanWrite; + public void Close() => _stream.Close(); + public void Dispose() => _stream.Dispose(); + + public Task ReadAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken) + => _stream.ReadAsync(buffer, offset, size, cancellationToken); + + public Task WriteAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken) + => _stream.WriteAsync(buffer, offset, size, cancellationToken); + } + + // ------------------------------------------------------------- + + // Main wrapper class public class TcpClientWrapper : ITcpClient { - private string _host; - private int _port; - private TcpClient? _tcpClient; - private NetworkStream? _stream; - private CancellationTokenSource _cts; + private readonly string _host; + private readonly int _port; + + private ISystemTcpClient? _tcpClient; + private INetworkStream? _stream; + // Змінено на nullable для безпечного використання у Disconnect() + private CancellationTokenSource? _cts; public bool Connected => _tcpClient != null && _tcpClient.Connected && _stream != null; public event EventHandler? MessageReceived; + // Factory for creating actual clients + private readonly Func _clientFactory; + + // Constructor for Production code public TcpClientWrapper(string host, int port) + : this(host, port, () => new SystemTcpClientAdapter(new TcpClient())) { } + + // Constructor for DI and testing + public TcpClientWrapper(string host, int port, Func clientFactory) { _host = host; _port = port; + _clientFactory = clientFactory; + // Ініціалізація _cts тут не потрібна для Connected=false, зробимо це в Connect() } public void Connect() @@ -36,11 +82,14 @@ public void Connect() return; } - _tcpClient = new TcpClient(); + _tcpClient = _clientFactory(); try { + // Скидаємо старий CTS (якщо він був) + _cts?.Dispose(); _cts = new CancellationTokenSource(); + _tcpClient.Connect(_host, _port); _stream = _tcpClient.GetStream(); Console.WriteLine($"Connected to {_host}:{_port}"); @@ -49,20 +98,35 @@ public void Connect() catch (Exception ex) { Console.WriteLine($"Failed to connect: {ex.Message}"); + // Ensure resources are nullified on failure + _tcpClient = null; + _stream = null; + _cts?.Dispose(); + _cts = null; } } public void Disconnect() { - if (Connected) + // Використовуємо локальну змінну для безпечного доступу + var clientToClose = Interlocked.Exchange(ref _tcpClient, null); + + if (clientToClose != null) { + // Скасування слухача _cts?.Cancel(); + + // Закриття ресурсів _stream?.Close(); - _tcpClient?.Close(); + clientToClose.Close(); + // Утилізація CTS + _cts?.Dispose(); _cts = null; - _tcpClient = null; + + // Скидання stream _stream = null; + Console.WriteLine("Disconnected."); } else @@ -76,7 +140,8 @@ public async Task SendMessageAsync(byte[] data) if (Connected && _stream != null && _stream.CanWrite) { Console.WriteLine($"Message sent: " + data.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); - await _stream.WriteAsync(data, 0, data.Length); + // Перевірка _cts на null не потрібна, якщо Connected=true + await _stream.WriteAsync(data, 0, data.Length, _cts!.Token); } else { @@ -90,7 +155,7 @@ public async Task SendMessageAsync(string str) if (Connected && _stream != null && _stream.CanWrite) { Console.WriteLine($"Message sent: " + data.Select(b => Convert.ToString(b, toBase: 16)).Aggregate((l, r) => $"{l} {r}")); - await _stream.WriteAsync(data, 0, data.Length); + await _stream.WriteAsync(data, 0, data.Length, _cts!.Token); } else { @@ -100,26 +165,37 @@ public async Task SendMessageAsync(string str) private async Task StartListeningAsync() { - if (Connected && _stream != null && _stream.CanRead) + // Надійна перевірка на null + if (_cts == null || _stream == null) return; + + var token = _cts.Token; + + if (Connected && _stream.CanRead) { try { Console.WriteLine($"Starting listening for incomming messages."); - while (!_cts.Token.IsCancellationRequested) + while (!token.IsCancellationRequested) { byte[] buffer = new byte[8194]; - int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, _cts.Token); + int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, token); + + if (bytesRead == 0) + { + break; // Connection closed by remote host + } + if (bytesRead > 0) { MessageReceived?.Invoke(this, buffer.AsSpan(0, bytesRead).ToArray()); } } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) { - //empty + // Cancellation requested } catch (Exception ex) { @@ -128,13 +204,14 @@ private async Task StartListeningAsync() finally { Console.WriteLine("Listener stopped."); + // Disconnect is called here, which closes resources and cleans up state. + Disconnect(); } } else { - throw new InvalidOperationException("Not connected to a server."); + // Internal method, no action needed if not connected } } } - -} +} \ No newline at end of file diff --git a/NetSdrClientApp/Networking/UdpClientWrapper.cs b/NetSdrClientApp/Networking/UdpClientWrapper.cs index 31e0b798..03c9715a 100644 --- a/NetSdrClientApp/Networking/UdpClientWrapper.cs +++ b/NetSdrClientApp/Networking/UdpClientWrapper.cs @@ -5,81 +5,149 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Collections.Concurrent; -public class UdpClientWrapper : IUdpClient +namespace NetSdrClientApp.Networking { - private readonly IPEndPoint _localEndPoint; - private CancellationTokenSource? _cts; - private UdpClient? _udpClient; + // Оголошення інтерфейсів IHashAlgorithm та IUdpClient видалено, + // оскільки вони знаходяться у файлі IUdpClient.cs і спричиняють помилки дублювання. - public event EventHandler? MessageReceived; - - public UdpClientWrapper(int port) + // Replacement for weak MD5 with SHA256 + public class Sha256Adapter : IHashAlgorithm { - _localEndPoint = new IPEndPoint(IPAddress.Any, port); + private readonly HashAlgorithm _sha256 = SHA256.Create(); + + public byte[] ComputeHash(byte[] buffer) => _sha256.ComputeHash(buffer); + + public void Dispose() + { + _sha256.Dispose(); + } } - public async Task StartListeningAsync() + // -------------------------------------------------------------------------------- + + // UdpClientWrapper implementation + public class UdpClientWrapper : IUdpClient { - _cts = new CancellationTokenSource(); - Console.WriteLine("Start listening for UDP messages..."); + private readonly IPEndPoint _localEndPoint; + private readonly IHashAlgorithm _hashAlgorithm; - try - { - _udpClient = new UdpClient(_localEndPoint); - while (!_cts.Token.IsCancellationRequested) - { - UdpReceiveResult result = await _udpClient.ReceiveAsync(_cts.Token); - MessageReceived?.Invoke(this, result.Buffer); + private UdpClient? _udpClient; + private CancellationTokenSource? _cts; + private bool _disposed = false; - Console.WriteLine($"Received from {result.RemoteEndPoint}"); - } - } - catch (OperationCanceledException ex) + public event EventHandler? MessageReceived; + + public UdpClientWrapper(int port) + : this(port, new Sha256Adapter()) { - //empty } - catch (Exception ex) + + public UdpClientWrapper(int port, IHashAlgorithm hashAlgorithm) { - Console.WriteLine($"Error receiving message: {ex.Message}"); + _localEndPoint = new IPEndPoint(IPAddress.Any, port); + _hashAlgorithm = hashAlgorithm; } - } - public void StopListening() - { - try + public async Task StartListeningAsync() { - _cts?.Cancel(); - _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); + if (_cts != null && !_cts.IsCancellationRequested) return; + if (_disposed) return; + + // Dispose previous CTS if present + _cts?.Dispose(); + _cts = new CancellationTokenSource(); + + Console.WriteLine("Start listening for UDP messages..."); + + try + { + _udpClient = new UdpClient(_localEndPoint); + + CancellationToken token = _cts.Token; + + while (!token.IsCancellationRequested) + { + UdpReceiveResult result = await _udpClient.ReceiveAsync(token); + MessageReceived?.Invoke(this, result.Buffer); + + Console.WriteLine($"Received from {result.RemoteEndPoint}"); + } + } + catch (OperationCanceledException) + { + // Expected exception on cancellation + } + catch (Exception ex) + { + Console.WriteLine($"Error receiving message: {ex.Message}"); + } + finally + { + Cleanup("Listener stopped."); + } } - catch (Exception ex) + + public void StopListening() => Cleanup("Stopped listening for UDP messages."); + + public void Exit() => Cleanup("Stopped listening for UDP messages."); + + private void Cleanup(string message) { - Console.WriteLine($"Error while stopping: {ex.Message}"); + if (_disposed) return; + + try + { + // 1. Cancel the token to stop the ReceiveAsync loop + _cts?.Cancel(); + + // 2. Close and dispose the UDP client + if (_udpClient != null) + { + _udpClient.Close(); + _udpClient.Dispose(); + _udpClient = null; + } + + Console.WriteLine(message); + } + catch (Exception ex) + { + Console.WriteLine($"Error while stopping: {ex.Message}"); + } } - } - public void Exit() - { - try + // Use standard logic for GetHashCode. + public override int GetHashCode() { - _cts?.Cancel(); - _udpClient?.Close(); - Console.WriteLine("Stopped listening for UDP messages."); + // Generate hash code based on immutable fields (port and address) + return HashCode.Combine(_localEndPoint.Address, _localEndPoint.Port); } - catch (Exception ex) + + // Implementation of IDisposable standard pattern + public void Dispose() { - Console.WriteLine($"Error while stopping: {ex.Message}"); + Dispose(true); + GC.SuppressFinalize(this); } - } - public override int GetHashCode() - { - var payload = $"{nameof(UdpClientWrapper)}|{_localEndPoint.Address}|{_localEndPoint.Port}"; + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // Stop listener and clean up UdpClient + Cleanup("Disposing UdpClientWrapper."); - using var md5 = MD5.Create(); - var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(payload)); + // Dispose IHashAlgorithm (SHA256) and CTS + _hashAlgorithm.Dispose(); + _cts?.Dispose(); + } - return BitConverter.ToInt32(hash, 0); + _disposed = true; + } + } } } \ No newline at end of file diff --git a/NetSdrClientAppTests/ArchitectureTests.cs b/NetSdrClientAppTests/ArchitectureTests.cs new file mode 100644 index 00000000..3e46055b --- /dev/null +++ b/NetSdrClientAppTests/ArchitectureTests.cs @@ -0,0 +1,54 @@ +using NetArchTest.Rules; +using NUnit.Framework; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace NetSdrClientAppTests +{ + public class ArchitectureTests + { + [Test] + public void App_Should_Not_Depend_On_EchoServer() + { + var result = Types.InAssembly(typeof(NetSdrClientApp.NetSdrClient).Assembly) + .That() + .ResideInNamespace("NetSdrClientApp") + .ShouldNot() + .HaveDependencyOn("EchoServer") + .GetResult(); + + Assert.That(result.IsSuccessful, Is.True); + } + + [Test] + public void Messages_Should_Not_Depend_On_Networking() + { + // Arrange + var result = Types.InAssembly(typeof(NetSdrClientApp.Messages.NetSdrMessageHelper).Assembly) + .That() + .ResideInNamespace("NetSdrClientApp.Messages") + .ShouldNot() + .HaveDependencyOn("NetSdrClientApp.Networking") + .GetResult(); + + // Assert + Assert.That(result.IsSuccessful, Is.True); + } + + [Test] + public void Networking_Should_Not_Depend_On_Messages() + { + // Arrange + var result = Types.InAssembly(typeof(NetSdrClientApp.Networking.ITcpClient).Assembly) + .That() + .ResideInNamespace("NetSdrClientApp.Networking") + .ShouldNot() + .HaveDependencyOn("NetSdrClientApp.Messages") + .GetResult(); + + // Assert + Assert.That(result.IsSuccessful, Is.True); + } + } +} diff --git a/NetSdrClientAppTests/NetSdrClientAppTests.csproj b/NetSdrClientAppTests/NetSdrClientAppTests.csproj index 3cbc46af..9213cef5 100644 --- a/NetSdrClientAppTests/NetSdrClientAppTests.csproj +++ b/NetSdrClientAppTests/NetSdrClientAppTests.csproj @@ -11,7 +11,12 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs index b40fff79..59e430c8 100644 --- a/NetSdrClientAppTests/NetSdrMessageHelperTests.cs +++ b/NetSdrClientAppTests/NetSdrMessageHelperTests.cs @@ -1,69 +1,313 @@ using NetSdrClientApp.Messages; +using NUnit.Framework; +using System.Linq; +using System; +using System.Text; +using System.Collections.Generic; namespace NetSdrClientAppTests { + [TestFixture] public class NetSdrMessageHelperTests { - [SetUp] - public void Setup() - { - } + // ------------------------------------------------------------------ + // GET MESSAGE TESTS + // ------------------------------------------------------------------ [Test] - public void GetControlItemMessageTest() + public void GetControlItemMessageTest_WithItemCode() { - //Arrange + // Arrange var type = NetSdrMessageHelper.MsgTypes.Ack; var code = NetSdrMessageHelper.ControlItemCodes.ReceiverState; - int parametersLength = 7500; + int parametersLength = 100; - //Act + // Act byte[] msg = NetSdrMessageHelper.GetControlItemMessage(type, code, new byte[parametersLength]); - var headerBytes = msg.Take(2); - var codeBytes = msg.Skip(2).Take(2); - var parametersBytes = msg.Skip(4); + // Assert + // 2 bytes (header) + 2 (code) + 100 (params) = 104 + Assert.That(msg.Length, Is.EqualTo(104)); - var num = BitConverter.ToUInt16(headerBytes.ToArray()); - var actualType = (NetSdrMessageHelper.MsgTypes)(num >> 13); - var actualLength = num - ((int)actualType << 13); - var actualCode = BitConverter.ToInt16(codeBytes.ToArray()); + // Check code (2 bytes) + var actualCode = BitConverter.ToUInt16(msg.Skip(2).Take(2).ToArray()); + Assert.That(actualCode, Is.EqualTo((ushort)code)); + } - //Assert - Assert.That(headerBytes.Count(), Is.EqualTo(2)); - Assert.That(msg.Length, Is.EqualTo(actualLength)); - Assert.That(type, Is.EqualTo(actualType)); + [Test] + public void GetControlItemMessageTest_WithoutItemCode() + { + // Arrange + var type = NetSdrMessageHelper.MsgTypes.Ack; + var code = NetSdrMessageHelper.ControlItemCodes.None; + int parametersLength = 100; - Assert.That(actualCode, Is.EqualTo((short)code)); + // Act + byte[] msg = NetSdrMessageHelper.GetControlItemMessage(type, code, new byte[parametersLength]); - Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); + // Assert + // 2 bytes (header) + 100 bytes parameters = 102 + Assert.That(msg.Length, Is.EqualTo(102)); } [Test] - public void GetDataItemMessageTest() + public void GetDataItemMessageTest_NormalLength() { - //Arrange + // Arrange var type = NetSdrMessageHelper.MsgTypes.DataItem2; int parametersLength = 7500; - //Act + // Act byte[] msg = NetSdrMessageHelper.GetDataItemMessage(type, new byte[parametersLength]); + // Assert (Check if the header is correct) var headerBytes = msg.Take(2); - var parametersBytes = msg.Skip(2); - var num = BitConverter.ToUInt16(headerBytes.ToArray()); var actualType = (NetSdrMessageHelper.MsgTypes)(num >> 13); - var actualLength = num - ((int)actualType << 13); + var actualLength = num & 0x1FFF; // Use mask for clarity - //Assert - Assert.That(headerBytes.Count(), Is.EqualTo(2)); - Assert.That(msg.Length, Is.EqualTo(actualLength)); + // TranslateHeader logic: + // msgLength = 7500 + 2 = 7502 (Length in header) + Assert.That(msg.Length, Is.EqualTo(7502)); + Assert.That(actualLength, Is.EqualTo(7502)); Assert.That(type, Is.EqualTo(actualType)); + } + + [Test] + public void GetHeader_ThrowsExceptionOnTooLongMessage() + { + // Arrange / Act / Assert + // _maxMessageLength = 8191. (8190 + 2) = 8192 > 8191 + int tooLongLength = 8190; + + Assert.Throws(() => + NetSdrMessageHelper.GetControlItemMessage( + NetSdrMessageHelper.MsgTypes.SetControlItem, + NetSdrMessageHelper.ControlItemCodes.None, + new byte[tooLongLength])); + } + + [Test] + public void GetHeader_DataItemEdgeCaseZeroLength() + { + // _maxDataItemMessageLength = 8194. lengthWithHeader = msgLength + 2. msgLength = 8192 + int msgLength = 8192; - Assert.That(parametersBytes.Count(), Is.EqualTo(parametersLength)); + // Act: Call GetMessage, which calls GetHeader + byte[] msg = NetSdrMessageHelper.GetDataItemMessage( + NetSdrMessageHelper.MsgTypes.DataItem0, + new byte[msgLength]); + + // Assert: Check that the actual length in the header is 0 + var headerBytes = msg.Take(2).ToArray(); + var num = BitConverter.ToUInt16(headerBytes); + + // Extract type and length from header + var actualType = (NetSdrMessageHelper.MsgTypes)(num >> 13); + var actualLengthInHeader = num & 0x1FFF; + + Assert.That(actualLengthInHeader, Is.EqualTo(0)); // Length field in header should be 0 + Assert.That(msg.Length, Is.EqualTo(8194)); // Actual physical length is 8194 + Assert.That(actualType, Is.EqualTo(NetSdrMessageHelper.MsgTypes.DataItem0)); + } + + // ------------------------------------------------------------------ + + [Test] + public void TranslateMessage_ShouldDecodeControlItemCorrectly() + { + // Arrange: Create a test message with ControlItemCode + var type = NetSdrMessageHelper.MsgTypes.SetControlItem; + var code = NetSdrMessageHelper.ControlItemCodes.IQOutputDataSampleRate; + byte[] parameters = { 0xAA, 0xBB }; + byte[] msg = NetSdrMessageHelper.GetControlItemMessage(type, code, parameters); + + // Act + bool success = NetSdrMessageHelper.TranslateMessage(msg, out var actualType, out var actualCode, out var sequenceNumber, out var body); + + // Assert + Assert.That(success, Is.True); + Assert.That(actualType, Is.EqualTo(type)); + Assert.That(actualCode, Is.EqualTo(code)); + Assert.That(body, Is.EqualTo(parameters)); + Assert.That(sequenceNumber, Is.EqualTo(0)); + } + + [Test] + public void TranslateMessage_ShouldDecodeDataItemCorrectly() + { + // Arrange: Create a test message with DataItem (DataItem0) + var type = NetSdrMessageHelper.MsgTypes.DataItem0; + ushort expectedSequenceNumber = 0xABCD; + byte[] dataPayload = { 0xAA, 0xBB, 0xCC }; + + // Combine SequenceNumber (2 bytes) and Payload (3 bytes) into the parameters for GetMessage + byte[] parameters = BitConverter.GetBytes(expectedSequenceNumber).Concat(dataPayload).ToArray(); + + byte[] msg = NetSdrMessageHelper.GetDataItemMessage(type, parameters); + + // Act + bool success = NetSdrMessageHelper.TranslateMessage(msg, out var actualType, out var actualCode, out var sequenceNumber, out var body); + + // Assert + Assert.That(success, Is.True); + Assert.That(actualType, Is.EqualTo(type)); + Assert.That(actualCode, Is.EqualTo(NetSdrMessageHelper.ControlItemCodes.None)); + Assert.That(sequenceNumber, Is.EqualTo(expectedSequenceNumber)); + + // The decoded body should only contain the dataPayload (3 bytes) + Assert.That(body, Is.EqualTo(dataPayload)); + Assert.That(body.Length, Is.EqualTo(dataPayload.Length)); + } + + [Test] + public void TranslateMessage_ShouldFailOnInvalidBodyLength() + { + // Arrange: Create a correct header but truncate 1 byte from the body + byte[] parameters = { 0xAA, 0xBB }; + byte[] correctMsg = NetSdrMessageHelper.GetControlItemMessage(NetSdrMessageHelper.MsgTypes.Ack, NetSdrMessageHelper.ControlItemCodes.None, parameters); + byte[] corruptedMsg = correctMsg.Take(correctMsg.Length - 1).ToArray(); + + // Act + bool success = NetSdrMessageHelper.TranslateMessage(corruptedMsg, out var actualType, out var actualCode, out var sequenceNumber, out var body); + + // Assert: Should return false due to length mismatch + Assert.That(success, Is.False); } - //TODO: add more NetSdrMessageHelper tests + [Test] + public void TranslateMessage_ShouldFailOnInvalidControlItemCode() + { + // Arrange: Generate a Control message type but insert a non-existent code (0xFFFF) + var type = NetSdrMessageHelper.MsgTypes.SetControlItem; + // Total length: Header (2) + Invalid Code (2) + Parameters (2) = 6 + byte[] header = BitConverter.GetBytes((ushort)((int)type << 13 | (6))); + byte[] invalidCode = BitConverter.GetBytes((ushort)0xFFFF); // Code not defined in Enum + byte[] msg = header.Concat(invalidCode).Concat(new byte[2]).ToArray(); + + // Act + bool success = NetSdrMessageHelper.TranslateMessage(msg, out var actualType, out var actualCode, out var sequenceNumber, out var body); + + // Assert: Should return false because the code is not defined + Assert.That(success, Is.False); + } + + [Test] + public void TranslateMessage_ShouldFailOnMessageShorterThanHeader() + { + // Arrange: Only 1 byte is provided (min header is 2 bytes) + byte[] shortMsg = { 0x01 }; + + // Act + bool success = NetSdrMessageHelper.TranslateMessage(shortMsg, out var actualType, out var actualCode, out var sequenceNumber, out var body); + + // Assert + Assert.That(success, Is.False); + } + + [Test] + public void TranslateMessage_ShouldFailOnControlBodyTooShort() + { + // Arrange: Control item type, but body length is 1 byte (requires 2 for code) + var type = NetSdrMessageHelper.MsgTypes.SetControlItem; + byte[] header = BitConverter.GetBytes((ushort)((int)type << 13 | (2 + 1))); // Total message length 3 (header + 1 byte body) + byte[] msg = header.Concat(new byte[] { 0xAA }).ToArray(); + + // Act + bool success = NetSdrMessageHelper.TranslateMessage(msg, out var actualType, out var actualCode, out var sequenceNumber, out var body); + + // Assert: Should fail because remainingLength < _msgControlItemLength + Assert.That(success, Is.False); + } + + [Test] + public void TranslateMessage_ShouldFailOnDataBodyTooShort() + { + // Arrange: Data item type, but body length is 1 byte (requires 2 for sequence number) + var type = NetSdrMessageHelper.MsgTypes.DataItem0; + byte[] header = BitConverter.GetBytes((ushort)((int)type << 13 | (2 + 1))); // Total message length 3 (header + 1 byte body) + byte[] msg = header.Concat(new byte[] { 0xAA }).ToArray(); + + // Act + bool success = NetSdrMessageHelper.TranslateMessage(msg, out var actualType, out var actualCode, out var sequenceNumber, out var body); + + // Assert: Should fail because remainingLength < _msgSequenceNumberLength + Assert.That(success, Is.False); + } + + + // ------------------------------------------------------------------ + // GET SAMPLES TESTS + // ------------------------------------------------------------------ + + [Test] + public void GetSamples_ShouldReturnExpectedIntegers_16Bit() + { + //Arrange + ushort sampleSize = 16; // 2 bytes per sample + byte[] body = { 0x01, 0x00, 0x02, 0x00 }; // 2 samples: 1, 2 + + //Act + var samples = NetSdrMessageHelper.GetSamples(sampleSize, body).ToArray(); + + //Assert + Assert.That(samples.Length, Is.EqualTo(2)); + Assert.That(samples[0], Is.EqualTo(1)); + Assert.That(samples[1], Is.EqualTo(2)); + } + + [Test] + public void GetSamples_ShouldHandle32BitSamples() + { + // Arrange: Testing 32-bit samples (4 bytes) + ushort sampleSize = 32; + byte[] body = { 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00 }; + + // Act + var samples = NetSdrMessageHelper.GetSamples(sampleSize, body).ToArray(); + + // Assert + Assert.That(samples.Length, Is.EqualTo(2)); + Assert.That(samples[0], Is.EqualTo(1)); + Assert.That(samples[1], Is.EqualTo(2)); + } + + [Test] + public void GetSamples_ShouldThrowOnTooLargeSampleSize() + { + // Assert: sampleSize > 32 bits + ushort sampleSize = 40; + + Assert.Throws(() => + NetSdrMessageHelper.GetSamples(sampleSize, Array.Empty()).ToArray()); + } + + [Test] + public void GetSamples_ShouldHandleIncompleteBody() + { + // Assert: Body is not a multiple of the sample size (16 bits = 2 bytes, body has 1 byte) + ushort sampleSize = 16; + byte[] body = { 0x01 }; + + // Act + var samples = NetSdrMessageHelper.GetSamples(sampleSize, body).ToArray(); + + // Assert: Should return an empty array + Assert.That(samples.Length, Is.EqualTo(0)); + } + + [Test] + public void GetSamples_ShouldHandleEmptyBody() + { + // Arrange + ushort sampleSize = 16; + byte[] emptyBody = Array.Empty(); + + // Act + var samples = NetSdrMessageHelper.GetSamples(sampleSize, emptyBody).ToArray(); + + // Assert + Assert.That(samples, Is.Empty); + } } } \ No newline at end of file diff --git a/NetSdrClientAppTests/TcpClientWrapperTests.cs b/NetSdrClientAppTests/TcpClientWrapperTests.cs new file mode 100644 index 00000000..666bc720 --- /dev/null +++ b/NetSdrClientAppTests/TcpClientWrapperTests.cs @@ -0,0 +1,320 @@ +using NetSdrClientApp.Networking; +using NUnit.Framework; +using Moq; +using System.Threading.Tasks; +using System; +using System.Threading; +using System.Net.Sockets; +using System.Collections.Generic; +using System.Text; // Added for string sending tests + +namespace NetSdrClientAppTests.Networking +{ + [TestFixture] + public class TcpClientWrapperTests + { + private Mock _clientMock = null!; + private Mock _streamMock = null!; + private TcpClientWrapper _wrapper = null!; + + // TaskCompletionSource ReadAsync + private TaskCompletionSource _readTcs = null!; + + [SetUp] + public void SetUp() + { + _readTcs = new TaskCompletionSource(); + _streamMock = new Mock(); + _clientMock = new Mock(); + + // Setup basic successful behavior + _clientMock.Setup(c => c.GetStream()).Returns(_streamMock.Object); + _clientMock.SetupGet(c => c.Connected).Returns(true); + _streamMock.SetupGet(s => s.CanRead).Returns(true); + _streamMock.SetupGet(s => s.CanWrite).Returns(true); + + // Default setup for ReadAsync: blocks indefinitely unless cancelled (via token) or set manually (via _readTcs) + _streamMock + .Setup(s => s.ReadAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((buffer, offset, size, token) => + { + // , ReadAsync , Disconnect + token.Register(() => _readTcs.TrySetCanceled()); + return _readTcs.Task; + }); + + // Factory returning our mock object for testing + Func factory = () => _clientMock.Object; + + // Initialize the wrapper for testing (using DI constructor) + _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 + // ------------------------------------------------------------------ + + [Test] + public void Connect_WhenNotConnected_ShouldConnectAndStartListening() + { + // Act + _wrapper.Connect(); + + // Assert + _clientMock.Verify(c => c.Connect("127.0.0.1", 5000), Times.Once); + _clientMock.Verify(c => c.GetStream(), Times.Once); + 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(), It.IsAny()), Times.Never); + Assert.That(_wrapper.Connected, Is.True); + } + + // ------------------------------------------------------------------ + // SCENARIO 2: DISCONNECTION + // ------------------------------------------------------------------ + + [Test] + public async Task Disconnect_WhenConnected_ShouldCloseResources() + { + // Arrange: + _wrapper.Connect(); + await Task.Delay(50); // Allow listener task to start + + // Act + _wrapper.Disconnect(); + 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); + Assert.That(_wrapper.Connected, Is.False); + } + + [Test] + public void Disconnect_WhenNotConnected_ShouldDoNothing() + { + // Act + _wrapper.Disconnect(); + + // Assert: Verify Close/Cancel methods were NOT called + _streamMock.Verify(s => s.Close(), Times.Never); + _clientMock.Verify(c => c.Close(), Times.Never); + } + + // ------------------------------------------------------------------ + // SCENARIO 3: CONNECTION ERROR HANDLING + // ------------------------------------------------------------------ + + [Test] + public void Connect_WhenFails_ShouldCatchExceptionAndCleanUp() + { + // Arrange: Set up mock so Connect throws an exception + _clientMock.Setup(c => c.Connect(It.IsAny(), It.IsAny())) + .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 + // ------------------------------------------------------------------ + + [Test] + public async Task SendMessageAsync_WhenConnected_ShouldWriteToStream() + { + // Arrange: Ensure Connect was successful + _wrapper.Connect(); + await Task.Delay(50); + byte[] testData = { 0x01, 0x02, 0x03 }; + + // Act + await _wrapper.SendMessageAsync(testData); + + // Assert + _streamMock.Verify(s => s.WriteAsync( + It.Is(arr => arr == testData), 0, testData.Length, It.IsAny()), + 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(arr => arr.SequenceEqual(expectedData)), + 0, + expectedData.Length, + It.IsAny()), + Times.Once); + } + + [Test] + public void SendMessageAsync_WhenNotConnected_ShouldThrowException() + { + // Act & Assert + Assert.ThrowsAsync( + () => _wrapper.SendMessageAsync(new byte[] { 0x01 })); + } + + // ------------------------------------------------------------------ + // SCENARIO 5: LISTENING LOGIC (Advanced Coverage) + // ------------------------------------------------------------------ + + [Test] + public async Task StartListeningAsync_WhenCancelled_ShouldStopListeningAndDisconnect() + { + // Arrange + _wrapper.Connect(); + await Task.Delay(50); + + // 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 + _streamMock.Verify(s => s.Close(), Times.AtLeastOnce); + _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(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .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(); + + // Setup ReadAsync to return data on the first call, then block indefinitely + var callCount = 0; + _streamMock.Setup(s => s.ReadAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((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."); + 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(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .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); + } + } +} \ No newline at end of file diff --git a/NetSdrClientAppTests/UdpClientWrapperTests.cs b/NetSdrClientAppTests/UdpClientWrapperTests.cs new file mode 100644 index 00000000..56e48d11 --- /dev/null +++ b/NetSdrClientAppTests/UdpClientWrapperTests.cs @@ -0,0 +1,235 @@ +using NUnit.Framework; +using Moq; +using System.Threading.Tasks; +using System; +using System.Threading; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Linq; +using NetSdrClientApp.Networking; +using System.Reflection; + +namespace NetSdrClientAppTests.Networking +{ + [TestFixture] + public class UdpClientWrapperTests + { + private Mock _hashMock = null!; + private UdpClientWrapper _wrapper = null!; + private int _testPort; // Instance field for the port + + // Helper to get an available dynamic port to avoid conflicts + 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(); + _testPort = GetAvailablePort(); // Get a unique port for the test fixture + _wrapper = new UdpClientWrapper(_testPort, _hashMock.Object); + } + + [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 + // ------------------------------------------------------------------ + [Test] + public void Constructor_ShouldInitializeCorrectly() + { + Assert.That(_wrapper, Is.Not.Null); + } + + // ------------------------------------------------------------------ + // TEST 2: GET HASH CODE (FIXED: Reliable hash check) + // ------------------------------------------------------------------ + [Test] + public void GetHashCode_ShouldReturnConsistentHash() + { + // Arrange + // Using a new, temporary wrapper for comparison + var wrapper2 = new UdpClientWrapper(GetPrivateField("_localEndPoint")!.Port, _hashMock.Object); + + // Act + int hashCode1 = _wrapper.GetHashCode(); + int hashCode2 = wrapper2.GetHashCode(); + + // Assert + 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 LOGIC (FIXED: Avoids Moq limitations and SocketException) + // ------------------------------------------------------------------ + + [Test] + 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 + // 1. Verify that the cancellation was requested + Assert.That(cts.IsCancellationRequested, Is.True, "Cancellation token should be cancelled."); + + // 2. Verify that UdpClient field is nullified after Cleanup. + Assert.That(GetPrivateField("_udpClient"), Is.Null, "Internal UdpClient should be nullified after Cleanup."); + } + + [Test] + 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 + Assert.That(cts.IsCancellationRequested, Is.True); + Assert.That(GetPrivateField("_udpClient"), Is.Null, "Internal UdpClient should be nullified after Exit/Cleanup."); + } + + // ------------------------------------------------------------------ + // TEST 4: DISPOSE (Coverage: Dispose(bool), IDisposable implementation) + // ------------------------------------------------------------------ + + [Test] + public void Dispose_ShouldCallCleanupDisposeHashAndMarkAsDisposed() + { + // Arrange + var cts = new CancellationTokenSource(); + SetPrivateField("_cts", cts); + + // Act + _wrapper.Dispose(); + + // Assert + // 1. Verify HashAlgorithm dispose + _hashMock.Verify(h => h.Dispose(), Times.Once, "IHashAlgorithm should be disposed."); + + // 2. Verify cancellation + 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 (Asynchronous logic coverage - NEW TESTS) + // ------------------------------------------------------------------ + + [Test] + public async Task StartListeningAsync_ShouldReceiveDataAndRaiseEvent() + { + // Arrange + byte[] expectedData = Encoding.ASCII.GetBytes("TestPacket"); + byte[] receivedData = Array.Empty(); + var receivedTcs = new TaskCompletionSource(); + + _wrapper.MessageReceived += (sender, data) => + { + receivedData = data; + receivedTcs.SetResult(true); + }; + + var listeningTask = _wrapper.StartListeningAsync(); + + // Allow a small delay for UdpClient to initialize and start listening + await Task.Delay(100); + + // Act: Send data using a separate client + using (var sender = new UdpClient()) + { + var targetEndpoint = new IPEndPoint(IPAddress.Loopback, _testPort); + await sender.SendAsync(expectedData, targetEndpoint); + } + + // Assert: Wait for the event to be raised (or timeout after 1 second) + Assert.That(await receivedTcs.Task.WaitAsync(TimeSpan.FromSeconds(1)), Is.True, "MessageReceived event was not raised."); + Assert.That(receivedData, Is.EqualTo(expectedData), "Received data does not match expected data."); + } + + [Test] + public async Task StartListeningAsync_ShouldStopListeningOnCancellation() + { + // Arrange + var listeningTask = _wrapper.StartListeningAsync(); + + // Allow a small delay for UdpClient to initialize + await Task.Delay(100); + + // Act: Stop listening which cancels the CTS and breaks the ReceiveAsync loop + _wrapper.StopListening(); + + // Assert + // 1. Task should complete within a short time (OperationCanceledException is expected internally) + Assert.That(async () => await listeningTask.WaitAsync(TimeSpan.FromSeconds(1)), Throws.Nothing, + "Listening task should complete gracefully (no exceptions thrown outside) on StopListening."); + + // 2. Verify that UdpClient is null after cleanup + Assert.That(GetPrivateField("_udpClient"), Is.Null, "UdpClient should be nullified after cancellation."); + } + + [Test] + public async Task StartListeningAsync_ShouldHandleStartWhenAlreadyRunning() + { + // 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) + 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 diff --git a/README.md b/README.md index 0eb9d3b4..4bf46145 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Лабораторні з реінжинірингу (8×) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=bugs)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) -[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=ppanchen_NetSdrClient&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=YehorYurch5_NetSdrClient&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=YehorYurch5_NetSdrClient&metric=coverage)](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=YehorYurch5_NetSdrClient&metric=bugs)](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=YehorYurch5_NetSdrClient&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=YehorYurch5_NetSdrClient&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient) +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=YehorYurch5_NetSdrClient&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=YehorYurch5_NetSdrClient&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=YehorYurch5_NetSdrClient&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient) Цей репозиторій використовується для курсу **реінжиніринг ПЗ**.