513fa1aa16
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.
129 lines
5.8 KiB
C#
129 lines
5.8 KiB
C#
using System.Reflection;
|
|
using System.Security.Claims;
|
|
using Bunit;
|
|
using Microsoft.AspNetCore.Components.Authorization;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.Communication;
|
|
using AuditLogPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Audit.ConfigurationAuditLog;
|
|
using ParkedMessagesPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Monitoring.ParkedMessages;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Pages;
|
|
|
|
/// <summary>
|
|
/// The detail modal/drawer on these surfaces is gated on a held id — user intent —
|
|
/// rather than on the row still resolving, so that a data refresh can never unmount
|
|
/// the subtree mid-render and dispose its event handlers (see
|
|
/// NotificationReportDetailModalTests.Modal_StaysOpen_WhenItsRowLeavesThePage).
|
|
///
|
|
/// That makes it the surface's own job to clear the id on navigation actions that
|
|
/// invalidate page-scoped state, and the criterion is per-surface:
|
|
///
|
|
/// • ParkedMessages and ConfigurationAuditLog have NO keyed detail fetch — their
|
|
/// modal content resolves from the loaded page alone. An entry paged out of view
|
|
/// can never resolve again, so the surface would sit on a permanently empty
|
|
/// notice recoverable only by paging back. These must clear on paging.
|
|
///
|
|
/// • NotificationReport and SiteCallsReport DO fetch detail by id. Their modal
|
|
/// still shows real content after the row leaves the page, so they deliberately
|
|
/// do NOT clear on paging, and are not covered here.
|
|
///
|
|
/// Clearing on an explicit navigation action is intent, not a resolve-driven
|
|
/// unmount, so it does not reopen the handler-disposal race.
|
|
/// </summary>
|
|
public class PageScopedDetailStateTests : BunitContext
|
|
{
|
|
private static void SetPrivate(object target, string field, object? value) =>
|
|
target.GetType()
|
|
.GetField(field, BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.SetValue(target, value);
|
|
|
|
private static object? GetPrivate(object target, string field) =>
|
|
target.GetType()
|
|
.GetField(field, BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.GetValue(target);
|
|
|
|
private static Task InvokePrivate(object target, string method, params object[] args) =>
|
|
(Task)target.GetType()
|
|
.GetMethod(method, BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.Invoke(target, args)!;
|
|
|
|
private void RegisterCommonServices()
|
|
{
|
|
var identity = new ClaimsIdentity(
|
|
new[] { new Claim(ClaimTypes.Name, "deployer") }, "TestCookie");
|
|
var stubAuth = new StubAuthStateProvider(
|
|
new AuthenticationState(new ClaimsPrincipal(identity)));
|
|
Services.AddSingleton<AuthenticationStateProvider>(stubAuth);
|
|
Services.AddScoped(_ => new SiteScopeService(stubAuth));
|
|
Services.AddScoped<IDialogService, DialogService>();
|
|
JSInterop.Mode = JSRuntimeMode.Loose;
|
|
}
|
|
|
|
/// <summary>Verifies that paging the Parked Messages list closes the open detail drawer.</summary>
|
|
/// <param name="pagingMethod">The name of the paging method to invoke.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Theory]
|
|
[InlineData("PrevPage")]
|
|
[InlineData("NextPage")]
|
|
public async Task ParkedMessages_Paging_ClosesTheDrawer(string pagingMethod)
|
|
{
|
|
RegisterCommonServices();
|
|
|
|
var siteRepo = Substitute.For<ISiteRepository>();
|
|
siteRepo.GetAllSitesAsync().Returns(new List<Site>());
|
|
Services.AddSingleton(siteRepo);
|
|
Services.AddSingleton(new CommunicationService(
|
|
Options.Create(new CommunicationOptions()),
|
|
NullLogger<CommunicationService>.Instance));
|
|
|
|
var cut = Render<ParkedMessagesPage>();
|
|
|
|
// Drawer open on some message.
|
|
await cut.InvokeAsync(() =>
|
|
SetPrivate(cut.Instance, "_drawerMessageId", "msg-being-viewed"));
|
|
Assert.Equal("msg-being-viewed", GetPrivate(cut.Instance, "_drawerMessageId"));
|
|
|
|
await cut.InvokeAsync(() => InvokePrivate(cut.Instance, pagingMethod));
|
|
|
|
// Paging away invalidates it — this surface has no keyed detail to fall back
|
|
// on, so leaving it set would strand the drawer on an empty notice.
|
|
Assert.Null(GetPrivate(cut.Instance, "_drawerMessageId"));
|
|
}
|
|
|
|
/// <summary>Verifies that paging the configuration audit log closes the open state modal.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ConfigurationAuditLog_Paging_ClosesTheStateModal()
|
|
{
|
|
RegisterCommonServices();
|
|
Services.AddSingleton(Substitute.For<ICentralUiRepository>());
|
|
|
|
var cut = Render<AuditLogPage>();
|
|
|
|
await cut.InvokeAsync(() => SetPrivate(cut.Instance, "_modalEntryId", 4242));
|
|
Assert.Equal(4242, GetPrivate(cut.Instance, "_modalEntryId"));
|
|
|
|
await cut.InvokeAsync(() => InvokePrivate(cut.Instance, "OnPageChanged", 2));
|
|
|
|
Assert.Null(GetPrivate(cut.Instance, "_modalEntryId"));
|
|
}
|
|
|
|
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
|
{
|
|
private readonly AuthenticationState _state;
|
|
/// <summary>Initializes the stub with the authentication state it returns.</summary>
|
|
/// <param name="state">The authentication state to return.</param>
|
|
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
|
/// <inheritdoc />
|
|
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
|
=> Task.FromResult(_state);
|
|
}
|
|
}
|