Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/SmtpConfigurationPageTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

320 lines
13 KiB
C#

using System.Security.Claims;
using ZB.MOM.WW.ScadaBridge.Security;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using SmtpConfigurationPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Notifications.SmtpConfiguration;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Pages;
/// <summary>
/// bUnit rendering tests for the SMTP Configuration page — specifically the TlsMode
/// field added so the UI exposes all five user-relevant SmtpConfiguration fields.
/// </summary>
public class SmtpConfigurationPageTests : BunitContext
{
private void WireAuth()
{
var claims = new[]
{
new Claim(JwtTokenService.UsernameClaimType, "tester"),
new Claim(JwtTokenService.RoleClaimType, "Administrator"),
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
Services.AddSingleton<AuthenticationStateProvider>(new TestAuthStateProvider(user));
Services.AddAuthorizationCore();
Services.AddTestConfigurationMutations();
}
private static SmtpConfiguration Sample() =>
new("smtp.example.com", "Basic", "noreply@example.com")
{
Id = 1,
Port = 587,
TlsMode = "StartTLS",
Credentials = "user:pass",
};
private static SmtpConfiguration OAuth2Sample() =>
new("smtp.example.com", "OAuth2", "noreply@example.com")
{
Id = 1,
Port = 587,
TlsMode = "StartTLS",
Credentials = "client-secret",
};
private static SmtpConfiguration EwsSample() =>
new("https://mail.example.com/ews/exchange.asmx", "Basic", "noreply@example.com")
{
Id = 2,
Port = 587,
TlsMode = "StartTLS",
Transport = "Ews",
Credentials = @"domain\user:pass",
};
private IRenderedComponent<SmtpConfigurationPage> RenderWith(
INotificationRepository repo, params SmtpConfiguration[] configs)
{
repo.GetAllSmtpConfigurationsAsync()
.Returns(Task.FromResult<IReadOnlyList<SmtpConfiguration>>(configs.ToList()));
foreach (var config in configs)
repo.GetSmtpConfigurationByIdAsync(config.Id).Returns(config);
Services.AddSingleton(repo);
WireAuth();
var cut = Render<SmtpConfigurationPage>();
cut.WaitForState(() => cut.Markup.Contains(configs[0].Host));
return cut;
}
private static void ClickEdit(IRenderedComponent<SmtpConfigurationPage> cut)
=> cut.FindAll("button").First(b => b.TextContent.Contains("Edit")).Click();
private static void ClickSave(IRenderedComponent<SmtpConfigurationPage> cut)
=> cut.FindAll("button").First(b => b.TextContent.Contains("Save")).Click();
/// <summary>Locates a select by an option it offers — the page has no element ids.</summary>
private static AngleSharp.Dom.IElement SelectWithOption(
IRenderedComponent<SmtpConfigurationPage> cut, string optionText)
=> cut.FindAll("select")
.Single(s => s.QuerySelectorAll("option").Any(o => o.TextContent == optionText));
private static bool HasSelectWithOption(
IRenderedComponent<SmtpConfigurationPage> cut, string optionText)
=> cut.FindAll("select")
.Any(s => s.QuerySelectorAll("option").Any(o => o.TextContent == optionText));
/// <summary>Verifies that the edit form renders the TLS mode select with all three modes.</summary>
[Fact]
public void EditForm_RendersTlsModeSelectWithAllThreeModes()
{
var repo = Substitute.For<INotificationRepository>();
repo.GetAllSmtpConfigurationsAsync()
.Returns(Task.FromResult<IReadOnlyList<SmtpConfiguration>>(
new List<SmtpConfiguration> { Sample() }));
Services.AddSingleton(repo);
WireAuth();
var cut = Render<SmtpConfigurationPage>();
cut.WaitForState(() => cut.Markup.Contains("smtp.example.com"));
cut.FindAll("button").First(b => b.TextContent.Contains("Edit")).Click();
cut.WaitForAssertion(() =>
{
var selects = cut.FindAll("select");
var tlsSelect = selects.Single(s => s.QuerySelectorAll("option")
.Any(o => o.TextContent == "StartTLS"));
var modes = tlsSelect.QuerySelectorAll("option").Select(o => o.TextContent).ToList();
Assert.Equal(new[] { "None", "StartTLS", "SSL" }, modes);
});
}
/// <summary>Verifies that the read-only view shows the TLS mode.</summary>
[Fact]
public void ReadOnlyView_ShowsTlsMode()
{
var repo = Substitute.For<INotificationRepository>();
repo.GetAllSmtpConfigurationsAsync()
.Returns(Task.FromResult<IReadOnlyList<SmtpConfiguration>>(
new List<SmtpConfiguration> { Sample() }));
Services.AddSingleton(repo);
WireAuth();
var cut = Render<SmtpConfigurationPage>();
cut.WaitForAssertion(() =>
{
Assert.Contains("TLS Mode", cut.Markup);
Assert.Contains("StartTLS", cut.Markup);
});
}
/// <summary>Verifies that saving an edit persists the chosen TLS mode.</summary>
[Fact]
public void SavingEdit_PersistsChosenTlsMode()
{
var config = Sample();
var repo = Substitute.For<INotificationRepository>();
repo.GetAllSmtpConfigurationsAsync()
.Returns(Task.FromResult<IReadOnlyList<SmtpConfiguration>>(
new List<SmtpConfiguration> { config }));
repo.GetSmtpConfigurationByIdAsync(config.Id).Returns(config);
Services.AddSingleton(repo);
WireAuth();
var cut = Render<SmtpConfigurationPage>();
cut.WaitForState(() => cut.Markup.Contains("smtp.example.com"));
cut.FindAll("button").First(b => b.TextContent.Contains("Edit")).Click();
var tlsSelect = cut.FindAll("select")
.Single(s => s.QuerySelectorAll("option").Any(o => o.TextContent == "StartTLS"));
tlsSelect.Change("SSL");
cut.FindAll("button").First(b => b.TextContent.Contains("Save")).Click();
cut.WaitForAssertion(() =>
{
repo.Received().UpdateSmtpConfigurationAsync(
Arg.Is<SmtpConfiguration>(c => c.TlsMode == "SSL"));
repo.Received().SaveChangesAsync();
});
}
// ========================================================================
// Transport selector (EWS) — form state machine
// ========================================================================
/// <summary>Verifies that the edit form with the EWS transport hides the port, TLS and auth type and forces basic auth.</summary>
[Fact]
public void EditForm_WithEwsTransport_HidesPortTlsAndAuthTypeAndForcesBasicAuth()
{
var repo = Substitute.For<INotificationRepository>();
var cut = RenderWith(repo, OAuth2Sample());
ClickEdit(cut);
SelectWithOption(cut, "Ews").Change("Ews");
// Port input, TLS select and Auth Type select are all gone under EWS.
Assert.Empty(cut.FindAll("input[type=number]"));
Assert.False(HasSelectWithOption(cut, "StartTLS"));
Assert.False(HasSelectWithOption(cut, "OAuth2"));
Assert.Contains("EWS URL", cut.Markup);
// 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.
cut.Find("input[type=text]").Change("https://mail.example.com/ews/exchange.asmx");
cut.Find("input[type=password]").Change(@"dom\user:pw");
ClickSave(cut);
cut.WaitForAssertion(() =>
repo.Received().UpdateSmtpConfigurationAsync(Arg.Is<SmtpConfiguration>(
c => c.Transport == "Ews"
&& c.AuthType == "Basic"
&& c.Host == "https://mail.example.com/ews/exchange.asmx")));
}
/// <summary>Verifies that switching the transport from EWS back to SMTP restores the auth type and shows the port and TLS fields again.</summary>
[Fact]
public void EditForm_SwitchingEwsBackToSmtp_RestoresAuthTypeAndReshowsPortAndTls()
{
var repo = Substitute.For<INotificationRepository>();
var cut = RenderWith(repo, OAuth2Sample());
ClickEdit(cut);
SelectWithOption(cut, "Ews").Change("Ews");
SelectWithOption(cut, "Ews").Change("Smtp");
// Port and TLS controls are back...
Assert.Single(cut.FindAll("input[type=number]"));
Assert.True(HasSelectWithOption(cut, "StartTLS"));
Assert.True(HasSelectWithOption(cut, "OAuth2"));
// ...and the round trip left the pre-switch values (OAuth2 / 587 / StartTLS) intact.
ClickSave(cut);
cut.WaitForAssertion(() =>
repo.Received().UpdateSmtpConfigurationAsync(Arg.Is<SmtpConfiguration>(
c => c.Transport == "Smtp"
&& c.AuthType == "OAuth2"
&& c.Port == 587
&& c.TlsMode == "StartTLS")));
}
/// <summary>Verifies that saving with the EWS transport and a non-HTTPS host shows a form error and does not persist.</summary>
[Fact]
public void Save_WithEwsTransportAndNonHttpsHost_ShowsFormErrorAndDoesNotPersist()
{
var repo = Substitute.For<INotificationRepository>();
var cut = RenderWith(repo, OAuth2Sample());
ClickEdit(cut);
SelectWithOption(cut, "Ews").Change("Ews");
cut.Find("input[type=text]").Change("http://mail.example.com/ews/exchange.asmx");
ClickSave(cut);
cut.WaitForAssertion(() => Assert.Contains("absolute https://", cut.Markup));
repo.DidNotReceive().UpdateSmtpConfigurationAsync(Arg.Any<SmtpConfiguration>());
repo.DidNotReceive().SaveChangesAsync();
}
/// <summary>Verifies that saving with the EWS transport and credentials without a colon shows a form error and does not persist.</summary>
[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();
}
/// <summary>Verifies that saving with the EWS transport and domain-qualified credentials persists.</summary>
[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();
});
}
/// <summary>Verifies that the read-only view shows the transport badge and hides the port and TLS for EWS rows.</summary>
[Fact]
public void ReadOnlyView_ShowsTransportBadgeAndHidesPortAndTlsForEwsRows()
{
var ewsCut = RenderWith(Substitute.For<INotificationRepository>(), EwsSample());
ewsCut.WaitForAssertion(() =>
{
Assert.Contains("<span class=\"badge bg-secondary\">Ews</span>", ewsCut.Markup);
Assert.Contains("EWS URL", ewsCut.Markup);
Assert.DoesNotContain("TLS Mode", ewsCut.Markup);
// The EWS URL is rendered bare — no ":port" suffix as the SMTP host row has.
Assert.DoesNotContain("exchange.asmx:587", ewsCut.Markup);
});
}
/// <summary>Verifies that a legacy row with a null transport shows the SMTP badge and keeps the port and TLS fields.</summary>
[Fact]
public void ReadOnlyView_LegacyRowWithNullTransport_ShowsSmtpBadgeAndKeepsPortAndTls()
{
var cut = RenderWith(Substitute.For<INotificationRepository>(), Sample());
cut.WaitForAssertion(() =>
{
Assert.Contains("<span class=\"badge bg-secondary\">Smtp</span>", cut.Markup);
Assert.Contains("smtp.example.com:587", cut.Markup);
Assert.Contains("TLS Mode", cut.Markup);
});
}
}