fix(management): EWS write gate requires username:password credentials
This commit is contained in:
@@ -1680,6 +1680,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
{
|
||||
Id = 1,
|
||||
Port = 25,
|
||||
Credentials = "example\\notify:fake-password",
|
||||
};
|
||||
notifRepo.GetSmtpConfigurationByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);
|
||||
_services.AddScoped(_ => notifRepo);
|
||||
@@ -1755,6 +1756,9 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("https://", response.Error);
|
||||
// The offending value is echoed so the operator sees what was rejected
|
||||
// (matches the delivery adapter's message style).
|
||||
Assert.Contains("http://ews.example.test/ews/exchange.asmx", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1784,6 +1788,97 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.Contains("Basic authentication", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateSmtpConfig_WithEwsTransportAndMalformedCredentials_ReturnsManagementError()
|
||||
{
|
||||
var notifRepo = Substitute.For<INotificationRepository>();
|
||||
var existing = new Commons.Entities.Notifications.SmtpConfiguration(
|
||||
"old.example.com", "Basic", "old@example.com")
|
||||
{
|
||||
Id = 1,
|
||||
Port = 25,
|
||||
Credentials = "example\\notify:fake-password",
|
||||
};
|
||||
notifRepo.GetSmtpConfigurationByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);
|
||||
_services.AddScoped(_ => notifRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
// A supplied credential with no colon at all — the EWS sender cannot build a
|
||||
// Basic header from it, so every send would park.
|
||||
var envelope = Envelope(
|
||||
new UpdateSmtpConfigCommand(
|
||||
1, "https://ews.example.test/ews/exchange.asmx", 443, "basic", "new@example.com",
|
||||
Credentials: "passwordwithoutcolon",
|
||||
Transport: "Ews"),
|
||||
"Administrator");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("username:password", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateSmtpConfig_WithEwsTransportAndEmptyCredentialHalf_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();
|
||||
// A trailing colon splits into two parts, but the password half is empty —
|
||||
// the adapter rejects it, so the write gate must too.
|
||||
var envelope = Envelope(
|
||||
new UpdateSmtpConfigCommand(
|
||||
1, "https://ews.example.test/ews/exchange.asmx", 443, "basic", "new@example.com",
|
||||
Credentials: "example\\notify:",
|
||||
Transport: "Ews"),
|
||||
"Administrator");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("username:password", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateSmtpConfig_FlippingToEwsWithStoredBadCredentials_ReturnsManagementError()
|
||||
{
|
||||
var notifRepo = Substitute.For<INotificationRepository>();
|
||||
var existing = new Commons.Entities.Notifications.SmtpConfiguration(
|
||||
"old.example.com", "Basic", "old@example.com")
|
||||
{
|
||||
Id = 1,
|
||||
Port = 25,
|
||||
Credentials = "bare-smtp-password",
|
||||
};
|
||||
notifRepo.GetSmtpConfigurationByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);
|
||||
_services.AddScoped(_ => notifRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
// Credentials omitted entirely: the gate validates the EFFECTIVE state, so the
|
||||
// preserved (unusable) stored value is what gets rejected.
|
||||
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<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("COMMAND_FAILED", response.ErrorCode);
|
||||
Assert.Contains("username:password", response.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateSmtpConfig_WithNullTransport_PreservesExistingValue()
|
||||
{
|
||||
@@ -1794,6 +1889,7 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Id = 1,
|
||||
Port = 443,
|
||||
Transport = "Ews",
|
||||
Credentials = "example\\notify:fake-password",
|
||||
};
|
||||
notifRepo.GetSmtpConfigurationByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);
|
||||
_services.AddScoped(_ => notifRepo);
|
||||
@@ -1815,6 +1911,59 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.Equal("https://ews2.example.test/ews/exchange.asmx", existing.Host);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Smtp")]
|
||||
[InlineData("Ews")]
|
||||
[InlineData("Graph")]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
public void UpdateSmtpConfig_TransportAcceptance_MatchesEmailTransportParser(string? transport)
|
||||
{
|
||||
// Parity pin: the write gate re-implements the transport parse inline (ManagementService
|
||||
// does not reference NotificationService), so this asserts the mirror accepts exactly the
|
||||
// values EmailTransportParser accepts. If the parser gains a transport, this goes red.
|
||||
var parserAccepts = true;
|
||||
try
|
||||
{
|
||||
NotificationService.EmailTransportParser.Parse(transport);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
parserAccepts = false;
|
||||
}
|
||||
|
||||
var notifRepo = Substitute.For<INotificationRepository>();
|
||||
// A fixture valid under EITHER transport, so the transport parse is the only variable.
|
||||
var existing = new Commons.Entities.Notifications.SmtpConfiguration(
|
||||
"https://ews.example.test/ews/exchange.asmx", "basic", "old@example.com")
|
||||
{
|
||||
Id = 1,
|
||||
Port = 443,
|
||||
Credentials = "example\\notify:fake-password",
|
||||
};
|
||||
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: transport),
|
||||
"Administrator");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
if (parserAccepts)
|
||||
{
|
||||
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Unknown email transport", error.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SMS Notifications (S5) — list Type discriminator + SMS-config management
|
||||
// ========================================================================
|
||||
|
||||
+5
@@ -28,5 +28,10 @@
|
||||
<!-- #206: SecuredWrite audit now routes through the real CentralAuditWriter so the
|
||||
handler tests verify SourceNode stamping end-to-end (actor → writer → repo). -->
|
||||
<ProjectReference Include="../../src/ZB.MOM.WW.ScadaBridge.AuditLog/ZB.MOM.WW.ScadaBridge.AuditLog.csproj" />
|
||||
<!-- Test-only: ManagementActor deliberately does NOT reference NotificationService, so its
|
||||
EWS write gate re-implements the transport parse inline. This reference exists purely so
|
||||
a parity test can pin that mirror to the real EmailTransportParser. Do not take a
|
||||
production dependency on it from ManagementService. -->
|
||||
<ProjectReference Include="../../src/ZB.MOM.WW.ScadaBridge.NotificationService/ZB.MOM.WW.ScadaBridge.NotificationService.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user