using System.Security.Claims; using Bunit; using Bunit.TestDoubles; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using ZB.MOM.WW.ScadaBridge.CentralUI.Services; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi; using ZB.MOM.WW.ScadaBridge.Security; using AuditLogPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Audit.AuditLogPage; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Pages; /// /// Tests for the M6 (K15) Trends panel added to the Audit Log page — one /// KpiTrendChart per AuditLog global metric, over a 24h/7d window. The /// panel is best-effort: a failing must /// degrade the affected charts without breaking the audit query UI (the filter /// bar + results grid that share the page). /// /// /// bUnit hosts the page directly, so the test registers every service the page /// and its child components inject: + /// the real authorization policies (the page carries /// [Authorize(OperationalAudit)] and an in-page AuthorizeView for /// the Export button), (AuditFilterBar), /// (AuditResultsGrid), and the new /// (the Trends panel). The page is wrapped /// in a the router would supply in /// production. /// /// public class AuditLogPageTrendTests : BunitContext { private static readonly DateTime Base = new(2026, 6, 15, 10, 0, 0, DateTimeKind.Utc); public AuditLogPageTrendTests() { // AuditResultsGrid's OnAfterRenderAsync wires its column resize/reorder UX // through audit-grid.js. Loose mode lets those unconfigured JS calls no-op // so these trend tests need not configure browser interop. JSInterop.Mode = JSRuntimeMode.Loose; } private static ClaimsPrincipal BuildPrincipal(params string[] roles) { var claims = new List { new(JwtTokenService.UsernameClaimType, "tester") }; claims.AddRange(roles.Select(r => new Claim(JwtTokenService.RoleClaimType, r))); return new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")); } private static IReadOnlyList ThreePoints() => new[] { new KpiSeriesPoint(Base, 1.0), new KpiSeriesPoint(Base.AddMinutes(20), 4.0), new KpiSeriesPoint(Base.AddMinutes(40), 2.0), }; private IRenderedComponent RenderAuditLogPage(IKpiHistoryQueryService kpiHistory) { var user = BuildPrincipal("Administrator"); Services.AddSingleton(new TestAuthStateProvider(user)); Services.AddAuthorizationCore(); AuthorizationPolicies.AddScadaBridgeAuthorization(Services); Services.AddSingleton(); // The page hosts AuditFilterBar + AuditResultsGrid (Bundle B) and the // K15 Trends panel — register a stand-in for each injected dependency. Services.AddSingleton(Substitute.For()); Services.AddSingleton(Substitute.For()); Services.AddSingleton(kpiHistory); var host = Render(parameters => parameters .Add(p => p.ChildContent, (RenderFragment)(builder => { builder.OpenComponent(0); builder.CloseComponent(); }))); return host.FindComponent(); } [Fact] public void TrendsPanel_WithSeries_RendersChartsWithPolyline() { var kpi = Substitute.For(); kpi.GetSeriesAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(ThreePoints())); var cut = RenderAuditLogPage(kpi); cut.WaitForAssertion(() => { // The panel container is present... Assert.Contains("data-test=\"audit-trends\"", cut.Markup); // ...and at least one chart rendered an actual polyline series. Assert.Contains("kpi-trend-", cut.Markup); Assert.Contains("(), Arg.Any(), Arg.Any(), Arg.Any()); kpi.Received().GetSeriesAsync( KpiSources.AuditLog, "errorEventsLastHour", KpiScopes.Global, null, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); kpi.Received().GetSeriesAsync( KpiSources.AuditLog, "backlogTotal", KpiScopes.Global, null, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] public void TrendsPanel_WhenQueryServiceThrows_PageStillRenders() { // Best-effort contract: a failing KPI-history query degrades the charts to // the unavailable placeholder but must NOT throw out of the page render. var kpi = Substitute.For(); kpi.GetSeriesAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns>>(_ => throw new InvalidOperationException("kpi down")); var cut = RenderAuditLogPage(kpi); cut.WaitForAssertion(() => { // The page rendered — heading + trends panel container both present — // even though every series fetch threw. Assert.Contains("Audit Log", cut.Markup); Assert.Contains("data-test=\"audit-trends\"", cut.Markup); }); } }