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 NSubstitute; using ScadaLink.CentralUI.Services; using ScadaLink.Commons.Entities.Sites; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Messages.Notification; using ScadaLink.Commons.Types; using ScadaLink.Communication; using ScadaLink.HealthMonitoring; using HealthPage = ScadaLink.CentralUI.Components.Pages.Monitoring.Health; namespace ScadaLink.CentralUI.Tests.Pages; /// /// bUnit rendering tests for the Health Monitoring dashboard (Task 24). /// /// Scope: the Notification Outbox KPI tile row added to the Health dashboard. /// is an interface (mockable), but /// is a concrete class whose outbox calls /// route through an injected notification-outbox ; the /// tests reuse the scripted-actor seam established by the Notification Report /// page tests (see NotificationReportPageTests). /// public class HealthPageTests : BunitContext { private readonly ActorSystem _system = ActorSystem.Create("health-page-tests"); private readonly CommunicationService _comms; // Mutable scripted reply — individual tests can override before rendering. private NotificationKpiResponse _kpiReply = new("k", true, null, QueueDepth: 12, StuckCount: 4, ParkedCount: 3, DeliveredLastInterval: 88, OldestPendingAge: TimeSpan.FromMinutes(6)); public HealthPageTests() { _comms = new CommunicationService( Options.Create(new CommunicationOptions()), NullLogger.Instance); var outbox = _system.ActorOf(Props.Create(() => new ScriptedOutboxActor(this))); _comms.SetNotificationOutbox(outbox); Services.AddSingleton(_comms); var aggregator = Substitute.For(); aggregator.GetAllSiteStates() .Returns(new Dictionary()); Services.AddSingleton(aggregator); var siteRepo = Substitute.For(); siteRepo.GetAllSitesAsync(Arg.Any()) .Returns(Task.FromResult>(new List())); Services.AddSingleton(siteRepo); // Audit Log (#23) M7 Bundle E — the Health page now also fetches the // Audit KPI snapshot. Stub it with an empty point-in-time reading so // the existing assertions (Notification Outbox tiles, Online/Offline // counts) keep passing; tests that target the Audit tiles set their // own substitute. var auditService = Substitute.For(); auditService.GetKpiSnapshotAsync(Arg.Any()) .Returns(Task.FromResult(new AuditLogKpiSnapshot(0, 0, 0, DateTime.UtcNow))); Services.AddSingleton(auditService); var claims = new[] { new Claim("Username", "tester"), new Claim(ClaimTypes.Role, "Admin"), }; var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")); Services.AddSingleton(new TestAuthStateProvider(user)); Services.AddAuthorizationCore(); } [Fact] public void Renders_OutboxKpiTiles_WithValues() { var cut = Render(); // KPI data arrives via an async actor Ask after first render. cut.WaitForAssertion(() => { Assert.Contains("Notification Outbox", cut.Markup); Assert.Contains("Queue Depth", cut.Markup); Assert.Contains("Stuck", cut.Markup); Assert.Contains("Parked", cut.Markup); // KPI numeric values surface in the tiles. Assert.Contains(">12<", cut.Markup); // QueueDepth Assert.Contains(">4<", cut.Markup); // StuckCount Assert.Contains(">3<", cut.Markup); // ParkedCount }); } [Fact] public void RendersLinkToTheNotificationKpisPage() { var cut = Render(); var link = cut.Find("a[href='/notifications/kpis']"); Assert.Contains("View details", link.TextContent); } [Fact] public void Renders_AuditKpiTiles_WithValues() { // Override the default empty snapshot — this test wants concrete values // to land in the three Audit tiles. var auditService = Substitute.For(); auditService.GetKpiSnapshotAsync(Arg.Any()) .Returns(Task.FromResult(new AuditLogKpiSnapshot( TotalEventsLastHour: 250, ErrorEventsLastHour: 5, BacklogTotal: 17, AsOfUtc: DateTime.UtcNow))); Services.AddSingleton(auditService); var cut = Render(); cut.WaitForAssertion(() => { // The three audit tiles render at the documented data-test selectors. Assert.Contains("data-test=\"audit-kpi-volume\"", cut.Markup); Assert.Contains("data-test=\"audit-kpi-error-rate\"", cut.Markup); Assert.Contains("data-test=\"audit-kpi-backlog\"", cut.Markup); // Volume shows the formatted thousand-separator value. Assert.Contains("250", cut.Markup); // Backlog renders 17. Assert.Contains("17", cut.Markup); }); } [Fact] public void OutboxKpiFailure_ShowsGracefulFallback() { _kpiReply = new NotificationKpiResponse( "k", false, "outbox repository unavailable", 0, 0, 0, 0, null); var cut = Render(); cut.WaitForAssertion(() => { // Failure must not crash the page; tiles fall back to a dash. Assert.Contains("Notification Outbox", cut.Markup); Assert.Contains("Queue Depth", cut.Markup); Assert.Contains(">—<", cut.Markup); }); } protected override void Dispose(bool disposing) { if (disposing) { _system.Terminate().Wait(TimeSpan.FromSeconds(5)); } base.Dispose(disposing); } /// /// Stand-in for the notification-outbox actor. Replies to the KPI request /// with the test's currently-scripted response. /// private sealed class ScriptedOutboxActor : ReceiveActor { public ScriptedOutboxActor(HealthPageTests test) { Receive(_ => Sender.Tell(test._kpiReply)); } } }