feat(sms): NotificationListForm adapter-gated Type selector + per-type recipients (S7)
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// bUnit tests for the adapter-gated Type selector + per-type recipient input on the
|
||||
/// Notification List create/edit form (SMS Notifications, S7).
|
||||
/// </summary>
|
||||
public class NotificationListFormTests : BunitContext
|
||||
{
|
||||
private readonly INotificationRepository _repo = Substitute.For<INotificationRepository>();
|
||||
|
||||
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<INotificationChannelCatalog>();
|
||||
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<AuthenticationStateProvider>(new TestAuthStateProvider(user));
|
||||
Services.AddAuthorizationCore();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeSelector_RendersRegisteredChannels()
|
||||
{
|
||||
var cut = Render<NotificationListForm>();
|
||||
|
||||
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<NotificationListForm>();
|
||||
|
||||
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<NotificationList?>(new NotificationList("Ops") { Id = 1, Type = NotificationType.Email }));
|
||||
_repo.GetRecipientsByListIdAsync(1)
|
||||
.Returns(Task.FromResult<IReadOnlyList<NotificationRecipient>>(new List<NotificationRecipient>()));
|
||||
|
||||
var cut = Render<NotificationListForm>(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<NotificationList?>(new NotificationList("On-Call") { Id = 2, Type = NotificationType.Sms }));
|
||||
_repo.GetRecipientsByListIdAsync(2)
|
||||
.Returns(Task.FromResult<IReadOnlyList<NotificationRecipient>>(new List<NotificationRecipient>()));
|
||||
|
||||
var cut = Render<NotificationListForm>(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<NotificationList?>(new NotificationList("Ops") { Id = 3, Type = NotificationType.Email }));
|
||||
_repo.GetRecipientsByListIdAsync(3)
|
||||
.Returns(Task.FromResult<IReadOnlyList<NotificationRecipient>>(new List<NotificationRecipient>()));
|
||||
|
||||
var cut = Render<NotificationListForm>(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<NotificationList>(l => captured = l))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var cut = Render<NotificationListForm>();
|
||||
|
||||
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<NotificationList?>(new NotificationList("On-Call") { Id = 4, Type = NotificationType.Sms }));
|
||||
_repo.GetRecipientsByListIdAsync(4)
|
||||
.Returns(Task.FromResult<IReadOnlyList<NotificationRecipient>>(new List<NotificationRecipient>()));
|
||||
_repo.AddRecipientAsync(Arg.Do<NotificationRecipient>(r => captured = r))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var cut = Render<NotificationListForm>(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<NotificationList?>(new NotificationList("On-Call") { Id = 5, Type = NotificationType.Sms }));
|
||||
_repo.GetRecipientsByListIdAsync(5)
|
||||
.Returns(Task.FromResult<IReadOnlyList<NotificationRecipient>>(new List<NotificationRecipient>()));
|
||||
|
||||
var cut = Render<NotificationListForm>(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<NotificationRecipient>());
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user