Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/AndreGoepel.AppFoundation.Hosting/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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, )",
Expand All @@ -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, )",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
<InternalsVisibleTo Include="AndreGoepel.AppFoundation.MailService.Tests" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AndreGoepel.AppFoundation.Core\AndreGoepel.AppFoundation.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="MailKit" />
<PackageReference Include="Marten" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using AndreGoepel.AppFoundation.Core;

namespace AndreGoepel.AppFoundation.MailService;

/// <summary>
Expand All @@ -15,9 +17,10 @@ public interface IEmailSettingsStore

/// <summary>
/// Persists the settings. <paramref name="newPassword"/> replaces the stored
/// SMTP password; pass <c>null</c> or empty to keep the current one.
/// SMTP password; pass <c>null</c> or empty to keep the current one. Fails when
/// no password is stored yet and none is supplied.
/// </summary>
Task SaveAsync(
Task<Result> SaveAsync(
EmailSettings settings,
string? newPassword,
CancellationToken cancellationToken = default
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using AndreGoepel.AppFoundation.Core;
using AndreGoepel.Marten.Configuration;
using Microsoft.AspNetCore.DataProtection;

Expand Down Expand Up @@ -28,7 +29,7 @@ public async Task<EmailSettings> LoadAsync(CancellationToken cancellationToken =
: new EmailSettings();
}

public async Task SaveAsync(
public async Task<Result> SaveAsync(
EmailSettings settings,
string? newPassword,
CancellationToken cancellationToken = default
Expand All @@ -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(
Expand All @@ -65,5 +66,7 @@ await store.SaveAsync(
},
cancellationToken
);

return Result.Ok();
}
}
3 changes: 3 additions & 0 deletions src/AndreGoepel.AppFoundation.MailService/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@
"WolverineFx": "6.22.0"
}
},
"andregoepel.appfoundation.core": {
"type": "Project"
},
"Npgsql": {
"type": "CentralTransitive",
"requested": "[9.0.4, )",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@attribute [Authorize(Roles = "Administrator")]
@inherits AndreGoepel.AppFoundation.Components.LocalizedComponentBase

@using AndreGoepel.AppFoundation.Core
@using AndreGoepel.AppFoundation.MailService

@inject IEmailSettingsStore SettingsStore
Expand Down Expand Up @@ -145,7 +146,7 @@
isSaving = true;
try
{
await SettingsStore.SaveAsync(
var result = await SettingsStore.SaveAsync(
new EmailSettings
{
SenderName = model.SenderName,
Expand All @@ -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

Check warning on line 178 in src/AndreGoepel.AppFoundation/Components/Administration/Pages/EmailSettingsPage.razor

View workflow job for this annotation

GitHub Actions / end-to-end

Possible null reference argument for parameter 'detail' in 'void NotificationService.Notify(NotificationSeverity severity = NotificationSeverity.Info, string summary = "", string detail = "", double duration = 3000, Action<NotificationMessage>? click = null, bool closeOnClick = false, object? payload = null, Action<NotificationMessage>? close = null)'.

Check warning on line 178 in src/AndreGoepel.AppFoundation/Components/Administration/Pages/EmailSettingsPage.razor

View workflow job for this annotation

GitHub Actions / end-to-end

Possible null reference argument for parameter 'detail' in 'void NotificationService.Notify(NotificationSeverity severity = NotificationSeverity.Info, string summary = "", string detail = "", double duration = 3000, Action<NotificationMessage>? click = null, bool closeOnClick = false, object? payload = null, Action<NotificationMessage>? close = null)'.
);
}
}
finally
{
Expand Down
1 change: 1 addition & 0 deletions src/AndreGoepel.AppFoundation/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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, )",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using AndreGoepel.AppFoundation.Core;
using AndreGoepel.Marten.Configuration;
using Microsoft.AspNetCore.DataProtection;
using NSubstitute;
Expand Down Expand Up @@ -31,7 +32,7 @@
store.LoadAsync<EmailSettingsDocument>(Arg.Any<CancellationToken>()).Returns(Document());

// Act
var settings = await BuildStore().LoadAsync();

Check warning on line 35 in tests/AndreGoepel.AppFoundation.MailService.Tests/MartenEmailSettingsStore.Tests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken to allow test cancellation to be more responsive. (https://xunit.net/xunit.analyzers/rules/xUnit1051)

// Assert
Assert.Equal("DB Sender", settings.SenderName);
Expand All @@ -43,7 +44,7 @@
public async Task LoadAsync_WithoutRecord_ReturnsBlankDefaults()
{
// Act
var settings = await BuildStore().LoadAsync();

Check warning on line 47 in tests/AndreGoepel.AppFoundation.MailService.Tests/MartenEmailSettingsStore.Tests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken to allow test cancellation to be more responsive. (https://xunit.net/xunit.analyzers/rules/xUnit1051)

// Assert
Assert.Equal("", settings.SenderName);
Expand All @@ -57,13 +58,13 @@
// Arrange
var emailStore = BuildStore();
EmailSettingsDocument? stored = null;
store.SaveAsync(

Check warning on line 61 in tests/AndreGoepel.AppFoundation.MailService.Tests/MartenEmailSettingsStore.Tests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
Arg.Do<EmailSettingsDocument>(document => stored = document),
Arg.Any<CancellationToken>()
);

// Act
await emailStore.SaveAsync(
var result = await emailStore.SaveAsync(
new EmailSettings
{
SenderName = "S",
Expand All @@ -75,6 +76,7 @@
);

// Assert
Assert.True(result.IsSuccess);
Assert.NotNull(stored);
Assert.NotEqual("new-secret", stored.ProtectedPassword);
Assert.Equal(
Expand All @@ -95,13 +97,13 @@
var existing = Document();
store.LoadAsync<EmailSettingsDocument>(Arg.Any<CancellationToken>()).Returns(existing);
EmailSettingsDocument? stored = null;
store.SaveAsync(

Check warning on line 100 in tests/AndreGoepel.AppFoundation.MailService.Tests/MartenEmailSettingsStore.Tests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
Arg.Do<EmailSettingsDocument>(document => stored = document),
Arg.Any<CancellationToken>()
);

// Act
await BuildStore()
var result = await BuildStore()
.SaveAsync(
new EmailSettings
{
Expand All @@ -114,26 +116,29 @@
);

// 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<InvalidOperationException>(() =>
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, )",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AndreGoepel.AppFoundation.Components.Administration.Pages;
using AndreGoepel.AppFoundation.Core;
using AndreGoepel.AppFoundation.MailService;
using Bunit;
using Microsoft.AspNetCore.Authorization;
Expand All @@ -13,13 +14,14 @@ public sealed class EmailSettingsPageTests : BunitContext
{
private readonly IEmailSettingsStore store = Substitute.For<IEmailSettingsStore>();
private readonly IEmailSender emailSender = Substitute.For<IEmailSender>();
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() =>
Expand Down Expand Up @@ -55,6 +57,9 @@ public void Submit_WithValidInput_SavesWithoutPasswordChange()
{
// Arrange
store.LoadAsync(Arg.Any<CancellationToken>()).Returns(Settings());
store
.SaveAsync(Arg.Any<EmailSettings>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
.Returns(Result.Ok());
var cut = Render<EmailSettingsPage>();

// Act
Expand All @@ -74,6 +79,30 @@ public void Submit_WithValidInput_SavesWithoutPasswordChange()
);
}

[Fact]
public void Submit_StoreReturnsFailure_ShowsErrorNotificationWithResultMessage()
{
// Arrange
store.LoadAsync(Arg.Any<CancellationToken>()).Returns(Settings());
store
.SaveAsync(Arg.Any<EmailSettings>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
.Returns(Result.Fail("An SMTP password is required for the first save."));
var cut = Render<EmailSettingsPage>();

// 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()
{
Expand Down
5 changes: 3 additions & 2 deletions tests/AndreGoepel.AppFoundation.Tests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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, )",
Expand All @@ -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, )",
Expand All @@ -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, )",
Expand Down
Loading