using System.Security.Claims;
using Akka.Actor;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ScadaLink.Commons.Entities.Sites;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Messages.Notification;
using ScadaLink.Commons.Types.Notifications;
using ScadaLink.Communication;
using ScadaLink.Security;
using NotificationKpisPage = ScadaLink.CentralUI.Components.Pages.Notifications.NotificationKpis;
namespace ScadaLink.CentralUI.Tests.Pages;
///
/// bUnit rendering tests for the Notification KPIs page.
///
/// Testability note: is a concrete class with
/// non-virtual methods, so NSubstitute cannot intercept it. Both the global and
/// per-site KPI calls route through an injected (the
/// notification-outbox proxy), so the tests wire a real, lightweight
/// with a scripted that
/// answers both and
/// — the same seam
/// SetNotificationOutbox exists for.
///
public class NotificationKpisPageTests : BunitContext
{
private readonly ActorSystem _system = ActorSystem.Create("notif-kpis-tests");
private readonly CommunicationService _comms;
// Mutable scripted replies — individual tests can override before rendering.
private NotificationKpiResponse _kpiReply =
new("k", true, null, QueueDepth: 7, StuckCount: 2, ParkedCount: 1,
DeliveredLastInterval: 42, OldestPendingAge: TimeSpan.FromMinutes(9));
private PerSiteNotificationKpiResponse _perSiteReply =
new("p", true, null, new List
{
new("plant-a", QueueDepth: 4, StuckCount: 1, ParkedCount: 0,
DeliveredLastInterval: 9, OldestPendingAge: TimeSpan.FromMinutes(7)),
});
public NotificationKpisPageTests()
{
_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 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 Page_RequiresDeploymentPolicy()
{
var attr = typeof(NotificationKpisPage)
.GetCustomAttributes(typeof(AuthorizeAttribute), true)
.Cast()
.FirstOrDefault();
Assert.NotNull(attr);
Assert.Equal(AuthorizationPolicies.RequireDeployment, attr!.Policy);
}
[Fact]
public void RendersGlobalTilesAndPerSiteRows()
{
var cut = Render();
cut.WaitForAssertion(() =>
{
Assert.Contains("Queue Depth", cut.Markup);
Assert.Contains("7", cut.Markup); // global tile value
// Per-site row — site identifier "plant-a" resolves to its friendly name.
Assert.Contains("Plant A", cut.Markup);
});
}
[Fact]
public void ShowsKpiError_WhenGlobalKpiQueryFails()
{
_kpiReply = new NotificationKpiResponse(
"k", false, "kpi down", 0, 0, 0, 0, null);
var cut = Render();
cut.WaitForAssertion(() => Assert.Contains("kpi down", cut.Markup));
}
[Fact]
public void ShowsPerSiteError_WhenPerSiteKpiQueryFails()
{
// Only the per-site path errors — the global KPI reply stays successful.
_perSiteReply = new PerSiteNotificationKpiResponse(
"p", false, "per-site down", new List());
var cut = Render();
cut.WaitForAssertion(() =>
{
Assert.Contains("Per-site KPIs unavailable: per-site down", cut.Markup);
// The two error paths are isolated — the global KPI alert (whose markup
// opens ">KPIs unavailable:", without the "Per-site " prefix) must not appear.
Assert.DoesNotContain(">KPIs unavailable:", cut.Markup);
});
}
[Fact]
public void ShowsPerSiteEmptyState_WhenNoSites()
{
_perSiteReply = new PerSiteNotificationKpiResponse(
"p", true, null, new List());
var cut = Render();
cut.WaitForAssertion(() => Assert.Contains("No per-site activity", 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 each KPI message
/// type with the test's currently-scripted response.
///
private sealed class ScriptedOutboxActor : ReceiveActor
{
public ScriptedOutboxActor(NotificationKpisPageTests test)
{
Receive(_ => Sender.Tell(test._kpiReply));
Receive(_ => Sender.Tell(test._perSiteReply));
}
}
}