using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Acls;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters;
using ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Acls;
using ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Configuration.Services;
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pages;
///
/// lmxopcua#785: the Delete buttons on RoleGrants, ClusterEdit, NodeEdit and AclEdit deleted on the
/// first click. Each must open the shared RawConfirmDialog; only Confirm deletes, Cancel keeps the item.
///
[Trait("Category", "Unit")]
public sealed class DeleteConfirmRenderTests : BunitContext
{
private const string ModalSelector = ".modal-dialog";
private IDbContextFactory NewDb(Action seed)
{
var name = $"del-{Guid.NewGuid():N}";
using (var db = RawTreeTestDb.CreateNamed(name)) { seed(db); db.SaveChanges(); }
var factory = RawTreeTestDb.Factory(name);
Services.AddSingleton(factory);
Services.AddSingleton(new FakeAuth());
return factory;
}
private static void SeedCluster(OtOpcUaConfigDbContext db) =>
db.ServerClusters.Add(new ServerCluster
{
ClusterId = "C1", Name = "Cluster One", Enterprise = "zb", Site = "s1",
RedundancyMode = RedundancyMode.None, CreatedBy = "test",
});
private static void ClickDelete(IRenderedComponent cut) =>
cut.Find("button.btn-outline-danger").Click();
// ---------------- ClusterEdit ----------------
[Fact]
public void ClusterEdit_Delete_confirms_first()
{
var f = NewDb(SeedCluster);
var cut = Render(p => p.Add(x => x.ClusterId, "C1"));
cut.FindAll(ModalSelector).ShouldBeEmpty();
ClickDelete(cut);
cut.Find(ModalSelector).TextContent.ShouldContain("C1");
ClusterCount(f).ShouldBe(1); // nothing deleted by merely opening the modal
cut.Find(".modal-footer .btn-outline-secondary").Click(); // Cancel
cut.FindAll(ModalSelector).ShouldBeEmpty();
ClusterCount(f).ShouldBe(1);
ClickDelete(cut);
cut.Find(".modal-footer .btn-danger").Click(); // Confirm
cut.WaitForAssertion(() => ClusterCount(f).ShouldBe(0));
}
// ---------------- NodeEdit ----------------
[Fact]
public void NodeEdit_Delete_confirms_first()
{
var f = NewDb(db =>
{
SeedCluster(db);
db.ClusterNodes.Add(new ClusterNode
{
NodeId = "n1", ClusterId = "C1", Host = "h1", ApplicationUri = "urn:n1", CreatedBy = "test",
});
});
var cut = Render(p => p.Add(x => x.ClusterId, "C1").Add(x => x.NodeId, "n1"));
cut.FindAll(ModalSelector).ShouldBeEmpty();
ClickDelete(cut);
cut.Find(ModalSelector).TextContent.ShouldContain("n1");
NodeCount(f).ShouldBe(1);
cut.Find(".modal-footer .btn-outline-secondary").Click();
cut.FindAll(ModalSelector).ShouldBeEmpty();
NodeCount(f).ShouldBe(1);
ClickDelete(cut);
cut.Find(".modal-footer .btn-danger").Click();
cut.WaitForAssertion(() => NodeCount(f).ShouldBe(0));
}
// ---------------- AclEdit ----------------
[Fact]
public void AclEdit_Delete_confirms_first()
{
var f = NewDb(db =>
{
SeedCluster(db);
db.NodeAcls.Add(new NodeAcl
{
NodeAclId = "acl-1", ClusterId = "C1", Principal = "ops-team",
ScopeKind = NodeAclScopeKind.Cluster, PermissionFlags = NodePermissions.Browse,
});
});
Services.AddSingleton(
new AclScopeService(f, new RawTreeService(f), new UnsTreeService(f)));
var cut = Render(p => p.Add(x => x.ClusterId, "C1").Add(x => x.NodeAclId, "acl-1"));
cut.FindAll(ModalSelector).ShouldBeEmpty();
ClickDelete(cut);
cut.Find(ModalSelector).TextContent.ShouldContain("acl-1");
AclCount(f).ShouldBe(1);
cut.Find(".modal-footer .btn-outline-secondary").Click();
cut.FindAll(ModalSelector).ShouldBeEmpty();
AclCount(f).ShouldBe(1);
ClickDelete(cut);
cut.Find(".modal-footer .btn-danger").Click();
cut.WaitForAssertion(() => AclCount(f).ShouldBe(0));
}
// ---------------- RoleGrants ----------------
[Fact]
public void RoleGrants_Delete_confirms_first()
{
var svc = new FakeMappings(new LdapGroupRoleMapping
{
Id = Guid.NewGuid(), LdapGroup = "UG_Admins", Role = AdminRole.Viewer, IsSystemWide = true,
});
Services.AddSingleton(svc);
Services.AddSingleton>(new FakeOptions());
var cut = Render();
cut.FindAll(ModalSelector).ShouldBeEmpty();
cut.Find("table button.btn-outline-danger").Click();
cut.Find(ModalSelector).TextContent.ShouldContain("UG_Admins");
svc.DeleteCalls.ShouldBe(0);
cut.Find(".modal-footer .btn-outline-secondary").Click();
cut.FindAll(ModalSelector).ShouldBeEmpty();
svc.DeleteCalls.ShouldBe(0);
cut.Markup.ShouldContain("UG_Admins");
cut.Find("table button.btn-outline-danger").Click();
cut.Find(".modal-footer .btn-danger").Click();
cut.WaitForAssertion(() => svc.DeleteCalls.ShouldBe(1));
}
private static int ClusterCount(IDbContextFactory f) { using var d = f.CreateDbContext(); return d.ServerClusters.Count(); }
private static int NodeCount(IDbContextFactory f) { using var d = f.CreateDbContext(); return d.ClusterNodes.Count(); }
private static int AclCount(IDbContextFactory f) { using var d = f.CreateDbContext(); return d.NodeAcls.Count(); }
private sealed class FakeAuth : AuthenticationStateProvider
{
public override Task GetAuthenticationStateAsync() =>
Task.FromResult(new AuthenticationState(new System.Security.Claims.ClaimsPrincipal(
new System.Security.Claims.ClaimsIdentity("test"))));
}
private sealed class FakeOptions : IOptionsSnapshot
{
public LdapOptions Value { get; } = new();
public LdapOptions Get(string? name) => Value;
}
private sealed class FakeMappings(LdapGroupRoleMapping row) : ILdapGroupRoleMappingService
{
private readonly List _rows = [row];
public int DeleteCalls { get; private set; }
public Task> GetByGroupsAsync(IEnumerable g, CancellationToken ct) =>
Task.FromResult>(_rows.ToList());
public Task> ListAllAsync(CancellationToken ct) =>
Task.FromResult>(_rows.ToList());
public Task CreateAsync(LdapGroupRoleMapping r, CancellationToken ct) { _rows.Add(r); return Task.FromResult(r); }
public Task DeleteAsync(Guid id, CancellationToken ct) { DeleteCalls++; _rows.RemoveAll(r => r.Id == id); return Task.CompletedTask; }
}
}