6a616a1ab2
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.
201 lines
7.8 KiB
C#
201 lines
7.8 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
|
|
|
/// <summary>
|
|
/// Verifies the bulk <see cref="UnsTreeService.ImportEquipmentAsync"/> path: valid rows insert,
|
|
/// rows whose MachineCode already exists (in the DB or earlier in the same batch) are skipped,
|
|
/// rows referencing an unknown UNS line or unknown driver are reported as errors, and the
|
|
/// decision-#122 driver-cluster guard rejects a driver in a different cluster than the line.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class UnsTreeServiceImportTests
|
|
{
|
|
private static (UnsTreeService Service, string DbName) Fresh()
|
|
{
|
|
var dbName = $"uns-import-{Guid.NewGuid():N}";
|
|
return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeds a line under an area in <paramref name="lineCluster"/>, plus an optional driver in
|
|
/// <paramref name="driverCluster"/>. The line id is always <c>LINE-1</c>; the driver (when
|
|
/// requested) is always <c>DRV-1</c>.
|
|
/// </summary>
|
|
private static void SeedLineAndDriver(string dbName, string lineCluster, string? driverCluster)
|
|
{
|
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
|
db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-1", ClusterId = lineCluster, Name = "a" });
|
|
db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-1", UnsAreaId = "AREA-1", Name = "l" });
|
|
if (driverCluster is not null)
|
|
{
|
|
db.DriverInstances.Add(new DriverInstance
|
|
{
|
|
DriverInstanceId = "DRV-1",
|
|
ClusterId = driverCluster,
|
|
Name = "drv",
|
|
DriverType = "Modbus",
|
|
DriverConfig = "{}",
|
|
});
|
|
}
|
|
db.SaveChanges();
|
|
}
|
|
|
|
private static EquipmentInput Input(string name, string machineCode, string unsLineId, string? driverInstanceId) =>
|
|
new(name, machineCode, unsLineId, driverInstanceId,
|
|
ZTag: null, SAPID: null, Manufacturer: null, Model: null, SerialNumber: null,
|
|
HardwareRevision: null, SoftwareRevision: null, YearOfConstruction: null,
|
|
AssetLocation: null, ManufacturerUri: null, DeviceManualUri: null, Enabled: true);
|
|
|
|
/// <summary>Valid rows insert with system-generated EQ- ids and are counted in <c>Inserted</c>.</summary>
|
|
[Fact]
|
|
public async Task Import_inserts_valid_rows()
|
|
{
|
|
var (service, dbName) = Fresh();
|
|
SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);
|
|
|
|
var result = await service.ImportEquipmentAsync(
|
|
[
|
|
Input("machine-1", "mc_1", "LINE-1", null),
|
|
Input("machine-2", "mc_2", "LINE-1", null),
|
|
]);
|
|
|
|
result.Inserted.ShouldBe(2);
|
|
result.Skipped.ShouldBe(0);
|
|
result.Errors.ShouldBeEmpty();
|
|
|
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
|
db.Equipment.Count().ShouldBe(2);
|
|
var eq = db.Equipment.Single(e => e.MachineCode == "mc_1");
|
|
eq.EquipmentId.ShouldStartWith("EQ-");
|
|
eq.EquipmentId.Length.ShouldBe(15); // "EQ-" + 12 hex chars
|
|
eq.Name.ShouldBe("machine-1");
|
|
eq.UnsLineId.ShouldBe("LINE-1");
|
|
eq.Enabled.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A row whose MachineCode already exists in the DB is skipped (not an error), and a second row
|
|
/// later in the batch that reuses an earlier row's MachineCode is also skipped.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Import_skips_duplicate_machinecode()
|
|
{
|
|
var (service, dbName) = Fresh();
|
|
SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);
|
|
// Pre-existing equipment with MachineCode "mc_existing".
|
|
await service.CreateEquipmentAsync(Input("machine-x", "mc_existing", "LINE-1", null));
|
|
|
|
var result = await service.ImportEquipmentAsync(
|
|
[
|
|
Input("machine-1", "mc_existing", "LINE-1", null), // dup of DB row → skip
|
|
Input("machine-2", "mc_new", "LINE-1", null), // inserts
|
|
Input("machine-3", "mc_new", "LINE-1", null), // dup of in-batch row → skip
|
|
]);
|
|
|
|
result.Inserted.ShouldBe(1);
|
|
result.Skipped.ShouldBe(2);
|
|
result.Errors.ShouldBeEmpty();
|
|
|
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
|
db.Equipment.Count(e => e.MachineCode == "mc_new").ShouldBe(1);
|
|
db.Equipment.Count(e => e.MachineCode == "mc_existing").ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>A row whose UnsLineId does not exist is reported as an error and not inserted.</summary>
|
|
[Fact]
|
|
public async Task Import_reports_unknown_line()
|
|
{
|
|
var (service, dbName) = Fresh();
|
|
SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);
|
|
|
|
var result = await service.ImportEquipmentAsync(
|
|
[
|
|
Input("machine-1", "mc_1", "LINE-BOGUS", null),
|
|
Input("machine-2", "mc_2", "LINE-1", null),
|
|
]);
|
|
|
|
result.Inserted.ShouldBe(1);
|
|
result.Skipped.ShouldBe(0);
|
|
result.Errors.Count.ShouldBe(1);
|
|
result.Errors[0].ShouldContain("mc_1");
|
|
result.Errors[0].ShouldContain("LINE-BOGUS");
|
|
|
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
|
db.Equipment.Any(e => e.MachineCode == "mc_1").ShouldBeFalse();
|
|
db.Equipment.Any(e => e.MachineCode == "mc_2").ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>A driver-bound row whose DriverInstanceId does not resolve is reported as an error.</summary>
|
|
[Fact]
|
|
public async Task Import_reports_unknown_driver()
|
|
{
|
|
var (service, dbName) = Fresh();
|
|
SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null); // no driver seeded
|
|
|
|
var result = await service.ImportEquipmentAsync(
|
|
[
|
|
Input("machine-1", "mc_1", "LINE-1", "DRV-GHOST"),
|
|
]);
|
|
|
|
result.Inserted.ShouldBe(0);
|
|
result.Skipped.ShouldBe(0);
|
|
result.Errors.Count.ShouldBe(1);
|
|
result.Errors[0].ShouldContain("mc_1");
|
|
result.Errors[0].ShouldContain("DRV-GHOST");
|
|
result.Errors[0].ShouldContain("not found");
|
|
|
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
|
db.Equipment.Any(e => e.MachineCode == "mc_1").ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The decision-#122 guard rejects a row that binds a driver living in a different cluster than
|
|
/// the row's UNS line.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Import_enforces_122_cluster()
|
|
{
|
|
var (service, dbName) = Fresh();
|
|
SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: "SITE-A");
|
|
|
|
var result = await service.ImportEquipmentAsync(
|
|
[
|
|
Input("machine-1", "mc_1", "LINE-1", "DRV-1"),
|
|
]);
|
|
|
|
result.Inserted.ShouldBe(0);
|
|
result.Skipped.ShouldBe(0);
|
|
result.Errors.Count.ShouldBe(1);
|
|
result.Errors[0].ShouldContain("mc_1");
|
|
result.Errors[0].ShouldContain("decision #122");
|
|
|
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
|
db.Equipment.Any(e => e.MachineCode == "mc_1").ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>A driver in the same cluster as the line imports cleanly.</summary>
|
|
[Fact]
|
|
public async Task Import_allows_driver_in_same_cluster()
|
|
{
|
|
var (service, dbName) = Fresh();
|
|
SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: "MAIN");
|
|
|
|
var result = await service.ImportEquipmentAsync(
|
|
[
|
|
Input("machine-1", "mc_1", "LINE-1", "DRV-1"),
|
|
]);
|
|
|
|
result.Inserted.ShouldBe(1);
|
|
result.Skipped.ShouldBe(0);
|
|
result.Errors.ShouldBeEmpty();
|
|
|
|
// v3: the driver passes the #122 guard but is not persisted (binding column retired).
|
|
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
|
db.Equipment.Single(e => e.MachineCode == "mc_1").UnsLineId.ShouldBe("LINE-1");
|
|
}
|
|
}
|