diff --git a/src/AndreGoepel.AppFoundation.Hosting/packages.lock.json b/src/AndreGoepel.AppFoundation.Hosting/packages.lock.json
index 697ea8b..9e449e9 100644
--- a/src/AndreGoepel.AppFoundation.Hosting/packages.lock.json
+++ b/src/AndreGoepel.AppFoundation.Hosting/packages.lock.json
@@ -588,7 +588,7 @@
"andregoepel.appfoundation": {
"type": "Project",
"dependencies": {
- "AndreGoepel.AppFoundation.Core": "[1.8.0, )",
+ "AndreGoepel.AppFoundation.Core": "[1.9.0, )",
"AndreGoepel.AppFoundation.MailService": "[1.8.0, )",
"AndreGoepel.Design.Blazor": "[1.4.2, )",
"AndreGoepel.Marten.Identity.Blazor": "[1.8.0, )",
@@ -603,6 +603,7 @@
"andregoepel.appfoundation.mailservice": {
"type": "Project",
"dependencies": {
+ "AndreGoepel.AppFoundation.Core": "[1.9.0, )",
"AndreGoepel.Marten.Configuration": "[1.0.1, )",
"MailKit": "[4.17.0, )",
"Marten": "[9.19.0, )",
diff --git a/src/AndreGoepel.AppFoundation.MailService/AndreGoepel.AppFoundation.MailService.csproj b/src/AndreGoepel.AppFoundation.MailService/AndreGoepel.AppFoundation.MailService.csproj
index 917177f..67c18bd 100644
--- a/src/AndreGoepel.AppFoundation.MailService/AndreGoepel.AppFoundation.MailService.csproj
+++ b/src/AndreGoepel.AppFoundation.MailService/AndreGoepel.AppFoundation.MailService.csproj
@@ -26,6 +26,10 @@
+
+
+
+
diff --git a/src/AndreGoepel.AppFoundation.MailService/IEmailSettingsStore.cs b/src/AndreGoepel.AppFoundation.MailService/IEmailSettingsStore.cs
index ef80687..52ef676 100644
--- a/src/AndreGoepel.AppFoundation.MailService/IEmailSettingsStore.cs
+++ b/src/AndreGoepel.AppFoundation.MailService/IEmailSettingsStore.cs
@@ -1,3 +1,5 @@
+using AndreGoepel.AppFoundation.Core;
+
namespace AndreGoepel.AppFoundation.MailService;
///
@@ -15,9 +17,10 @@ public interface IEmailSettingsStore
///
/// Persists the settings. replaces the stored
- /// SMTP password; pass null or empty to keep the current one.
+ /// SMTP password; pass null or empty to keep the current one. Fails when
+ /// no password is stored yet and none is supplied.
///
- Task SaveAsync(
+ Task SaveAsync(
EmailSettings settings,
string? newPassword,
CancellationToken cancellationToken = default
diff --git a/src/AndreGoepel.AppFoundation.MailService/MartenEmailSettingsStore.cs b/src/AndreGoepel.AppFoundation.MailService/MartenEmailSettingsStore.cs
index e5512f1..334a54e 100644
--- a/src/AndreGoepel.AppFoundation.MailService/MartenEmailSettingsStore.cs
+++ b/src/AndreGoepel.AppFoundation.MailService/MartenEmailSettingsStore.cs
@@ -1,3 +1,4 @@
+using AndreGoepel.AppFoundation.Core;
using AndreGoepel.Marten.Configuration;
using Microsoft.AspNetCore.DataProtection;
@@ -28,7 +29,7 @@ public async Task LoadAsync(CancellationToken cancellationToken =
: new EmailSettings();
}
- public async Task SaveAsync(
+ public async Task SaveAsync(
EmailSettings settings,
string? newPassword,
CancellationToken cancellationToken = default
@@ -48,7 +49,7 @@ public async Task SaveAsync(
}
else
{
- throw new InvalidOperationException("An SMTP password is required for the first save.");
+ return Result.Fail("An SMTP password is required for the first save.");
}
await store.SaveAsync(
@@ -65,5 +66,7 @@ await store.SaveAsync(
},
cancellationToken
);
+
+ return Result.Ok();
}
}
diff --git a/src/AndreGoepel.AppFoundation.MailService/packages.lock.json b/src/AndreGoepel.AppFoundation.MailService/packages.lock.json
index 4e5e955..6b6ce3a 100644
--- a/src/AndreGoepel.AppFoundation.MailService/packages.lock.json
+++ b/src/AndreGoepel.AppFoundation.MailService/packages.lock.json
@@ -225,6 +225,9 @@
"WolverineFx": "6.22.0"
}
},
+ "andregoepel.appfoundation.core": {
+ "type": "Project"
+ },
"Npgsql": {
"type": "CentralTransitive",
"requested": "[9.0.4, )",
diff --git a/src/AndreGoepel.AppFoundation/Components/Administration/Pages/EmailSettingsPage.razor b/src/AndreGoepel.AppFoundation/Components/Administration/Pages/EmailSettingsPage.razor
index 67bb141..4cd9874 100644
--- a/src/AndreGoepel.AppFoundation/Components/Administration/Pages/EmailSettingsPage.razor
+++ b/src/AndreGoepel.AppFoundation/Components/Administration/Pages/EmailSettingsPage.razor
@@ -3,6 +3,7 @@
@attribute [Authorize(Roles = "Administrator")]
@inherits AndreGoepel.AppFoundation.Components.LocalizedComponentBase
+@using AndreGoepel.AppFoundation.Core
@using AndreGoepel.AppFoundation.MailService
@inject IEmailSettingsStore SettingsStore
@@ -145,7 +146,7 @@
isSaving = true;
try
{
- await SettingsStore.SaveAsync(
+ var result = await SettingsStore.SaveAsync(
new EmailSettings
{
SenderName = model.SenderName,
@@ -159,21 +160,24 @@
string.IsNullOrEmpty(model.Password) ? null : model.Password
);
- hasPassword = true;
- Input.Password = "";
- NotificationService.Notify(
- NotificationSeverity.Success,
- T("EmailSettings.SavedNotificationTitle"),
- T("EmailSettings.SavedNotificationMessage")
- );
- }
- catch (Exception ex)
- {
- NotificationService.Notify(
- NotificationSeverity.Error,
- T("EmailSettings.ErrorSavingSettingsTitle"),
- ex.Message
- );
+ if (result.IsSuccess)
+ {
+ hasPassword = true;
+ Input.Password = "";
+ NotificationService.Notify(
+ NotificationSeverity.Success,
+ T("EmailSettings.SavedNotificationTitle"),
+ T("EmailSettings.SavedNotificationMessage")
+ );
+ }
+ else
+ {
+ NotificationService.Notify(
+ NotificationSeverity.Error,
+ T("EmailSettings.ErrorSavingSettingsTitle"),
+ result.Error
+ );
+ }
}
finally
{
diff --git a/src/AndreGoepel.AppFoundation/packages.lock.json b/src/AndreGoepel.AppFoundation/packages.lock.json
index b974720..a301498 100644
--- a/src/AndreGoepel.AppFoundation/packages.lock.json
+++ b/src/AndreGoepel.AppFoundation/packages.lock.json
@@ -273,6 +273,7 @@
"andregoepel.appfoundation.mailservice": {
"type": "Project",
"dependencies": {
+ "AndreGoepel.AppFoundation.Core": "[1.9.0, )",
"AndreGoepel.Marten.Configuration": "[1.0.1, )",
"MailKit": "[4.17.0, )",
"Marten": "[9.19.0, )",
diff --git a/tests/AndreGoepel.AppFoundation.MailService.Tests/MartenEmailSettingsStore.Tests.cs b/tests/AndreGoepel.AppFoundation.MailService.Tests/MartenEmailSettingsStore.Tests.cs
index 28a180f..adbaaa6 100644
--- a/tests/AndreGoepel.AppFoundation.MailService.Tests/MartenEmailSettingsStore.Tests.cs
+++ b/tests/AndreGoepel.AppFoundation.MailService.Tests/MartenEmailSettingsStore.Tests.cs
@@ -1,3 +1,4 @@
+using AndreGoepel.AppFoundation.Core;
using AndreGoepel.Marten.Configuration;
using Microsoft.AspNetCore.DataProtection;
using NSubstitute;
@@ -63,7 +64,7 @@ public async Task SaveAsync_WithNewPassword_StoresProtectedPassword()
);
// Act
- await emailStore.SaveAsync(
+ var result = await emailStore.SaveAsync(
new EmailSettings
{
SenderName = "S",
@@ -75,6 +76,7 @@ await emailStore.SaveAsync(
);
// Assert
+ Assert.True(result.IsSuccess);
Assert.NotNull(stored);
Assert.NotEqual("new-secret", stored.ProtectedPassword);
Assert.Equal(
@@ -101,7 +103,7 @@ public async Task SaveAsync_WithoutPassword_KeepsExistingProtectedPassword()
);
// Act
- await BuildStore()
+ var result = await BuildStore()
.SaveAsync(
new EmailSettings
{
@@ -114,26 +116,29 @@ await BuildStore()
);
// Assert
+ Assert.True(result.IsSuccess);
Assert.NotNull(stored);
Assert.Equal("protected", stored.ProtectedPassword);
}
[Fact]
- public async Task SaveAsync_FirstSaveWithoutAnyPassword_Throws()
+ public async Task SaveAsync_FirstSaveWithoutAnyPassword_ReturnsFailure()
{
- // Act + Assert
- await Assert.ThrowsAsync(() =>
- BuildStore()
- .SaveAsync(
- new EmailSettings
- {
- SenderName = "S",
- SenderEmail = "s@example.com",
- Server = "smtp",
- Username = "u",
- },
- newPassword: null
- )
- );
+ // Act
+ var result = await BuildStore()
+ .SaveAsync(
+ new EmailSettings
+ {
+ SenderName = "S",
+ SenderEmail = "s@example.com",
+ Server = "smtp",
+ Username = "u",
+ },
+ newPassword: null
+ );
+
+ // Assert
+ Assert.True(result.IsFailure);
+ Assert.Equal("An SMTP password is required for the first save.", result.Error);
}
}
diff --git a/tests/AndreGoepel.AppFoundation.MailService.Tests/packages.lock.json b/tests/AndreGoepel.AppFoundation.MailService.Tests/packages.lock.json
index 91c8fbf..200909a 100644
--- a/tests/AndreGoepel.AppFoundation.MailService.Tests/packages.lock.json
+++ b/tests/AndreGoepel.AppFoundation.MailService.Tests/packages.lock.json
@@ -359,9 +359,13 @@
"xunit.v3.runner.common": "[3.2.2]"
}
},
+ "andregoepel.appfoundation.core": {
+ "type": "Project"
+ },
"andregoepel.appfoundation.mailservice": {
"type": "Project",
"dependencies": {
+ "AndreGoepel.AppFoundation.Core": "[1.9.0, )",
"AndreGoepel.Marten.Configuration": "[1.0.1, )",
"MailKit": "[4.17.0, )",
"Marten": "[9.19.0, )",
diff --git a/tests/AndreGoepel.AppFoundation.Tests/Components/Administration/EmailSettingsPage.Tests.cs b/tests/AndreGoepel.AppFoundation.Tests/Components/Administration/EmailSettingsPage.Tests.cs
index 8bec6a2..4fc6b11 100644
--- a/tests/AndreGoepel.AppFoundation.Tests/Components/Administration/EmailSettingsPage.Tests.cs
+++ b/tests/AndreGoepel.AppFoundation.Tests/Components/Administration/EmailSettingsPage.Tests.cs
@@ -1,4 +1,5 @@
using AndreGoepel.AppFoundation.Components.Administration.Pages;
+using AndreGoepel.AppFoundation.Core;
using AndreGoepel.AppFoundation.MailService;
using Bunit;
using Microsoft.AspNetCore.Authorization;
@@ -13,13 +14,14 @@ public sealed class EmailSettingsPageTests : BunitContext
{
private readonly IEmailSettingsStore store = Substitute.For();
private readonly IEmailSender emailSender = Substitute.For();
+ private readonly NotificationService notificationService = new();
public EmailSettingsPageTests()
{
JSInterop.Mode = JSRuntimeMode.Loose;
Services.AddSingleton(store);
Services.AddSingleton(emailSender);
- Services.AddSingleton(new NotificationService());
+ Services.AddSingleton(notificationService);
}
private static EmailSettings Settings() =>
@@ -55,6 +57,9 @@ public void Submit_WithValidInput_SavesWithoutPasswordChange()
{
// Arrange
store.LoadAsync(Arg.Any()).Returns(Settings());
+ store
+ .SaveAsync(Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(Result.Ok());
var cut = Render();
// Act
@@ -74,6 +79,30 @@ public void Submit_WithValidInput_SavesWithoutPasswordChange()
);
}
+ [Fact]
+ public void Submit_StoreReturnsFailure_ShowsErrorNotificationWithResultMessage()
+ {
+ // Arrange
+ store.LoadAsync(Arg.Any()).Returns(Settings());
+ store
+ .SaveAsync(Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(Result.Fail("An SMTP password is required for the first save."));
+ var cut = Render();
+
+ // Act
+ cut.Find("form").Submit();
+
+ // Assert
+ cut.WaitForAssertion(() =>
+ Assert.Contains(
+ notificationService.Messages,
+ message =>
+ message.Severity == NotificationSeverity.Error
+ && message.Detail == "An SMTP password is required for the first save."
+ )
+ );
+ }
+
[Fact]
public void Route_IsAdministrationEmailSettings_AndRequiresAdministratorRole()
{
diff --git a/tests/AndreGoepel.AppFoundation.Tests/packages.lock.json b/tests/AndreGoepel.AppFoundation.Tests/packages.lock.json
index c0a866a..b549563 100644
--- a/tests/AndreGoepel.AppFoundation.Tests/packages.lock.json
+++ b/tests/AndreGoepel.AppFoundation.Tests/packages.lock.json
@@ -728,7 +728,7 @@
"andregoepel.appfoundation": {
"type": "Project",
"dependencies": {
- "AndreGoepel.AppFoundation.Core": "[1.8.0, )",
+ "AndreGoepel.AppFoundation.Core": "[1.9.0, )",
"AndreGoepel.AppFoundation.MailService": "[1.8.0, )",
"AndreGoepel.Design.Blazor": "[1.4.2, )",
"AndreGoepel.Marten.Identity.Blazor": "[1.8.0, )",
@@ -744,7 +744,7 @@
"type": "Project",
"dependencies": {
"AndreGoepel.AppFoundation": "[1.8.0, )",
- "AndreGoepel.AppFoundation.Core": "[1.8.0, )",
+ "AndreGoepel.AppFoundation.Core": "[1.9.0, )",
"AndreGoepel.AppFoundation.MailService": "[1.8.0, )",
"AndreGoepel.AppFoundation.ServiceDefaults": "[1.8.0, )",
"AndreGoepel.Marten.Configuration": "[1.0.1, )",
@@ -762,6 +762,7 @@
"andregoepel.appfoundation.mailservice": {
"type": "Project",
"dependencies": {
+ "AndreGoepel.AppFoundation.Core": "[1.9.0, )",
"AndreGoepel.Marten.Configuration": "[1.0.1, )",
"MailKit": "[4.17.0, )",
"Marten": "[9.19.0, )",