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