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×)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
-[](https://sonarcloud.io/summary/new_code?id=ppanchen_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient)
+[](https://sonarcloud.io/summary/new_code?id=YehorYurch5_NetSdrClient)
Цей репозиторій використовується для курсу **реінжиніринг ПЗ**.