using Microsoft.EntityFrameworkCore; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// /// Verifies and /// : refuse-if-children deletes that /// mirror the Area/Line/Equipment convention but carry no RowVersion concurrency /// token (the entity has none — Option B, no migration). /// /// /// The EF InMemory provider does not enforce FK Restrict, so the refuse-if-children /// behaviour is driven by an explicit child pre-check in the service (not the DB throwing), /// which is what these tests exercise. /// [Trait("Category", "Unit")] public sealed class UnsTreeServiceDeleteClusterTests { private static (UnsTreeService Service, string DbName) Fresh() { var dbName = $"uns-delcluster-{Guid.NewGuid():N}"; return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName); } private static ServerCluster NewCluster(string clusterId, string enterprise) => new() { ClusterId = clusterId, Name = clusterId, Enterprise = enterprise, Site = "site", RedundancyMode = RedundancyMode.None, CreatedBy = "test", }; // ----- DeleteCluster ----- /// Deleting a child-free cluster removes the row. [Fact] public async Task DeleteCluster_empty_removes_row() { var (service, dbName) = Fresh(); using (var db = UnsTreeTestDb.CreateNamed(dbName)) { db.ServerClusters.Add(NewCluster("CL-EMPTY", "zb")); db.SaveChanges(); } var result = await service.DeleteClusterAsync("CL-EMPTY"); result.Ok.ShouldBeTrue(); result.Error.ShouldBeNull(); using var verify = UnsTreeTestDb.CreateNamed(dbName); verify.ServerClusters.Any(c => c.ClusterId == "CL-EMPTY").ShouldBeFalse(); } /// Deleting a cluster that is already gone is a no-op success (idempotent). [Fact] public async Task DeleteCluster_already_gone_returns_ok() { var (service, _) = Fresh(); var result = await service.DeleteClusterAsync("GHOST-CLUSTER"); result.Ok.ShouldBeTrue(); result.Error.ShouldBeNull(); } /// A cluster with a UNS area refuses deletion with a friendly message and stays put. [Fact] public async Task DeleteCluster_with_area_refuses_and_keeps_row() { var (service, dbName) = Fresh(); using (var db = UnsTreeTestDb.CreateNamed(dbName)) { db.ServerClusters.Add(NewCluster("CL-AREA", "zb")); db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-CL", ClusterId = "CL-AREA", Name = "a" }); db.SaveChanges(); } var result = await service.DeleteClusterAsync("CL-AREA"); result.Ok.ShouldBeFalse(); result.Error.ShouldNotBeNull(); result.Error.ShouldContain("CL-AREA"); using var verify = UnsTreeTestDb.CreateNamed(dbName); verify.ServerClusters.Any(c => c.ClusterId == "CL-AREA").ShouldBeTrue(); } /// A cluster with a driver instance refuses deletion and stays put. [Fact] public async Task DeleteCluster_with_driver_refuses_and_keeps_row() { var (service, dbName) = Fresh(); using (var db = UnsTreeTestDb.CreateNamed(dbName)) { db.ServerClusters.Add(NewCluster("CL-DRV", "zb")); db.DriverInstances.Add(new DriverInstance { DriverInstanceId = "DRV-CL", ClusterId = "CL-DRV", NamespaceId = "NS-1", Name = "drv", DriverType = "ModbusTcp", DriverConfig = "{}", }); db.SaveChanges(); } var result = await service.DeleteClusterAsync("CL-DRV"); result.Ok.ShouldBeFalse(); result.Error.ShouldNotBeNull(); using var verify = UnsTreeTestDb.CreateNamed(dbName); verify.ServerClusters.Any(c => c.ClusterId == "CL-DRV").ShouldBeTrue(); } /// A cluster with a cluster node refuses deletion with a friendly message and stays put. [Fact] public async Task DeleteCluster_with_cluster_node_refuses_and_keeps_row() { var (service, dbName) = Fresh(); using (var db = UnsTreeTestDb.CreateNamed(dbName)) { db.ServerClusters.Add(NewCluster("CL-NODE", "zb")); db.ClusterNodes.Add(new ClusterNode { NodeId = "NODE-CL", ClusterId = "CL-NODE", Host = "host-a", ApplicationUri = "urn:zb:cl-node:a", CreatedBy = "test", }); db.SaveChanges(); } var result = await service.DeleteClusterAsync("CL-NODE"); result.Ok.ShouldBeFalse(); result.Error.ShouldNotBeNull(); result.Error.ShouldContain("CL-NODE"); using var verify = UnsTreeTestDb.CreateNamed(dbName); verify.ServerClusters.Any(c => c.ClusterId == "CL-NODE").ShouldBeTrue(); } /// A cluster with a namespace refuses deletion with a friendly message and stays put. [Fact] public async Task DeleteCluster_with_namespace_refuses_and_keeps_row() { var (service, dbName) = Fresh(); using (var db = UnsTreeTestDb.CreateNamed(dbName)) { db.ServerClusters.Add(NewCluster("CL-NS", "zb")); db.Namespaces.Add(new Namespace { NamespaceId = "NS-CL", ClusterId = "CL-NS", Kind = NamespaceKind.Equipment, NamespaceUri = "urn:zb:cl-ns:equipment", }); db.SaveChanges(); } var result = await service.DeleteClusterAsync("CL-NS"); result.Ok.ShouldBeFalse(); result.Error.ShouldNotBeNull(); result.Error.ShouldContain("CL-NS"); using var verify = UnsTreeTestDb.CreateNamed(dbName); verify.ServerClusters.Any(c => c.ClusterId == "CL-NS").ShouldBeTrue(); } // ----- DeleteEnterprise ----- /// Deleting an enterprise whose every cluster is child-free removes them all. [Fact] public async Task DeleteEnterprise_all_empty_removes_all_clusters() { var (service, dbName) = Fresh(); using (var db = UnsTreeTestDb.CreateNamed(dbName)) { db.ServerClusters.Add(NewCluster("ENT1-A", "ent-1")); db.ServerClusters.Add(NewCluster("ENT1-B", "ent-1")); // A cluster under a different enterprise must survive. db.ServerClusters.Add(NewCluster("ENT2-A", "ent-2")); db.SaveChanges(); } var result = await service.DeleteEnterpriseAsync("ent-1"); result.Ok.ShouldBeTrue(); result.Error.ShouldBeNull(); using var verify = UnsTreeTestDb.CreateNamed(dbName); verify.ServerClusters.Any(c => c.Enterprise == "ent-1").ShouldBeFalse(); verify.ServerClusters.Any(c => c.ClusterId == "ENT2-A").ShouldBeTrue(); } /// Deleting an enterprise with no matching clusters is a no-op success. [Fact] public async Task DeleteEnterprise_no_matches_returns_ok() { var (service, _) = Fresh(); var result = await service.DeleteEnterpriseAsync("nonexistent-enterprise"); result.Ok.ShouldBeTrue(); result.Error.ShouldBeNull(); } /// /// When one cluster under the enterprise has children, the whole delete refuses (naming the /// blocking cluster) and nothing is deleted — no partial enterprise removal. /// [Fact] public async Task DeleteEnterprise_one_cluster_with_children_refuses_and_deletes_nothing() { var (service, dbName) = Fresh(); using (var db = UnsTreeTestDb.CreateNamed(dbName)) { db.ServerClusters.Add(NewCluster("ENT3-EMPTY", "ent-3")); db.ServerClusters.Add(NewCluster("ENT3-FULL", "ent-3")); db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-ENT3", ClusterId = "ENT3-FULL", Name = "a" }); db.SaveChanges(); } var result = await service.DeleteEnterpriseAsync("ent-3"); result.Ok.ShouldBeFalse(); result.Error.ShouldNotBeNull(); result.Error.ShouldContain("ENT3-FULL"); // Nothing partial: BOTH clusters must still be present. using var verify = UnsTreeTestDb.CreateNamed(dbName); verify.ServerClusters.Any(c => c.ClusterId == "ENT3-EMPTY").ShouldBeTrue(); verify.ServerClusters.Any(c => c.ClusterId == "ENT3-FULL").ShouldBeTrue(); } }