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.
240 lines
10 KiB
C#
240 lines
10 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.Instances;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
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>
|
|
/// bUnit tests for the Debug View's instance selector after it moved from a flat
|
|
/// <c><select></c> to the shared <c>InstancePicker</c> area-tree combobox
|
|
/// (design 2026-09-08). Pins that the page hands the picker the site's areas and
|
|
/// its Enabled-only instance list, that drilling down the tree selects an instance,
|
|
/// that the selection still enables <em>Connect</em>, and that a live connection
|
|
/// locks the picker down.
|
|
/// </summary>
|
|
public class DebugViewInstancePickerTests : BunitContext
|
|
{
|
|
private const int SiteId = 1;
|
|
|
|
private readonly ITemplateEngineRepository _repo = Substitute.For<ITemplateEngineRepository>();
|
|
|
|
private void RegisterServices()
|
|
{
|
|
// DebugView touches localStorage on render; let bUnit answer loosely.
|
|
JSInterop.Mode = JSRuntimeMode.Loose;
|
|
|
|
_repo.GetInstancesBySiteIdAsync(SiteId).Returns(new List<Instance>
|
|
{
|
|
new("Sim01") { Id = 7, SiteId = SiteId, AreaId = 2, State = InstanceState.Enabled },
|
|
new("Off01") { Id = 8, SiteId = SiteId, State = InstanceState.Disabled },
|
|
});
|
|
_repo.GetAreasBySiteIdAsync(SiteId).Returns(new List<Area>
|
|
{
|
|
new("Line1") { Id = 1, SiteId = SiteId },
|
|
new("Cell A") { Id = 2, SiteId = SiteId, ParentAreaId = 1 },
|
|
});
|
|
Services.AddSingleton(_repo);
|
|
|
|
var siteRepo = Substitute.For<ISiteRepository>();
|
|
siteRepo.GetAllSitesAsync().Returns(new List<Site>
|
|
{
|
|
new("Site One", "site-a") { Id = SiteId },
|
|
});
|
|
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);
|
|
|
|
// No SiteId claim => system-wide, so site 1 passes the scope re-check.
|
|
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>Initializes the provider with a fixed authentication state.</summary>
|
|
/// <param name="state">The state.</param>
|
|
public StubAuthStateProvider(AuthenticationState state) => _state = state;
|
|
/// <inheritdoc />
|
|
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
|
=> Task.FromResult(_state);
|
|
}
|
|
|
|
/// <summary>Renders the page, selects the seeded site and opens the picker panel.</summary>
|
|
private IRenderedComponent<DebugViewPage> RenderWithOpenPicker()
|
|
{
|
|
RegisterServices();
|
|
var cut = Render<DebugViewPage>();
|
|
cut.Find("[data-test='debug-site-select']").Change(SiteId.ToString());
|
|
cut.Find("[data-test='debug-instance-toggle']").Click();
|
|
return cut;
|
|
}
|
|
|
|
private static void ClickRow(IRenderedComponent<DebugViewPage> cut, string text) =>
|
|
cut.FindAll("[data-test='debug-instance-row']")
|
|
.First(r => r.TextContent.Trim() == text)
|
|
.Click();
|
|
|
|
/// <summary>Verifies that the instance selector is the picker component rather than a flat select.</summary>
|
|
[Fact]
|
|
public void InstanceSelector_IsThePicker_NotAFlatSelect()
|
|
{
|
|
RegisterServices();
|
|
var cut = Render<DebugViewPage>();
|
|
|
|
Assert.Single(cut.FindAll("[data-test='debug-instance-picker']"));
|
|
Assert.Empty(cut.FindAll("select[data-test='debug-instance-select']"));
|
|
}
|
|
|
|
/// <summary>Verifies that opening the picker shows only the root area.</summary>
|
|
[Fact]
|
|
public void OpeningThePicker_ShowsOnlyTheRootArea()
|
|
{
|
|
var cut = RenderWithOpenPicker();
|
|
|
|
var rows = cut.FindAll("[data-test='debug-instance-row']");
|
|
Assert.Single(rows);
|
|
Assert.Equal("Line1", rows[0].TextContent.Trim());
|
|
Assert.False(rows[0].HasAttribute("data-instance-id"));
|
|
}
|
|
|
|
/// <summary>Verifies that drilling down reveals only the enabled instance.</summary>
|
|
[Fact]
|
|
public void DrillingDown_RevealsTheEnabledInstanceOnly()
|
|
{
|
|
var cut = RenderWithOpenPicker();
|
|
|
|
ClickRow(cut, "Line1");
|
|
ClickRow(cut, "Cell A");
|
|
|
|
var instanceRow = cut.Find("[data-instance-id='7']");
|
|
Assert.Equal("Sim01 (Enabled)", instanceRow.TextContent.Trim());
|
|
|
|
// Off01 is Disabled, so the page filters it out before the picker sees it.
|
|
Assert.DoesNotContain(
|
|
cut.FindAll("[data-test='debug-instance-row']"),
|
|
r => r.TextContent.Contains("Off01"));
|
|
}
|
|
|
|
/// <summary>Verifies that picking an instance fills the field and enables the Connect button.</summary>
|
|
[Fact]
|
|
public void PickingAnInstance_FillsTheField_AndEnablesConnect()
|
|
{
|
|
var cut = RenderWithOpenPicker();
|
|
|
|
ClickRow(cut, "Line1");
|
|
ClickRow(cut, "Cell A");
|
|
cut.Find("[data-instance-id='7']").Click();
|
|
|
|
Assert.Equal(
|
|
"Sim01 (Enabled)",
|
|
cut.Find("[data-test='debug-instance-input']").GetAttribute("value"));
|
|
|
|
var connect = cut.FindAll("button.btn.btn-primary.btn-sm")
|
|
.First(b => b.TextContent.Contains("Connect"));
|
|
Assert.False(connect.HasAttribute("disabled"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The picker must stay disabled for the WHOLE site load, not just until the site id lands.
|
|
/// Blazor renders as soon as <c>LoadInstancesForSite</c> hits its first await, and at that point
|
|
/// the page is not <c>_connected</c> — so the field was already enabled while the site's lists
|
|
/// were still loading. Typing into it then (an operator, or Playwright's <c>FillAsync</c>, which
|
|
/// waits only for "enabled") lost the text: the picker reads the freshly-assigned lists as
|
|
/// "the site changed" and clears the field and closes the panel.
|
|
/// </summary>
|
|
[Fact]
|
|
public void PickerStaysDisabled_UntilTheSitesInstancesHaveLoaded()
|
|
{
|
|
RegisterServices();
|
|
// Hold the instance load open so the render that follows the first await is observable.
|
|
var gate = new TaskCompletionSource<IReadOnlyList<Instance>>();
|
|
_repo.GetInstancesBySiteIdAsync(SiteId).Returns(gate.Task);
|
|
|
|
var cut = Render<DebugViewPage>();
|
|
cut.Find("[data-test='debug-site-select']").Change(SiteId.ToString());
|
|
|
|
Assert.True(cut.Find("[data-test='debug-instance-input']").HasAttribute("disabled"));
|
|
Assert.True(cut.Find("[data-test='debug-instance-toggle']").HasAttribute("disabled"));
|
|
|
|
cut.InvokeAsync(() => gate.SetResult(
|
|
[new Instance("Sim01") { Id = 7, SiteId = SiteId, AreaId = 2, State = InstanceState.Enabled }]));
|
|
|
|
cut.WaitForAssertion(() =>
|
|
Assert.False(cut.Find("[data-test='debug-instance-input']").HasAttribute("disabled")));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The localStorage auto-reconnect must apply the same membership guard the
|
|
/// <c>?instanceId=</c> deep link does. Off01 is Disabled, so the page filters it out of the
|
|
/// list the picker is handed — restoring it anyway left the field blank while the page held
|
|
/// a non-zero id and auto-connected to it. The stored id is dropped instead, and the site
|
|
/// stays selected and loaded so the operator can pick again.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AutoReconnect_FromStorage_SkipsAnInstanceThatIsNoLongerAvailable()
|
|
{
|
|
RegisterServices();
|
|
JSInterop.Setup<string>("localStorage.getItem", "debugView.siteId").SetResult(SiteId.ToString());
|
|
JSInterop.Setup<string>("localStorage.getItem", "debugView.instanceId").SetResult("8");
|
|
|
|
var cut = Render<DebugViewPage>();
|
|
|
|
cut.WaitForAssertion(() =>
|
|
Assert.Equal(string.Empty, cut.Find("[data-test='debug-instance-input']").GetAttribute("value")));
|
|
|
|
// Nothing is selected, so Connect stays disabled — and the stale id is forgotten.
|
|
var connect = cut.FindAll("button.btn.btn-primary.btn-sm").First(b => b.TextContent.Contains("Connect"));
|
|
Assert.True(connect.HasAttribute("disabled"));
|
|
JSInterop.VerifyInvoke("localStorage.removeItem");
|
|
}
|
|
|
|
/// <summary>Verifies that the picker is disabled while the view is connected.</summary>
|
|
[Fact]
|
|
public void WhileConnected_ThePickerIsDisabled()
|
|
{
|
|
RegisterServices();
|
|
var cut = Render<DebugViewPage>();
|
|
cut.Find("[data-test='debug-site-select']").Change(SiteId.ToString());
|
|
|
|
// Mark the page connected the way the other DebugView page tests do —
|
|
// driving the real gRPC stream is not available to a bUnit render.
|
|
cut.InvokeAsync(() => typeof(DebugViewPage)
|
|
.GetField("_connected", BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.SetValue(cut.Instance, true));
|
|
cut.Render();
|
|
|
|
Assert.True(cut.Find("[data-test='debug-instance-input']").HasAttribute("disabled"));
|
|
Assert.True(cut.Find("[data-test='debug-instance-toggle']").HasAttribute("disabled"));
|
|
}
|
|
}
|