Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceDeleteClusterTests.cs
T
Joseph Doherty 6a616a1ab2 test(adminui): migrate AdminUI.Tests to v3 greenfield schema + Batch-1 stubs
Migrate tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests (was 358 build errors) to
the v3 Raw-only Tag schema and the Batch-1 AdminUI stubs. Production untouched.

- Driver-page serialization tests: drop the retired pre-declared Tags editor
  (page *TagRow types + DriverOptions.Tags); multi-device pages keep their
  Devices editor (ToOptions(devices)). Enum-serialization round-trip coverage
  (CpuType/ModbusFamily/MelsecFamily/AbCipPlcFamily/AbLegacyPlcFamily/FocasCncSeries)
  preserved intact.
- UnsTreeTestDb: reseed to v3 (Device->Tag raw slice, no EquipmentId/DriverInstanceId
  on Tag, no Namespace).
- UnsTreeService tag tests: assert Batch-1 stubs (CreateTag/UpdateTag refusal,
  empty tag/tag-driver lists); DeleteTag stays live against a raw Tag.
- Equipment #122 guard retained (validates input driver vs line cluster); drop the
  retired persisted-binding assertions + NamespaceId seeds. Area/Line #122 driver-
  orphan guard retired -> assert cross-cluster move now succeeds.
- DeleteCluster: drop the deleted-Namespace child-check test.
- OpcUaClientTagConfigModel/TagConfigValidator: address key FullName -> nodeId.
- ScriptTagCatalog: project surviving Name/DataType/TagConfig; DriverInstanceId null.
- VirtualTag {{equip}} equipment-tag-derived base is dark -> 3 tests skipped (Batch-3).

Result: build green; 507 passed / 3 skipped / 0 failed.
2026-07-15 21:50:48 -04:00

224 lines
8.1 KiB
C#

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;
/// <summary>
/// Verifies <see cref="UnsTreeService.DeleteClusterAsync"/> and
/// <see cref="UnsTreeService.DeleteEnterpriseAsync"/>: refuse-if-children deletes that
/// mirror the Area/Line/Equipment convention but carry no <c>RowVersion</c> concurrency
/// token (the <see cref="ServerCluster"/> entity has none — Option B, no migration).
/// </summary>
/// <remarks>
/// The EF InMemory provider does not enforce FK <c>Restrict</c>, 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.
/// </remarks>
[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 -----
/// <summary>Deleting a child-free cluster removes the row.</summary>
[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();
}
/// <summary>Deleting a cluster that is already gone is a no-op success (idempotent).</summary>
[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();
}
/// <summary>A cluster with a UNS area refuses deletion with a friendly message and stays put.</summary>
[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();
}
/// <summary>A cluster with a driver instance refuses deletion and stays put.</summary>
[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",
Name = "drv",
DriverType = "Modbus",
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();
}
/// <summary>A cluster with a cluster node refuses deletion with a friendly message and stays put.</summary>
[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();
}
// v3 Batch-1: the Namespace entity was deleted (Raw/UNS split), so DescribeClusterChildrenAsync
// no longer carries a namespace child-check. The former DeleteCluster_with_namespace_refuses test
// was removed. The cluster-nodes / UNS-areas / driver-instances refuse-if-children checks (above)
// remain the enforced set.
// ----- DeleteEnterprise -----
/// <summary>Deleting an enterprise whose every cluster is child-free removes them all.</summary>
[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();
}
/// <summary>Deleting an enterprise with no matching clusters is a no-op success.</summary>
[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();
}
/// <summary>
/// When one cluster under the enterprise has children, the whole delete refuses (naming the
/// blocking cluster) and nothing is deleted — no partial enterprise removal.
/// </summary>
[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();
}
}