Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/SmsConfigurationPageTests.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

217 lines
8.8 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 SmsConfigurationPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Notifications.SmsConfiguration;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Pages;
/// <summary>
/// bUnit rendering tests for the SMS Configuration page — verifies the read-only list,
/// that the stored Auth Token value is never rendered (only a presence indicator),
/// repository-direct save on create/edit, and the preserve-if-blank secret handling.
/// </summary>
public class SmsConfigurationPageTests : 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 const string SecretToken = "super-secret-auth-token";
private static SmsConfiguration Sample() =>
new("ACcccccccccccccccccccccccccccccccc", "+15551234567")
{
Id = 1,
MessagingServiceSid = "MGtest_messaging_service",
ApiBaseUrl = "https://api.example.com",
AuthToken = SecretToken,
ConnectionTimeoutSeconds = 30,
MaxRetries = 10,
RetryDelay = TimeSpan.FromMinutes(1),
};
private static INotificationRepository RepoWith(params SmsConfiguration[] configs)
{
var repo = Substitute.For<INotificationRepository>();
repo.GetAllSmsConfigurationsAsync()
.Returns(Task.FromResult<IReadOnlyList<SmsConfiguration>>(configs.ToList()));
return repo;
}
/// <summary>Verifies that the read-only view shows the config row but never renders the auth token value.</summary>
[Fact]
public void ReadOnlyView_ShowsConfigRow_ButNeverRendersAuthTokenValue()
{
var repo = RepoWith(Sample());
Services.AddSingleton(repo);
WireAuth();
var cut = Render<SmsConfigurationPage>();
cut.WaitForAssertion(() =>
{
// Config row fields render.
Assert.Contains("ACcccccccccccccccccccccccccccccccc", cut.Markup);
Assert.Contains("+15551234567", cut.Markup);
Assert.Contains("MGtest_messaging_service", cut.Markup);
// Auth Token shows a presence indicator only — never the value.
Assert.Contains("Auth Token", cut.Markup);
Assert.Contains("(stored)", cut.Markup);
Assert.DoesNotContain(SecretToken, cut.Markup);
});
}
/// <summary>Verifies that the edit form does not prefill the auth token but does prefill the messaging service SID.</summary>
[Fact]
public void EditForm_DoesNotPrefillAuthToken_ButPrefillsMessagingServiceSid()
{
var repo = RepoWith(Sample());
Services.AddSingleton(repo);
WireAuth();
var cut = Render<SmsConfigurationPage>();
cut.WaitForState(() => cut.Markup.Contains("ACcccccccccccccccccccccccccccccccc"));
cut.FindAll("button").First(b => b.TextContent.Contains("Edit")).Click();
cut.WaitForAssertion(() =>
{
// The secret is never placed into the form markup (no input pre-fill).
Assert.DoesNotContain(SecretToken, cut.Markup);
// Non-secret fields, including MessagingServiceSid, are pre-filled.
var inputs = cut.FindAll("input");
Assert.Contains(inputs, i => i.GetAttribute("value") == "MGtest_messaging_service");
});
}
/// <summary>Verifies that saving a new configuration calls Add and SaveChanges.</summary>
[Fact]
public void SavingNewConfig_CallsAddAndSaveChanges()
{
var repo = RepoWith();
Services.AddSingleton(repo);
WireAuth();
var cut = Render<SmsConfigurationPage>();
cut.WaitForState(() => cut.Markup.Contains("No SMS configuration set."));
cut.FindAll("button").First(b => b.TextContent.Contains("Add SMS configuration")).Click();
// Re-query between each Change(): two-way binding re-renders the form and
// invalidates previously found element references.
cut.FindAll("input[type=text]")[0].Change("ACdddddddddddddddddddddddddddddddd"); // Account SID
cut.FindAll("input[type=text]")[1].Change("+15559876543"); // From Number
cut.FindAll("input[type=password]").First().Change("new-token");
cut.FindAll("button").First(b => b.TextContent.Contains("Save")).Click();
cut.WaitForAssertion(() =>
{
repo.Received().AddSmsConfigurationAsync(
Arg.Is<SmsConfiguration>(c =>
c.AccountSid == "ACdddddddddddddddddddddddddddddddd" &&
c.FromNumber == "+15559876543" &&
c.AuthToken == "new-token"));
repo.Received().SaveChangesAsync();
});
}
/// <summary>Verifies that a new configuration with only a messaging service SID and no from number saves successfully.</summary>
[Fact]
public void SavingNewConfig_MessagingServiceSidOnly_NoFromNumber_Saves()
{
// A Twilio Messaging-Service-only config is valid with no From number.
// The either-or validation must accept it and persist a null FromNumber.
var repo = RepoWith();
Services.AddSingleton(repo);
WireAuth();
var cut = Render<SmsConfigurationPage>();
cut.WaitForState(() => cut.Markup.Contains("No SMS configuration set."));
cut.FindAll("button").First(b => b.TextContent.Contains("Add SMS configuration")).Click();
// Account SID + Messaging Service SID, leaving From Number (index 1) blank.
cut.FindAll("input[type=text]")[0].Change("ACeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); // Account SID
cut.FindAll("input[type=text]")[2].Change("MGmessaging_service"); // Messaging Service SID
cut.FindAll("input[type=password]").First().Change("new-token");
cut.FindAll("button").First(b => b.TextContent.Contains("Save")).Click();
cut.WaitForAssertion(() =>
{
repo.Received().AddSmsConfigurationAsync(
Arg.Is<SmsConfiguration>(c =>
c.AccountSid == "ACeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" &&
c.FromNumber == null &&
c.MessagingServiceSid == "MGmessaging_service" &&
c.AuthToken == "new-token"));
repo.Received().SaveChangesAsync();
});
}
/// <summary>Verifies that saving an edit with a blank auth token preserves the existing token.</summary>
[Fact]
public void SavingEdit_WithBlankAuthToken_PreservesExistingToken()
{
var config = Sample();
var repo = RepoWith(config);
Services.AddSingleton(repo);
WireAuth();
var cut = Render<SmsConfigurationPage>();
cut.WaitForState(() => cut.Markup.Contains("ACcccccccccccccccccccccccccccccccc"));
cut.FindAll("button").First(b => b.TextContent.Contains("Edit")).Click();
// Leave the (blank) Auth Token input untouched, then save.
cut.FindAll("button").First(b => b.TextContent.Contains("Save")).Click();
cut.WaitForAssertion(() =>
{
repo.Received().UpdateSmsConfigurationAsync(
Arg.Is<SmsConfiguration>(c =>
c.AuthToken == SecretToken &&
c.MessagingServiceSid == "MGtest_messaging_service"));
repo.Received().SaveChangesAsync();
});
}
/// <summary>Verifies that saving an edit with a new auth token overwrites the token.</summary>
[Fact]
public void SavingEdit_WithNewAuthToken_OverwritesToken()
{
var config = Sample();
var repo = RepoWith(config);
Services.AddSingleton(repo);
WireAuth();
var cut = Render<SmsConfigurationPage>();
cut.WaitForState(() => cut.Markup.Contains("ACcccccccccccccccccccccccccccccccc"));
cut.FindAll("button").First(b => b.TextContent.Contains("Edit")).Click();
cut.FindAll("input[type=password]").First().Change("rotated-token");
cut.FindAll("button").First(b => b.TextContent.Contains("Save")).Click();
cut.WaitForAssertion(() =>
{
repo.Received().UpdateSmsConfigurationAsync(
Arg.Is<SmsConfiguration>(c => c.AuthToken == "rotated-token"));
repo.Received().SaveChangesAsync();
});
}
}