diff --git a/src/ByteSync.Client/Services/Communications/ConnectionService.cs b/src/ByteSync.Client/Services/Communications/ConnectionService.cs index 5de448b7c..1979dfc06 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,17 @@ 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 retryDelaySleepDurationProvider = RetryDelaySleepDurationProvider; + 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); @@ -226,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/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/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); 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] diff --git a/tests/ByteSync.Client.UnitTests/Services/Misc/Factories/PolicyFactoryTests.cs b/tests/ByteSync.Client.UnitTests/Services/Misc/Factories/PolicyFactoryTests.cs index a6940af02..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(1000); - + 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(1000); - - 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/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(); diff --git a/tests/ByteSync.Client.UnitTests/TestUtilities/Mine/TestRsa.cs b/tests/ByteSync.Client.UnitTests/TestUtilities/Mine/TestRsa.cs index 613e635ce..b23dd0fe1 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,8 @@ public void Test1_DifferentMessages() decryptedMessage.Should().Be(aliceMessage); } - publicKeys.Count.Should().Be(100); - encryptedMessages.Count.Should().Be(100); - - // 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.Count.Should().Be(10); + encryptedMessages.Count.Should().Be(10); } [Test] @@ -55,7 +51,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 +78,14 @@ public void Test1_SameMessages() decryptedMessage.Should().Be(aliceMessage); } - publicKeys.Count.Should().Be(100); - encryptedMessages.Count.Should().Be(100); - - // 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.Count.Should().Be(10); + encryptedMessages.Count.Should().Be(10); } [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 +99,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 @@ -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 454db29ff..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; @@ -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() + public async Task 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 tcs = new TaskCompletionSource(); + + var vm = CreateVm(check, trustParams); + vm.Container = new FlyoutContainerViewModel { CanCloseCurrentFlyout = false }; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _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 + + var canExecute = vm.CopyToClipboardCommand.CanExecute + .Replay(1) + .RefCount(); + + (await canExecute.Take(1).Timeout(TimeSpan.FromSeconds(1)).ToTask()).Should().BeTrue(); + + var canExecuteFalseTask = canExecute + .FirstAsync(v => !v) + .Timeout(TimeSpan.FromSeconds(1)) + .ToTask(); + + var cancelTask = vm.CancelCommand.Execute().ToTask(); + + (await canExecuteFalseTask).Should().BeFalse(); + + var canExecuteTrueTask = canExecute + .FirstAsync(v => v) + .Timeout(TimeSpan.FromSeconds(1)) + .ToTask(); + tcs.SetResult(); - - // Wait until CanExecute becomes true again - sw.Restart(); - while (!canExecuteCopy && sw.ElapsedMilliseconds < 1000) + + (await canExecuteTrueTask).Should().BeTrue(); + await cancelTask; + } + + 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) { - Thread.Sleep(10); } - - canExecuteCopy.Should().BeTrue(); + + protected override Task DelayAsync(TimeSpan delay) => Task.CompletedTask; } -} \ No newline at end of file +}