Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
77cdca9
lab1: configure SonarCloud CI
MinTins Mar 22, 2026
16992a5
lab1: fix sonarcloud keys, remove build.yml
MinTins Mar 22, 2026
7b7ab27
lab1: fix sonarcloud keys
MinTins Mar 22, 2026
d186042
lab1: fix
MinTins Mar 22, 2026
54ab14c
lab1: disable qualitygate wait, suppress node deprecation warning
MinTins Mar 22, 2026
f92d12b
lab2: make _tcpClient readonly
MinTins Mar 22, 2026
4c83db4
lab2: make _udpClient readonly
MinTins Mar 22, 2026
f1144bc
lab2: remove empty statement in NetSdrClient
MinTins Mar 22, 2026
1c999d9
lab2: discard unused out variables type, code, sequenceNum
MinTins Mar 22, 2026
6642cb9
lab2: make _host readonly in TcpClientWrapper
MinTins Mar 22, 2026
00a7706
lab2: make _port readonly in TcpClientWrapper
MinTins Mar 22, 2026
ccfac4b
lab2: remove unused exception variable in OperationCanceledException …
MinTins Mar 22, 2026
86430a8
lab2: make _cancellationTokenSource readonly in EchoServer
MinTins Mar 22, 2026
635625a
lab3: add unit tests, coverage 87%, fix Enum.IsDefined bug, exclude i…
MinTins Mar 22, 2026
1fec8b4
lab3: fix sonarcloud.yml
MinTins Mar 22, 2026
e27cde8
lab4: remove code duplications — extract helper, delegate duplicate m…
MinTins May 28, 2026
6b578ca
ci: trigger SonarCloud analysis on master (lab4 baseline)
MinTins May 28, 2026
bb69177
Merge branch 'lab4/fix-duplications' into master
MinTins May 28, 2026
6b72e76
lab5: add NetArchTest architecture rules + intentional breaking rule
MinTins May 28, 2026
e37b32e
Merge branch 'lab5/arch-tests' into master (lab5 step1: breaking rule…
MinTins May 28, 2026
031a602
lab5: remove intentional breaking rule — all arch tests green
MinTins May 28, 2026
b1757f5
Merge branch 'lab5/arch-tests' into master (lab5 fix: all arch tests …
MinTins May 28, 2026
032d3d3
lab5: fix ArchitectureTests — account for UdpClientWrapper in global …
MinTins May 28, 2026
f44246d
Merge branch 'lab5/arch-tests' into master (lab5: all arch tests green)
MinTins May 28, 2026
eee3944
lab6: refactor EchoServer for testability + add EchoServerTests
MinTins May 28, 2026
9cedee8
lab6: add EchoServerTests to sonarcloud.yml coverage collection
MinTins May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 26 additions & 40 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,3 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

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

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

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

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

name: SonarCloud analysis

on:
Expand All @@ -36,14 +8,16 @@ on:
workflow_dispatch:

permissions:
pull-requests: read # allows SonarCloud to decorate PRs with analysis results
pull-requests: read

jobs:
sonar-check:
name: Sonar Check
runs-on: windows-latest # безпечно для будь-яких .NET проектів
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
with: { fetch-depth: 0 }

- uses: actions/setup-dotnet@v4
Expand All @@ -56,27 +30,39 @@ jobs:
dotnet tool install --global dotnet-sonarscanner
echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH
dotnet sonarscanner begin `
/k:"ppanchen_NetSdrClient" `
/o:"ppanchen" `
/k:"MinTins_ReengineeringCourse" `
/o:"roman-flakei" `
/d:sonar.token="${{ secrets.SONAR_TOKEN }}" `
/d:sonar.cs.opencover.reportsPaths="**/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.qualitygate.wait=false
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 NetSdrClientAppTests with coverage
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 EchoServerTests with coverage
run: |
dotnet test EchoServerTests/EchoServerTests.csproj -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 }}"
Expand Down
166 changes: 166 additions & 0 deletions EchoServerTests/EchoServerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using EchoTcpServer;
using Moq;
using NUnit.Framework;

namespace EchoServerTests
{
public class EchoServerTests
{
private Mock<ITcpListener> _listenerMock = null!;
private EchoServer _server = null!;

[SetUp]
public void Setup()
{
_listenerMock = new Mock<ITcpListener>();
_server = new EchoServer(_listenerMock.Object);
}

// ---------------------------------------------------------------
// StartAsync — запускає listener і зупиняється при скасуванні
// ---------------------------------------------------------------

[Test]
public async Task StartAsync_CallsListenerStart()
{
// Arrange: AcceptTcpClientAsync кидає ObjectDisposedException одразу
// щоб вийти з циклу без реального сокета
_listenerMock
.Setup(l => l.AcceptTcpClientAsync())
.ThrowsAsync(new ObjectDisposedException("listener"));

// Act
await _server.StartAsync();

// Assert
_listenerMock.Verify(l => l.Start(), Times.Once);
}

[Test]
public async Task StartAsync_ExitsLoop_OnObjectDisposedException()
{
// Arrange
_listenerMock
.Setup(l => l.AcceptTcpClientAsync())
.ThrowsAsync(new ObjectDisposedException("listener"));

// Act — не має зависнути
var task = _server.StartAsync();
await Task.WhenAny(task, Task.Delay(2000));

// Assert — метод завершився без timeout
Assert.That(task.IsCompleted, Is.True);
}

// ---------------------------------------------------------------
// Stop — скасовує токен і зупиняє listener
// ---------------------------------------------------------------

[Test]
public void Stop_CallsListenerStop()
{
// Act
_server.Stop();

// Assert
_listenerMock.Verify(l => l.Stop(), Times.Once);
}

[Test]
public void Stop_CanBeCalledWithoutStart()
{
// Act & Assert — не кидає виняток
Assert.DoesNotThrow(() => _server.Stop());
}

// ---------------------------------------------------------------
// HandleClientAsync — основна логіка echo
// ---------------------------------------------------------------

[Test]
public async Task HandleClientAsync_EchoesDataBack()
{
// Arrange: два TcpClient спілкуються через loopback
using var serverSocket = new TcpListener(System.Net.IPAddress.Loopback, 0);
serverSocket.Start();
int port = ((System.Net.IPEndPoint)serverSocket.LocalEndpoint).Port;

using var clientTcp = new TcpClient();
await clientTcp.ConnectAsync(System.Net.IPAddress.Loopback, port);
using var serverClient = await serverSocket.AcceptTcpClientAsync();
serverSocket.Stop();

var cts = new CancellationTokenSource();
var handleTask = _server.HandleClientAsync(serverClient, cts.Token);

// Act: надіслати дані і прочитати echo
byte[] sent = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var clientStream = clientTcp.GetStream();
await clientStream.WriteAsync(sent, 0, sent.Length);

byte[] received = new byte[sent.Length];
int bytesRead = await clientStream.ReadAsync(received, 0, received.Length);

// Assert
Assert.That(bytesRead, Is.EqualTo(sent.Length));
Assert.That(received, Is.EqualTo(sent));

// Cleanup
cts.Cancel();
clientTcp.Close();
await Task.WhenAny(handleTask, Task.Delay(1000));
}

[Test]
public async Task HandleClientAsync_StopsOnCancellation()
{
// Arrange
using var serverSocket = new TcpListener(System.Net.IPAddress.Loopback, 0);
serverSocket.Start();
int port = ((System.Net.IPEndPoint)serverSocket.LocalEndpoint).Port;

using var clientTcp = new TcpClient();
await clientTcp.ConnectAsync(System.Net.IPAddress.Loopback, port);
using var serverClient = await serverSocket.AcceptTcpClientAsync();
serverSocket.Stop();

var cts = new CancellationTokenSource();

// Act: скасувати токен одразу
cts.Cancel();
var handleTask = _server.HandleClientAsync(serverClient, cts.Token);
await Task.WhenAny(handleTask, Task.Delay(2000));

// Assert — завершилось без зависання
Assert.That(handleTask.IsCompleted, Is.True);
}

[Test]
public async Task HandleClientAsync_ClosesClientOnCompletion()
{
// Arrange
using var serverSocket = new TcpListener(System.Net.IPAddress.Loopback, 0);
serverSocket.Start();
int port = ((System.Net.IPEndPoint)serverSocket.LocalEndpoint).Port;

using var clientTcp = new TcpClient();
await clientTcp.ConnectAsync(System.Net.IPAddress.Loopback, port);
using var serverClient = await serverSocket.AcceptTcpClientAsync();
serverSocket.Stop();

var cts = new CancellationTokenSource();
cts.Cancel();

// Act
await Task.WhenAny(
_server.HandleClientAsync(serverClient, cts.Token),
Task.Delay(2000));

// Assert — після завершення клієнт закритий
Assert.That(serverClient.Connected, Is.False);
}
}
}
32 changes: 32 additions & 0 deletions EchoServerTests/EchoServerTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="coverlet.msbuild" Version="10.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit.Analyzers" Version="4.7.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\EchoTcpServer\EchoServer.csproj" />
</ItemGroup>

</Project>
81 changes: 81 additions & 0 deletions EchoTcpServer/EchoServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

namespace EchoTcpServer
{
/// <summary>
/// TCP echo server. Accepts connections and echoes back all received bytes.
/// Refactored for testability: accepts ITcpListener so real socket can be
/// replaced with a mock in unit tests.
/// </summary>
public class EchoServer
{
private readonly ITcpListener _listener;
private readonly CancellationTokenSource _cts;

public EchoServer(ITcpListener listener)
{
_listener = listener;
_cts = new CancellationTokenSource();
}

public async Task StartAsync()
{
_listener.Start();
Console.WriteLine("Server started.");

while (!_cts.Token.IsCancellationRequested)
{
try
{
TcpClient client = await _listener.AcceptTcpClientAsync();
Console.WriteLine("Client connected.");
_ = Task.Run(() => HandleClientAsync(client, _cts.Token));
}
catch (ObjectDisposedException)
{
break;
}
}

Console.WriteLine("Server shutdown.");
}

// internal — accessible from EchoServerTests via InternalsVisibleTo
internal 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)
{
await stream.WriteAsync(buffer, 0, bytesRead, token);
Console.WriteLine($"Echoed {bytesRead} bytes to the client.");
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
client.Close();
Console.WriteLine("Client disconnected.");
}
}

public void Stop()
{
_cts.Cancel();
_listener.Stop();
_cts.Dispose();
Console.WriteLine("Server stopped.");
}
}
}
Loading