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; } } }