fix(centralui): dispose ConfigurationAuditLog browser-time module (review 2026-10-21 07/M-1)
This commit is contained in:
+14
@@ -4,6 +4,7 @@
|
|||||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit
|
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit
|
||||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||||
@attribute [Authorize(Policy = AuthorizationPolicies.OperationalAudit)]
|
@attribute [Authorize(Policy = AuthorizationPolicies.OperationalAudit)]
|
||||||
|
@implements IAsyncDisposable
|
||||||
@inject ICentralUiRepository CentralUiRepository
|
@inject ICentralUiRepository CentralUiRepository
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@@ -286,6 +287,19 @@
|
|||||||
private const string BrowserTimeModulePath = "./_content/ZB.MOM.WW.ScadaBridge.CentralUI/js/browser-time.js";
|
private const string BrowserTimeModulePath = "./_content/ZB.MOM.WW.ScadaBridge.CentralUI/js/browser-time.js";
|
||||||
private IJSObjectReference? _browserTimeModule;
|
private IJSObjectReference? _browserTimeModule;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases the dynamically imported browser-time module so its JS-side object reference
|
||||||
|
/// is not retained for the life of the circuit. A disconnected circuit has already
|
||||||
|
/// dropped it, so that failure is expected and swallowed.
|
||||||
|
/// </summary>
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_browserTimeModule is null) return;
|
||||||
|
try { await _browserTimeModule.DisposeAsync(); }
|
||||||
|
catch (JSDisconnectedException) { /* circuit already gone */ }
|
||||||
|
_browserTimeModule = null;
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (!firstRender) return;
|
if (!firstRender) return;
|
||||||
|
|||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Bunit;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using NSubstitute;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||||
|
using AuditLogPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Audit.ConfigurationAuditLog;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Audit;
|
||||||
|
|
||||||
|
/// <summary>Review 2026-10-21 07/M-1: the browser-time JS module must be disposed with the page.</summary>
|
||||||
|
public class ConfigurationAuditLogDisposeTests : BunitContext
|
||||||
|
{
|
||||||
|
private sealed class StubAuth(AuthenticationState state) : AuthenticationStateProvider
|
||||||
|
{
|
||||||
|
public override Task<AuthenticationState> GetAuthenticationStateAsync() => Task.FromResult(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (IJSObjectReference module, IRenderedComponent<AuditLogPage> cut) Arrange(bool moduleThrowsOnDispose)
|
||||||
|
{
|
||||||
|
var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "u") }, "T");
|
||||||
|
var auth = new StubAuth(new AuthenticationState(new ClaimsPrincipal(identity)));
|
||||||
|
Services.AddSingleton<AuthenticationStateProvider>(auth);
|
||||||
|
Services.AddScoped(_ => new SiteScopeService(auth));
|
||||||
|
Services.AddScoped<IDialogService, DialogService>();
|
||||||
|
Services.AddSingleton(Substitute.For<ICentralUiRepository>());
|
||||||
|
|
||||||
|
var module = Substitute.For<IJSObjectReference>();
|
||||||
|
module.InvokeAsync<int>("getTimezoneOffsetMinutes", Arg.Any<object?[]>()).Returns(new ValueTask<int>(0));
|
||||||
|
if (moduleThrowsOnDispose)
|
||||||
|
module.DisposeAsync().Returns(_ => ValueTask.FromException(new JSDisconnectedException("gone")));
|
||||||
|
var js = Substitute.For<IJSRuntime>();
|
||||||
|
js.InvokeAsync<IJSObjectReference>("import", Arg.Any<object?[]>())
|
||||||
|
.Returns(new ValueTask<IJSObjectReference>(module));
|
||||||
|
Services.AddSingleton(js);
|
||||||
|
return (module, Render<AuditLogPage>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Disposing the page disposes the imported module.</summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task Dispose_DisposesBrowserTimeModule()
|
||||||
|
{
|
||||||
|
var (module, cut) = Arrange(moduleThrowsOnDispose: false);
|
||||||
|
await cut.InvokeAsync(async () => await ((IAsyncDisposable)cut.Instance).DisposeAsync());
|
||||||
|
await module.Received(1).DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A disconnected circuit's dispose failure is swallowed.</summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task Dispose_SwallowsJSDisconnected()
|
||||||
|
{
|
||||||
|
var (_, cut) = Arrange(moduleThrowsOnDispose: true);
|
||||||
|
await cut.InvokeAsync(async () => await ((IAsyncDisposable)cut.Instance).DisposeAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user