feat(management): Transport on UpdateSmtpConfigCommand with EWS shape validation

This commit is contained in:
Joseph Doherty
2026-08-10 06:33:29 -04:00
parent 8657fae14f
commit 4e633b1e64
3 changed files with 210 additions and 1 deletions
@@ -1667,6 +1667,154 @@ public class ManagementActorTests : TestKit, IDisposable
Assert.Equal("old.scope/.default", existing.OAuth2Scope);
}
// ========================================================================
// UpdateSmtpConfig — Transport (EWS) write-path gate
// ========================================================================
[Fact]
public void UpdateSmtpConfig_WithEwsTransport_PersistsTransportAndSurfacesItInPublicShape()
{
var notifRepo = Substitute.For<INotificationRepository>();
var existing = new Commons.Entities.Notifications.SmtpConfiguration(
"old.example.com", "Basic", "old@example.com")
{
Id = 1,
Port = 25,
};
notifRepo.GetSmtpConfigurationByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);
_services.AddScoped(_ => notifRepo);
var actor = CreateActor();
var envelope = Envelope(
new UpdateSmtpConfigCommand(
1, "https://ews.example.test/ews/exchange.asmx", 443, "basic", "new@example.com",
Transport: "Ews"),
"Administrator");
actor.Tell(envelope);
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
Assert.Equal("Ews", existing.Transport);
Assert.Equal("https://ews.example.test/ews/exchange.asmx", existing.Host);
// The transport is a non-secret field, so it belongs on the public shape
// the response and audit afterState carry.
Assert.Contains("\"transport\":\"Ews\"", response.JsonData, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void UpdateSmtpConfig_WithUnknownTransport_ReturnsManagementError()
{
var notifRepo = Substitute.For<INotificationRepository>();
var existing = new Commons.Entities.Notifications.SmtpConfiguration(
"old.example.com", "Basic", "old@example.com")
{
Id = 1,
Port = 25,
};
notifRepo.GetSmtpConfigurationByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);
_services.AddScoped(_ => notifRepo);
var actor = CreateActor();
var envelope = Envelope(
new UpdateSmtpConfigCommand(
1, "https://graph.example.test/", 443, "basic", "new@example.com",
Transport: "Graph"),
"Administrator");
actor.Tell(envelope);
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
Assert.Contains("Unknown email transport", response.Error);
Assert.Contains("Graph", response.Error);
}
[Fact]
public void UpdateSmtpConfig_WithEwsTransportAndNonHttpsHost_ReturnsManagementError()
{
var notifRepo = Substitute.For<INotificationRepository>();
var existing = new Commons.Entities.Notifications.SmtpConfiguration(
"old.example.com", "Basic", "old@example.com")
{
Id = 1,
Port = 25,
};
notifRepo.GetSmtpConfigurationByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);
_services.AddScoped(_ => notifRepo);
var actor = CreateActor();
var envelope = Envelope(
new UpdateSmtpConfigCommand(
1, "http://ews.example.test/ews/exchange.asmx", 80, "basic", "new@example.com",
Transport: "Ews"),
"Administrator");
actor.Tell(envelope);
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
Assert.Contains("https://", response.Error);
}
[Fact]
public void UpdateSmtpConfig_WithEwsTransportAndOAuth2Auth_ReturnsManagementError()
{
var notifRepo = Substitute.For<INotificationRepository>();
var existing = new Commons.Entities.Notifications.SmtpConfiguration(
"old.example.com", "Basic", "old@example.com")
{
Id = 1,
Port = 25,
};
notifRepo.GetSmtpConfigurationByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);
_services.AddScoped(_ => notifRepo);
var actor = CreateActor();
var envelope = Envelope(
new UpdateSmtpConfigCommand(
1, "https://ews.example.test/ews/exchange.asmx", 443, "oauth2", "new@example.com",
Transport: "Ews"),
"Administrator");
actor.Tell(envelope);
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
Assert.Contains("Basic authentication", response.Error);
}
[Fact]
public void UpdateSmtpConfig_WithNullTransport_PreservesExistingValue()
{
var notifRepo = Substitute.For<INotificationRepository>();
var existing = new Commons.Entities.Notifications.SmtpConfiguration(
"https://ews.example.test/ews/exchange.asmx", "basic", "old@example.com")
{
Id = 1,
Port = 443,
Transport = "Ews",
};
notifRepo.GetSmtpConfigurationByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);
_services.AddScoped(_ => notifRepo);
var actor = CreateActor();
// Transport omitted (a pre-EWS caller, or Newtonsoft deserializing a
// payload without the field): the stored transport survives, and the
// EWS shape rules are still enforced against that preserved value.
var envelope = Envelope(
new UpdateSmtpConfigCommand(
1, "https://ews2.example.test/ews/exchange.asmx", 443, "basic", "new@example.com"),
"Administrator");
actor.Tell(envelope);
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
Assert.Equal("Ews", existing.Transport);
Assert.Equal("https://ews2.example.test/ews/exchange.asmx", existing.Host);
}
// ========================================================================
// SMS Notifications (S5) — list Type discriminator + SMS-config management
// ========================================================================