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;
///
/// 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.
///
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(new TestAuthStateProvider(user));
Services.AddAuthorizationCore();
}
private const string SecretToken = "super-secret-auth-token";
private static SmsConfiguration Sample() =>
new("ACtest_account_sid", "+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();
repo.GetAllSmsConfigurationsAsync()
.Returns(Task.FromResult>(configs.ToList()));
return repo;
}
[Fact]
public void ReadOnlyView_ShowsConfigRow_ButNeverRendersAuthTokenValue()
{
var repo = RepoWith(Sample());
Services.AddSingleton(repo);
WireAuth();
var cut = Render();
cut.WaitForAssertion(() =>
{
// Config row fields render.
Assert.Contains("ACtest_account_sid", 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);
});
}
[Fact]
public void EditForm_DoesNotPrefillAuthToken_ButPrefillsMessagingServiceSid()
{
var repo = RepoWith(Sample());
Services.AddSingleton(repo);
WireAuth();
var cut = Render();
cut.WaitForState(() => cut.Markup.Contains("ACtest_account_sid"));
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");
});
}
[Fact]
public void SavingNewConfig_CallsAddAndSaveChanges()
{
var repo = RepoWith();
Services.AddSingleton(repo);
WireAuth();
var cut = Render();
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("ACnew_account"); // 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(c =>
c.AccountSid == "ACnew_account" &&
c.FromNumber == "+15559876543" &&
c.AuthToken == "new-token"));
repo.Received().SaveChangesAsync();
});
}
[Fact]
public void SavingNewConfig_MessagingServiceSidOnly_NoFromNumber_Saves()
{
// UI-Med-2: 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();
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("ACmsg_account"); // 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(c =>
c.AccountSid == "ACmsg_account" &&
c.FromNumber == null &&
c.MessagingServiceSid == "MGmessaging_service" &&
c.AuthToken == "new-token"));
repo.Received().SaveChangesAsync();
});
}
[Fact]
public void SavingEdit_WithBlankAuthToken_PreservesExistingToken()
{
var config = Sample();
var repo = RepoWith(config);
Services.AddSingleton(repo);
WireAuth();
var cut = Render();
cut.WaitForState(() => cut.Markup.Contains("ACtest_account_sid"));
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(c =>
c.AuthToken == SecretToken &&
c.MessagingServiceSid == "MGtest_messaging_service"));
repo.Received().SaveChangesAsync();
});
}
[Fact]
public void SavingEdit_WithNewAuthToken_OverwritesToken()
{
var config = Sample();
var repo = RepoWith(config);
Services.AddSingleton(repo);
WireAuth();
var cut = Render();
cut.WaitForState(() => cut.Markup.Contains("ACtest_account_sid"));
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(c => c.AuthToken == "rotated-token"));
repo.Received().SaveChangesAsync();
});
}
}