using System.Security.Claims; using Bunit; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using ZB.MOM.WW.ScadaBridge.CentralUI.Services; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Security; using NotificationListForm = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Notifications.NotificationListForm; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Pages; /// /// bUnit tests for the adapter-gated Type selector + per-type recipient input on the /// Notification List create/edit form (SMS Notifications, S7). /// public class NotificationListFormTests : BunitContext { private readonly INotificationRepository _repo = Substitute.For(); public NotificationListFormTests() { Services.AddSingleton(_repo); // The Type selector derives its options from INotificationChannelCatalog // (S7). Register a substitute exposing both channels so the selector renders // Email + SMS without pulling the real adapter graph into the test. var catalog = Substitute.For(); catalog.SupportedChannels.Returns( new[] { NotificationType.Email, NotificationType.Sms }); Services.AddSingleton(catalog); AddTestAuth(); } private void AddTestAuth() { var claims = new[] { new Claim(JwtTokenService.UsernameClaimType, "tester"), new Claim(JwtTokenService.RoleClaimType, "Designer"), }; var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")); Services.AddSingleton(new TestAuthStateProvider(user)); Services.AddAuthorizationCore(); } [Fact] public void TypeSelector_RendersRegisteredChannels() { var cut = Render(); cut.WaitForAssertion(() => { var optionTexts = cut.FindAll("select.form-select option") .Select(o => o.TextContent.Trim()) .ToList(); Assert.Contains("Email", optionTexts); Assert.Contains("SMS", optionTexts); }); } [Fact] public void TypeSelector_IsEnabled_OnCreate() { var cut = Render(); cut.WaitForAssertion(() => { var select = cut.Find("select.form-select"); Assert.False(select.HasAttribute("disabled")); }); } [Fact] public void SelectingEmail_ShowsEmailInput_NotPhone() { // Edit mode renders the recipient form (recipients only show once a list exists). _repo.GetNotificationListByIdAsync(1) .Returns(Task.FromResult(new NotificationList("Ops") { Id = 1, Type = NotificationType.Email })); _repo.GetRecipientsByListIdAsync(1) .Returns(Task.FromResult>(new List())); var cut = Render(p => p.Add(c => c.Id, 1)); cut.WaitForAssertion(() => { Assert.NotNull(cut.Find("input[type=email]")); Assert.Empty(cut.FindAll("input[type=tel]")); // The recipients table column header reflects the list type. Assert.Contains("Email", cut.FindAll("th").Select(t => t.TextContent.Trim())); }); } [Fact] public void SelectingSms_ShowsPhoneInput_NotEmail() { _repo.GetNotificationListByIdAsync(2) .Returns(Task.FromResult(new NotificationList("On-Call") { Id = 2, Type = NotificationType.Sms })); _repo.GetRecipientsByListIdAsync(2) .Returns(Task.FromResult>(new List())); var cut = Render(p => p.Add(c => c.Id, 2)); cut.WaitForAssertion(() => { Assert.NotNull(cut.Find("input[type=tel]")); Assert.Empty(cut.FindAll("input[type=email]")); Assert.Contains("Phone", cut.FindAll("th").Select(t => t.TextContent.Trim())); }); } [Fact] public void TypeSelector_IsDisabled_OnEdit() { _repo.GetNotificationListByIdAsync(3) .Returns(Task.FromResult(new NotificationList("Ops") { Id = 3, Type = NotificationType.Email })); _repo.GetRecipientsByListIdAsync(3) .Returns(Task.FromResult>(new List())); var cut = Render(p => p.Add(c => c.Id, 3)); cut.WaitForAssertion(() => { var select = cut.Find("select.form-select"); Assert.True(select.HasAttribute("disabled")); }); } [Fact] public void CreatingSmsList_PersistsTypeSms() { NotificationList? captured = null; _repo.AddNotificationListAsync(Arg.Do(l => captured = l)) .Returns(Task.CompletedTask); var cut = Render(); cut.Find("input[type=text]").Change("On-Call"); // Switch the Type selector to SMS, then save. cut.Find("select.form-select").Change(NotificationType.Sms.ToString()); cut.FindAll("button").First(b => b.TextContent.Trim() == "Save").Click(); cut.WaitForAssertion(() => { Assert.NotNull(captured); Assert.Equal(NotificationType.Sms, captured!.Type); Assert.Equal("On-Call", captured.Name); }); } [Fact] public void AddingSmsRecipient_CreatesForSmsRecipient_WithPhoneSet() { NotificationRecipient? captured = null; _repo.GetNotificationListByIdAsync(4) .Returns(Task.FromResult(new NotificationList("On-Call") { Id = 4, Type = NotificationType.Sms })); _repo.GetRecipientsByListIdAsync(4) .Returns(Task.FromResult>(new List())); _repo.AddRecipientAsync(Arg.Do(r => captured = r)) .Returns(Task.CompletedTask); var cut = Render(p => p.Add(c => c.Id, 4)); cut.WaitForState(() => cut.FindAll("input[type=tel]").Count > 0); // The first text input is the list Name; the recipient Name input is the last. cut.FindAll("input[type=text]").Last().Change("Jane"); cut.Find("input[type=tel]").Change("+15551234567"); cut.FindAll("button").First(b => b.TextContent.Trim() == "Add").Click(); cut.WaitForAssertion(() => { Assert.NotNull(captured); Assert.Equal("+15551234567", captured!.PhoneNumber); Assert.Null(captured.EmailAddress); Assert.Equal(4, captured.NotificationListId); }); } [Fact] public void AddingSmsRecipient_RejectsInvalidPhone() { _repo.GetNotificationListByIdAsync(5) .Returns(Task.FromResult(new NotificationList("On-Call") { Id = 5, Type = NotificationType.Sms })); _repo.GetRecipientsByListIdAsync(5) .Returns(Task.FromResult>(new List())); var cut = Render(p => p.Add(c => c.Id, 5)); cut.WaitForState(() => cut.FindAll("input[type=tel]").Count > 0); // The first text input is the list Name; the recipient Name input is the last. cut.FindAll("input[type=text]").Last().Change("Jane"); cut.Find("input[type=tel]").Change("not-a-number"); cut.FindAll("button").First(b => b.TextContent.Trim() == "Add").Click(); cut.WaitForAssertion(() => { Assert.Contains("valid phone number", cut.Markup); _repo.DidNotReceive().AddRecipientAsync(Arg.Any()); }); } }