diff --git a/Crypter.API/Controllers/UserSettingController.cs b/Crypter.API/Controllers/UserSettingController.cs index 2457b0df..5d8690d0 100644 --- a/Crypter.API/Controllers/UserSettingController.cs +++ b/Crypter.API/Controllers/UserSettingController.cs @@ -229,7 +229,8 @@ IActionResult MakeErrorResponse(UpdateNotificationSettingsError error) #pragma warning disable CS8524 return error switch { - UpdateNotificationSettingsError.UnknownError => MakeErrorResponseBase(HttpStatusCode.InternalServerError, error) + UpdateNotificationSettingsError.UnknownError => MakeErrorResponseBase(HttpStatusCode.InternalServerError, error), + UpdateNotificationSettingsError.MissingNotificationChannel => MakeErrorResponseBase(HttpStatusCode.BadRequest, error) }; #pragma warning restore CS8524 } diff --git a/Crypter.Common/Contracts/Features/UserSettings/NotificationSettings/UpdateNotificationSettingsError.cs b/Crypter.Common/Contracts/Features/UserSettings/NotificationSettings/UpdateNotificationSettingsError.cs index d7e2c279..77c188c4 100644 --- a/Crypter.Common/Contracts/Features/UserSettings/NotificationSettings/UpdateNotificationSettingsError.cs +++ b/Crypter.Common/Contracts/Features/UserSettings/NotificationSettings/UpdateNotificationSettingsError.cs @@ -28,5 +28,6 @@ namespace Crypter.Common.Contracts.Features.UserSettings.NotificationSettings; public enum UpdateNotificationSettingsError { - UnknownError + UnknownError, + MissingNotificationChannel } diff --git a/Crypter.Core/Features/UserSettings/Commands/UpdateNotificationSettingsCommand.cs b/Crypter.Core/Features/UserSettings/Commands/UpdateNotificationSettingsCommand.cs index 75caf7eb..f2c378c9 100644 --- a/Crypter.Core/Features/UserSettings/Commands/UpdateNotificationSettingsCommand.cs +++ b/Crypter.Core/Features/UserSettings/Commands/UpdateNotificationSettingsCommand.cs @@ -55,14 +55,20 @@ public async Task> { var userData = await _dataContext.Users .Where(x => x.Id == request.UserId) - .Select(x => new { x.NotificationSetting }) + .Select(x => new { x.EmailAddress, x.NotificationSetting }) .FirstOrDefaultAsync(CancellationToken.None); if (userData is null) { return UpdateNotificationSettingsError.UnknownError; } - + + bool enablingNotifications = request.Request.NotifyOnTransferReceived || request.Request.EmailNotifications; + if (enablingNotifications && string.IsNullOrEmpty(userData.EmailAddress)) + { + return UpdateNotificationSettingsError.MissingNotificationChannel; + } + if (userData.NotificationSetting is null) { UserNotificationSettingEntity newNotificationSettings = diff --git a/Crypter.Test/Integration_Tests/UserSettings_Tests/UpdateNotificationSettings_Tests.cs b/Crypter.Test/Integration_Tests/UserSettings_Tests/UpdateNotificationSettings_Tests.cs index 3992b13d..cea5c3f3 100644 --- a/Crypter.Test/Integration_Tests/UserSettings_Tests/UpdateNotificationSettings_Tests.cs +++ b/Crypter.Test/Integration_Tests/UserSettings_Tests/UpdateNotificationSettings_Tests.cs @@ -24,14 +24,24 @@ * Contact the current copyright holder to discuss commercial license options. */ +using System.Linq; using System.Threading.Tasks; using Crypter.Common.Client.Interfaces.HttpClients; using Crypter.Common.Client.Interfaces.Repositories; using Crypter.Common.Contracts.Features.UserAuthentication; +using Crypter.Common.Contracts.Features.UserSettings; using Crypter.Common.Contracts.Features.UserSettings.NotificationSettings; using Crypter.Common.Enums; +using Crypter.Common.Infrastructure; +using Crypter.Crypto.Common; +using Crypter.Crypto.Common.DigitalSignature; +using Crypter.Crypto.Providers.Default; +using Crypter.DataAccess; +using Crypter.DataAccess.Entities; using EasyMonads; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace Crypter.Test.Integration_Tests.UserSettings_Tests; @@ -43,10 +53,19 @@ internal class UpdateNotificationSettings_Tests private ICrypterApiClient? _client; private ITokenRepository? _clientTokenRepository; + private DefaultCryptoProvider? _cryptoProvider; + private Ed25519KeyPair? _emailVerificationKeyPair; + [SetUp] public async Task SetupTestAsync() { - _factory = await AssemblySetup.CreateWebApplicationFactoryAsync(); + _cryptoProvider = new DefaultCryptoProvider(); + _emailVerificationKeyPair = _cryptoProvider.DigitalSignature.GenerateKeyPair(); + ICryptoProvider mockCryptoProvider = Mocks.CreateDeterministicCryptoProvider(_emailVerificationKeyPair); + IServiceCollection overrideServices = new ServiceCollection(); + overrideServices.AddSingleton(mockCryptoProvider); + + _factory = await AssemblySetup.CreateWebApplicationFactoryAsync(true, overrideServices); (_client, _clientTokenRepository) = AssemblySetup.SetupCrypterApiClient(_factory.CreateClient()); await AssemblySetup.InitializeRespawnerAsync(); } @@ -56,32 +75,124 @@ public async Task TeardownTestAsync() { if (_factory is not null) { - await _factory.DisposeAsync(); + await _factory.DisposeAsync(); } await AssemblySetup.ResetServerDataAsync(); } [Test] public async Task Update_Notification_Preferences_Works_Async() + { + await RegisterAndLoginAsync(); + + NotificationSettings request = new NotificationSettings(false, false); + Either result = + await _client!.UserSetting.UpdateNotificationSettingsAsync(request); + + Assert.That(result.IsRight, Is.True); + } + + [Test] + public async Task Update_Notification_Preferences_Works_For_Verified_Email_Address_Async() + { + await RegisterAndLoginAsync(TestData.DefaultEmailAdress); + await VerifyEmailAddressAsync(); + + NotificationSettings request = new NotificationSettings(true, true); + Either result = + await _client!.UserSetting.UpdateNotificationSettingsAsync(request); + + Assert.That(result.IsRight, Is.True); + result.DoRight(settings => + { + Assert.That(settings.EmailNotifications, Is.True); + Assert.That(settings.NotifyOnTransferReceived, Is.True); + }); + } + + [Test] + public async Task Update_Notification_Preferences_Fails_Without_Email_Address_Async() + { + await RegisterAndLoginAsync(); + + NotificationSettings request = new NotificationSettings(true, true); + Either result = + await _client!.UserSetting.UpdateNotificationSettingsAsync(request); + + Assert.That(result.IsLeft, Is.True); + Assert.That(result.LeftOrDefault(UpdateNotificationSettingsError.UnknownError), + Is.EqualTo(UpdateNotificationSettingsError.MissingNotificationChannel)); + } + + [Test] + public async Task Update_Notification_Preferences_Fails_For_Unverified_Email_Address_Async() + { + await RegisterAndLoginAsync(TestData.DefaultEmailAdress); + + NotificationSettings request = new NotificationSettings(true, true); + Either result = + await _client!.UserSetting.UpdateNotificationSettingsAsync(request); + + Assert.That(result.IsLeft, Is.True); + Assert.That(result.LeftOrDefault(UpdateNotificationSettingsError.UnknownError), + Is.EqualTo(UpdateNotificationSettingsError.MissingNotificationChannel)); + } + + [Test] + public async Task Disable_Notification_Preferences_Works_Without_Email_Address_Async() + { + await RegisterAndLoginAsync(); + + NotificationSettings request = new NotificationSettings(false, false); + Either result = + await _client!.UserSetting.UpdateNotificationSettingsAsync(request); + + Assert.That(result.IsRight, Is.True); + result.DoRight(settings => + { + Assert.That(settings.EmailNotifications, Is.False); + Assert.That(settings.NotifyOnTransferReceived, Is.False); + }); + } + + private async Task RegisterAndLoginAsync(string? emailAddress = null) { RegistrationRequest registrationRequest = - TestData.GetRegistrationRequest(TestData.DefaultUsername, TestData.DefaultPassword); - Either _ = await _client!.UserAuthentication.RegisterAsync(registrationRequest); + TestData.GetRegistrationRequest(TestData.DefaultUsername, TestData.DefaultPassword, emailAddress); + Either registrationResult = + await _client!.UserAuthentication.RegisterAsync(registrationRequest); + Assert.That(registrationResult.IsRight, Is.True); LoginRequest loginRequest = TestData.GetLoginRequest(TestData.DefaultUsername, TestData.DefaultPassword); Either loginResult = await _client!.UserAuthentication.LoginAsync(loginRequest); + Assert.That(loginResult.IsRight, Is.True); await loginResult.DoRightAsync(async loginResponse => { await _clientTokenRepository!.StoreAuthenticationTokenAsync(loginResponse.AuthenticationToken); await _clientTokenRepository!.StoreRefreshTokenAsync(loginResponse.RefreshToken, TokenType.Session); }); + } - NotificationSettings request = new NotificationSettings(false, false); - Either result = - await _client!.UserSetting.UpdateNotificationSettingsAsync(request); + private async Task VerifyEmailAddressAsync() + { + // Allow the background service to "send" the verification email and save the email verification data + await Task.Delay(5000); + + using IServiceScope scope = _factory!.Services.CreateScope(); + DataContext dataContext = scope.ServiceProvider.GetRequiredService(); + UserEmailChangeEntity changeData = await dataContext.UserEmailChangeRequests + .Where(x => x.User!.Username == TestData.DefaultUsername) + .FirstAsync(); + + string encodedVerificationCode = UrlSafeEncoder.EncodeGuidUrlSafe(changeData.Code!.Value); + byte[] signedVerificationCode = _cryptoProvider!.DigitalSignature.GenerateSignature( + _emailVerificationKeyPair!.PrivateKey, changeData.Code.Value.ToByteArray()); + string encodedSignature = UrlSafeEncoder.EncodeBytesUrlSafe(signedVerificationCode); + VerifyEmailAddressRequest request = new VerifyEmailAddressRequest(encodedVerificationCode, encodedSignature); + Either result = await _client!.UserSetting.VerifyUserEmailAddressAsync(request); Assert.That(result.IsRight, Is.True); } } diff --git a/Crypter.Web/Shared/UserSettings/UserSettingsNotificationSettings.razor b/Crypter.Web/Shared/UserSettings/UserSettingsNotificationSettings.razor index 4cd7a2e9..15595922 100644 --- a/Crypter.Web/Shared/UserSettings/UserSettingsNotificationSettings.razor +++ b/Crypter.Web/Shared/UserSettings/UserSettingsNotificationSettings.razor @@ -29,12 +29,12 @@
- +

Notification Methods

- +
*Alternative notifications methods are not available yet diff --git a/Crypter.Web/Shared/UserSettings/UserSettingsNotificationSettings.razor.cs b/Crypter.Web/Shared/UserSettings/UserSettingsNotificationSettings.razor.cs index 7a351f48..373f391a 100644 --- a/Crypter.Web/Shared/UserSettings/UserSettingsNotificationSettings.razor.cs +++ b/Crypter.Web/Shared/UserSettings/UserSettingsNotificationSettings.razor.cs @@ -100,7 +100,7 @@ await CrypterApiService.UserSetting.UpdateNotificationSettingsAsync(newNotificat private void OnContactInfoChanged(object? sender, UserContactInfoChangedEventArgs args) { - _emailAddressVerified = args.RequestedEmailAddress; + _emailAddressVerified = args.VerifiedEmailAddress.IsSome; StateHasChanged(); }