using System.Security.Claims; using Akka.Actor; using Bunit; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ScadaLink.CentralUI.Components.Shared; using NSubstitute; using ScadaLink.Commons.Entities.Sites; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Messages.Notification; using ScadaLink.Communication; using NotificationReportPage = ScadaLink.CentralUI.Components.Pages.Notifications.NotificationReport; namespace ScadaLink.CentralUI.Tests.Pages; /// /// bUnit tests for the Notification Report row-detail modal — double-clicking a /// notification row opens a Bootstrap modal showing that notification's full, /// untruncated details. /// /// Mirrors 's seam: the report's /// calls route through an injected scripted /// actor (the notification-outbox proxy). /// public class NotificationReportDetailModalTests : BunitContext { private readonly ActorSystem _system = ActorSystem.Create("notif-report-modal-tests"); private readonly CommunicationService _comms; private NotificationOutboxQueryResponse _queryReply = new("q", true, null, new List { new("notif-aaaaaaaa-1111-full-id", "Email", "Ops On-Call", "Pump fault at Plant-A", "Parked", RetryCount: 3, LastError: "SMTP timeout connecting to mail relay", SourceSiteId: "plant-a", SourceInstanceId: "Pump-001", CreatedAt: DateTimeOffset.UtcNow.AddMinutes(-30), DeliveredAt: null, IsStuck: true), new("notif-bbbbbbbb-2222-full-id", "Email", "Maintenance", "Daily summary", "Delivered", RetryCount: 0, LastError: null, SourceSiteId: "plant-b", SourceInstanceId: null, CreatedAt: DateTimeOffset.UtcNow.AddHours(-2), DeliveredAt: DateTimeOffset.UtcNow.AddHours(-2), IsStuck: false), }, TotalCount: 2); public NotificationReportDetailModalTests() { _comms = new CommunicationService( Options.Create(new CommunicationOptions()), NullLogger.Instance); var outbox = _system.ActorOf(Props.Create(() => new ScriptedOutboxActor(this))); _comms.SetNotificationOutbox(outbox); Services.AddSingleton(_comms); Services.AddSingleton(new AlwaysConfirmDialogService()); var siteRepo = Substitute.For(); siteRepo.GetAllSitesAsync(Arg.Any()) .Returns(Task.FromResult>(new List { new("Plant A", "plant-a") { Id = 1 }, new("Plant B", "plant-b") { Id = 2 }, })); Services.AddSingleton(siteRepo); var claims = new[] { new Claim("Username", "tester"), new Claim(ClaimTypes.Role, "Deployment"), }; var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")); Services.AddSingleton(new TestAuthStateProvider(user)); Services.AddAuthorizationCore(); } [Fact] public void DoubleClickRow_OpensDetailModal() { var cut = Render(); cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A")); // No modal initially. Assert.Empty(cut.FindAll(".modal.show")); var row = cut.FindAll("tbody tr") .First(r => r.TextContent.Contains("Pump fault at Plant-A")); row.DoubleClick(); cut.WaitForAssertion(() => { var modal = cut.Find(".modal.show"); Assert.Contains("Pump fault at Plant-A", modal.TextContent); Assert.Contains("Ops On-Call", modal.TextContent); }); } [Fact] public void Modal_ShowsFullNotificationId_NotTruncated() { var cut = Render(); cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A")); var row = cut.FindAll("tbody tr") .First(r => r.TextContent.Contains("Pump fault at Plant-A")); row.DoubleClick(); cut.WaitForAssertion(() => { var modal = cut.Find(".modal.show"); // The grid renders ShortId(...) (first 12 chars); the modal must show // the complete identifier. Assert.Contains("notif-aaaaaaaa-1111-full-id", modal.TextContent); }); } [Fact] public void CloseButton_DismissesModal() { var cut = Render(); cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A")); var row = cut.FindAll("tbody tr") .First(r => r.TextContent.Contains("Pump fault at Plant-A")); row.DoubleClick(); cut.WaitForState(() => cut.FindAll(".modal.show").Count > 0); var closeButton = cut.Find(".modal.show .modal-footer button"); closeButton.Click(); cut.WaitForAssertion(() => Assert.Empty(cut.FindAll(".modal.show"))); } [Fact] public void Modal_ShowsLastError_WhenPresent() { var cut = Render(); cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A")); var row = cut.FindAll("tbody tr") .First(r => r.TextContent.Contains("Pump fault at Plant-A")); row.DoubleClick(); cut.WaitForAssertion(() => { var modal = cut.Find(".modal.show"); Assert.Contains("SMTP timeout connecting to mail relay", modal.TextContent); }); } protected override void Dispose(bool disposing) { if (disposing) { _system.Terminate().Wait(TimeSpan.FromSeconds(5)); } base.Dispose(disposing); } private sealed class ScriptedOutboxActor : ReceiveActor { public ScriptedOutboxActor(NotificationReportDetailModalTests test) { Receive(_ => Sender.Tell(test._queryReply)); Receive(r => Sender.Tell(new RetryNotificationResponse(r.CorrelationId, true, null))); Receive(r => Sender.Tell(new DiscardNotificationResponse(r.CorrelationId, true, null))); } } private sealed class AlwaysConfirmDialogService : IDialogService { public Task ConfirmAsync(string title, string message, bool danger = false) => Task.FromResult(true); public Task PromptAsync( string title, string label, string initialValue = "", string? placeholder = null) => Task.FromResult(null); } }