fix(ui): mirror the EWS username:password credentials rule on the SMTP page

This commit is contained in:
Joseph Doherty
2026-08-10 06:53:15 -04:00
parent 3e490f2a7d
commit 82f52e81ce
2 changed files with 60 additions and 0 deletions
@@ -308,6 +308,24 @@
return; return;
} }
// Mirror of the same gate's credentials rule, with the identical FIRST-colon-only
// split: a domain-qualified user name contains no colon but a password may. This
// page has no preserve-if-blank behaviour — the field's value IS what gets
// persisted, and a blank field clears the stored credential — so the rule applies
// on edit as well as add, exactly as the management gate validates the effective row.
if (IsEwsForm)
{
var credentialParts = _credentials?.Trim().Split(':', 2) ?? [];
if (credentialParts.Length != 2
|| credentialParts[0].Length == 0
|| credentialParts[1].Length == 0)
{
_formError = "EWS credentials must be in 'username:password' form "
+ @"(username may be 'domain\username').";
return;
}
}
try try
{ {
if (_editingSmtp != null) if (_editingSmtp != null)
@@ -182,6 +182,7 @@ public class SmtpConfigurationPageTests : BunitContext
// The forced auth type is only observable through what gets persisted: the row // The forced auth type is only observable through what gets persisted: the row
// loaded as OAuth2 must save as Basic because EWS supports Basic only. // loaded as OAuth2 must save as Basic because EWS supports Basic only.
cut.Find("input[type=text]").Change("https://mail.example.com/ews/exchange.asmx"); cut.Find("input[type=text]").Change("https://mail.example.com/ews/exchange.asmx");
cut.Find("input[type=password]").Change(@"dom\user:pw");
ClickSave(cut); ClickSave(cut);
cut.WaitForAssertion(() => cut.WaitForAssertion(() =>
@@ -233,6 +234,47 @@ public class SmtpConfigurationPageTests : BunitContext
repo.DidNotReceive().SaveChangesAsync(); repo.DidNotReceive().SaveChangesAsync();
} }
[Fact]
public void Save_WithEwsTransportAndCredentialsWithoutColon_ShowsFormErrorAndDoesNotPersist()
{
var repo = Substitute.For<INotificationRepository>();
// The realistic mistake: an existing SMTP row whose credential is a bare password
// is flipped to Ews. Host is valid, so only the credentials rule can reject it.
var cut = RenderWith(repo, OAuth2Sample());
ClickEdit(cut);
SelectWithOption(cut, "Ews").Change("Ews");
cut.Find("input[type=text]").Change("https://mail.example.com/ews/exchange.asmx");
cut.Find("input[type=password]").Change("bare-password");
ClickSave(cut);
cut.WaitForAssertion(() => Assert.Contains("username:password", cut.Markup));
repo.DidNotReceive().UpdateSmtpConfigurationAsync(Arg.Any<SmtpConfiguration>());
repo.DidNotReceive().SaveChangesAsync();
}
[Fact]
public void Save_WithEwsTransportAndDomainQualifiedCredentials_Persists()
{
var repo = Substitute.For<INotificationRepository>();
var cut = RenderWith(repo, OAuth2Sample());
ClickEdit(cut);
SelectWithOption(cut, "Ews").Change("Ews");
cut.Find("input[type=text]").Change("https://mail.example.com/ews/exchange.asmx");
// A domain-qualified user name carries no colon of its own; the split takes the
// FIRST colon only, so a password containing colons stays intact.
cut.Find("input[type=password]").Change(@"dom\user:pw:with:colons");
ClickSave(cut);
cut.WaitForAssertion(() =>
{
repo.Received().UpdateSmtpConfigurationAsync(Arg.Is<SmtpConfiguration>(
c => c.Transport == "Ews" && c.Credentials == @"dom\user:pw:with:colons"));
repo.Received().SaveChangesAsync();
});
}
[Fact] [Fact]
public void ReadOnlyView_ShowsTransportBadgeAndHidesPortAndTlsForEwsRows() public void ReadOnlyView_ShowsTransportBadgeAndHidesPortAndTlsForEwsRows()
{ {