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; /// Review 2026-10-21 07/M-1: the browser-time JS module must be disposed with the page. public class ConfigurationAuditLogDisposeTests : BunitContext { private sealed class StubAuth(AuthenticationState state) : AuthenticationStateProvider { public override Task GetAuthenticationStateAsync() => Task.FromResult(state); } private (IJSObjectReference module, IRenderedComponent 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(auth); Services.AddScoped(_ => new SiteScopeService(auth)); Services.AddScoped(); Services.AddSingleton(Substitute.For()); var module = Substitute.For(); module.InvokeAsync("getTimezoneOffsetMinutes", Arg.Any()).Returns(new ValueTask(0)); if (moduleThrowsOnDispose) module.DisposeAsync().Returns(_ => ValueTask.FromException(new JSDisconnectedException("gone"))); var js = Substitute.For(); js.InvokeAsync("import", Arg.Any()) .Returns(new ValueTask(module)); Services.AddSingleton(js); return (module, Render()); } /// Disposing the page disposes the imported module. /// A task that represents the asynchronous operation. [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(); } /// A disconnected circuit's dispose failure is swallowed. /// A task that represents the asynchronous operation. [Fact] public async Task Dispose_SwallowsJSDisconnected() { var (_, cut) = Arrange(moduleThrowsOnDispose: true); await cut.InvokeAsync(async () => await ((IAsyncDisposable)cut.Instance).DisposeAsync()); } }