Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 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
8425ce9
Merge branch 'lab6/echo-server-tests' into master
MinTins May 28, 2026
9cedee8
lab6: add EchoServerTests to sonarcloud.yml coverage collection
MinTins May 28, 2026
856d002
Merge branch 'lab6/echo-server-tests' into master (lab6: fix coverage…
MinTins May 28, 2026
a5b9c3c
lab7: update dependencies + add dependabot.yml
MinTins May 28, 2026
f1007ae
Merge branch 'lab7/update-dependencies' into master
MinTins May 28, 2026
30f605e
lab8: green Quality Gate — fix reliability issues, add tests, enable …
MinTins May 28, 2026
3662928
Merge branch 'lab8/green-quality-gate' into master
MinTins May 28, 2026
aa25642
ci: retrigger after Quality Gate assignment
MinTins May 28, 2026
6643e70
lab8: fix coverage — replace partial classes with proper test file
MinTins May 28, 2026
ca4c25d
Merge branch 'lab8/green-quality-gate' into master (lab8: fix coverag…
MinTins May 28, 2026
bb27081
lab8: fix all SonarCloud issues — maintainability + reliability
MinTins May 28, 2026
87083a7
Merge branch 'lab8/green-quality-gate' into master (lab8: fix all Son…
MinTins May 28, 2026
c28168e
lab8: fix compile errors — static method calls, namespace braces, IDi…
MinTins May 28, 2026
7180d1a
Merge branch 'lab8/green-quality-gate' into master (lab8: fix compile…
MinTins May 28, 2026
18d65a9
lab8: fix compile errors + remaining warnings
MinTins May 28, 2026
cd0d429
Merge branch 'lab8/green-quality-gate' into master (lab8: fix compile…
MinTins May 28, 2026
35b9e1c
lab8: fix Rule 3 in ArchitectureTests — UdpClientWrapper now in Netwo…
MinTins May 28, 2026
7245711
Merge branch 'lab8/green-quality-gate' into master (lab8: fix arch te…
MinTins May 28, 2026
ec81e39
lab8: fix coverage — revert static HandleClientAsync, exclude infra, …
MinTins May 28, 2026
7ee877a
Merge branch 'lab8/green-quality-gate' into master (lab8: fix coverage)
MinTins May 28, 2026
8d4487d
lab8: fix NetSdrMessageHelperTests.cs — new tests were outside class …
MinTins May 28, 2026
3da67a7
Merge branch 'lab8/green-quality-gate' into master (lab8: fix test fi…
MinTins May 28, 2026
e8caac5
lab8: fix Blocker bugs + remaining smells
MinTins May 28, 2026
72163c4
Merge branch 'lab8/green-quality-gate' into master (lab8: fix Blocker…
MinTins May 28, 2026
7aea382
lab8: fix remaining smells — IDisposable, pragma, Assert.Multiple
MinTins May 28, 2026
8e11278
Merge branch 'lab8/green-quality-gate' into master (lab8: fix remaini…
MinTins May 28, 2026
aaa506e
lab8: final fixes — static HandleClientAsync, exclude FileStream, add…
MinTins May 28, 2026
2372f6e
Merge branch 'lab8/green-quality-gate' into master (lab8: final fixes)
MinTins May 28, 2026
0130078
lab8: push coverage toward 100% on new code
MinTins May 28, 2026
d5b1882
Merge branch 'lab8/green-quality-gate' into master (lab8: push covera…
MinTins May 28, 2026
b1f155b
lab8: fix test assertion — DataItem body is 8190 (8192 - 2 seq bytes)
MinTins May 28, 2026
bd4c17d
Merge branch 'lab8/green-quality-gate' into master (lab8: fix test as…
MinTins May 28, 2026
81807a7
lab8: achieve 100% coverage on new code + fix ReadAsync issue
MinTins May 28, 2026
ad07943
Merge branch 'lab8/green-quality-gate' into master (lab8: 100% covera…
MinTins May 28, 2026
d9f9af4
lab8: fill empty catch block in HandleClientAsync (S108)
MinTins May 28, 2026
01ce6eb
Merge branch 'lab8/green-quality-gate' into master (lab8: fix empty c…
MinTins May 28, 2026
f519687
docs: update README badges to point to MinTins_ReengineeringCourse pr…
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
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "nuget"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
69 changes: 30 additions & 39 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,44 @@ 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
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

- name: Tests NetSdrArchTests
run: |
dotnet test NetSdrArchTests/NetSdrArchTests.csproj -c Release --no-build
shell: pwsh

# 3) END: SonarScanner
- name: SonarScanner End
run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
Expand Down
266 changes: 266 additions & 0 deletions EchoServerTests/EchoServerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
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 = EchoServer.HandleClientAsync(serverClient, cts.Token);

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

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

// Assert
Assert.Multiple(() =>
{
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 = EchoServer.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(
EchoServer.HandleClientAsync(serverClient, cts.Token),
Task.Delay(2000));

// Assert — після завершення клієнт закритий
Assert.That(serverClient.Connected, Is.False);
}
[Test]
public async Task HandleClientAsync_HandlesException_DoesNotThrow()
{
// Arrange: клієнт що одразу закривається — ReadAsync кине виняток
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();

// Закрити клієнта до HandleClientAsync — ReadAsync кине IOException
clientTcp.Close();

// Act — не має кинути виняток (catch block)
var task = EchoServer.HandleClientAsync(serverClient, cts.Token);
await Task.WhenAny(task, Task.Delay(2000));

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

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

var startTask = _server.StartAsync();
await Task.WhenAny(startTask, Task.Delay(1000));

// Act
_server.Stop();

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

[Test]
public async Task HandleClientAsync_MultipleChunks_EchoesAll()
{
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 = EchoServer.HandleClientAsync(serverClient, cts.Token);

var clientStream = clientTcp.GetStream();

// Надіслати два чанки
byte[] chunk1 = new byte[] { 0x01, 0x02 };
byte[] chunk2 = new byte[] { 0x03, 0x04 };

await clientStream.WriteAsync(chunk1.AsMemory());
byte[] recv1 = new byte[2];
await ReadExactAsync(clientStream, recv1, recv1.Length);

await clientStream.WriteAsync(chunk2.AsMemory());
byte[] recv2 = new byte[2];
await ReadExactAsync(clientStream, recv2, recv2.Length);

Assert.Multiple(() =>
{
Assert.That(recv1, Is.EqualTo(chunk1));
Assert.That(recv2, Is.EqualTo(chunk2));
});

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

private static async Task ReadExactAsync(
System.IO.Stream stream, byte[] buffer, int count)
{
int offset = 0;
while (offset < count)
{
int read = await stream.ReadAsync(
buffer.AsMemory(offset, count - offset));
if (read == 0) break;
offset += read;
}
}

}
}
Loading