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.
113 lines
4.9 KiB
C#
113 lines
4.9 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.Commons.Entities.Sites;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.Communication;
|
|
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
|
using DebugViewPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment.DebugView;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
|
|
|
/// <summary>
|
|
/// Regression tests for the debug view disposal guard. The <c>DebugView</c> stream callbacks
|
|
/// (<c>onEvent</c>/<c>onTerminated</c>) run on an Akka/gRPC thread and capture
|
|
/// <c>this</c> and <c>_toast</c>. If the user navigates away, an in-flight
|
|
/// callback could still call <c>_toast.ShowError(...)</c> /
|
|
/// <c>InvokeAsync(StateHasChanged)</c> on a disposed component. The fix adds a
|
|
/// <c>_disposed</c> flag checked at the top of every callback, set in
|
|
/// <c>Dispose()</c> before the stream is stopped.
|
|
/// <para>
|
|
/// The Akka-thread timing race itself is not deterministically reproducible in
|
|
/// a unit test (<see cref="DebugStreamService"/> is a non-virtual concrete
|
|
/// class with no seam to inject and later fire the callbacks). These tests pin
|
|
/// the observable parts of the fix: the component exposes a disposal guard, and
|
|
/// disposal is clean and idempotent.
|
|
/// </para>
|
|
/// </summary>
|
|
public class DebugViewDisposalTests : BunitContext
|
|
{
|
|
private void RegisterServices()
|
|
{
|
|
// DebugView touches localStorage on render; let bUnit answer loosely.
|
|
JSInterop.Mode = JSRuntimeMode.Loose;
|
|
|
|
var repo = Substitute.For<ITemplateEngineRepository>();
|
|
var siteRepo = Substitute.For<ISiteRepository>();
|
|
siteRepo.GetAllSitesAsync().Returns(new List<Site>());
|
|
Services.AddSingleton(repo);
|
|
Services.AddSingleton(siteRepo);
|
|
|
|
var comms = new CommunicationService(
|
|
Options.Create(new CommunicationOptions()),
|
|
NullLogger<CommunicationService>.Instance);
|
|
Services.AddSingleton(comms);
|
|
|
|
var grpcFactory = new SiteStreamGrpcClientFactory(NullLoggerFactory.Instance);
|
|
// An empty throwaway provider — these tests never call StartStreamAsync,
|
|
// so the provider is unused. (Services.BuildServiceProvider() would leak
|
|
// an undisposed provider.)
|
|
var debugStream = new DebugStreamService(
|
|
comms, new ServiceCollection().BuildServiceProvider(), grpcFactory,
|
|
NullLogger<DebugStreamService>.Instance);
|
|
Services.AddSingleton(debugStream);
|
|
|
|
var identity = new ClaimsIdentity(
|
|
new[] { new Claim(ClaimTypes.Name, "deployer") }, "TestCookie");
|
|
var authState = new AuthenticationState(new ClaimsPrincipal(identity));
|
|
var stubAuth = new StubAuthStateProvider(authState);
|
|
Services.AddSingleton<AuthenticationStateProvider>(stubAuth);
|
|
Services.AddScoped(_ => new SiteScopeService(stubAuth));
|
|
}
|
|
|
|
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
|
{
|
|
private readonly AuthenticationState _state;
|
|
/// <summary>Creates a provider that always returns the given authentication state.</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);
|
|
}
|
|
|
|
/// <summary>Verifies that the debug view exposes a boolean disposal guard field.</summary>
|
|
[Fact]
|
|
public void DebugView_HasDisposalGuardField()
|
|
{
|
|
// The fix introduces a `_disposed` flag that every stream callback
|
|
// checks before touching component state.
|
|
var field = typeof(DebugViewPage).GetField(
|
|
"_disposed", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
Assert.NotNull(field);
|
|
Assert.Equal(typeof(bool), field!.FieldType);
|
|
}
|
|
|
|
/// <summary>Verifies that disposing the debug view sets the disposal guard and that disposing twice does not throw.</summary>
|
|
[Fact]
|
|
public void DebugView_Dispose_SetsDisposedFlag_AndIsIdempotent()
|
|
{
|
|
RegisterServices();
|
|
var cut = Render<DebugViewPage>();
|
|
|
|
var field = typeof(DebugViewPage).GetField(
|
|
"_disposed", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
Assert.False((bool)field.GetValue(cut.Instance)!);
|
|
|
|
cut.Instance.Dispose();
|
|
Assert.True((bool)field.GetValue(cut.Instance)!,
|
|
"Dispose() must set the guard so in-flight callbacks no-op.");
|
|
|
|
// Disposing again must not throw (idempotent).
|
|
var ex = Record.Exception(() => cut.Instance.Dispose());
|
|
Assert.Null(ex);
|
|
}
|
|
}
|