Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/AuditLogPageTrendTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

185 lines
8.7 KiB
C#

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 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);
/// <summary>Initializes a new instance of the <see cref="AuditLogPageTrendTests"/> class.</summary>
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();
Services.AddTestConfigurationMutations();
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>();
}
/// <summary>Verifies the case: trends Panel, with Series, renders Charts With Polyline.</summary>
[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>());
}
/// <summary>Verifies the case: trends Panel, long Range Windows, render And Refetch With Matching Span.</summary>
[Fact]
public void TrendsPanel_LongRangeWindows_RenderAndRefetchWithMatchingSpan()
{
// The 30d (720h) / 90d (2160h) buttons added for the hourly-rollup path
// must render alongside 24h/7d and, when clicked, re-fetch
// every series over the corresponding range — the query service then
// transparently routes the long windows to hourly rollups.
var ranges = new List<(DateTime From, DateTime To)>();
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(ci =>
{
ranges.Add((ci.ArgAt<DateTime>(4), ci.ArgAt<DateTime>(5)));
return Task.FromResult(ThreePoints());
});
var cut = RenderAuditLogPage(kpi);
// Both new window buttons render in the toggle group.
cut.WaitForAssertion(() =>
{
Assert.Contains(cut.FindAll("button"), b => b.TextContent.Trim() == "30d");
Assert.Contains(cut.FindAll("button"), b => b.TextContent.Trim() == "90d");
});
// Click 30d — every series re-fetches over a ~720h span.
cut.FindAll("button").First(b => b.TextContent.Trim() == "30d").Click();
cut.WaitForAssertion(() =>
Assert.Contains(ranges, r => Math.Abs((r.To - r.From).TotalHours - 720) < 0.5));
// Click 90d — every series re-fetches over a ~2160h span.
cut.FindAll("button").First(b => b.TextContent.Trim() == "90d").Click();
cut.WaitForAssertion(() =>
Assert.Contains(ranges, r => Math.Abs((r.To - r.From).TotalHours - 2160) < 0.5));
}
/// <summary>Verifies the case: trends Panel, when Query Service Throws, page Still Renders.</summary>
[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);
});
}
}