feat(kpi): K15 — Audit Log trend charts
This commit is contained in:
@@ -71,6 +71,9 @@ public class AuditLogPagePermissionTests : BunitContext
|
||||
// a permitted render is exercised end-to-end.
|
||||
Services.AddSingleton(Substitute.For<ISiteRepository>());
|
||||
Services.AddSingleton(Substitute.For<IAuditLogQueryService>());
|
||||
// M6 (K15): the page now injects IKpiHistoryQueryService for the Trends
|
||||
// panel — register a stand-in so a permitted render is exercised end-to-end.
|
||||
Services.AddSingleton(Substitute.For<IKpiHistoryQueryService>());
|
||||
}
|
||||
|
||||
private IRenderedComponent<AuditLogPage> RenderAuditLogPage(params string[] roles)
|
||||
|
||||
@@ -62,6 +62,9 @@ public class AuditLogPageScaffoldTests : BunitContext
|
||||
// Provide stand-ins so the scaffold smoke tests still render the page.
|
||||
Services.AddSingleton(Substitute.For<ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories.ISiteRepository>());
|
||||
Services.AddSingleton(_queryService);
|
||||
// M6 (K15): the page now injects IKpiHistoryQueryService for the Trends
|
||||
// panel. Provide a stand-in so the scaffold/drill-in renders still work.
|
||||
Services.AddSingleton(Substitute.For<IKpiHistoryQueryService>());
|
||||
|
||||
if (!string.IsNullOrEmpty(query))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the M6 (K15) Trends panel added to the Audit Log page — one
|
||||
/// <c>KpiTrendChart</c> per AuditLog global metric, over a 24h/7d window. The
|
||||
/// panel is best-effort: a failing <see cref="IKpiHistoryQueryService"/> must
|
||||
/// degrade the affected charts without breaking the audit query UI (the filter
|
||||
/// bar + results grid that share the page).
|
||||
///
|
||||
/// <para>
|
||||
/// bUnit hosts the page directly, so the test registers every service the page
|
||||
/// and its child components inject: <see cref="AuthenticationStateProvider"/> +
|
||||
/// the real authorization policies (the page carries
|
||||
/// <c>[Authorize(OperationalAudit)]</c> and an in-page <c>AuthorizeView</c> for
|
||||
/// the Export button), <see cref="ISiteRepository"/> (AuditFilterBar),
|
||||
/// <see cref="IAuditLogQueryService"/> (AuditResultsGrid), and the new
|
||||
/// <see cref="IKpiHistoryQueryService"/> (the Trends panel). The page is wrapped
|
||||
/// in a <see cref="CascadingAuthenticationState"/> the router would supply in
|
||||
/// production.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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<Claim> { new(JwtTokenService.UsernameClaimType, "tester") };
|
||||
claims.AddRange(roles.Select(r => new Claim(JwtTokenService.RoleClaimType, r)));
|
||||
return new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<KpiSeriesPoint> ThreePoints() => new[]
|
||||
{
|
||||
new KpiSeriesPoint(Base, 1.0),
|
||||
new KpiSeriesPoint(Base.AddMinutes(20), 4.0),
|
||||
new KpiSeriesPoint(Base.AddMinutes(40), 2.0),
|
||||
};
|
||||
|
||||
private IRenderedComponent<AuditLogPage> RenderAuditLogPage(IKpiHistoryQueryService kpiHistory)
|
||||
{
|
||||
var user = BuildPrincipal("Administrator");
|
||||
Services.AddSingleton<AuthenticationStateProvider>(new TestAuthStateProvider(user));
|
||||
Services.AddAuthorizationCore();
|
||||
AuthorizationPolicies.AddScadaBridgeAuthorization(Services);
|
||||
Services.AddSingleton<IAuthorizationService, DefaultAuthorizationService>();
|
||||
|
||||
// The page hosts AuditFilterBar + AuditResultsGrid (Bundle B) and the
|
||||
// K15 Trends panel — register a stand-in for each injected dependency.
|
||||
Services.AddSingleton(Substitute.For<ISiteRepository>());
|
||||
Services.AddSingleton(Substitute.For<IAuditLogQueryService>());
|
||||
Services.AddSingleton(kpiHistory);
|
||||
|
||||
var host = Render<CascadingAuthenticationState>(parameters => parameters
|
||||
.Add(p => p.ChildContent, (RenderFragment)(builder =>
|
||||
{
|
||||
builder.OpenComponent<AuditLogPage>(0);
|
||||
builder.CloseComponent();
|
||||
})));
|
||||
|
||||
return host.FindComponent<AuditLogPage>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrendsPanel_WithSeries_RendersChartsWithPolyline()
|
||||
{
|
||||
var kpi = Substitute.For<IKpiHistoryQueryService>();
|
||||
kpi.GetSeriesAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
|
||||
Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.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("<polyline", cut.Markup);
|
||||
// The audit query UI still rendered alongside the trends panel.
|
||||
Assert.Contains("Audit Log", cut.Markup);
|
||||
});
|
||||
|
||||
// All three AuditLog global metrics were queried (one chart each).
|
||||
kpi.Received().GetSeriesAsync(
|
||||
KpiSources.AuditLog, "totalEventsLastHour", KpiScopes.Global, null,
|
||||
Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>());
|
||||
kpi.Received().GetSeriesAsync(
|
||||
KpiSources.AuditLog, "errorEventsLastHour", KpiScopes.Global, null,
|
||||
Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>());
|
||||
kpi.Received().GetSeriesAsync(
|
||||
KpiSources.AuditLog, "backlogTotal", KpiScopes.Global, null,
|
||||
Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[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<IKpiHistoryQueryService>();
|
||||
kpi.GetSeriesAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
|
||||
Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
|
||||
.Returns<Task<IReadOnlyList<KpiSeriesPoint>>>(_ => 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user