From 0cc543c70f55df383e83facd260d5baf05f6c8b8 Mon Sep 17 00:00:00 2001 From: tacosontitan Date: Fri, 16 Jun 2023 17:14:28 -0500 Subject: [PATCH 1/6] Fixed a bug in Crypter.Core with TransferUploadService permitting jobs to enqueue even when the recipient wasn't found. --- Crypter.Core/Services/TransferUploadService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Crypter.Core/Services/TransferUploadService.cs b/Crypter.Core/Services/TransferUploadService.cs index db69e4b34..34d949822 100644 --- a/Crypter.Core/Services/TransferUploadService.cs +++ b/Crypter.Core/Services/TransferUploadService.cs @@ -290,7 +290,8 @@ await maybeUserId.IfSomeAsync( && x.NotificationSetting.EmailNotifications) .FirstOrDefaultAsync(); - _backgroundJobClient.Enqueue(() => _hangfireBackgroundService.SendTransferNotificationAsync(itemId, itemType)); + if (user is null) + _backgroundJobClient.Enqueue(() => _hangfireBackgroundService.SendTransferNotificationAsync(itemId, itemType)); }); return Unit.Default; } From 54a940aceed9b5cb03b3b6f46b6020f424757256 Mon Sep 17 00:00:00 2001 From: tacosontitan Date: Fri, 16 Jun 2023 17:15:41 -0500 Subject: [PATCH 2/6] Marked the Users repository in Crypter.Core as virtual to support mocking in tests. --- Crypter.Core/DataContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Crypter.Core/DataContext.cs b/Crypter.Core/DataContext.cs index 25ae9a19f..14ef41346 100644 --- a/Crypter.Core/DataContext.cs +++ b/Crypter.Core/DataContext.cs @@ -75,7 +75,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) base.OnConfiguring(optionsBuilder); } - public DbSet Users { get; set; } + public virtual DbSet Users { get; set; } public DbSet UserProfiles { get; set; } public DbSet UserKeyPairs { get; set; } public DbSet UserPrivacySettings { get; set; } From 9a20dceaccfdda2a00b03718c3ad4301aecb0d84 Mon Sep 17 00:00:00 2001 From: tacosontitan Date: Fri, 16 Jun 2023 17:16:25 -0500 Subject: [PATCH 3/6] Created shared classes for testing asynchronous operations when working with Entity Framework. --- Crypter.Test/Shared/TestAsyncEnumerable.cs | 67 +++++++++++++++++ Crypter.Test/Shared/TestAsyncEnumerator.cs | 73 +++++++++++++++++++ Crypter.Test/Shared/TestAsyncQueryProvider.cs | 51 +++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 Crypter.Test/Shared/TestAsyncEnumerable.cs create mode 100644 Crypter.Test/Shared/TestAsyncEnumerator.cs create mode 100644 Crypter.Test/Shared/TestAsyncQueryProvider.cs diff --git a/Crypter.Test/Shared/TestAsyncEnumerable.cs b/Crypter.Test/Shared/TestAsyncEnumerable.cs new file mode 100644 index 000000000..2b7034243 --- /dev/null +++ b/Crypter.Test/Shared/TestAsyncEnumerable.cs @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; + +namespace Crypter.Test.Shared +{ + /// + /// Represents a test async enumerable. + /// + /// The type of the elements in the enumerable. + internal class TestAsyncEnumerable : EnumerableQuery, IAsyncEnumerable, IQueryable + { + /// + /// Initializes a new instance of the class. + /// + /// The enumerable to use. + public TestAsyncEnumerable(IEnumerable enumerable) + : base(enumerable) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The expression representing the enumerable. + public TestAsyncEnumerable(Expression expression) + : base(expression) + { } + + /// + public IAsyncEnumerator GetEnumerator() => + new TestAsyncEnumerator(this.AsEnumerable().GetEnumerator()); + + /// + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => + new TestAsyncEnumerator(this.AsEnumerable().GetEnumerator()); + + /// + IQueryProvider IQueryable.Provider => new TestAsyncQueryProvider(this); + } +} \ No newline at end of file diff --git a/Crypter.Test/Shared/TestAsyncEnumerator.cs b/Crypter.Test/Shared/TestAsyncEnumerator.cs new file mode 100644 index 000000000..5a40374e5 --- /dev/null +++ b/Crypter.Test/Shared/TestAsyncEnumerator.cs @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2023 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Crypter.Test.Shared +{ + /// + /// This class is used to mock an asynchronous enumerator. + /// + /// Specifies the type of data the enumerator is working with. + internal class TestAsyncEnumerator : IAsyncEnumerator + { + private readonly IEnumerator _inner; + + /// + /// Initializes a new instance of the class. + /// + /// The inner enumerator. + public TestAsyncEnumerator(IEnumerator inner) => + _inner = inner; + + /// + public void Dispose() => + _inner.Dispose(); + + /// + public T Current => _inner.Current; + + /// + public Task MoveNext(CancellationToken cancellationToken) => + Task.FromResult(_inner.MoveNext()); + + /// + public async ValueTask MoveNextAsync() + { + return await Task.FromResult(_inner.MoveNext()); + } + + /// + public ValueTask DisposeAsync() + { + _inner.Dispose(); + return default; + } + } + +} \ No newline at end of file diff --git a/Crypter.Test/Shared/TestAsyncQueryProvider.cs b/Crypter.Test/Shared/TestAsyncQueryProvider.cs new file mode 100644 index 000000000..44fdb5b1b --- /dev/null +++ b/Crypter.Test/Shared/TestAsyncQueryProvider.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query; + +namespace Crypter.Test.Shared +{ + /// + /// Represents a test async query provider. + /// + /// The type of the entities in the query. + internal class TestAsyncQueryProvider : IAsyncQueryProvider + { + private readonly IQueryProvider _inner; + + /// + /// Initializes a new instance of the class. + /// + /// The inner query provider. + internal TestAsyncQueryProvider(IQueryProvider inner) => _inner = inner; + + /// + public IQueryable CreateQuery(Expression expression) => + new TestAsyncEnumerable(expression); + + /// + public IQueryable CreateQuery(Expression expression) => + new TestAsyncEnumerable(expression); + + /// + public object Execute(Expression expression) => _inner.Execute(expression); + + /// + public TResult Execute(Expression expression) => _inner.Execute(expression); + + /// + public IAsyncEnumerable ExecuteAsync(Expression expression) => + new TestAsyncEnumerable(expression); + + /// + public Task ExecuteAsync(Expression expression, CancellationToken cancellationToken) => + Task.FromResult(Execute(expression)); + + /// + TResult IAsyncQueryProvider.ExecuteAsync(Expression expression, CancellationToken cancellationToken) => + ExecuteAsync(expression, cancellationToken).GetAwaiter().GetResult(); + } +} From 754336e3f15656563c05704d66978d3b51c37b8e Mon Sep 17 00:00:00 2001 From: tacosontitan Date: Fri, 16 Jun 2023 17:17:46 -0500 Subject: [PATCH 4/6] Created a dummy background job client to allow assertion of results when testing the Enqueue extension method. --- .../Models/DummyBackgroundJobClient.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 Crypter.Test/Core_Tests/Models/DummyBackgroundJobClient.cs diff --git a/Crypter.Test/Core_Tests/Models/DummyBackgroundJobClient.cs b/Crypter.Test/Core_Tests/Models/DummyBackgroundJobClient.cs new file mode 100644 index 000000000..baed39372 --- /dev/null +++ b/Crypter.Test/Core_Tests/Models/DummyBackgroundJobClient.cs @@ -0,0 +1,38 @@ + + +using System; +using System.Collections.Generic; +using Hangfire; +using Hangfire.Annotations; +using Hangfire.Common; +using Hangfire.States; + +namespace Crypter.Test.Core_Tests.Models +{ + /// + /// A dummy implementation of that can be used for testing. + /// + internal sealed class DummyBackgroundJobClient : + IBackgroundJobClient + { + /// + /// Gets a list of jobs that have been created. + /// + /// This is used to help with assertions related to the Enqueue extension method. + public List Jobs { get; set; } = new(); + + public bool ChangeState( + [NotNull] string jobId, + [NotNull] IState state, + [CanBeNull] string expectedState) => + throw new NotImplementedException("This method is not currently needed to support testing of Crypter."); + + public string Create( + [NotNull] Job job, + [NotNull] IState state) + { + Jobs.Add(job); + return Guid.NewGuid().ToString(); + } + } +} From e589544ad4f6143584e81e1b19d1239358b9d0fb Mon Sep 17 00:00:00 2001 From: tacosontitan Date: Fri, 16 Jun 2023 17:18:12 -0500 Subject: [PATCH 5/6] Created a test fixture for TransferUploadService to test the fix for bug 547. --- .../TransferUploadService_Tests.cs | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 Crypter.Test/Core_Tests/Services_Tests/TransferUploadService_Tests.cs diff --git a/Crypter.Test/Core_Tests/Services_Tests/TransferUploadService_Tests.cs b/Crypter.Test/Core_Tests/Services_Tests/TransferUploadService_Tests.cs new file mode 100644 index 000000000..8742fd495 --- /dev/null +++ b/Crypter.Test/Core_Tests/Services_Tests/TransferUploadService_Tests.cs @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2023 Crypter File Transfer + * + * This file is part of the Crypter file transfer project. + * + * Crypter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Crypter source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the aforementioned license + * by purchasing a commercial license. Buying such a license is mandatory + * as soon as you develop commercial activities involving the Crypter source + * code without disclosing the source code of your own applications. + * + * Contact the current copyright holder to discuss commercial license options. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Crypter.Common.Contracts.Features.Metrics; +using Crypter.Common.Contracts.Features.Transfer; +using Crypter.Common.Enums; +using Crypter.Common.Monads; +using Crypter.Core; +using Crypter.Core.Entities; +using Crypter.Core.Repositories; +using Crypter.Core.Services; +using Crypter.Test.Core_Tests.Models; +using Crypter.Test.Shared; +using Microsoft.EntityFrameworkCore; +using Moq; +using NUnit.Framework; + +namespace Crypter.Test.Core_Tests.Services_Tests +{ + [TestFixture] + public class TransferUploadService_Tests + { + private Random _random; + private DataContext _dataContext; + private TransferUploadService _uploadService; + private ITransferRepository _transferStorageService; + private IServerMetricsService _serverMetricsService; + private DummyBackgroundJobClient _backgroundJobClient; + private IHangfireBackgroundService _hangfireBackgroundService; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + _random = new Random(); + _dataContext = GenerateMockDataContext(); + _backgroundJobClient = GenerateMockBackgroundJobClient(); + _serverMetricsService = GenerateMockServerMetricsService(); + _transferStorageService = GenerateMockTransferStorageService(); + _hangfireBackgroundService = GenerateMockHangfireBackgroundService(); + + // This must happen last. + _uploadService = GenerateMockUploadService(); + } + + [Test] + public async Task Upload_File_Transfer_Async_Null_User_Does_Not_Enqueue_Client() + { + Guid senderId = Guid.Empty; + string recipientUsername = string.Empty; + Stream mockStream = GenerateMockStream(); + UploadFileTransferRequest request = GenerateMockRequest(); + _ = await _uploadService.UploadFileTransferAsync( + senderId, + recipientUsername, + request, + mockStream + ); + + Assert.IsEmpty(_backgroundJobClient.Jobs); + } + + private TransferUploadService GenerateMockUploadService() + { + var uploadService = new TransferUploadService( + context: _dataContext, + serverMetricsService: _serverMetricsService, + transferStorageService: _transferStorageService, + hangfireBackgroundService: _hangfireBackgroundService, + backgroundJobClient: _backgroundJobClient, + hashIdService: null + ); + + return uploadService; + } + + private Stream GenerateMockStream() + { + byte[] buffer = new byte[1024]; + int scale = _random.Next(100, 1000); + Stream mockStream = new MemoryStream(buffer); + while (scale > 0) + { + int bytesToWrite = Math.Min(buffer.Length, scale); + _random.NextBytes(buffer); + mockStream.Write(buffer, 0, bytesToWrite); + scale -= bytesToWrite; + } + + return mockStream; + } + + /// NOTE: This method is because it doesn't access instance members. + private static DataContext GenerateMockDataContext() + { + DbSet users = GenerateMockUsers(); + Mock mockDataContext = new(); + _ = mockDataContext.Setup(context => context.Users).Returns(users); + return mockDataContext.Object; + } + + /// NOTE: This method is because it doesn't access instance members. + private static DummyBackgroundJobClient GenerateMockBackgroundJobClient() => new(); + + /// NOTE: This method is because it doesn't access instance members. + private static IServerMetricsService GenerateMockServerMetricsService() + { + Mock mockServerMetricsService = new(); + _ = mockServerMetricsService.Setup(service => + service.GetAggregateDiskMetricsAsync(CancellationToken.None)) + .Returns( + Task.FromResult( + new PublicStorageMetricsResponse( + allocated: int.MaxValue, + available: int.MaxValue + ) + ) + ); + + return mockServerMetricsService.Object; + } + + /// NOTE: This method is because it doesn't access instance members. + private static ITransferRepository GenerateMockTransferStorageService() + { + Mock mockTransferStorageService = new(); + _ = mockTransferStorageService.Setup(service => service.SaveTransferAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + )).Returns(Task.FromResult(true)); + return mockTransferStorageService.Object; + } + + /// NOTE: This method is because it doesn't access instance members. + private static IHangfireBackgroundService GenerateMockHangfireBackgroundService() + { + Mock mockHangfireBackgroundService = new(); + _ = mockHangfireBackgroundService.Setup(service => service.SendTransferNotificationAsync( + It.IsAny(), + It.IsAny() + )).Returns(Task.CompletedTask); + return mockHangfireBackgroundService.Object; + } + + /// NOTE: This method is because it doesn't access instance members. + private static DbSet GenerateMockUsers() + { + IEnumerable users = GenerateUsers(); + IQueryable usersQueryable = users.AsQueryable(); + + Mock> usersDbSet = new(); + + _ = usersDbSet.As>() + .Setup(m => m.GetAsyncEnumerator(It.IsAny())) + .Returns(new TestAsyncEnumerator(usersQueryable.GetEnumerator())); + + _ = usersDbSet.As>() + .Setup(m => m.Provider) + .Returns(new TestAsyncQueryProvider(usersQueryable.Provider)); + + _ = usersDbSet.As>().Setup(m => m.Expression).Returns(usersQueryable.Expression); + _ = usersDbSet.As>().Setup(m => m.ElementType).Returns(usersQueryable.ElementType); + _ = usersDbSet.As>().Setup(m => m.GetEnumerator()).Returns(() => usersQueryable.GetEnumerator()); + return usersDbSet.Object; + } + + /// NOTE: This method is because it doesn't access instance members. + private static IEnumerable GenerateUsers() => Enumerable.Range(0, 10).Select( + index => new UserEntity( + id: Guid.NewGuid(), + username: $"user{index}", + emailAddress: $"user{index}@example.com", + passwordHash: null, + passwordSalt: null, + serverPasswordVersion: 1, + clientPasswordVersion: 1, + emailVerified: true, + created: DateTime.Now.AddDays(-index), + lastLogin: DateTime.Now.AddMinutes(-index) + ) + ); + + private static UploadFileTransferRequest GenerateMockRequest() + { + UploadFileTransferRequest request = new( + fileName: "sample.txt", + "text/plain", + publicKey: null, + keyExchangeNonce: null, + proof: null, + lifetimeHours: 3 + ); + + return request; + } + } +} From 1f0268e4a6f9217fe3e336d9615438f8642a4797 Mon Sep 17 00:00:00 2001 From: tacosontitan Date: Fri, 16 Jun 2023 17:28:54 -0500 Subject: [PATCH 6/6] Fixed a bug with TransferUploadService to handle unlocated recipients during transfers. --- Crypter.Core/Services/TransferUploadService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Crypter.Core/Services/TransferUploadService.cs b/Crypter.Core/Services/TransferUploadService.cs index 34d949822..d0d7efbea 100644 --- a/Crypter.Core/Services/TransferUploadService.cs +++ b/Crypter.Core/Services/TransferUploadService.cs @@ -290,7 +290,7 @@ await maybeUserId.IfSomeAsync( && x.NotificationSetting.EmailNotifications) .FirstOrDefaultAsync(); - if (user is null) + if (user is not null) _backgroundJobClient.Enqueue(() => _hangfireBackgroundService.SendTransferNotificationAsync(itemId, itemType)); }); return Unit.Default;