feat(smtp): UpdateSmtpConfigCommand carries TlsMode + Credentials

Add two optional nullable fields (TlsMode, Credentials) to the
UpdateSmtpConfigCommand record. The handler applies preserve-if-null
semantics: an update that omits a field leaves the existing value
intact, so existing 5-arg callers remain non-breaking.
This commit is contained in:
Joseph Doherty
2026-05-21 02:11:03 -04:00
parent 932fda5594
commit ec92d55ebf
3 changed files with 71 additions and 1 deletions

View File

@@ -6,4 +6,4 @@ public record CreateNotificationListCommand(string Name, IReadOnlyList<string> R
public record UpdateNotificationListCommand(int NotificationListId, string Name, IReadOnlyList<string> RecipientEmails);
public record DeleteNotificationListCommand(int NotificationListId);
public record ListSmtpConfigsCommand;
public record UpdateSmtpConfigCommand(int SmtpConfigId, string Server, int Port, string AuthMode, string FromAddress);
public record UpdateSmtpConfigCommand(int SmtpConfigId, string Server, int Port, string AuthMode, string FromAddress, string? TlsMode = null, string? Credentials = null);

View File

@@ -1124,6 +1124,10 @@ public class ManagementActor : ReceiveActor
config.Port = cmd.Port;
config.AuthType = cmd.AuthMode;
config.FromAddress = cmd.FromAddress;
// Preserve-if-null: an update that omits TlsMode/Credentials leaves the
// existing values intact (non-breaking for callers that do not send them).
if (cmd.TlsMode is not null) config.TlsMode = cmd.TlsMode;
if (cmd.Credentials is not null) config.Credentials = cmd.Credentials;
await repo.UpdateSmtpConfigurationAsync(config);
await repo.SaveChangesAsync();
await AuditAsync(sp, user, "Update", "SmtpConfiguration", config.Id.ToString(), config.Host, config);

View File

@@ -1002,6 +1002,72 @@ public class ManagementActorTests : TestKit, IDisposable
Assert.Contains(envelope.CorrelationId, response.Error);
}
// ========================================================================
// UpdateSmtpConfig — TlsMode + Credentials plumbing (preserve-if-null)
// ========================================================================
[Fact]
public void UpdateSmtpConfig_WithTlsModeAndCredentials_PersistsThem()
{
var notifRepo = Substitute.For<INotificationRepository>();
var existing = new Commons.Entities.Notifications.SmtpConfiguration(
"old.example.com", "OAuth2", "old@example.com")
{
Id = 1,
Port = 25,
TlsMode = "StartTLS",
Credentials = "old-secret",
};
notifRepo.GetSmtpConfigurationByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);
_services.AddScoped(_ => notifRepo);
var actor = CreateActor();
var envelope = Envelope(
new UpdateSmtpConfigCommand(1, "new.example.com", 465, "Basic", "new@example.com", "SSL", "user:pass"),
"Design");
actor.Tell(envelope);
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
Assert.Equal("SSL", existing.TlsMode);
Assert.Equal("user:pass", existing.Credentials);
Assert.Equal("new.example.com", existing.Host);
Assert.Equal("Basic", existing.AuthType);
}
[Fact]
public void UpdateSmtpConfig_WithNullTlsModeAndCredentials_PreservesExistingValues()
{
var notifRepo = Substitute.For<INotificationRepository>();
var existing = new Commons.Entities.Notifications.SmtpConfiguration(
"old.example.com", "OAuth2", "old@example.com")
{
Id = 1,
Port = 25,
TlsMode = "StartTLS",
Credentials = "old-secret",
};
notifRepo.GetSmtpConfigurationByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);
_services.AddScoped(_ => notifRepo);
var actor = CreateActor();
var envelope = Envelope(
new UpdateSmtpConfigCommand(1, "new.example.com", 465, "Basic", "new@example.com"),
"Design");
actor.Tell(envelope);
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
// Omitted fields are preserved, not nulled.
Assert.Equal("StartTLS", existing.TlsMode);
Assert.Equal("old-secret", existing.Credentials);
// Provided fields are still updated.
Assert.Equal("new.example.com", existing.Host);
Assert.Equal("Basic", existing.AuthType);
}
[Fact]
public void CuratedHandlerFailure_SurfacesTheCuratedMessage()
{