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 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(); }); } }