From 638d13d16534ee2230ff7861c7cfe824ce7a4304 Mon Sep 17 00:00:00 2001 From: NETTOUR Marwan Date: Wed, 8 Apr 2026 07:56:06 -0400 Subject: [PATCH 1/9] [refactor] Replace Path.GetTempPath with AbstractTester in tests --- src/ByteSync.Client/ByteSync.Client.csproj | 1 - .../Transfers/R2DownloadResume_Tests.cs | 10 ++++++---- .../Transfers/R2UploadDownload_Tests.cs | 10 ++++++---- .../SynchronizationDownloadFinalizerTests.cs | 12 +++++++----- ...ventoryComparerPropagateAccessIssuesTests.cs | 17 +++++------------ .../LocalApplicationDataManagerTests.cs | 12 +++++++----- .../DataNodes/DataNodeSourcesViewModelTests.cs | 6 ++++-- 7 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/ByteSync.Client/ByteSync.Client.csproj b/src/ByteSync.Client/ByteSync.Client.csproj index f574ba42b..cd7a5455a 100644 --- a/src/ByteSync.Client/ByteSync.Client.csproj +++ b/src/ByteSync.Client/ByteSync.Client.csproj @@ -109,7 +109,6 @@ ResXFileCodeGenerator Resources.Designer.cs - diff --git a/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2DownloadResume_Tests.cs b/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2DownloadResume_Tests.cs index f3cc094e9..85bd87e77 100644 --- a/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2DownloadResume_Tests.cs +++ b/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2DownloadResume_Tests.cs @@ -6,6 +6,7 @@ using ByteSync.Common.Business.Actions; using ByteSync.Common.Business.Inventories; using ByteSync.Common.Business.SharedFiles; +using ByteSync.TestsCommon; using ByteSync.DependencyInjection; using ByteSync.Interfaces.Controls.Communications.Http; using ByteSync.Interfaces.Factories; @@ -22,13 +23,14 @@ namespace ByteSync.Client.IntegrationTests.Services.Communications.Transfers; -public class R2DownloadResume_Tests +public class R2DownloadResume_Tests : AbstractTester { private ILifetimeScope _clientScope = null!; [SetUp] public void SetUp() { + CreateTestDirectory(); // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (ByteSync.Services.ContainerProvider.Container == null) { @@ -99,7 +101,7 @@ public async Task Download_WithTransientFailure_Should_Retry_And_Succeed() Source = new SharedDataPart { ClientInstanceId = shared.ClientInstanceId, - RootPath = Path.GetTempPath(), + RootPath = TestDirectory.FullName, InventoryPartType = FileSystemTypes.File, Name = "itests", InventoryCodeAndId = "itests" @@ -113,7 +115,7 @@ public async Task Download_WithTransientFailure_Should_Retry_And_Succeed() sag.Targets.Add(new SharedDataPart { ClientInstanceId = shared.ClientInstanceId, - RootPath = Path.GetTempFileName(), + RootPath = Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N") + ".tmp"), InventoryPartType = FileSystemTypes.File, Name = "itests", InventoryCodeAndId = "itests" @@ -123,7 +125,7 @@ public async Task Download_WithTransientFailure_Should_Retry_And_Succeed() // First upload a file so we can download it var inputContent = new string('z', 1_000_000); - var tempFile = Path.GetTempFileName(); + var tempFile = Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N") + ".tmp"); await File.WriteAllTextAsync(tempFile, inputContent); var uploader = uploaderFactory.Build(tempFile, shared); diff --git a/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2UploadDownload_Tests.cs b/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2UploadDownload_Tests.cs index 6052c0150..f6a3ca14b 100644 --- a/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2UploadDownload_Tests.cs +++ b/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2UploadDownload_Tests.cs @@ -4,6 +4,7 @@ using ByteSync.Client.IntegrationTests.TestHelpers; using ByteSync.Common.Business.Inventories; using ByteSync.Common.Business.SharedFiles; +using ByteSync.TestsCommon; using ByteSync.DependencyInjection; using ByteSync.Interfaces.Controls.Communications.Http; using ByteSync.Interfaces.Factories; @@ -19,13 +20,14 @@ namespace ByteSync.Client.IntegrationTests.Services.Communications.Transfers; -public class R2UploadDownload_Tests +public class R2UploadDownload_Tests : AbstractTester { private ILifetimeScope _clientScope = null!; [SetUp] public void SetUp() { + CreateTestDirectory(); // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (ByteSync.Services.ContainerProvider.Container == null) { @@ -93,7 +95,7 @@ public async Task Upload_Then_Download_Should_Succeed_With_Small_Chunks() Source = new SharedDataPart { ClientInstanceId = shared.ClientInstanceId, - RootPath = Path.GetTempPath(), + RootPath = TestDirectory.FullName, InventoryPartType = FileSystemTypes.File, Name = "itests", InventoryCodeAndId = "itests" @@ -107,7 +109,7 @@ public async Task Upload_Then_Download_Should_Succeed_With_Small_Chunks() sag.Targets.Add(new SharedDataPart { ClientInstanceId = shared.ClientInstanceId, - RootPath = Path.GetTempFileName(), + RootPath = Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N") + ".tmp"), InventoryPartType = FileSystemTypes.File, Name = "itests", InventoryCodeAndId = "itests" @@ -116,7 +118,7 @@ public async Task Upload_Then_Download_Should_Succeed_With_Small_Chunks() sharedActionsGroupRepository.SetSharedActionsGroups([sag]); var inputContent = new string('x', 1_000_000); - var tempFile = Path.GetTempFileName(); + var tempFile = Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N") + ".tmp"); await File.WriteAllTextAsync(tempFile, inputContent); var uploader = uploaderFactory.Build(tempFile, shared); diff --git a/tests/ByteSync.Client.UnitTests/Services/Communications/Transfers/Downloading/SynchronizationDownloadFinalizerTests.cs b/tests/ByteSync.Client.UnitTests/Services/Communications/Transfers/Downloading/SynchronizationDownloadFinalizerTests.cs index 563f80c38..56cf47a1a 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Communications/Transfers/Downloading/SynchronizationDownloadFinalizerTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Communications/Transfers/Downloading/SynchronizationDownloadFinalizerTests.cs @@ -8,11 +8,12 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; +using ByteSync.TestsCommon; namespace ByteSync.Client.UnitTests.Services.Communications.Transfers.Downloading; [TestFixture] -public class SynchronizationDownloadFinalizerTests +public class SynchronizationDownloadFinalizerTests : AbstractTester { private Mock _deltaManager = null!; private Mock _temporaryFileManagerFactory = null!; @@ -24,6 +25,7 @@ public class SynchronizationDownloadFinalizerTests [SetUp] public void Setup() { + CreateTestDirectory(); _deltaManager = new Mock(MockBehavior.Strict); _temporaryFileManagerFactory = new Mock(MockBehavior.Strict); _fileDatesSetter = new Mock(MockBehavior.Strict); @@ -276,15 +278,15 @@ private static void CreateEntryWithContent(ZipArchive archive, string entryName, writer.Write(content); } - private static string GetTempFilePath() + private string GetTempFilePath() { - var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var path = Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N")); return path; } - private static string GetNewTempPath(string extension) + private string GetNewTempPath(string extension) { - return Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + extension); + return Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N") + extension); } } \ No newline at end of file diff --git a/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerPropagateAccessIssuesTests.cs b/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerPropagateAccessIssuesTests.cs index 63cdb5e3c..90b4e6c10 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerPropagateAccessIssuesTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerPropagateAccessIssuesTests.cs @@ -7,32 +7,25 @@ using ByteSync.Models.FileSystems; using ByteSync.Models.Inventories; using ByteSync.Services.Comparisons; +using ByteSync.TestsCommon; using FluentAssertions; using NUnit.Framework; namespace ByteSync.Client.UnitTests.Services.Comparisons; [TestFixture] -public class InventoryComparerPropagateAccessIssuesTests +public class InventoryComparerPropagateAccessIssuesTests : AbstractTester { private string _tempDirectory = null!; [SetUp] public void Setup() { - _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(_tempDirectory); - } - - [TearDown] - public void TearDown() - { - if (Directory.Exists(_tempDirectory)) - { - Directory.Delete(_tempDirectory, true); - } + CreateTestDirectory(); + _tempDirectory = TestDirectory.FullName; } + private static string CreateInventoryZipFile(string directory, Inventory inventory) { var zipPath = Path.Combine(directory, $"{Guid.NewGuid()}.zip"); diff --git a/tests/ByteSync.Client.UnitTests/Services/Configurations/LocalApplicationDataManagerTests.cs b/tests/ByteSync.Client.UnitTests/Services/Configurations/LocalApplicationDataManagerTests.cs index 2b2788347..ba2faf57b 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Configurations/LocalApplicationDataManagerTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Configurations/LocalApplicationDataManagerTests.cs @@ -8,17 +8,19 @@ using FluentAssertions; using Moq; using NUnit.Framework; +using ByteSync.TestsCommon; namespace ByteSync.Client.UnitTests.Services.Configurations; [TestFixture] -public class LocalApplicationDataManagerTests +public class LocalApplicationDataManagerTests : AbstractTester { private Mock _environmentServiceMock = null!; [SetUp] public void SetUp() { + CreateTestDirectory(); _environmentServiceMock = new Mock(); _environmentServiceMock.SetupGet(e => e.ExecutionMode).Returns(ExecutionMode.Regular); _environmentServiceMock.SetupProperty(e => e.Arguments, []); @@ -81,7 +83,7 @@ public void ApplicationDataPath_Should_Be_Logical_When_Not_Store() public void ApplicationDataPath_Should_Append_CustomSuffix_When_DebugArgumentProvided(OSPlatforms osPlatform) { // Arrange - var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var tempRoot = TestDirectory.FullName; Directory.CreateDirectory(tempRoot); var assemblyDirectory = Directory.CreateDirectory(Path.Combine(tempRoot, "Portable")).FullName; var assemblyPath = Path.Combine(assemblyDirectory, "ByteSync.exe"); @@ -131,7 +133,7 @@ public void ApplicationDataPath_Should_Append_CustomSuffix_When_DebugArgumentPro public void ApplicationDataPath_Should_Create_DebugDirectory_When_DebugModeWithoutOverride(OSPlatforms osPlatform) { // Arrange - var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var tempRoot = TestDirectory.FullName; Directory.CreateDirectory(tempRoot); var assemblyDirectory = Directory.CreateDirectory(Path.Combine(tempRoot, "Portable")).FullName; var assemblyPath = Path.Combine(assemblyDirectory, "ByteSync.exe"); @@ -180,7 +182,7 @@ public void ApplicationDataPath_Should_Create_DebugDirectory_When_DebugModeWitho public void LogFilePath_Should_Return_MostRecent_Log_Excluding_Debug() { // Arrange - var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var tempRoot = TestDirectory.FullName; Directory.CreateDirectory(tempRoot); var assemblyDirectory = Directory.CreateDirectory(Path.Combine(tempRoot, "Portable")).FullName; var assemblyPath = Path.Combine(assemblyDirectory, "ByteSync.exe"); @@ -243,7 +245,7 @@ public void LogFilePath_Should_Return_MostRecent_Log_Excluding_Debug() public void DebugLogFilePath_Should_Return_MostRecent_Debug_Log() { // Arrange - var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var tempRoot = TestDirectory.FullName; Directory.CreateDirectory(tempRoot); var assemblyDirectory = Directory.CreateDirectory(Path.Combine(tempRoot, "Portable")).FullName; var assemblyPath = Path.Combine(assemblyDirectory, "ByteSync.exe"); diff --git a/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeSourcesViewModelTests.cs b/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeSourcesViewModelTests.cs index 8477454ec..12f77d243 100644 --- a/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeSourcesViewModelTests.cs +++ b/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeSourcesViewModelTests.cs @@ -12,11 +12,12 @@ using FluentAssertions; using Moq; using NUnit.Framework; +using ByteSync.TestsCommon; namespace ByteSync.Client.UnitTests.ViewModels.Sessions.DataNodes; [TestFixture] -public class DataNodeSourcesViewModelTests +public class DataNodeSourcesViewModelTests : AbstractTester { private Mock _sessionServiceMock = null!; private Mock _dataSourceServiceMock = null!; @@ -29,6 +30,7 @@ public class DataNodeSourcesViewModelTests [SetUp] public void SetUp() { + CreateTestDirectory(); _dataNode = new DataNode { Id = "DN_1" }; _sessionServiceMock = new Mock(); _dataSourceServiceMock = new Mock(); @@ -58,7 +60,7 @@ public void Constructor_WithAllDependencies_ShouldCreateInstance() public async Task AddDirectoryCommand_WhenUserSelectsDirectory_ShouldCallCreateAndTryAddDataSource() { // Arrange - var selectedPath = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar); + var selectedPath = TestDirectory.FullName.TrimEnd(Path.DirectorySeparatorChar); _fileDialogServiceMock .Setup(f => f.ShowOpenFolderDialogAsync(It.IsAny())) .ReturnsAsync(selectedPath); From cadf6ada2e44ddb4a06cf84dad129a967eb6d049 Mon Sep 17 00:00:00 2001 From: NETTOUR Marwan Date: Wed, 8 Apr 2026 08:10:32 -0400 Subject: [PATCH 2/9] [fix] restore file --- src/ByteSync.Client/ByteSync.Client.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ByteSync.Client/ByteSync.Client.csproj b/src/ByteSync.Client/ByteSync.Client.csproj index cd7a5455a..f574ba42b 100644 --- a/src/ByteSync.Client/ByteSync.Client.csproj +++ b/src/ByteSync.Client/ByteSync.Client.csproj @@ -109,6 +109,7 @@ ResXFileCodeGenerator Resources.Designer.cs + From b5216adf20a91fb2b3194238f9c46616e1b276c3 Mon Sep 17 00:00:00 2001 From: NETTOUR Marwan Date: Thu, 9 Apr 2026 03:45:21 -0400 Subject: [PATCH 3/9] [fix] Copilot comments & remove all getfrompath from tests --- .../Transfers/R2DownloadResume_Tests.cs | 5 + ...ventoryComparerIncompletePartsFlatTests.cs | 11 +- ...ntoryComparerPropagateAccessIssuesTests.cs | 9 + .../Inventories/FileSystemInspectorTests.cs | 175 +++++++----------- .../InventoryLoaderIncompleteFlagTests.cs | 11 +- .../PosixFileTypeClassifierTests.cs | 112 +++++------ 6 files changed, 138 insertions(+), 185 deletions(-) diff --git a/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2DownloadResume_Tests.cs b/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2DownloadResume_Tests.cs index 85bd87e77..1351b7b32 100644 --- a/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2DownloadResume_Tests.cs +++ b/tests/ByteSync.Client.IntegrationTests/Services/Communications/Transfers/R2DownloadResume_Tests.cs @@ -60,6 +60,11 @@ public void SetUp() public void TearDown() { _clientScope.Dispose(); + + if (TestDirectory?.Exists == true) + { + TestDirectory.Delete(true); + } } [Test] diff --git a/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerIncompletePartsFlatTests.cs b/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerIncompletePartsFlatTests.cs index 34ce854fb..6221b7cad 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerIncompletePartsFlatTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerIncompletePartsFlatTests.cs @@ -8,28 +8,29 @@ using ByteSync.Models.Inventories; using ByteSync.Services.Comparisons; using FluentAssertions; +using ByteSync.TestsCommon; using NUnit.Framework; namespace ByteSync.Client.UnitTests.Services.Comparisons; [TestFixture] -public class InventoryComparerIncompletePartsFlatTests +public class InventoryComparerIncompletePartsFlatTests : AbstractTester { private string _tempDirectory = null!; [SetUp] public void Setup() { - _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(_tempDirectory); + CreateTestDirectory(); + _tempDirectory = TestDirectory.FullName; } [TearDown] public void TearDown() { - if (Directory.Exists(_tempDirectory)) + if (TestDirectory?.Exists == true) { - Directory.Delete(_tempDirectory, true); + TestDirectory.Delete(true); } } diff --git a/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerPropagateAccessIssuesTests.cs b/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerPropagateAccessIssuesTests.cs index 90b4e6c10..6c44889ba 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerPropagateAccessIssuesTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Comparisons/InventoryComparerPropagateAccessIssuesTests.cs @@ -24,6 +24,15 @@ public void Setup() CreateTestDirectory(); _tempDirectory = TestDirectory.FullName; } + + [TearDown] + public void TearDown() + { + if (TestDirectory?.Exists == true) + { + TestDirectory.Delete(true); + } + } private static string CreateInventoryZipFile(string directory, Inventory inventory) diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs index 6a61e25cc..dc31fee82 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs @@ -2,89 +2,84 @@ using ByteSync.Common.Business.Misc; using ByteSync.Interfaces.Controls.Inventories; using ByteSync.Services.Inventories; +using ByteSync.TestsCommon; using FluentAssertions; using Moq; using NUnit.Framework; namespace ByteSync.Client.UnitTests.Services.Inventories; -public class FileSystemInspectorTests +public class FileSystemInspectorTests : AbstractTester { + [SetUp] + public void SetUp() + { + CreateTestDirectory(); + } + + [TearDown] + public void TearDown() + { + if (TestDirectory?.Exists == true) + { + TestDirectory.Delete(true); + } + } + [Test] public void ClassifyEntry_ReturnsDirectory_ForDirectoryInfo() { var posix = new Mock(MockBehavior.Strict); posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Unknown); var inspector = new FileSystemInspector(posix.Object); - var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); - - try - { - var result = inspector.ClassifyEntry(tempDirectory); - - result.Should().Be(FileSystemEntryKind.Directory); - } - finally - { - Directory.Delete(tempDirectory.FullName, true); - } + var tempDirectory = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N"))); + + var result = inspector.ClassifyEntry(tempDirectory); + + result.Should().Be(FileSystemEntryKind.Directory); } - + [Test] public void ClassifyEntry_ReturnsRegularFile_ForFileInfo() { var posix = new Mock(MockBehavior.Strict); posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Unknown); var inspector = new FileSystemInspector(posix.Object); - var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + var tempDirectory = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N"))); var tempFilePath = Path.Combine(tempDirectory.FullName, "file.txt"); File.WriteAllText(tempFilePath, "x"); var fileInfo = new FileInfo(tempFilePath); - - try - { - var result = inspector.ClassifyEntry(fileInfo); - - result.Should().Be(FileSystemEntryKind.RegularFile); - } - finally - { - Directory.Delete(tempDirectory.FullName, true); - } + + var result = inspector.ClassifyEntry(fileInfo); + + result.Should().Be(FileSystemEntryKind.RegularFile); } - + [Test] public void ClassifyEntry_ReturnsSymlink_WhenLinkTargetExists() { var posix = new Mock(MockBehavior.Strict); posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Unknown); var inspector = new FileSystemInspector(posix.Object); - var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + var tempDirectory = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N"))); var targetPath = Path.Combine(tempDirectory.FullName, "target.txt"); File.WriteAllText(targetPath, "x"); var linkPath = Path.Combine(tempDirectory.FullName, "link.txt"); - + try { - try - { - File.CreateSymbolicLink(linkPath, targetPath); - } - catch (Exception ex) - { - Assert.Ignore($"Symbolic link creation failed: {ex.GetType().Name}"); - } - - var result = inspector.ClassifyEntry(new FileInfo(linkPath)); - - result.Should().Be(FileSystemEntryKind.Symlink); + File.CreateSymbolicLink(linkPath, targetPath); } - finally + catch (Exception ex) { - Directory.Delete(tempDirectory.FullName, true); + Assert.Ignore($"Symbolic link creation failed: {ex.GetType().Name}"); } + + var result = inspector.ClassifyEntry(new FileInfo(linkPath)); + + result.Should().Be(FileSystemEntryKind.Symlink); } - + [Test] [Platform(Include = "Linux,MacOsX")] public void ClassifyEntry_ReturnsPosixSpecialKind_WhenClassifierProvidesOne() @@ -92,23 +87,16 @@ public void ClassifyEntry_ReturnsPosixSpecialKind_WhenClassifierProvidesOne() var posix = new Mock(MockBehavior.Strict); posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Fifo); var inspector = new FileSystemInspector(posix.Object); - var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + var tempDirectory = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N"))); var tempFilePath = Path.Combine(tempDirectory.FullName, "file.txt"); File.WriteAllText(tempFilePath, "x"); var fileInfo = new FileInfo(tempFilePath); - - try - { - var result = inspector.ClassifyEntry(fileInfo); - - result.Should().Be(FileSystemEntryKind.Fifo); - } - finally - { - Directory.Delete(tempDirectory.FullName, true); - } + + var result = inspector.ClassifyEntry(fileInfo); + + result.Should().Be(FileSystemEntryKind.Fifo); } - + [Test] [Platform(Include = "Linux,MacOsX")] public void ClassifyEntry_FallsBackToRegularFile_WhenPosixClassifierThrows() @@ -116,100 +104,61 @@ public void ClassifyEntry_FallsBackToRegularFile_WhenPosixClassifierThrows() var posix = new Mock(MockBehavior.Strict); posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Throws(new InvalidOperationException("boom")); var inspector = new FileSystemInspector(posix.Object); - var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + var tempDirectory = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N"))); var tempFilePath = Path.Combine(tempDirectory.FullName, "file.txt"); File.WriteAllText(tempFilePath, "x"); var fileInfo = new FileInfo(tempFilePath); - - try - { - var result = inspector.ClassifyEntry(fileInfo); - - result.Should().Be(FileSystemEntryKind.RegularFile); - } - finally - { - Directory.Delete(tempDirectory.FullName, true); - } + + var result = inspector.ClassifyEntry(fileInfo); + + result.Should().Be(FileSystemEntryKind.RegularFile); } [Test] public void IsNoiseDirectoryName_ShouldReturnTrue_ForKnownNoiseDirectory() { var inspector = new FileSystemInspector(); - var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); - var noiseDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "$RECYCLE.BIN")); + var noiseDirectory = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "$RECYCLE.BIN")); - try - { - var result = inspector.IsNoiseDirectoryName(noiseDirectory, OSPlatforms.Windows); + var result = inspector.IsNoiseDirectoryName(noiseDirectory, OSPlatforms.Windows); - result.Should().BeTrue(); - } - finally - { - Directory.Delete(tempDirectory.FullName, true); - } + result.Should().BeTrue(); } [Test] public void IsNoiseDirectoryName_ShouldReturnFalse_ForUnknownDirectory() { var inspector = new FileSystemInspector(); - var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); - var regularDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "regular")); + var regularDirectory = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "regular")); - try - { - var result = inspector.IsNoiseDirectoryName(regularDirectory, OSPlatforms.Windows); + var result = inspector.IsNoiseDirectoryName(regularDirectory, OSPlatforms.Windows); - result.Should().BeFalse(); - } - finally - { - Directory.Delete(tempDirectory.FullName, true); - } + result.Should().BeFalse(); } [Test] public void IsNoiseFileName_ShouldReturnTrue_ForKnownNoiseFile() { var inspector = new FileSystemInspector(); - var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); - var filePath = Path.Combine(tempDirectory.FullName, "thumbs.db"); + var filePath = Path.Combine(TestDirectory.FullName, "thumbs.db"); File.WriteAllText(filePath, "x"); var fileInfo = new FileInfo(filePath); - try - { - var result = inspector.IsNoiseFileName(fileInfo, OSPlatforms.Windows); + var result = inspector.IsNoiseFileName(fileInfo, OSPlatforms.Windows); - result.Should().BeTrue(); - } - finally - { - Directory.Delete(tempDirectory.FullName, true); - } + result.Should().BeTrue(); } [Test] public void IsNoiseFileName_ShouldReturnFalse_ForUnknownFile() { var inspector = new FileSystemInspector(); - var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); - var filePath = Path.Combine(tempDirectory.FullName, "regular.txt"); + var filePath = Path.Combine(TestDirectory.FullName, "regular.txt"); File.WriteAllText(filePath, "x"); var fileInfo = new FileInfo(filePath); - try - { - var result = inspector.IsNoiseFileName(fileInfo, OSPlatforms.Windows); + var result = inspector.IsNoiseFileName(fileInfo, OSPlatforms.Windows); - result.Should().BeFalse(); - } - finally - { - Directory.Delete(tempDirectory.FullName, true); - } + result.Should().BeFalse(); } } diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryLoaderIncompleteFlagTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryLoaderIncompleteFlagTests.cs index 81638399b..e1a3571e5 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryLoaderIncompleteFlagTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryLoaderIncompleteFlagTests.cs @@ -7,28 +7,29 @@ using ByteSync.Models.Inventories; using ByteSync.Services.Inventories; using FluentAssertions; +using ByteSync.TestsCommon; using NUnit.Framework; namespace ByteSync.Client.UnitTests.Services.Inventories; [TestFixture] -public class InventoryLoaderIncompleteFlagTests +public class InventoryLoaderIncompleteFlagTests : AbstractTester { private string _tempDirectory = null!; [SetUp] public void Setup() { - _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(_tempDirectory); + CreateTestDirectory(); + _tempDirectory = TestDirectory.FullName; } [TearDown] public void TearDown() { - if (Directory.Exists(_tempDirectory)) + if (TestDirectory?.Exists == true) { - Directory.Delete(_tempDirectory, true); + TestDirectory.Delete(true); } } diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/PosixFileTypeClassifierTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/PosixFileTypeClassifierTests.cs index f29975bae..3fe67199a 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/PosixFileTypeClassifierTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/PosixFileTypeClassifierTests.cs @@ -1,13 +1,29 @@ using System.IO; using ByteSync.Business.Inventories; using ByteSync.Services.Inventories; +using ByteSync.TestsCommon; using FluentAssertions; using NUnit.Framework; namespace ByteSync.Client.UnitTests.Services.Inventories; -public class PosixFileTypeClassifierTests +public class PosixFileTypeClassifierTests : AbstractTester { + [SetUp] + public void SetUp() + { + CreateTestDirectory(); + } + + [TearDown] + public void TearDown() + { + if (TestDirectory?.Exists == true) + { + TestDirectory.Delete(true); + } + } + [Test] [Platform(Include = "Linux,MacOsX")] [TestCase("/dev/null", FileSystemEntryKind.CharacterDevice)] @@ -36,26 +52,19 @@ public void ClassifyPosixEntry_ReturnsExpected(string path, FileSystemEntryKind public void ClassifyPosixEntry_ReturnsRegularFile_ForTempFile() { var classifier = new PosixFileTypeClassifier(); - var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var tempDirectory = Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDirectory); var tempFile = Path.Combine(tempDirectory, "file.txt"); File.WriteAllText(tempFile, "data"); - try - { - var result = classifier.ClassifyPosixEntry(tempFile); + var result = classifier.ClassifyPosixEntry(tempFile); - if (result == FileSystemEntryKind.Unknown) - { - Assert.Ignore($"POSIX classification returned Unknown for '{tempFile}'."); - } - - result.Should().Be(FileSystemEntryKind.RegularFile); - } - finally + if (result == FileSystemEntryKind.Unknown) { - Directory.Delete(tempDirectory, true); + Assert.Ignore($"POSIX classification returned Unknown for '{tempFile}'."); } + + result.Should().Be(FileSystemEntryKind.RegularFile); } [Test] @@ -63,24 +72,17 @@ public void ClassifyPosixEntry_ReturnsRegularFile_ForTempFile() public void ClassifyPosixEntry_ReturnsDirectory_ForTempDirectory() { var classifier = new PosixFileTypeClassifier(); - var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var tempDirectory = Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDirectory); - try - { - var result = classifier.ClassifyPosixEntry(tempDirectory); - - if (result == FileSystemEntryKind.Unknown) - { - Assert.Ignore($"POSIX classification returned Unknown for '{tempDirectory}'."); - } + var result = classifier.ClassifyPosixEntry(tempDirectory); - result.Should().Be(FileSystemEntryKind.Directory); - } - finally + if (result == FileSystemEntryKind.Unknown) { - Directory.Delete(tempDirectory, true); + Assert.Ignore($"POSIX classification returned Unknown for '{tempDirectory}'."); } + + result.Should().Be(FileSystemEntryKind.Directory); } [Test] @@ -88,7 +90,7 @@ public void ClassifyPosixEntry_ReturnsDirectory_ForTempDirectory() public void ClassifyPosixEntry_ReturnsUnknown_ForMissingPath() { var classifier = new PosixFileTypeClassifier(); - var missingPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "missing"); + var missingPath = Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N"), "missing"); var result = classifier.ClassifyPosixEntry(missingPath); @@ -100,7 +102,7 @@ public void ClassifyPosixEntry_ReturnsUnknown_ForMissingPath() public void ClassifyPosixEntry_ReturnsSymlink_WhenSupported() { var classifier = new PosixFileTypeClassifier(); - var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var tempDirectory = Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDirectory); var targetFile = Path.Combine(tempDirectory, "target.txt"); File.WriteAllText(targetFile, "data"); @@ -108,28 +110,21 @@ public void ClassifyPosixEntry_ReturnsSymlink_WhenSupported() try { - try - { - File.CreateSymbolicLink(linkPath, targetFile); - } - catch (Exception ex) - { - Assert.Ignore($"Symbolic link creation failed: {ex.GetType().Name}"); - } - - var result = classifier.ClassifyPosixEntry(linkPath); - - if (result == FileSystemEntryKind.Unknown) - { - Assert.Ignore($"POSIX classification returned Unknown for '{linkPath}'."); - } - - result.Should().Be(FileSystemEntryKind.Symlink); + File.CreateSymbolicLink(linkPath, targetFile); } - finally + catch (Exception ex) + { + Assert.Ignore($"Symbolic link creation failed: {ex.GetType().Name}"); + } + + var result = classifier.ClassifyPosixEntry(linkPath); + + if (result == FileSystemEntryKind.Unknown) { - Directory.Delete(tempDirectory, true); + Assert.Ignore($"POSIX classification returned Unknown for '{linkPath}'."); } + + result.Should().Be(FileSystemEntryKind.Symlink); } [Test] @@ -137,26 +132,19 @@ public void ClassifyPosixEntry_ReturnsSymlink_WhenSupported() public void ClassifyPosixEntry_ReturnsUnknown_WhenUnixFileInfoThrows() { var classifier = new PosixFileTypeClassifier(_ => throw new InvalidOperationException("fail")); - var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var tempDirectory = Path.Combine(TestDirectory.FullName, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDirectory); var tempFile = Path.Combine(tempDirectory, "file.txt"); File.WriteAllText(tempFile, "data"); - try - { - var result = classifier.ClassifyPosixEntry(tempFile); - - if (result == FileSystemEntryKind.Unknown) - { - Assert.Ignore($"POSIX classification returned Unknown for '{tempFile}'."); - } - - result.Should().Be(FileSystemEntryKind.RegularFile); - } - finally + var result = classifier.ClassifyPosixEntry(tempFile); + + if (result == FileSystemEntryKind.Unknown) { - Directory.Delete(tempDirectory, true); + Assert.Ignore($"POSIX classification returned Unknown for '{tempFile}'."); } + + result.Should().Be(FileSystemEntryKind.RegularFile); } [Test] From 3b81bc6a12d201e7be73a2526b1d6efe04635dd3 Mon Sep 17 00:00:00 2001 From: NETTOUR Marwan Date: Thu, 9 Apr 2026 09:07:03 -0400 Subject: [PATCH 4/9] [refactor] perf(tests): reduce CancellationToken timeout from 1000ms to 50ms in PolicyFactoryTests, perf(tests): reduce CancellationToken timeout from 1000ms to 50ms in PolicyFactoryTests --- .../Misc/Factories/PolicyFactoryTests.cs | 4 ++-- .../TestUtilities/Mine/TestRsa.cs | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/ByteSync.Client.UnitTests/Services/Misc/Factories/PolicyFactoryTests.cs b/tests/ByteSync.Client.UnitTests/Services/Misc/Factories/PolicyFactoryTests.cs index a6940af02..b7ffce961 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Misc/Factories/PolicyFactoryTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Misc/Factories/PolicyFactoryTests.cs @@ -33,7 +33,7 @@ public async Task BuildFileUploadPolicy_ShouldRetry_On_HttpRequestException_Stat var policy = _factory.BuildFileUploadPolicy(); using var cts = new CancellationTokenSource(); - cts.CancelAfter(1000); + cts.CancelAfter(50); Func act = async () => { @@ -62,7 +62,7 @@ public async Task BuildFileUploadPolicy_ShouldRetry_On_ApiException_StatusCodes( var policy = _factory.BuildFileUploadPolicy(); using var cts = new CancellationTokenSource(); - cts.CancelAfter(1000); + cts.CancelAfter(50); Func act = async () => { await policy.ExecuteAsync(async _ => { throw new ApiException("api error", status); }, cts.Token); }; diff --git a/tests/ByteSync.Client.UnitTests/TestUtilities/Mine/TestRsa.cs b/tests/ByteSync.Client.UnitTests/TestUtilities/Mine/TestRsa.cs index 613e635ce..1748350af 100644 --- a/tests/ByteSync.Client.UnitTests/TestUtilities/Mine/TestRsa.cs +++ b/tests/ByteSync.Client.UnitTests/TestUtilities/Mine/TestRsa.cs @@ -14,7 +14,7 @@ public void Test1_DifferentMessages() var publicKeys = new List(); var encryptedMessages = new List(); - for (int i = 0; i < 100; i++) + for (int i = 0; i < 10; i++) { var rsaBob = RSA.Create(); // Alice can encrypt, Bob can decrypt @@ -41,12 +41,12 @@ public void Test1_DifferentMessages() decryptedMessage.Should().Be(aliceMessage); } - publicKeys.Count.Should().Be(100); - encryptedMessages.Count.Should().Be(100); + publicKeys.Count.Should().Be(10); + encryptedMessages.Count.Should().Be(10); // We verify that the control we were applying works correctly - publicKeys.Any(pk => pk.SequenceEqual(publicKeys[50])).Should().BeTrue(); - encryptedMessages.Any(em => em.SequenceEqual(encryptedMessages[50])).Should().BeTrue(); + publicKeys.Any(pk => pk.SequenceEqual(publicKeys[5])).Should().BeTrue(); + encryptedMessages.Any(em => em.SequenceEqual(encryptedMessages[5])).Should().BeTrue(); } [Test] @@ -55,7 +55,7 @@ public void Test1_SameMessages() var publicKeys = new List(); var encryptedMessages = new List(); - for (int i = 0; i < 100; i++) + for (int i = 0; i < 10; i++) { var rsaBob = RSA.Create(); // Alice can encrypt, Bob can decrypt @@ -82,18 +82,18 @@ public void Test1_SameMessages() decryptedMessage.Should().Be(aliceMessage); } - publicKeys.Count.Should().Be(100); - encryptedMessages.Count.Should().Be(100); + publicKeys.Count.Should().Be(10); + encryptedMessages.Count.Should().Be(10); // We verify that the control we were applying works correctly - publicKeys.Any(pk => pk.SequenceEqual(publicKeys[50])).Should().BeTrue(); - encryptedMessages.Any(em => em.SequenceEqual(encryptedMessages[50])).Should().BeTrue(); + publicKeys.Any(pk => pk.SequenceEqual(publicKeys[5])).Should().BeTrue(); + encryptedMessages.Any(em => em.SequenceEqual(encryptedMessages[5])).Should().BeTrue(); } [Test] public void Test1_PublicKeyUnicity() { - for (int i = 0; i < 100; i++) + for (int i = 0; i < 10; i++) { var rsaBob = RSA.Create(); // Alice can encrypt, Bob can decrypt @@ -107,7 +107,7 @@ public void Test1_PublicKeyUnicity() [Test] public void Test1_EncryptedMessageNonUnicity() { - for (int i = 0; i < 100; i++) + for (int i = 0; i < 10; i++) { var rsaBob = RSA.Create(); // Alice can encrypt, Bob can decrypt From f1a362e5f02668cec8bd7408716d0d861d6ec776 Mon Sep 17 00:00:00 2001 From: NETTOUR Marwan Date: Thu, 9 Apr 2026 09:32:31 -0400 Subject: [PATCH 5/9] [test] perf(tests): inject IScheduler into TimeTrackingComputer and rewrite tests with TestScheduler --- .../TimeTracking/TimeTrackingComputer.cs | 9 +- .../TimeTracking/TimeTrackingComputerTests.cs | 411 ++++++++---------- 2 files changed, 182 insertions(+), 238 deletions(-) diff --git a/src/ByteSync.Client/Services/TimeTracking/TimeTrackingComputer.cs b/src/ByteSync.Client/Services/TimeTracking/TimeTrackingComputer.cs index d681b66b8..c4cb3fd0e 100644 --- a/src/ByteSync.Client/Services/TimeTracking/TimeTrackingComputer.cs +++ b/src/ByteSync.Client/Services/TimeTracking/TimeTrackingComputer.cs @@ -1,4 +1,5 @@ -using System.Reactive.Linq; +using System.Reactive.Concurrency; +using System.Reactive.Linq; using System.Reactive.Subjects; using ByteSync.Business.Misc; using ByteSync.Interfaces.Controls.TimeTracking; @@ -11,10 +12,12 @@ public class TimeTrackingComputer : ITimeTrackingComputer private readonly BehaviorSubject _isStarted; private readonly IDataTrackingStrategy _dataTrackingStrategy; + private readonly IScheduler _scheduler; - public TimeTrackingComputer(IDataTrackingStrategy dataTrackingStrategy) + public TimeTrackingComputer(IDataTrackingStrategy dataTrackingStrategy, IScheduler? scheduler = null) { _dataTrackingStrategy = dataTrackingStrategy; + _scheduler = scheduler ?? Scheduler.Default; _timeTrack = new BehaviorSubject(new TimeTrack()); _isStarted = new BehaviorSubject(false); @@ -49,7 +52,7 @@ public IObservable RemainingTime { if (isStarted) { - return Observable.Interval(TimeSpan.FromSeconds(1)); + return Observable.Interval(TimeSpan.FromSeconds(1), _scheduler); } else { diff --git a/tests/ByteSync.Client.UnitTests/Services/TimeTracking/TimeTrackingComputerTests.cs b/tests/ByteSync.Client.UnitTests/Services/TimeTracking/TimeTrackingComputerTests.cs index b1e1996a3..e9ed935cb 100644 --- a/tests/ByteSync.Client.UnitTests/Services/TimeTracking/TimeTrackingComputerTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/TimeTracking/TimeTrackingComputerTests.cs @@ -5,6 +5,7 @@ using ByteSync.Interfaces.Controls.TimeTracking; using ByteSync.Services.TimeTracking; using FluentAssertions; +using Microsoft.Reactive.Testing; using Moq; using NUnit.Framework; @@ -15,505 +16,445 @@ public class TimeTrackingComputerTests { private Mock _dataTrackingStrategyMock = null!; private BehaviorSubject<(long IdentifiedSize, long ProcessedSize)> _dataSubject = null!; - + private TestScheduler _scheduler = null!; + [SetUp] public void SetUp() { + _scheduler = new TestScheduler(); _dataTrackingStrategyMock = new Mock(); _dataSubject = new BehaviorSubject<(long IdentifiedSize, long ProcessedSize)>((0, 0)); _dataTrackingStrategyMock.Setup(x => x.GetDataObservable()).Returns(_dataSubject); } - + [TearDown] public void TearDown() { _dataSubject.Dispose(); } - + private TimeTrackingComputer CreateSut() { - return new TimeTrackingComputer(_dataTrackingStrategyMock.Object); + return new TimeTrackingComputer(_dataTrackingStrategyMock.Object, _scheduler); } - private static void WaitFor(TimeSpan delay) - { - using var gate = new ManualResetEventSlim(false); - gate.Wait(delay); - } - [Test] public void Constructor_ShouldInitialize_WithDefaultValues() { var sut = CreateSut(); - + sut.Should().NotBeNull(); sut.LastDataHandledDateTime.Should().BeNull(); } - + [Test] public void Start_ShouldInitialize_TimeTrack() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; TimeTrack? capturedTimeTrack = null; - var emissionReceived = new ManualResetEvent(false); - + sut.Start(startDateTime); - - using var subscription = sut.RemainingTime.Subscribe(tt => - { - capturedTimeTrack = tt; - emissionReceived.Set(); - }); - - emissionReceived.WaitOne(TimeSpan.FromSeconds(2)).Should().BeTrue("observable should emit at least once"); - + + using var subscription = sut.RemainingTime.Subscribe(tt => capturedTimeTrack = tt); + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + capturedTimeTrack.Should().NotBeNull(); capturedTimeTrack!.StartDateTime.Should().BeCloseTo(startDateTime.LocalDateTime, TimeSpan.FromSeconds(1)); } - + [Test] public void Start_ShouldReset_PreviousData() { var sut = CreateSut(); var firstStart = DateTimeOffset.Now.AddMinutes(-10); var secondStart = DateTimeOffset.Now; - var emissionReceived = new ManualResetEvent(false); - + sut.Start(firstStart); _dataSubject.OnNext((1000, 500)); - WaitFor(TimeSpan.FromMilliseconds(50)); - + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(50).Ticks); + sut.Start(secondStart); - + TimeTrack? capturedTimeTrack = null; - using var subscription = sut.RemainingTime.Subscribe(tt => - { - capturedTimeTrack = tt; - emissionReceived.Set(); - }); - - emissionReceived.WaitOne(TimeSpan.FromSeconds(2)).Should().BeTrue("observable should emit at least once"); - + using var subscription = sut.RemainingTime.Subscribe(tt => capturedTimeTrack = tt); + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + capturedTimeTrack.Should().NotBeNull(); capturedTimeTrack!.StartDateTime.Should().BeCloseTo(secondStart.LocalDateTime, TimeSpan.FromSeconds(1)); } - + [Test] public void Stop_ShouldStop_TimeTracking() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; var emissions = new List(); - + sut.Start(startDateTime); - + using var subscription = sut.RemainingTime.Subscribe(tt => emissions.Add(tt)); - - WaitFor(TimeSpan.FromMilliseconds(1500)); - + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(2).Ticks); + sut.Stop(); - - WaitFor(TimeSpan.FromMilliseconds(100)); - + emissions.Should().NotBeEmpty(); var finalTimeTrack = emissions.Last(); finalTimeTrack.RemainingTime.Should().Be(TimeSpan.Zero); } - + [Test] public void Stop_WhenStartDateTimeIsSet_ShouldCalculate_EstimatedEndDateTime() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; TimeTrack? capturedTimeTrack = null; - var emissionReceived = new ManualResetEvent(false); - + sut.Start(startDateTime); - - using var subscription = sut.RemainingTime.Subscribe(tt => - { - capturedTimeTrack = tt; - emissionReceived.Set(); - }); - - emissionReceived.WaitOne(TimeSpan.FromSeconds(2)).Should().BeTrue("observable should emit at least once"); - + + using var subscription = sut.RemainingTime.Subscribe(tt => capturedTimeTrack = tt); + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + sut.Stop(); - - WaitFor(TimeSpan.FromMilliseconds(100)); - + capturedTimeTrack.Should().NotBeNull(); capturedTimeTrack!.EstimatedEndDateTime.Should().NotBeNull(); capturedTimeTrack.RemainingTime.Should().Be(TimeSpan.Zero); } - + [Test] public void DataUpdate_WhenNotStarted_ShouldNotUpdate_TimeTrack() { var sut = CreateSut(); var emissions = new List(); - + using var subscription = sut.RemainingTime.Take(2).Subscribe(emissions.Add); - + _dataSubject.OnNext((1000, 100)); - - WaitFor(TimeSpan.FromMilliseconds(100)); - + + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(100).Ticks); + emissions.Should().BeEmpty(); } - + [Test] public void DataUpdate_WhenStarted_ShouldUpdate_TimeTrack() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; var emissions = new List(); - + sut.Start(startDateTime); - + using var subscription = sut.RemainingTime.Take(2).Subscribe(emissions.Add); - - WaitFor(TimeSpan.FromMilliseconds(100)); - + + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(100).Ticks); + _dataSubject.OnNext((1000, 100)); - - WaitFor(TimeSpan.FromMilliseconds(2100)); - + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(2).Ticks); + sut.LastDataHandledDateTime.Should().NotBeNull(); emissions.Should().NotBeEmpty(); } - + [Test] public void DataUpdate_WithValidProgress_ShouldCalculate_EstimatedEndDateTime() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now.AddSeconds(-10); TimeTrack? capturedTimeTrack = null; - var emissionReceived = new ManualResetEvent(false); - + sut.Start(startDateTime); - + _dataSubject.OnNext((1000, 100)); - - using var subscription = sut.RemainingTime.Subscribe(tt => - { - capturedTimeTrack = tt; - emissionReceived.Set(); - }); - - emissionReceived.WaitOne(TimeSpan.FromSeconds(2)).Should().BeTrue("observable should emit at least once"); - + + using var subscription = sut.RemainingTime.Subscribe(tt => capturedTimeTrack = tt); + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + capturedTimeTrack.Should().NotBeNull(); capturedTimeTrack!.EstimatedEndDateTime.Should().NotBeNull(); capturedTimeTrack.EstimatedEndDateTime.Should().BeAfter(DateTime.Now); } - + [Test] public void DataUpdate_WithZeroTotalData_ShouldNotCalculate_EstimatedEndDateTime() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; TimeTrack? capturedTimeTrack = null; - var emissionReceived = new ManualResetEvent(false); - + sut.Start(startDateTime); - + _dataSubject.OnNext((0, 0)); - - using var subscription = sut.RemainingTime.Subscribe(tt => - { - capturedTimeTrack = tt; - emissionReceived.Set(); - }); - - emissionReceived.WaitOne(TimeSpan.FromSeconds(2)).Should().BeTrue("observable should emit at least once"); - + + using var subscription = sut.RemainingTime.Subscribe(tt => capturedTimeTrack = tt); + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + capturedTimeTrack.Should().NotBeNull(); capturedTimeTrack!.EstimatedEndDateTime.Should().BeNull(); } - + [Test] public void DataUpdate_WithZeroHandledData_ShouldNotCalculate_EstimatedEndDateTime() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; TimeTrack? capturedTimeTrack = null; - var emissionReceived = new ManualResetEvent(false); - + sut.Start(startDateTime); - + _dataSubject.OnNext((1000, 0)); - - using var subscription = sut.RemainingTime.Subscribe(tt => - { - capturedTimeTrack = tt; - emissionReceived.Set(); - }); - - emissionReceived.WaitOne(TimeSpan.FromSeconds(2)).Should().BeTrue("observable should emit at least once"); - + + using var subscription = sut.RemainingTime.Subscribe(tt => capturedTimeTrack = tt); + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + capturedTimeTrack.Should().NotBeNull(); capturedTimeTrack!.EstimatedEndDateTime.Should().BeNull(); } - + [Test] public void DataUpdate_WithHandledDataGreaterThanTotal_ShouldNotCalculate_EstimatedEndDateTime() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; TimeTrack? capturedTimeTrack = null; - var emissionReceived = new ManualResetEvent(false); - + sut.Start(startDateTime); - + _dataSubject.OnNext((1000, 1500)); - - using var subscription = sut.RemainingTime.Subscribe(tt => - { - capturedTimeTrack = tt; - emissionReceived.Set(); - }); - - emissionReceived.WaitOne(TimeSpan.FromSeconds(2)).Should().BeTrue("observable should emit at least once"); - + + using var subscription = sut.RemainingTime.Subscribe(tt => capturedTimeTrack = tt); + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + capturedTimeTrack.Should().NotBeNull(); capturedTimeTrack!.EstimatedEndDateTime.Should().BeNull(); } - + [Test] public void RemainingTime_WhenNotStarted_ShouldNotEmit() { var sut = CreateSut(); var emissions = new List(); - + using var subscription = sut.RemainingTime - .Timeout(TimeSpan.FromMilliseconds(500)) + .Timeout(TimeSpan.FromMilliseconds(500), _scheduler) .Subscribe( tt => emissions.Add(tt), _ => { }); - - WaitFor(TimeSpan.FromMilliseconds(600)); - + + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(600).Ticks); + emissions.Should().BeEmpty(); } - + [Test] public void RemainingTime_WhenStarted_ShouldEmit_Periodically() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; var emissions = new ConcurrentQueue(); - using var receivedTwoEmissions = new ManualResetEventSlim(false); - + sut.Start(startDateTime); - + using var subscription = sut.RemainingTime .Take(3) - .Subscribe(tt => - { - emissions.Enqueue(tt); - if (emissions.Count >= 2) - { - receivedTwoEmissions.Set(); - } - }); - - var receivedTwoEmissionsWithinTimeout = receivedTwoEmissions.Wait(TimeSpan.FromSeconds(5)); - - receivedTwoEmissionsWithinTimeout.Should().BeTrue("remaining time should emit periodically while tracking is started"); - emissions.Should().HaveCountGreaterThanOrEqualTo(2); + .Subscribe(tt => emissions.Enqueue(tt)); + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(3).Ticks); + + emissions.Should().HaveCountGreaterThanOrEqualTo(2, "remaining time should emit periodically while tracking is started"); emissions.All(tt => tt.StartDateTime.HasValue).Should().BeTrue(); } - + [Test] public void RemainingTime_AfterStop_ShouldStop_Emitting() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; var emissionCount = 0; - + sut.Start(startDateTime); - + using var subscription = sut.RemainingTime.Subscribe(_ => emissionCount++); - - WaitFor(TimeSpan.FromMilliseconds(2100)); - + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(2).Ticks); + sut.Stop(); - - WaitFor(TimeSpan.FromMilliseconds(100)); - + var countAfterStop = emissionCount; - - WaitFor(TimeSpan.FromMilliseconds(1500)); - + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(2).Ticks); + var countAfterWait = emissionCount; - + countAfterWait.Should().Be(countAfterStop); } - + [Test] public void ProgressCalculation_WithHalfCompletion_ShouldEstimate_DoubleElapsedTime() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now.AddSeconds(-10); TimeTrack? capturedTimeTrack = null; - var emissionReceived = new ManualResetEvent(false); - + sut.Start(startDateTime); - + _dataSubject.OnNext((1000, 500)); - - using var subscription = sut.RemainingTime.Subscribe(tt => - { - capturedTimeTrack = tt; - emissionReceived.Set(); - }); - - emissionReceived.WaitOne(TimeSpan.FromSeconds(2)).Should().BeTrue("observable should emit at least once"); - + + using var subscription = sut.RemainingTime.Subscribe(tt => capturedTimeTrack = tt); + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + capturedTimeTrack.Should().NotBeNull(); capturedTimeTrack!.EstimatedEndDateTime.Should().NotBeNull(); - + var elapsedSeconds = (capturedTimeTrack.EstimatedEndDateTime!.Value - startDateTime.LocalDateTime).TotalSeconds; elapsedSeconds.Should().BeGreaterThan(15); } - + [Test] public void MultipleDataUpdates_ShouldUpdate_EstimatedEndDateTime() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now.AddSeconds(-5); var emissions = new List(); - + sut.Start(startDateTime); - + using var subscription = sut.RemainingTime.Subscribe(tt => emissions.Add(tt)); - - WaitFor(TimeSpan.FromMilliseconds(100)); - + + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(100).Ticks); + _dataSubject.OnNext((1000, 100)); - WaitFor(TimeSpan.FromMilliseconds(1100)); - + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + _dataSubject.OnNext((1000, 200)); - WaitFor(TimeSpan.FromMilliseconds(1100)); - + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + _dataSubject.OnNext((1000, 300)); - WaitFor(TimeSpan.FromMilliseconds(1100)); - + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + emissions.Should().HaveCountGreaterThanOrEqualTo(3); - + var emissionsWithEstimatedEnd = emissions.Where(e => e.EstimatedEndDateTime.HasValue).ToList(); emissionsWithEstimatedEnd.Should().NotBeEmpty(); } - + [Test] public void Start_ThenDataUpdate_ShouldSet_LastDataHandledDateTime() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; var beforeUpdate = DateTime.Now; - + sut.Start(startDateTime); - - WaitFor(TimeSpan.FromMilliseconds(50)); - + + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(50).Ticks); + _dataSubject.OnNext((1000, 100)); - - WaitFor(TimeSpan.FromMilliseconds(100)); - + + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(100).Ticks); + var afterUpdate = DateTime.Now; - + sut.LastDataHandledDateTime.Should().NotBeNull(); sut.LastDataHandledDateTime!.Value.Should().BeOnOrAfter(beforeUpdate); sut.LastDataHandledDateTime!.Value.Should().BeOnOrBefore(afterUpdate); } - + [Test] public void RemainingTime_Observable_ShouldCombine_IntervalAndTimeTrack() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; var emissions = new List(); - + sut.Start(startDateTime); - + _dataSubject.OnNext((1000, 250)); - + using var subscription = sut.RemainingTime .Take(3) .Subscribe(tt => emissions.Add(tt)); - - WaitFor(TimeSpan.FromMilliseconds(3500)); - + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(3).Ticks); + emissions.Should().HaveCountGreaterThanOrEqualTo(2); emissions.Should().OnlyContain(tt => tt.StartDateTime.HasValue); } - + [Test] public void Stop_WithoutStart_ShouldNotThrow() { var sut = CreateSut(); - + var act = () => sut.Stop(); - + act.Should().NotThrow(); } - + [Test] public void Constructor_ShouldSubscribe_ToDataTrackingStrategy() { CreateSut(); - + _dataTrackingStrategyMock.Verify(x => x.GetDataObservable(), Times.Once); } - + [Test] public void Start_WithoutDataUpdate_ShouldNotCalculate_EstimatedEndDateTime() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now; var emissions = new List(); - var expectedEmissions = new ManualResetEvent(false); - + bool completed = false; + sut.Start(startDateTime); - + using var subscription = sut.RemainingTime.Take(2).Subscribe( onNext: tt => emissions.Add(tt), - onCompleted: () => expectedEmissions.Set()); - - expectedEmissions.WaitOne(TimeSpan.FromSeconds(3)).Should().BeTrue("observable should complete after 2 emissions"); - + onCompleted: () => completed = true); + + _scheduler.AdvanceBy(TimeSpan.FromSeconds(3).Ticks); + + completed.Should().BeTrue("observable should complete after 2 emissions"); emissions.Should().HaveCount(2); emissions.All(tt => tt.EstimatedEndDateTime == null).Should().BeTrue(); } - + [Test] public void CompleteProcess_FromStartToStop_ShouldUpdate_AllFields() { var sut = CreateSut(); var startDateTime = DateTimeOffset.Now.AddSeconds(-2); var emissions = new List(); - + sut.Start(startDateTime); - + using var subscription = sut.RemainingTime.Subscribe(tt => emissions.Add(tt)); - - WaitFor(TimeSpan.FromMilliseconds(100)); - + + _scheduler.AdvanceBy(TimeSpan.FromMilliseconds(100).Ticks); + _dataSubject.OnNext((1000, 250)); - WaitFor(TimeSpan.FromMilliseconds(1100)); - + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + _dataSubject.OnNext((1000, 500)); - WaitFor(TimeSpan.FromMilliseconds(1100)); - + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + _dataSubject.OnNext((1000, 750)); - WaitFor(TimeSpan.FromMilliseconds(1100)); - + _scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + sut.Stop(); - - WaitFor(TimeSpan.FromMilliseconds(100)); - + emissions.Should().NotBeEmpty(); var finalEmission = emissions.Last(); finalEmission.StartDateTime.Should().NotBeNull(); From 26f5ec93b74d847aeb86391cb044d5a87ec76d8d Mon Sep 17 00:00:00 2001 From: NETTOUR Marwan Date: Thu, 9 Apr 2026 11:09:12 -0400 Subject: [PATCH 6/9] perf(tests): add RetryDelaySleepDurationProvider override in ConnectionService to skip retry delays in tests --- .../Services/Communications/ConnectionService.cs | 6 ++++-- .../Services/Communications/ConnectionServiceTests.cs | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ByteSync.Client/Services/Communications/ConnectionService.cs b/src/ByteSync.Client/Services/Communications/ConnectionService.cs index 5de448b7c..1ecfbeacb 100644 --- a/src/ByteSync.Client/Services/Communications/ConnectionService.cs +++ b/src/ByteSync.Client/Services/Communications/ConnectionService.cs @@ -1,4 +1,4 @@ -using System.Net.Http; +using System.Net.Http; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading; @@ -65,13 +65,15 @@ public ConnectionService(IConnectionFactory connectionFactory, IAuthenticationTo public ByteSyncEndpoint? CurrentEndPoint { get; set; } public string? ClientInstanceId => CurrentEndPoint?.ClientInstanceId; + + public Func? RetryDelaySleepDurationProvider { get; set; } public async Task StartConnectionAsync() { var retryPolicy = Policy .Handle(ex => !(ex is BuildConnectionException bce && bce.InitialConnectionStatus == InitialConnectionStatus.VersionNotAllowed)) .WaitAndRetryForeverAsync( - retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + retryAttempt => RetryDelaySleepDurationProvider?.Invoke(retryAttempt) ?? TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (exception, _, _) => { ConnectionStatusSubject.OnNext(ConnectionStatuses.NotConnected); diff --git a/tests/ByteSync.Client.UnitTests/Services/Communications/ConnectionServiceTests.cs b/tests/ByteSync.Client.UnitTests/Services/Communications/ConnectionServiceTests.cs index 0bca76a7a..7b7664d97 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Communications/ConnectionServiceTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Communications/ConnectionServiceTests.cs @@ -1,4 +1,4 @@ -using ByteSync.Business.Communications; +using ByteSync.Business.Communications; using ByteSync.Common.Business.Auth; using ByteSync.Common.Business.EndPoints; using ByteSync.Exceptions; @@ -33,6 +33,8 @@ public void SetUp() _mockAuthenticationTokensRepository.Object, _mockLogger.Object ); + + _connectionService.RetryDelaySleepDurationProvider = _ => TimeSpan.Zero; } [Test] From 84510d6133d68566fbfa8caf7181732bfd510fb4 Mon Sep 17 00:00:00 2001 From: NETTOUR Marwan Date: Thu, 9 Apr 2026 11:10:14 -0400 Subject: [PATCH 7/9] [test] perf(tests): extract virtual DelayAsync in AddTrustedClientViewModel and override in tests --- .../TrustedNetworks/AddTrustedClientViewModel.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ByteSync.Client/ViewModels/TrustedNetworks/AddTrustedClientViewModel.cs b/src/ByteSync.Client/ViewModels/TrustedNetworks/AddTrustedClientViewModel.cs index b8ea0beec..144831fca 100644 --- a/src/ByteSync.Client/ViewModels/TrustedNetworks/AddTrustedClientViewModel.cs +++ b/src/ByteSync.Client/ViewModels/TrustedNetworks/AddTrustedClientViewModel.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -279,13 +279,13 @@ private async Task ValidateClient() _publicKeysManager.Trust(TrustedPublicKey); ShowSuccess = true; - await Task.Delay(TimeSpan.FromSeconds(3)); + await DelayAsync(TimeSpan.FromSeconds(3)); ShowSuccess = false; } else { ShowError = true; - await Task.Delay(TimeSpan.FromSeconds(3)); + await DelayAsync(TimeSpan.FromSeconds(3)); ShowError = false; } @@ -309,7 +309,7 @@ private async Task RejectClient() var task2 = _publicKeysTruster.OnPublicKeyValidationCanceled(PublicKeyCheckData!, TrustDataParameters); ShowError = true; - await Task.Delay(TimeSpan.FromSeconds(3)); + await DelayAsync(TimeSpan.FromSeconds(3)); ShowError = false; await Task.WhenAll(task, task2); @@ -330,6 +330,8 @@ private async Task Cancel() await _publicKeysTruster.OnPublicKeyValidationCanceled(PublicKeyCheckData!, TrustDataParameters); } + protected virtual Task DelayAsync(TimeSpan delay) => Task.Delay(delay); + private string[] BuildSafetyWords() { var safetyWordsComputer = new SafetyWordsComputer(SafetyWordsValues.AVAILABLE_WORDS); From b54c73c72b4c60bc27577d99bc16056cdfd4c07a Mon Sep 17 00:00:00 2001 From: NETTOUR Marwan Date: Thu, 9 Apr 2026 11:11:00 -0400 Subject: [PATCH 8/9] [test] tests for AddTrusteClientViewModel --- .../AddTrustedClientViewModelTests.cs | 152 +++++++++--------- 1 file changed, 74 insertions(+), 78 deletions(-) diff --git a/tests/ByteSync.Client.UnitTests/ViewModels/TrustedNetworks/AddTrustedClientViewModelTests.cs b/tests/ByteSync.Client.UnitTests/ViewModels/TrustedNetworks/AddTrustedClientViewModelTests.cs index 454db29ff..6e4a6446e 100644 --- a/tests/ByteSync.Client.UnitTests/ViewModels/TrustedNetworks/AddTrustedClientViewModelTests.cs +++ b/tests/ByteSync.Client.UnitTests/ViewModels/TrustedNetworks/AddTrustedClientViewModelTests.cs @@ -24,7 +24,7 @@ public class AddTrustedClientViewModelTests private Mock _appSettings = null!; private Mock _truster = null!; private Mock> _logger = null!; - + private PublicKeyCheckData CreateCheckData() { var issuer = new PublicKeyInfo @@ -32,13 +32,13 @@ private PublicKeyCheckData CreateCheckData() ClientId = "OtherClient", PublicKey = Encoding.UTF8.GetBytes("OTHER_PUBLIC_KEY") }; - + return new PublicKeyCheckData { IssuerPublicKeyInfo = issuer, }; } - + [SetUp] public void SetUp() { @@ -46,13 +46,13 @@ public void SetUp() _appSettings = new Mock(); _truster = new Mock(); _logger = new Mock>(); - + _appSettings.Setup(a => a.GetCurrentApplicationSettings()) .Returns(new ApplicationSettings { ClientId = "MyClient" }); - + _publicKeysManager.Setup(m => m.GetMyPublicKeyInfo()) .Returns(new PublicKeyInfo { ClientId = "MyClient", PublicKey = Encoding.UTF8.GetBytes("MY_PUBLIC_KEY") }); - + _publicKeysManager.Setup(m => m.BuildTrustedPublicKey(It.IsAny())) .Returns((PublicKeyCheckData p) => new TrustedPublicKey { @@ -61,7 +61,7 @@ public void SetUp() SafetyKey = new string('0', 64) }); } - + private TrustDataParameters CreateTrustParams(out PeerTrustProcessData peer, bool otherFinished, bool success) { peer = new PeerTrustProcessData("OtherClient"); @@ -69,169 +69,165 @@ private TrustDataParameters CreateTrustParams(out PeerTrustProcessData peer, boo { peer.SetOtherPartyChecked(success); } - + return new TrustDataParameters(0, 2, false, "S1", peer); } - + + private TestableAddTrustedClientViewModel CreateVm(PublicKeyCheckData check, TrustDataParameters trustParams) + { + return new TestableAddTrustedClientViewModel(check, trustParams, _publicKeysManager.Object, _appSettings.Object, + _truster.Object, _logger.Object, null!); + } + [Test] public async Task ValidateClient_Success_Should_Trust_And_Close() { var check = CreateCheckData(); var trustParams = CreateTrustParams(out var peer, true, true); - - var vm = new AddTrustedClientViewModel(check, trustParams, _publicKeysManager.Object, _appSettings.Object, - _truster.Object, _logger.Object, null!) - { - Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = false } - }; - + + var vm = CreateVm(check, trustParams); + vm.Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = false }; + _truster.Setup(t => t.OnPublicKeyValidationFinished(It.IsAny(), trustParams, true)) .Returns(async () => { peer.SetMyPartyChecked(true); await Task.CompletedTask; }); - + bool closeRequested = false; vm.CloseFlyoutRequested += (_, _) => closeRequested = true; - + await vm.ValidateClientCommand.Execute(); - + _publicKeysManager.Verify(m => m.Trust(It.IsAny()), Times.Once); - vm.ShowSuccess.Should().BeFalse(); // after delay, it returns to false + vm.ShowSuccess.Should().BeFalse(); vm.ShowError.Should().BeFalse(); vm.Container.CanCloseCurrentFlyout.Should().BeTrue(); closeRequested.Should().BeTrue(); } - + [Test] public async Task ValidateClient_Failure_Should_ShowError_And_Close() { var check = CreateCheckData(); var trustParams = CreateTrustParams(out var peer, true, false); - - var vm = new AddTrustedClientViewModel(check, trustParams, _publicKeysManager.Object, _appSettings.Object, - _truster.Object, _logger.Object, null!) - { - Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = false } - }; - + + var vm = CreateVm(check, trustParams); + vm.Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = false }; + _truster.Setup(t => t.OnPublicKeyValidationFinished(It.IsAny(), trustParams, true)) .Returns(async () => { peer.SetMyPartyChecked(true); await Task.CompletedTask; }); - + bool closeRequested = false; vm.CloseFlyoutRequested += (_, _) => closeRequested = true; - + await vm.ValidateClientCommand.Execute(); - + _publicKeysManager.Verify(m => m.Trust(It.IsAny()), Times.Never); vm.ShowError.Should().BeFalse(); vm.Container.CanCloseCurrentFlyout.Should().BeTrue(); closeRequested.Should().BeTrue(); } - + [Test] public async Task RejectClient_Should_Call_Truster_Cancel_And_Close() { var check = CreateCheckData(); var trustParams = CreateTrustParams(out _, true, false); - - var vm = new AddTrustedClientViewModel(check, trustParams, _publicKeysManager.Object, _appSettings.Object, - _truster.Object, _logger.Object, null!) - { - Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = false } - }; - + + var vm = CreateVm(check, trustParams); + vm.Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = false }; + bool closeRequested = false; vm.CloseFlyoutRequested += (_, _) => closeRequested = true; - + await vm.RejectClientCommand.Execute(); - + _truster.Verify(t => t.OnPublicKeyValidationFinished(It.IsAny(), trustParams, false), Times.Once); _truster.Verify(t => t.OnPublicKeyValidationCanceled(It.IsAny(), trustParams), Times.Once); vm.Container.CanCloseCurrentFlyout.Should().BeTrue(); closeRequested.Should().BeTrue(); } - + [Test] public void EmptyConstructor_Should_Work_Fine() { var vm = new AddTrustedClientViewModel(); - + vm.Should().NotBeNull(); } - + [Test] public void OnDisplayed_Should_Disable_Flyout_Closing() { var check = CreateCheckData(); var trustParams = CreateTrustParams(out _, true, true); - - var vm = new AddTrustedClientViewModel(check, trustParams, _publicKeysManager.Object, _appSettings.Object, - _truster.Object, _logger.Object, null!) - { - Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = true } - }; - - // Act + + var vm = CreateVm(check, trustParams); + vm.Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = true }; + vm.OnDisplayed(); - - // Assert + vm.Container.CanCloseCurrentFlyout.Should().BeFalse(); } - + [Test] public void WhenActivated_Toggles_CanExecute_While_Command_IsExecuting() { var check = CreateCheckData(); var trustParams = CreateTrustParams(out _, true, true); - - var vm = new AddTrustedClientViewModel(check, trustParams, _publicKeysManager.Object, _appSettings.Object, - _truster.Object, _logger.Object, null!) - { - Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = false } - }; - - // Arrange a long-running Cancel to keep IsExecuting = true + + var vm = CreateVm(check, trustParams); + vm.Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = false }; + var tcs = new TaskCompletionSource(); _truster.Setup(t => t.OnPublicKeyValidationCanceled(It.IsAny(), trustParams)) .Returns(tcs.Task); - - // Activate to wire up WhenActivated subscriptions + vm.Activator.Activate(); - + bool canExecuteCopy = true; using var sub = vm.CopyToClipboardCommand.CanExecute.Subscribe(v => canExecuteCopy = v); - - // Initially true + canExecuteCopy.Should().BeTrue(); - - // Start Cancel (this flips canRun to false while executing) + vm.CancelCommand.Execute().Subscribe(); - - // Wait until CanExecute becomes false + var sw = Stopwatch.StartNew(); while (canExecuteCopy && sw.ElapsedMilliseconds < 1000) { Thread.Sleep(10); } - + canExecuteCopy.Should().BeFalse(); - - // Complete the cancel to release IsExecuting + tcs.SetResult(); - - // Wait until CanExecute becomes true again + sw.Restart(); while (!canExecuteCopy && sw.ElapsedMilliseconds < 1000) { Thread.Sleep(10); } - + canExecuteCopy.Should().BeTrue(); } + + private class TestableAddTrustedClientViewModel : AddTrustedClientViewModel + { + public TestableAddTrustedClientViewModel(PublicKeyCheckData? publicKeyCheckData, + TrustDataParameters trustDataParameters, IPublicKeysManager publicKeysManager, + IApplicationSettingsRepository applicationSettingsManager, IPublicKeysTruster publicKeysTruster, + ILogger logger, Views.MainWindow mainWindow) + : base(publicKeyCheckData, trustDataParameters, publicKeysManager, applicationSettingsManager, + publicKeysTruster, logger, mainWindow) + { + } + + protected override Task DelayAsync(TimeSpan delay) => Task.CompletedTask; + } } \ No newline at end of file From 49aee7fde46c8b7d262f687421e87f48a7f893f6 Mon Sep 17 00:00:00 2001 From: NETTOUR Marwan Date: Mon, 13 Apr 2026 03:30:42 -0400 Subject: [PATCH 9/9] [fix] Stabilize retry delay handling and flaky unit tests (Copilot comments) --- .../Communications/ConnectionService.cs | 6 ++- .../Services/Misc/Factories/PolicyFactory.cs | 15 +++++-- .../Misc/Factories/PolicyFactoryTests.cs | 26 +++++------- .../TestUtilities/Mine/TestRsa.cs | 10 +---- .../DataNodeSourcesViewModelTests.cs | 13 +++++- .../AddTrustedClientViewModelTests.cs | 42 +++++++++---------- 6 files changed, 59 insertions(+), 53 deletions(-) diff --git a/src/ByteSync.Client/Services/Communications/ConnectionService.cs b/src/ByteSync.Client/Services/Communications/ConnectionService.cs index 1ecfbeacb..1979dfc06 100644 --- a/src/ByteSync.Client/Services/Communications/ConnectionService.cs +++ b/src/ByteSync.Client/Services/Communications/ConnectionService.cs @@ -70,10 +70,12 @@ public ConnectionService(IConnectionFactory connectionFactory, IAuthenticationTo public async Task StartConnectionAsync() { + var retryDelaySleepDurationProvider = RetryDelaySleepDurationProvider; + var retryPolicy = Policy .Handle(ex => !(ex is BuildConnectionException bce && bce.InitialConnectionStatus == InitialConnectionStatus.VersionNotAllowed)) .WaitAndRetryForeverAsync( - retryAttempt => RetryDelaySleepDurationProvider?.Invoke(retryAttempt) ?? TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + retryAttempt => retryDelaySleepDurationProvider?.Invoke(retryAttempt) ?? TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (exception, _, _) => { ConnectionStatusSubject.OnNext(ConnectionStatuses.NotConnected); @@ -228,4 +230,4 @@ private Task CancelCurrentRefreshCancellationTokenSource() return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/ByteSync.Client/Services/Misc/Factories/PolicyFactory.cs b/src/ByteSync.Client/Services/Misc/Factories/PolicyFactory.cs index 65f8b881e..5e7758c3e 100644 --- a/src/ByteSync.Client/Services/Misc/Factories/PolicyFactory.cs +++ b/src/ByteSync.Client/Services/Misc/Factories/PolicyFactory.cs @@ -12,15 +12,22 @@ namespace ByteSync.Services.Misc.Factories; public class PolicyFactory : IPolicyFactory { private readonly ILogger _logger; + private readonly Func _sleepDurationProvider; public PolicyFactory(ILogger logger) + : this(logger, DefaultSleepDurationProvider) + { + } + + public PolicyFactory(ILogger logger, Func sleepDurationProvider) { _logger = logger; + _sleepDurationProvider = sleepDurationProvider; } private const int MAX_RETRIES = 5; - private TimeSpan SleepDurationProvider(int retryAttempt) + private static TimeSpan DefaultSleepDurationProvider(int retryAttempt) { // Exponential backoff with jitter: 2^({attempt}-1) seconds + 0-500ms var baseSeconds = Math.Pow(2, Math.Max(0, retryAttempt - 1)); @@ -39,7 +46,7 @@ public AsyncRetryPolicy BuildFileDownloadPolicy() var policy = Policy .HandleResult(x => !x.IsSuccess) .Or(e => e.StatusCode == HttpStatusCode.Forbidden) - .WaitAndRetryAsync(MAX_RETRIES, SleepDurationProvider, onRetryAsync: async (response, timeSpan, retryCount, _) => + .WaitAndRetryAsync(MAX_RETRIES, _sleepDurationProvider, onRetryAsync: async (response, timeSpan, retryCount, _) => { _logger.LogError(response.Exception, "FileTransferOperation failed (Attempt number {AttemptNumber}). ResponseCode:{ResponseCode} ExceptionType:{ExceptionType}, ExceptionMessage:{ExceptionMessage}. Waiting {WaitingTime} seconds before retry", @@ -68,7 +75,7 @@ public AsyncRetryPolicy BuildFileUploadPolicy() || ex.HttpStatusCode == HttpStatusCode.InternalServerError) .Or() .Or() - .WaitAndRetryAsync(MAX_RETRIES, SleepDurationProvider, onRetryAsync: async (response, timeSpan, retryCount, _) => + .WaitAndRetryAsync(MAX_RETRIES, _sleepDurationProvider, onRetryAsync: async (response, timeSpan, retryCount, _) => { _logger.LogError(response.Exception, "FileTransferOperation failed (Attempt number {AttemptNumber}). ResponseCode:{ResponseCode} ExceptionType:{ExceptionType}, ExceptionMessage:{ExceptionMessage}. Waiting {WaitingTime} seconds before retry", @@ -78,4 +85,4 @@ public AsyncRetryPolicy BuildFileUploadPolicy() return policy; } -} \ No newline at end of file +} diff --git a/tests/ByteSync.Client.UnitTests/Services/Misc/Factories/PolicyFactoryTests.cs b/tests/ByteSync.Client.UnitTests/Services/Misc/Factories/PolicyFactoryTests.cs index b7ffce961..c49792503 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Misc/Factories/PolicyFactoryTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Misc/Factories/PolicyFactoryTests.cs @@ -18,7 +18,7 @@ public class PolicyFactoryTests public void SetUp() { _mockLogger = new Mock>(); - _factory = new PolicyFactory(_mockLogger.Object); + _factory = new PolicyFactory(_mockLogger.Object, _ => TimeSpan.Zero); } [TestCase(HttpStatusCode.Forbidden)] @@ -31,17 +31,14 @@ public void SetUp() public async Task BuildFileUploadPolicy_ShouldRetry_On_HttpRequestException_StatusCodes(HttpStatusCode status) { var policy = _factory.BuildFileUploadPolicy(); - - using var cts = new CancellationTokenSource(); - cts.CancelAfter(50); - + Func act = async () => { await policy.ExecuteAsync(async _ => { throw new HttpRequestException("test", inner: null, statusCode: status); }, - cts.Token); + CancellationToken.None); }; - - await act.Should().ThrowAsync(); + + await act.Should().ThrowAsync(); _mockLogger.Verify(x => x.Log( It.Is(l => l == LogLevel.Error), @@ -60,13 +57,10 @@ public async Task BuildFileUploadPolicy_ShouldRetry_On_HttpRequestException_Stat public async Task BuildFileUploadPolicy_ShouldRetry_On_ApiException_StatusCodes(HttpStatusCode status) { var policy = _factory.BuildFileUploadPolicy(); - - using var cts = new CancellationTokenSource(); - cts.CancelAfter(50); - - Func act = async () => { await policy.ExecuteAsync(async _ => { throw new ApiException("api error", status); }, cts.Token); }; - - await act.Should().ThrowAsync(); + + Func act = async () => { await policy.ExecuteAsync(async _ => { throw new ApiException("api error", status); }, CancellationToken.None); }; + + await act.Should().ThrowAsync(); _mockLogger.Verify(x => x.Log( It.Is(l => l == LogLevel.Error), @@ -101,4 +95,4 @@ await policy.ExecuteAsync( It.IsAny(), It.IsAny>()), Times.Never); } -} \ No newline at end of file +} diff --git a/tests/ByteSync.Client.UnitTests/TestUtilities/Mine/TestRsa.cs b/tests/ByteSync.Client.UnitTests/TestUtilities/Mine/TestRsa.cs index 1748350af..b23dd0fe1 100644 --- a/tests/ByteSync.Client.UnitTests/TestUtilities/Mine/TestRsa.cs +++ b/tests/ByteSync.Client.UnitTests/TestUtilities/Mine/TestRsa.cs @@ -43,10 +43,6 @@ public void Test1_DifferentMessages() publicKeys.Count.Should().Be(10); encryptedMessages.Count.Should().Be(10); - - // We verify that the control we were applying works correctly - publicKeys.Any(pk => pk.SequenceEqual(publicKeys[5])).Should().BeTrue(); - encryptedMessages.Any(em => em.SequenceEqual(encryptedMessages[5])).Should().BeTrue(); } [Test] @@ -84,10 +80,6 @@ public void Test1_SameMessages() publicKeys.Count.Should().Be(10); encryptedMessages.Count.Should().Be(10); - - // We verify that the control we were applying works correctly - publicKeys.Any(pk => pk.SequenceEqual(publicKeys[5])).Should().BeTrue(); - encryptedMessages.Any(em => em.SequenceEqual(encryptedMessages[5])).Should().BeTrue(); } [Test] @@ -125,4 +117,4 @@ public void Test1_EncryptedMessageNonUnicity() encryptedMessage1.SequenceEqual(encryptedMessage2).Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeSourcesViewModelTests.cs b/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeSourcesViewModelTests.cs index 12f77d243..9c1589d17 100644 --- a/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeSourcesViewModelTests.cs +++ b/tests/ByteSync.Client.UnitTests/ViewModels/Sessions/DataNodes/DataNodeSourcesViewModelTests.cs @@ -42,6 +42,17 @@ public void SetUp() _dataSourceRepositoryMock.SetupGet(r => r.ObservableCache).Returns(_cache); } + [TearDown] + public void TearDown() + { + _cache?.Dispose(); + + if (TestDirectory?.Exists == true) + { + TestDirectory.Delete(true); + } + } + [Test] public void Constructor_WithAllDependencies_ShouldCreateInstance() { @@ -164,4 +175,4 @@ private DataNodeSourcesViewModel CreateViewModel() _dataSourceRepositoryMock.Object, _fileDialogServiceMock.Object); } -} \ No newline at end of file +} diff --git a/tests/ByteSync.Client.UnitTests/ViewModels/TrustedNetworks/AddTrustedClientViewModelTests.cs b/tests/ByteSync.Client.UnitTests/ViewModels/TrustedNetworks/AddTrustedClientViewModelTests.cs index 6e4a6446e..a95738399 100644 --- a/tests/ByteSync.Client.UnitTests/ViewModels/TrustedNetworks/AddTrustedClientViewModelTests.cs +++ b/tests/ByteSync.Client.UnitTests/ViewModels/TrustedNetworks/AddTrustedClientViewModelTests.cs @@ -1,5 +1,5 @@ -using System.Diagnostics; using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; using System.Text; using ByteSync.Business; using ByteSync.Business.Communications; @@ -177,7 +177,7 @@ public void OnDisplayed_Should_Disable_Flyout_Closing() } [Test] - public void WhenActivated_Toggles_CanExecute_While_Command_IsExecuting() + public async Task WhenActivated_Toggles_CanExecute_While_Command_IsExecuting() { var check = CreateCheckData(); var trustParams = CreateTrustParams(out _, true, true); @@ -185,36 +185,36 @@ public void WhenActivated_Toggles_CanExecute_While_Command_IsExecuting() var vm = CreateVm(check, trustParams); vm.Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = false }; - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _truster.Setup(t => t.OnPublicKeyValidationCanceled(It.IsAny(), trustParams)) .Returns(tcs.Task); vm.Activator.Activate(); - bool canExecuteCopy = true; - using var sub = vm.CopyToClipboardCommand.CanExecute.Subscribe(v => canExecuteCopy = v); + var canExecute = vm.CopyToClipboardCommand.CanExecute + .Replay(1) + .RefCount(); - canExecuteCopy.Should().BeTrue(); + (await canExecute.Take(1).Timeout(TimeSpan.FromSeconds(1)).ToTask()).Should().BeTrue(); - vm.CancelCommand.Execute().Subscribe(); + var canExecuteFalseTask = canExecute + .FirstAsync(v => !v) + .Timeout(TimeSpan.FromSeconds(1)) + .ToTask(); - var sw = Stopwatch.StartNew(); - while (canExecuteCopy && sw.ElapsedMilliseconds < 1000) - { - Thread.Sleep(10); - } + var cancelTask = vm.CancelCommand.Execute().ToTask(); - canExecuteCopy.Should().BeFalse(); + (await canExecuteFalseTask).Should().BeFalse(); - tcs.SetResult(); + var canExecuteTrueTask = canExecute + .FirstAsync(v => v) + .Timeout(TimeSpan.FromSeconds(1)) + .ToTask(); - sw.Restart(); - while (!canExecuteCopy && sw.ElapsedMilliseconds < 1000) - { - Thread.Sleep(10); - } + tcs.SetResult(); - canExecuteCopy.Should().BeTrue(); + (await canExecuteTrueTask).Should().BeTrue(); + await cancelTask; } private class TestableAddTrustedClientViewModel : AddTrustedClientViewModel @@ -230,4 +230,4 @@ public TestableAddTrustedClientViewModel(PublicKeyCheckData? publicKeyCheckData, protected override Task DelayAsync(TimeSpan delay) => Task.CompletedTask; } -} \ No newline at end of file +}