183 lines
8.4 KiB
C#
183 lines
8.4 KiB
C#
using System.Security.Claims;
|
|
using Bunit;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.Components.Authorization;
|
|
using Microsoft.AspNetCore.Components.Rendering;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
using ZB.MOM.WW.ScadaBridge.Communication;
|
|
using ZB.MOM.WW.ScadaBridge.DeploymentManager;
|
|
using ZB.MOM.WW.ScadaBridge.Security;
|
|
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services;
|
|
using TopologyPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment.Topology;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
|
|
|
/// <summary>
|
|
/// Pins the verbs on the Topology instance-action confirmations (deferred-work
|
|
/// register row 28): <em>Disable</em> must read "Disable", not the red button's
|
|
/// historical "Delete" default, while a genuine Delete must still read "Delete".
|
|
/// Like <c>FailoverConfirmLabelTests</c> these run the real <see cref="DialogService"/>
|
|
/// behind a real <c>DialogHost</c>, so they pin what the user reads rather than
|
|
/// the argument a substitute received.
|
|
/// </summary>
|
|
public class TopologyDisableConfirmLabelTests : BunitContext
|
|
{
|
|
private readonly ITemplateEngineRepository _repo = Substitute.For<ITemplateEngineRepository>();
|
|
private readonly ISiteRepository _siteRepo = Substitute.For<ISiteRepository>();
|
|
private readonly IDeploymentManagerRepository _deployRepo = Substitute.For<IDeploymentManagerRepository>();
|
|
private readonly IFlatteningPipeline _pipeline = Substitute.For<IFlatteningPipeline>();
|
|
private readonly IAuditService _audit = Substitute.For<IAuditService>();
|
|
private readonly DialogService _dialog = new();
|
|
|
|
public TopologyDisableConfirmLabelTests()
|
|
{
|
|
Services.AddSingleton(_repo);
|
|
Services.AddSingleton(_siteRepo);
|
|
Services.AddSingleton(_deployRepo);
|
|
Services.AddSingleton(_pipeline);
|
|
Services.AddSingleton(_audit);
|
|
|
|
// DeploymentService has non-mockable concrete deps; instantiate them directly
|
|
// (the same DI block TopologyPageTests uses to render this page).
|
|
Services.AddSingleton(new CommunicationService(
|
|
Options.Create(new CommunicationOptions()),
|
|
NullLogger<CommunicationService>.Instance));
|
|
Services.AddSingleton(new OperationLockManager());
|
|
Services.AddSingleton(Options.Create(new DeploymentManagerOptions
|
|
{
|
|
OperationLockTimeout = TimeSpan.FromSeconds(5)
|
|
}));
|
|
Services.AddSingleton(Options.Create(new CommunicationOptions()));
|
|
Services.AddSingleton<ILogger<DeploymentService>>(NullLogger<DeploymentService>.Instance);
|
|
Services.AddScoped<ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening.DiffService>();
|
|
Services.AddScoped<ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening.RevisionHashService>();
|
|
Services.AddSingleton<IDeploymentStatusNotifier>(
|
|
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance));
|
|
Services.AddSingleton(Substitute.For<IStaleInstanceProbe>());
|
|
Services.AddScoped<DeploymentService>();
|
|
Services.AddScoped<AreaService>();
|
|
Services.AddScoped<InstanceService>();
|
|
Services.AddScoped<ZB.MOM.WW.ScadaBridge.CentralUI.Auth.SiteScopeService>();
|
|
|
|
// The concrete service registered as the interface, so the page and the
|
|
// host share one instance (DialogHost injects IDialogService and casts).
|
|
Services.AddSingleton<IDialogService>(_dialog);
|
|
|
|
var claims = new[]
|
|
{
|
|
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
|
new Claim(JwtTokenService.RoleClaimType, Roles.Deployer)
|
|
};
|
|
Services.AddSingleton<AuthenticationStateProvider>(
|
|
new TestAuthStateProvider(new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"))));
|
|
Services.AddAuthorizationCore();
|
|
Services.AddTestConfigurationMutations();
|
|
|
|
JSInterop.Mode = JSRuntimeMode.Loose;
|
|
}
|
|
|
|
/// <summary>One site with one enabled instance, so both Disable and Delete are offered.</summary>
|
|
private void SeedOneEnabledInstance()
|
|
{
|
|
_siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
|
|
.Returns(Task.FromResult<IReadOnlyList<Site>>(
|
|
new List<Site> { new("Plant-A", "plant-a") { Id = 1 } }));
|
|
_repo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
|
|
.Returns(Task.FromResult<IReadOnlyList<Template>>(new List<Template>()));
|
|
_repo.GetTopologyInstancesAsync(1, null, 0, 50, Arg.Any<CancellationToken>())
|
|
.Returns(Task.FromResult<IReadOnlyList<Instance>>(new List<Instance>
|
|
{
|
|
new("Pump-001") { Id = 100, SiteId = 1, State = InstanceState.Enabled }
|
|
}));
|
|
_repo.GetAreasBySiteIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.FromResult<IReadOnlyList<Area>>(new List<Area>()));
|
|
}
|
|
|
|
/// <summary>Renders Topology next to a live DialogHost, as MainLayout does.</summary>
|
|
private IRenderedComponent<TestHostWrapper> RenderPageWithHost() => Render<TestHostWrapper>();
|
|
|
|
/// <summary>Wrapper so the page and the dialog host share one render tree.</summary>
|
|
public sealed class TestHostWrapper : ComponentBase
|
|
{
|
|
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
|
{
|
|
builder.OpenComponent<TopologyPage>(0);
|
|
builder.CloseComponent();
|
|
builder.OpenComponent<DialogHost>(1);
|
|
builder.CloseComponent();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expands to the instance row, opens its context menu and clicks the named item,
|
|
/// then returns the confirmation modal's footer buttons.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The menu click is deliberately not awaited: with a real DialogService the page's
|
|
/// handler parks on <c>await ConfirmAsync(...)</c> until the dialog is answered.
|
|
/// </remarks>
|
|
private (IRenderedComponent<TestHostWrapper> cut, IReadOnlyList<AngleSharp.Dom.IElement> footer)
|
|
OpenInstanceConfirm(string menuItem)
|
|
{
|
|
SeedOneEnabledInstance();
|
|
var cut = RenderPageWithHost();
|
|
cut.InvokeAsync(() => cut.FindComponent<TopologyPage>().Instance.LoadBranchAsync(1, null)).GetAwaiter().GetResult();
|
|
cut.FindComponent<TopologyPage>().Render();
|
|
|
|
cut.FindAll(".tv-row").First(r => r.QuerySelector(".tv-label")?.TextContent == "Plant-A")
|
|
.QuerySelector(".tv-toggle")!.Click();
|
|
|
|
var row = cut.FindAll(".tv-row").First(r => r.QuerySelector(".tv-label")?.TextContent == "Pump-001");
|
|
row.ContextMenu();
|
|
|
|
_ = cut.Find(".dropdown-menu.show")
|
|
.QuerySelectorAll("button.dropdown-item")
|
|
.First(b => b.TextContent.Trim() == menuItem)
|
|
.ClickAsync(new());
|
|
|
|
cut.WaitForState(() => cut.FindAll("div.modal-footer button").Count > 0);
|
|
return (cut, cut.FindAll("div.modal-footer button").ToList());
|
|
}
|
|
|
|
[Fact]
|
|
public void Disable_confirm_button_says_disable_not_delete()
|
|
{
|
|
var (_, footer) = OpenInstanceConfirm("Disable");
|
|
var confirm = footer.Last();
|
|
|
|
Assert.Equal("Disable", confirm.TextContent.Trim());
|
|
Assert.Contains("btn-danger", confirm.ClassList);
|
|
Assert.Contains("Cancel", footer.Select(b => b.TextContent.Trim()));
|
|
}
|
|
|
|
[Fact]
|
|
public void Disable_confirm_body_names_the_instance()
|
|
{
|
|
var (cut, _) = OpenInstanceConfirm("Disable");
|
|
Assert.Contains("Disable instance 'Pump-001'?", cut.Find(".modal-body").TextContent);
|
|
}
|
|
|
|
[Fact]
|
|
public void Delete_confirm_button_still_says_delete()
|
|
{
|
|
// The negative twin: relabelling Disable must not have changed the genuine
|
|
// delete path, which relies on the danger-confirm default.
|
|
var confirm = OpenInstanceConfirm("Delete").footer.Last();
|
|
|
|
Assert.Equal("Delete", confirm.TextContent.Trim());
|
|
Assert.Contains("btn-danger", confirm.ClassList);
|
|
}
|
|
}
|