Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceDeleteClusterTests.cs
T
Joseph Doherty b211911f98
v2-ci / unit-tests (push) Failing after 13m22s
test: migrate AdminUI/ControlPlane/Runtime/Host tests to xunit.v3 (#756)
Akka.TestKit.Xunit2 -> Akka.TestKit.Xunit 1.5.62 (xunit.v3 TestKit; needs xunit.v3 3.2.2, set via
VersionOverride on the four projects). Removes the xunit 2.9.3 pin. IAsyncLifetime returns ValueTask,
SqlServerFactAttribute gains the source-info ctor, and xUnit1051 call sites pass
TestContext.Current.CancellationToken. Test counts unchanged.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 08:57:33 -05:00

224 lines
8.4 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", TestContext.Current.CancellationToken);
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", TestContext.Current.CancellationToken);
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", TestContext.Current.CancellationToken);
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", TestContext.Current.CancellationToken);
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", TestContext.Current.CancellationToken);
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", TestContext.Current.CancellationToken);
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", TestContext.Current.CancellationToken);
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", TestContext.Current.CancellationToken);
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();
}
}