From ec92d55ebf34830051273201b2241523fee6dac9 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 21 May 2026 02:11:03 -0400 Subject: [PATCH] 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. --- .../Management/NotificationCommands.cs | 2 +- .../ManagementActor.cs | 4 ++ .../ManagementActorTests.cs | 66 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/ScadaLink.Commons/Messages/Management/NotificationCommands.cs b/src/ScadaLink.Commons/Messages/Management/NotificationCommands.cs index 86b47a0..aa5f221 100644 --- a/src/ScadaLink.Commons/Messages/Management/NotificationCommands.cs +++ b/src/ScadaLink.Commons/Messages/Management/NotificationCommands.cs @@ -6,4 +6,4 @@ public record CreateNotificationListCommand(string Name, IReadOnlyList R public record UpdateNotificationListCommand(int NotificationListId, string Name, IReadOnlyList 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); diff --git a/src/ScadaLink.ManagementService/ManagementActor.cs b/src/ScadaLink.ManagementService/ManagementActor.cs index 3d3f784..10e79d7 100644 --- a/src/ScadaLink.ManagementService/ManagementActor.cs +++ b/src/ScadaLink.ManagementService/ManagementActor.cs @@ -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); diff --git a/tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs b/tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs index d2b4787..8bad690 100644 --- a/tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs +++ b/tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs @@ -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(); + 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()).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(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(); + 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()).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(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() {