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.
This commit is contained in:
+28
-23
@@ -4,6 +4,11 @@ using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
// v3 Batch-1 migration: OpcUaClientTagConfigModel's address key was renamed FullName → NodeId and
|
||||
// its serialized JSON key moved from PascalCase "FullName" to camelCase "nodeId" (TagConfig no longer
|
||||
// carries identity; the upstream node reference is an ordinary driver-address field). The tests below
|
||||
// track that rename; the preserve-unknown-keys round-trip coverage (unrecognised keys + the TagModal
|
||||
// isHistorized/historianTagname merge keys) is retained unchanged.
|
||||
public sealed class OpcUaClientTagConfigModelTests
|
||||
{
|
||||
[Theory]
|
||||
@@ -15,52 +20,52 @@ public sealed class OpcUaClientTagConfigModelTests
|
||||
{
|
||||
var m = OpcUaClientTagConfigModel.FromJson(json);
|
||||
|
||||
m.FullName.ShouldBe("");
|
||||
m.NodeId.ShouldBe("");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromJson_reads_FullName()
|
||||
public void FromJson_reads_NodeId()
|
||||
{
|
||||
var m = OpcUaClientTagConfigModel.FromJson(
|
||||
"""{"FullName":"nsu=urn:srv;s=Line3.Temp"}""");
|
||||
"""{"nodeId":"nsu=urn:srv;s=Line3.Temp"}""");
|
||||
|
||||
m.FullName.ShouldBe("nsu=urn:srv;s=Line3.Temp");
|
||||
m.NodeId.ShouldBe("nsu=urn:srv;s=Line3.Temp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Round_trip_preserves_FullName()
|
||||
public void Round_trip_preserves_NodeId()
|
||||
{
|
||||
var m = new OpcUaClientTagConfigModel { FullName = "ns=2;s=Line3.Temp" };
|
||||
var m = new OpcUaClientTagConfigModel { NodeId = "ns=2;s=Line3.Temp" };
|
||||
|
||||
var json = m.ToJson();
|
||||
var m2 = OpcUaClientTagConfigModel.FromJson(json);
|
||||
|
||||
m2.FullName.ShouldBe("ns=2;s=Line3.Temp");
|
||||
m2.NodeId.ShouldBe("ns=2;s=Line3.Temp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToJson_emits_PascalCase_FullName()
|
||||
public void ToJson_emits_camelCase_NodeId()
|
||||
{
|
||||
var m = new OpcUaClientTagConfigModel { FullName = "ns=2;s=Line3.Temp" };
|
||||
var m = new OpcUaClientTagConfigModel { NodeId = "ns=2;s=Line3.Temp" };
|
||||
|
||||
var json = m.ToJson();
|
||||
|
||||
// FullName is the composer/walker contract key — PascalCase, case-sensitive.
|
||||
json.ShouldContain("\"FullName\":\"ns=2;s=Line3.Temp\"");
|
||||
json.ShouldNotContain("\"fullName\"", Case.Sensitive);
|
||||
// nodeId is the v3 address key — camelCase, replacing the legacy PascalCase FullName identity key.
|
||||
json.ShouldContain("\"nodeId\":\"ns=2;s=Line3.Temp\"");
|
||||
json.ShouldNotContain("\"FullName\"", Case.Sensitive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromJson_then_ToJson_preserves_unknown_keys()
|
||||
{
|
||||
var json = OpcUaClientTagConfigModel
|
||||
.FromJson("""{"FullName":"ns=2;s=X","samplingIntervalMs":250}""")
|
||||
.FromJson("""{"nodeId":"ns=2;s=X","samplingIntervalMs":250}""")
|
||||
.ToJson();
|
||||
|
||||
json.ShouldContain("samplingIntervalMs");
|
||||
json.ShouldContain("250");
|
||||
// and the exposed field still round-trips
|
||||
json.ShouldContain("\"FullName\":\"ns=2;s=X\"");
|
||||
json.ShouldContain("\"nodeId\":\"ns=2;s=X\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -69,32 +74,32 @@ public sealed class OpcUaClientTagConfigModelTests
|
||||
// The TagModal-merge seam writes isHistorized/historianTagname at the TagConfig root; this model
|
||||
// does NOT model them, so they must survive a load→save untouched as preserved unknown keys.
|
||||
var json = OpcUaClientTagConfigModel
|
||||
.FromJson("""{"FullName":"ns=2;s=X","isHistorized":true,"historianTagname":"Line3_Temp"}""")
|
||||
.FromJson("""{"nodeId":"ns=2;s=X","isHistorized":true,"historianTagname":"Line3_Temp"}""")
|
||||
.ToJson();
|
||||
|
||||
json.ShouldContain("\"isHistorized\":true");
|
||||
json.ShouldContain("\"historianTagname\":\"Line3_Temp\"");
|
||||
json.ShouldContain("\"FullName\":\"ns=2;s=X\"");
|
||||
json.ShouldContain("\"nodeId\":\"ns=2;s=X\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToJson_trims_FullName()
|
||||
public void ToJson_trims_NodeId()
|
||||
{
|
||||
var json = new OpcUaClientTagConfigModel { FullName = " ns=2;s=X " }.ToJson();
|
||||
var json = new OpcUaClientTagConfigModel { NodeId = " ns=2;s=X " }.ToJson();
|
||||
|
||||
json.ShouldContain("\"FullName\":\"ns=2;s=X\"");
|
||||
json.ShouldContain("\"nodeId\":\"ns=2;s=X\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_returns_error_when_FullName_blank()
|
||||
public void Validate_returns_error_when_NodeId_blank()
|
||||
{
|
||||
new OpcUaClientTagConfigModel().Validate().ShouldNotBeNullOrEmpty();
|
||||
new OpcUaClientTagConfigModel { FullName = " " }.Validate().ShouldNotBeNullOrEmpty();
|
||||
new OpcUaClientTagConfigModel { NodeId = " " }.Validate().ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_returns_null_when_FullName_present()
|
||||
public void Validate_returns_null_when_NodeId_present()
|
||||
{
|
||||
new OpcUaClientTagConfigModel { FullName = "ns=2;s=X" }.Validate().ShouldBeNull();
|
||||
new OpcUaClientTagConfigModel { NodeId = "ns=2;s=X" }.Validate().ShouldBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,19 +147,19 @@ public sealed class TagArrayConfigTests
|
||||
/// Full seam: the array helper and a driver-typed editor compose over the same canonical TagConfig
|
||||
/// blob without clobbering each other. Setting the array keys, round-tripping through the OpcUaClient
|
||||
/// typed editor (FromJson → ToJson), then reading the array keys back recovers them intact — and the
|
||||
/// editor's own FullName field survives the array merge.
|
||||
/// editor's own NodeId field survives the array merge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Array_keys_survive_a_typed_editor_round_trip()
|
||||
{
|
||||
var withArray = TagArrayConfig.Set("""{"FullName":"ns=2;s=X"}""", isArray: true, arrayLength: 12);
|
||||
var withArray = TagArrayConfig.Set("""{"nodeId":"ns=2;s=X"}""", isArray: true, arrayLength: 12);
|
||||
|
||||
var afterEditor = OpcUaClientTagConfigModel.FromJson(withArray).ToJson();
|
||||
|
||||
var a = TagArrayConfig.Read(afterEditor);
|
||||
a.IsArray.ShouldBeTrue();
|
||||
a.ArrayLength.ShouldBe(12u);
|
||||
OpcUaClientTagConfigModel.FromJson(afterEditor).FullName.ShouldBe("ns=2;s=X");
|
||||
OpcUaClientTagConfigModel.FromJson(afterEditor).NodeId.ShouldBe("ns=2;s=X");
|
||||
}
|
||||
|
||||
/// <summary>The array helper and the historize helper compose over the same blob (both preserve
|
||||
|
||||
@@ -37,9 +37,10 @@ public sealed class TagConfigValidatorTests
|
||||
TagConfigValidator.Validate(driverType, null).ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
// v3 Batch-1: the OpcUaClient editor's address key was renamed FullName → nodeId (camelCase).
|
||||
[Fact]
|
||||
public void OpcUaClient_with_full_name_is_valid()
|
||||
=> TagConfigValidator.Validate("OpcUaClient", """{"FullName":"ns=2;s=Line3.Temp"}""").ShouldBeNull();
|
||||
public void OpcUaClient_with_node_id_is_valid()
|
||||
=> TagConfigValidator.Validate("OpcUaClient", """{"nodeId":"ns=2;s=Line3.Temp"}""").ShouldBeNull();
|
||||
|
||||
[Fact]
|
||||
public void S7_with_address_is_valid()
|
||||
|
||||
@@ -128,19 +128,19 @@ public sealed class TagHistorizeConfigTests
|
||||
/// Full seam: the history helper and a driver-typed editor compose over the same canonical TagConfig
|
||||
/// blob without clobbering each other. Setting the history keys, round-tripping through the
|
||||
/// OpcUaClient typed editor (FromJson → ToJson), then reading the history keys back recovers them
|
||||
/// intact — and the editor's own FullName field survives the history merge.
|
||||
/// intact — and the editor's own NodeId field survives the history merge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void History_keys_survive_a_typed_editor_round_trip()
|
||||
{
|
||||
var withHistory = TagHistorizeConfig.Set(
|
||||
"""{"FullName":"ns=2;s=X"}""", isHistorized: true, historianTagname: "TN");
|
||||
"""{"nodeId":"ns=2;s=X"}""", isHistorized: true, historianTagname: "TN");
|
||||
|
||||
var afterEditor = OpcUaClientTagConfigModel.FromJson(withHistory).ToJson();
|
||||
|
||||
var h = TagHistorizeConfig.Read(afterEditor);
|
||||
h.IsHistorized.ShouldBeTrue();
|
||||
h.HistorianTagname.ShouldBe("TN");
|
||||
OpcUaClientTagConfigModel.FromJson(afterEditor).FullName.ShouldBe("ns=2;s=X");
|
||||
OpcUaClientTagConfigModel.FromJson(afterEditor).NodeId.ShouldBe("ns=2;s=X");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,10 +87,15 @@ public sealed class UnsTreeServiceAreaLineTests
|
||||
area.ClusterId.ShouldBe("MAIN");
|
||||
}
|
||||
|
||||
/// <summary>The #122 guard blocks moving an area to a new cluster when driver-bound
|
||||
/// equipment would be orphaned from its driver's cluster.</summary>
|
||||
/// <summary>
|
||||
/// v3 Batch-1: the decision-#122 driver-orphan guard was RETIRED — equipment no longer binds a
|
||||
/// driver (<c>Equipment.DriverInstanceId</c> is gone), so a cluster move can never orphan
|
||||
/// driver-bound equipment. The former blocked/allowed guard tests are replaced by this one, which
|
||||
/// proves a cross-cluster area reassignment now succeeds unconditionally (equipment under the area
|
||||
/// rides along). The equipment↔driver cluster invariant is re-covered by the Batch-2 Raw-tree tests.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UpdateArea_reassign_cluster_blocked_when_driver_bound_equipment_would_orphan()
|
||||
public async Task UpdateArea_reassign_cluster_now_allowed_guard_retired()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
|
||||
@@ -106,16 +111,6 @@ public sealed class UnsTreeServiceAreaLineTests
|
||||
UnsLineId = "LINE-X",
|
||||
Name = "m",
|
||||
MachineCode = "machine_x",
|
||||
DriverInstanceId = "DRV-MAIN",
|
||||
});
|
||||
db.DriverInstances.Add(new DriverInstance
|
||||
{
|
||||
DriverInstanceId = "DRV-MAIN",
|
||||
ClusterId = "MAIN",
|
||||
NamespaceId = "NS-1",
|
||||
Name = "drv",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
});
|
||||
db.SaveChanges();
|
||||
rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-X").RowVersion;
|
||||
@@ -123,97 +118,12 @@ public sealed class UnsTreeServiceAreaLineTests
|
||||
|
||||
var result = await service.UpdateAreaAsync("AREA-X", "a", null, "SITE-A", rv);
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Error.ShouldNotBeNull();
|
||||
result.Error.ShouldContain("decision #122");
|
||||
result.Error.ShouldContain("EQ-BOUND");
|
||||
result.Error.ShouldContain("SITE-A");
|
||||
result.Error.ShouldContain("MAIN");
|
||||
|
||||
// The area must not have been moved.
|
||||
using var verify = UnsTreeTestDb.CreateNamed(dbName);
|
||||
verify.UnsAreas.Single(a => a.UnsAreaId == "AREA-X").ClusterId.ShouldBe("MAIN");
|
||||
}
|
||||
|
||||
/// <summary>The #122 guard allows the move when the area's driver-bound equipment's driver
|
||||
/// is already in the target cluster (driverCluster == newClusterId → no orphan).</summary>
|
||||
[Fact]
|
||||
public async Task UpdateArea_reassign_cluster_allowed_when_driver_is_in_target_cluster()
|
||||
{
|
||||
// Seed: AREA-Z in cluster MAIN, a line, equipment bound to DRV-SITE-A whose cluster is
|
||||
// SITE-A. Reassigning the area to SITE-A must be allowed because the driver is already
|
||||
// there — the #122 guard's `driverCluster != newClusterId` condition is false.
|
||||
var (service, dbName) = Fresh();
|
||||
|
||||
byte[] rv;
|
||||
using (var db = UnsTreeTestDb.CreateNamed(dbName))
|
||||
{
|
||||
db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-Z", ClusterId = "MAIN", Name = "a" });
|
||||
db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-Z", UnsAreaId = "AREA-Z", Name = "l" });
|
||||
db.DriverInstances.Add(new DriverInstance
|
||||
{
|
||||
DriverInstanceId = "DRV-SITE-A",
|
||||
ClusterId = "SITE-A",
|
||||
NamespaceId = "NS-1",
|
||||
Name = "drv",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
});
|
||||
db.Equipment.Add(new Equipment
|
||||
{
|
||||
EquipmentId = "EQ-BOUND-Z",
|
||||
EquipmentUuid = Guid.NewGuid(),
|
||||
UnsLineId = "LINE-Z",
|
||||
Name = "m",
|
||||
MachineCode = "machine_z",
|
||||
DriverInstanceId = "DRV-SITE-A",
|
||||
});
|
||||
db.SaveChanges();
|
||||
rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-Z").RowVersion;
|
||||
}
|
||||
|
||||
var result = await service.UpdateAreaAsync("AREA-Z", "a", null, "SITE-A", rv);
|
||||
|
||||
result.Ok.ShouldBeTrue();
|
||||
result.Error.ShouldBeNull();
|
||||
|
||||
// Verify the area actually moved to SITE-A via a fresh context.
|
||||
// The area moved to SITE-A.
|
||||
using var verify = UnsTreeTestDb.CreateNamed(dbName);
|
||||
verify.UnsAreas.Single(a => a.UnsAreaId == "AREA-Z").ClusterId.ShouldBe("SITE-A");
|
||||
}
|
||||
|
||||
/// <summary>The #122 guard allows the move when the equipment under the area is driver-less
|
||||
/// (DriverInstanceId == null).</summary>
|
||||
[Fact]
|
||||
public async Task UpdateArea_reassign_cluster_allowed_when_equipment_driverless()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
|
||||
byte[] rv;
|
||||
using (var db = UnsTreeTestDb.CreateNamed(dbName))
|
||||
{
|
||||
db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-Y", ClusterId = "MAIN", Name = "a" });
|
||||
db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-Y", UnsAreaId = "AREA-Y", Name = "l" });
|
||||
db.Equipment.Add(new Equipment
|
||||
{
|
||||
EquipmentId = "EQ-FREE",
|
||||
EquipmentUuid = Guid.NewGuid(),
|
||||
UnsLineId = "LINE-Y",
|
||||
Name = "m",
|
||||
MachineCode = "machine_y",
|
||||
DriverInstanceId = null,
|
||||
});
|
||||
db.SaveChanges();
|
||||
rv = db.UnsAreas.Single(a => a.UnsAreaId == "AREA-Y").RowVersion;
|
||||
}
|
||||
|
||||
var result = await service.UpdateAreaAsync("AREA-Y", "a", null, "SITE-A", rv);
|
||||
|
||||
result.Ok.ShouldBeTrue();
|
||||
result.Error.ShouldBeNull();
|
||||
|
||||
using var verify = UnsTreeTestDb.CreateNamed(dbName);
|
||||
verify.UnsAreas.Single(a => a.UnsAreaId == "AREA-Y").ClusterId.ShouldBe("SITE-A");
|
||||
verify.UnsAreas.Single(a => a.UnsAreaId == "AREA-X").ClusterId.ShouldBe("SITE-A");
|
||||
}
|
||||
|
||||
/// <summary>Updating an area that no longer exists returns the row-gone error.</summary>
|
||||
@@ -337,30 +247,22 @@ public sealed class UnsTreeServiceAreaLineTests
|
||||
result.Error.ShouldBe("Row no longer exists.");
|
||||
}
|
||||
|
||||
/// <summary>The #122 guard blocks reparenting a line to an area in a different cluster when
|
||||
/// the line's equipment is driver-bound (the driver lives in the original cluster).</summary>
|
||||
/// <summary>
|
||||
/// v3 Batch-1: the decision-#122 driver-orphan guard was RETIRED (equipment no longer binds a
|
||||
/// driver). The former blocked/allowed line-reparent guard tests are replaced by this one, which
|
||||
/// proves reparenting a line to an area in a different cluster now succeeds unconditionally.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UpdateLine_reparent_to_other_cluster_blocked_when_driver_bound()
|
||||
public async Task UpdateLine_reparent_to_other_cluster_now_allowed_guard_retired()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
|
||||
byte[] rv;
|
||||
using (var db = UnsTreeTestDb.CreateNamed(dbName))
|
||||
{
|
||||
// Source area A1 in cluster MAIN.
|
||||
db.UnsAreas.Add(new UnsArea { UnsAreaId = "A1-MAIN", ClusterId = "MAIN", Name = "area-main" });
|
||||
// Target area A2 in cluster SITE-A.
|
||||
db.UnsAreas.Add(new UnsArea { UnsAreaId = "A2-SITE-A", ClusterId = "SITE-A", Name = "area-site-a" });
|
||||
db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-BOUND", UnsAreaId = "A1-MAIN", Name = "line" });
|
||||
db.DriverInstances.Add(new DriverInstance
|
||||
{
|
||||
DriverInstanceId = "DRV-MAIN-122",
|
||||
ClusterId = "MAIN",
|
||||
NamespaceId = "NS-1",
|
||||
Name = "drv",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
});
|
||||
db.Equipment.Add(new Equipment
|
||||
{
|
||||
EquipmentId = "EQ-LINE-BOUND",
|
||||
@@ -368,7 +270,6 @@ public sealed class UnsTreeServiceAreaLineTests
|
||||
UnsLineId = "LINE-BOUND",
|
||||
Name = "eq",
|
||||
MachineCode = "mc_line_bound",
|
||||
DriverInstanceId = "DRV-MAIN-122",
|
||||
});
|
||||
db.SaveChanges();
|
||||
rv = db.UnsLines.Single(l => l.UnsLineId == "LINE-BOUND").RowVersion;
|
||||
@@ -376,52 +277,12 @@ public sealed class UnsTreeServiceAreaLineTests
|
||||
|
||||
var result = await service.UpdateLineAsync("LINE-BOUND", "line", null, "A2-SITE-A", rv);
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Error.ShouldNotBeNull();
|
||||
result.Error.ShouldContain("decision #122");
|
||||
result.Error.ShouldContain("EQ-LINE-BOUND");
|
||||
result.Error.ShouldContain("A2-SITE-A");
|
||||
result.Error.ShouldContain("MAIN");
|
||||
|
||||
// The line must not have moved.
|
||||
using var verify = UnsTreeTestDb.CreateNamed(dbName);
|
||||
verify.UnsLines.Single(l => l.UnsLineId == "LINE-BOUND").UnsAreaId.ShouldBe("A1-MAIN");
|
||||
}
|
||||
|
||||
/// <summary>The #122 guard allows reparenting a line to an area in a different cluster when
|
||||
/// the line's equipment is driver-less (DriverInstanceId == null).</summary>
|
||||
[Fact]
|
||||
public async Task UpdateLine_reparent_to_other_cluster_allowed_when_equipment_driverless()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
|
||||
byte[] rv;
|
||||
using (var db = UnsTreeTestDb.CreateNamed(dbName))
|
||||
{
|
||||
db.UnsAreas.Add(new UnsArea { UnsAreaId = "A1-FREE", ClusterId = "MAIN", Name = "area-main" });
|
||||
db.UnsAreas.Add(new UnsArea { UnsAreaId = "A2-FREE", ClusterId = "SITE-A", Name = "area-site-a" });
|
||||
db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-FREE", UnsAreaId = "A1-FREE", Name = "line" });
|
||||
db.Equipment.Add(new Equipment
|
||||
{
|
||||
EquipmentId = "EQ-LINE-FREE",
|
||||
EquipmentUuid = Guid.NewGuid(),
|
||||
UnsLineId = "LINE-FREE",
|
||||
Name = "eq",
|
||||
MachineCode = "mc_line_free",
|
||||
DriverInstanceId = null,
|
||||
});
|
||||
db.SaveChanges();
|
||||
rv = db.UnsLines.Single(l => l.UnsLineId == "LINE-FREE").RowVersion;
|
||||
}
|
||||
|
||||
var result = await service.UpdateLineAsync("LINE-FREE", "line", null, "A2-FREE", rv);
|
||||
|
||||
result.Ok.ShouldBeTrue();
|
||||
result.Error.ShouldBeNull();
|
||||
|
||||
// The line's UnsAreaId must have changed to A2-FREE.
|
||||
// The line moved to the target area.
|
||||
using var verify = UnsTreeTestDb.CreateNamed(dbName);
|
||||
verify.UnsLines.Single(l => l.UnsLineId == "LINE-FREE").UnsAreaId.ShouldBe("A2-FREE");
|
||||
verify.UnsLines.Single(l => l.UnsLineId == "LINE-BOUND").UnsAreaId.ShouldBe("A2-SITE-A");
|
||||
}
|
||||
|
||||
// ----- DeleteLine -----
|
||||
|
||||
+4
-28
@@ -105,7 +105,6 @@ public sealed class UnsTreeServiceDeleteClusterTests
|
||||
{
|
||||
DriverInstanceId = "DRV-CL",
|
||||
ClusterId = "CL-DRV",
|
||||
NamespaceId = "NS-1",
|
||||
Name = "drv",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
@@ -151,33 +150,10 @@ public sealed class UnsTreeServiceDeleteClusterTests
|
||||
verify.ServerClusters.Any(c => c.ClusterId == "CL-NODE").ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>A cluster with a namespace refuses deletion with a friendly message and stays put.</summary>
|
||||
[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();
|
||||
}
|
||||
// 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 -----
|
||||
|
||||
|
||||
+6
-7
@@ -14,16 +14,15 @@ public sealed class UnsTreeServiceEquipmentChildRowsTests
|
||||
return new UnsTreeService(UnsTreeTestDb.Factory(dbName));
|
||||
}
|
||||
|
||||
// v3 Batch-1: the equipment↔Tag binding was retired (Tag is Raw-only; authoring moved to the Raw
|
||||
// tree in Batch 2). LoadTagsForEquipmentAsync is a stub that always returns empty — no equipment
|
||||
// has bound driver tags to list. The former in-name-order / EquipmentId-scoping assertions moved
|
||||
// with the authoring surface to the Batch-2 Raw-tree tests.
|
||||
[Fact]
|
||||
public async Task LoadTagsForEquipment_returns_tags_in_name_order_scoped()
|
||||
public async Task LoadTagsForEquipment_returns_empty_stub()
|
||||
{
|
||||
var rows = await SeededService().LoadTagsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId);
|
||||
rows.Count.ShouldBe(2); // the EquipmentId=null orphan tag is excluded
|
||||
rows[0].TagId.ShouldBe("TAG-2"); // "running" < "speed"
|
||||
rows[0].Name.ShouldBe("running");
|
||||
rows[0].DataType.ShouldBe("Boolean");
|
||||
rows[1].TagId.ShouldBe("TAG-1");
|
||||
rows[1].DataType.ShouldBe("Float");
|
||||
rows.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -41,7 +41,6 @@ public sealed class UnsTreeServiceEquipmentTests
|
||||
{
|
||||
DriverInstanceId = "DRV-1",
|
||||
ClusterId = driverCluster,
|
||||
NamespaceId = "NS-1",
|
||||
Name = "drv",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
@@ -125,7 +124,11 @@ public sealed class UnsTreeServiceEquipmentTests
|
||||
db.Equipment.Any(e => e.MachineCode == "machine_001").ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Binding equipment to a driver in the same cluster as the line is allowed.</summary>
|
||||
/// <summary>
|
||||
/// Passing a driver in the same cluster as the line passes the #122 guard and the equipment
|
||||
/// persists. v3: the equipment↔driver binding column was retired, so the driver is validated by
|
||||
/// the guard but not stored — the assertion drops to equipment existence.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CreateEquipment_driver_in_same_cluster_allowed()
|
||||
{
|
||||
@@ -138,7 +141,7 @@ public sealed class UnsTreeServiceEquipmentTests
|
||||
result.Error.ShouldBeNull();
|
||||
|
||||
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||
db.Equipment.Single(e => e.MachineCode == "machine_001").DriverInstanceId.ShouldBe("DRV-1");
|
||||
db.Equipment.Single(e => e.MachineCode == "machine_001").UnsLineId.ShouldBe("LINE-1");
|
||||
}
|
||||
|
||||
/// <summary>Driver-less equipment is allowed regardless of cluster (no #122 guard applies).</summary>
|
||||
@@ -154,7 +157,7 @@ public sealed class UnsTreeServiceEquipmentTests
|
||||
result.Error.ShouldBeNull();
|
||||
|
||||
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||
db.Equipment.Single(e => e.MachineCode == "machine_001").DriverInstanceId.ShouldBeNull();
|
||||
db.Equipment.Single(e => e.MachineCode == "machine_001").UnsLineId.ShouldBe("LINE-1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -172,7 +175,6 @@ public sealed class UnsTreeServiceEquipmentTests
|
||||
{
|
||||
DriverInstanceId = "DRV-1",
|
||||
ClusterId = "MAIN",
|
||||
NamespaceId = "NS-1",
|
||||
Name = "drv",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
@@ -286,8 +288,9 @@ public sealed class UnsTreeServiceEquipmentTests
|
||||
result.Error.ShouldNotBeNull();
|
||||
result.Error.ShouldContain("decision #122");
|
||||
|
||||
// The equipment row is unchanged (v3 has no persisted driver-binding column to check).
|
||||
using var verify = UnsTreeTestDb.CreateNamed(dbName);
|
||||
verify.Equipment.Single(e => e.EquipmentId == equipmentId).DriverInstanceId.ShouldBeNull();
|
||||
verify.Equipment.Single(e => e.EquipmentId == equipmentId).MachineCode.ShouldBe("machine_001");
|
||||
}
|
||||
|
||||
/// <summary>Updating equipment with a MachineCode that already belongs to another row is blocked.</summary>
|
||||
@@ -344,8 +347,9 @@ public sealed class UnsTreeServiceEquipmentTests
|
||||
result.Ok.ShouldBeTrue();
|
||||
result.Error.ShouldBeNull();
|
||||
|
||||
// v3: the driver passes the #122 guard but is not persisted (binding column retired).
|
||||
using var verify = UnsTreeTestDb.CreateNamed(dbName);
|
||||
verify.Equipment.Single(e => e.EquipmentId == equipmentId).DriverInstanceId.ShouldBe("DRV-1");
|
||||
verify.Equipment.Single(e => e.EquipmentId == equipmentId).UnsLineId.ShouldBe("LINE-1");
|
||||
}
|
||||
|
||||
/// <summary>CreateEquipmentAsync returns the generated EQ- id in <c>CreatedId</c> so callers can navigate to the new page.</summary>
|
||||
|
||||
@@ -36,7 +36,6 @@ public sealed class UnsTreeServiceImportTests
|
||||
{
|
||||
DriverInstanceId = "DRV-1",
|
||||
ClusterId = driverCluster,
|
||||
NamespaceId = "NS-1",
|
||||
Name = "drv",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
@@ -194,7 +193,8 @@ public sealed class UnsTreeServiceImportTests
|
||||
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").DriverInstanceId.ShouldBe("DRV-1");
|
||||
db.Equipment.Single(e => e.MachineCode == "mc_1").UnsLineId.ShouldBe("LINE-1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,6 @@ public sealed class UnsTreeServiceLoadEditTests
|
||||
{
|
||||
DriverInstanceId = "DRV-B",
|
||||
ClusterId = UnsTreeTestDb.PopulatedClusterId,
|
||||
NamespaceId = "NS-1",
|
||||
Name = "modbus-b",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
@@ -117,7 +116,6 @@ public sealed class UnsTreeServiceLoadEditTests
|
||||
{
|
||||
DriverInstanceId = "DRV-A",
|
||||
ClusterId = UnsTreeTestDb.PopulatedClusterId,
|
||||
NamespaceId = "NS-1",
|
||||
Name = "galaxy-a",
|
||||
DriverType = "Galaxy",
|
||||
DriverConfig = "{}",
|
||||
@@ -127,7 +125,6 @@ public sealed class UnsTreeServiceLoadEditTests
|
||||
{
|
||||
DriverInstanceId = "DRV-OTHER",
|
||||
ClusterId = UnsTreeTestDb.EmptyClusterId,
|
||||
NamespaceId = "NS-2",
|
||||
Name = "other",
|
||||
DriverType = "S7",
|
||||
DriverConfig = "{}",
|
||||
@@ -144,7 +141,11 @@ public sealed class UnsTreeServiceLoadEditTests
|
||||
drivers[1].Display.ShouldBe("DRV-B — modbus-b (Modbus)");
|
||||
}
|
||||
|
||||
/// <summary>Loading a seeded tag maps its fields, owning equipment, and a non-empty RowVersion.</summary>
|
||||
/// <summary>
|
||||
/// Loading a seeded Raw tag maps its surviving fields (Name/DataType/AccessLevel/TagConfig) and a
|
||||
/// non-empty RowVersion. v3: Tag is Raw-only — the equipment + driver bindings were retired, so the
|
||||
/// DTO's <c>EquipmentId</c> / <c>DriverInstanceId</c> project as empty strings.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task LoadTag_returns_dto()
|
||||
{
|
||||
@@ -154,9 +155,9 @@ public sealed class UnsTreeServiceLoadEditTests
|
||||
|
||||
dto.ShouldNotBeNull();
|
||||
dto.TagId.ShouldBe("TAG-1");
|
||||
dto.EquipmentId.ShouldBe(UnsTreeTestDb.SeededEquipmentId);
|
||||
dto.EquipmentId.ShouldBe(string.Empty);
|
||||
dto.Name.ShouldBe("speed");
|
||||
dto.DriverInstanceId.ShouldBe("DRV-1");
|
||||
dto.DriverInstanceId.ShouldBe(string.Empty);
|
||||
dto.DataType.ShouldBe("Float");
|
||||
dto.AccessLevel.ShouldBe(TagAccessLevel.Read);
|
||||
dto.TagConfig.ShouldBe("{}");
|
||||
|
||||
@@ -70,8 +70,9 @@ public sealed class UnsTreeServiceStructureTests
|
||||
.Children.Single()
|
||||
.Children.Single();
|
||||
|
||||
// Seed: 2 driver tags + 1 virtual tag (the orphan tag has no equipment and is excluded).
|
||||
equipment.ChildCount.ShouldBe(3);
|
||||
// v3: Tag is Raw-only — driver tags no longer bind to equipment, so only the 1 virtual tag
|
||||
// contributes to the per-equipment badge count (the 2 seeded raw tags are not counted).
|
||||
equipment.ChildCount.ShouldBe(1);
|
||||
equipment.HasLazyChildren.ShouldBeFalse();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,20 +7,20 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <see cref="UnsTreeService.LoadTagDriversForEquipmentAsync"/> surfaces each
|
||||
/// candidate driver's <c>DriverType</c> and <c>DriverConfig</c> alongside its id and display string,
|
||||
/// so the UNS TagModal can dispatch to a per-driver-type typed tag-config editor (F-uns-1) and feed
|
||||
/// the selected gateway's config to the Galaxy live-browse address picker.
|
||||
/// v3 Batch-1 migration: the per-equipment candidate-driver loader
|
||||
/// (<see cref="UnsTreeService.LoadTagDriversForEquipmentAsync"/>) is RETIRED. The equipment↔driver
|
||||
/// binding and the <c>Namespace</c>/<c>NamespaceKind</c> entities were deleted — equipment no longer
|
||||
/// scopes a candidate-driver list, and per-driver-typed tag authoring moved to the Raw tree (/raw) in
|
||||
/// Batch 2. The loader now always returns empty; the former "surfaces DriverType/DriverConfig" test
|
||||
/// (which fed the TagModal editor dispatch + Galaxy live-browse picker) is replaced by the stub
|
||||
/// assertion below. That editor-dispatch coverage is re-established under the Batch-2 Raw-tree tests.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class UnsTreeServiceTagDriversTests
|
||||
{
|
||||
/// <summary>
|
||||
/// A driver loaded for an equipment carries its <c>DriverType</c> and <c>DriverConfig</c> in the
|
||||
/// returned tuple.
|
||||
/// </summary>
|
||||
/// <summary>The retired loader returns empty even when a matching cluster driver exists.</summary>
|
||||
[Fact]
|
||||
public async Task LoadTagDriversForEquipment_surfaces_driver_type_and_config()
|
||||
public async Task LoadTagDriversForEquipment_returns_empty_stub()
|
||||
{
|
||||
var dbName = $"uns-tagdrivers-{Guid.NewGuid():N}";
|
||||
|
||||
@@ -45,18 +45,10 @@ public sealed class UnsTreeServiceTagDriversTests
|
||||
Name = "machine-1",
|
||||
MachineCode = "machine_001",
|
||||
});
|
||||
db.Namespaces.Add(new Namespace
|
||||
{
|
||||
NamespaceId = "NS-EQ",
|
||||
ClusterId = "MAIN",
|
||||
Kind = NamespaceKind.Equipment,
|
||||
NamespaceUri = "urn:zb:eq",
|
||||
});
|
||||
db.DriverInstances.Add(new DriverInstance
|
||||
{
|
||||
DriverInstanceId = "DRV-EQ",
|
||||
ClusterId = "MAIN",
|
||||
NamespaceId = "NS-EQ",
|
||||
Name = "equipment driver",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = """{"endpoint":"10.0.0.1:502"}""",
|
||||
@@ -68,10 +60,6 @@ public sealed class UnsTreeServiceTagDriversTests
|
||||
|
||||
var drivers = await service.LoadTagDriversForEquipmentAsync("EQ-1");
|
||||
|
||||
drivers.Count.ShouldBe(1);
|
||||
drivers[0].DriverInstanceId.ShouldBe("DRV-EQ");
|
||||
drivers[0].DriverType.ShouldBe("Modbus");
|
||||
// The picker (Galaxy live-browse) opens its session against the selected driver's config.
|
||||
drivers[0].DriverConfig.ShouldBe("""{"endpoint":"10.0.0.1:502"}""");
|
||||
drivers.ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
@@ -8,39 +7,46 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the equipment-bound Tag CRUD mutations on <see cref="UnsTreeService"/>, including
|
||||
/// TagConfig JSON validity, the namespace-kind guard (tree tags must bind to an Equipment-kind
|
||||
/// namespace), the decision-#122 driver-cluster guard, duplicate-id / duplicate-name guards, and
|
||||
/// the driver-candidate loader scoped to the equipment's cluster.
|
||||
/// v3 Batch-1 migration of the former equipment-bound Tag CRUD tests. Per-equipment tag authoring
|
||||
/// was RETIRED: <see cref="Tag"/> is now Raw-only (no <c>EquipmentId</c>/<c>DriverInstanceId</c>/
|
||||
/// <c>FolderPath</c>), the <c>Namespace</c>/<c>NamespaceKind</c> entities were deleted, and tag
|
||||
/// authoring moved to the Raw tree (<c>/raw</c>) in Batch 2. So <see cref="UnsTreeService"/>'s
|
||||
/// <c>CreateTagAsync</c> / <c>UpdateTagAsync</c> now return a uniform Batch-1 refusal, and
|
||||
/// <c>LoadTagsForEquipmentAsync</c> / <c>LoadTagDriversForEquipmentAsync</c> return empty. The tests
|
||||
/// below assert that stub contract (preserving the intent of the old CRUD-guard / driver-loader
|
||||
/// tests) plus the still-live <c>DeleteTagAsync</c> path (which continues to remove a raw Tag row).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The EF InMemory provider does not enforce <c>RowVersion</c> concurrency, so the
|
||||
/// <c>DbUpdateConcurrencyException</c> branches are not exercised here by design.
|
||||
/// The former TagConfig-JSON-validity, decision-#122 driver-cluster guard, Equipment-kind-namespace
|
||||
/// guard, and duplicate-id/name guards moved with the authoring surface to the Raw tree — re-covered
|
||||
/// under the Batch-2 Raw-tree service tests.
|
||||
/// </remarks>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class UnsTreeServiceTagTests
|
||||
{
|
||||
/// <summary>The Batch-1 refusal message the retired per-equipment tag authoring now returns.</summary>
|
||||
private const string TagAuthoringMovedError = "Tag authoring moved to the Raw tree (/raw) in v3 Batch 2.";
|
||||
|
||||
private static (UnsTreeService Service, string DbName) Fresh()
|
||||
{
|
||||
var dbName = $"uns-tag-{Guid.NewGuid():N}";
|
||||
return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds an area→line→equipment path in <paramref name="equipmentCluster"/>. The equipment id is
|
||||
/// always <c>EQ-1</c>. Optionally seeds an Equipment-kind driver (<c>DRV-EQ</c>) in the equipment's
|
||||
/// cluster, a non-Equipment-kind (Simulated) driver (<c>DRV-SP</c>) in the equipment's cluster, and
|
||||
/// an Equipment-kind driver (<c>DRV-OTHER</c>) in <paramref name="otherCluster"/>.
|
||||
/// </summary>
|
||||
private static void SeedHierarchyAndDrivers(
|
||||
string dbName,
|
||||
string equipmentCluster,
|
||||
bool seedEquipmentDriver = false,
|
||||
bool seedNonEquipmentDriver = false,
|
||||
string? otherCluster = null)
|
||||
/// <summary>Seeds an area→line→equipment path in <paramref name="cluster"/> under equipment <c>EQ-1</c>.</summary>
|
||||
private static void SeedHierarchy(string dbName, string cluster = "MAIN")
|
||||
{
|
||||
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||
db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-1", ClusterId = equipmentCluster, Name = "a" });
|
||||
db.ServerClusters.Add(new ServerCluster
|
||||
{
|
||||
ClusterId = cluster,
|
||||
Name = cluster,
|
||||
Enterprise = "zb",
|
||||
Site = "s",
|
||||
RedundancyMode = RedundancyMode.None,
|
||||
CreatedBy = "test",
|
||||
});
|
||||
db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-1", ClusterId = cluster, Name = "a" });
|
||||
db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-1", UnsAreaId = "AREA-1", Name = "l" });
|
||||
db.Equipment.Add(new Equipment
|
||||
{
|
||||
@@ -50,303 +56,77 @@ public sealed class UnsTreeServiceTagTests
|
||||
Name = "machine-1",
|
||||
MachineCode = "machine_001",
|
||||
});
|
||||
|
||||
// Equipment-kind namespace in the equipment's cluster.
|
||||
db.Namespaces.Add(new Namespace
|
||||
{
|
||||
NamespaceId = "NS-EQ",
|
||||
ClusterId = equipmentCluster,
|
||||
Kind = NamespaceKind.Equipment,
|
||||
NamespaceUri = "urn:zb:eq",
|
||||
});
|
||||
// Non-Equipment-kind (Simulated) namespace in the equipment's cluster.
|
||||
db.Namespaces.Add(new Namespace
|
||||
{
|
||||
NamespaceId = "NS-SP",
|
||||
ClusterId = equipmentCluster,
|
||||
Kind = NamespaceKind.Simulated,
|
||||
NamespaceUri = "urn:zb:sp",
|
||||
});
|
||||
|
||||
if (seedEquipmentDriver)
|
||||
{
|
||||
db.DriverInstances.Add(new DriverInstance
|
||||
{
|
||||
DriverInstanceId = "DRV-EQ",
|
||||
ClusterId = equipmentCluster,
|
||||
NamespaceId = "NS-EQ",
|
||||
Name = "equipment driver",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
});
|
||||
}
|
||||
|
||||
if (seedNonEquipmentDriver)
|
||||
{
|
||||
db.DriverInstances.Add(new DriverInstance
|
||||
{
|
||||
DriverInstanceId = "DRV-SP",
|
||||
ClusterId = equipmentCluster,
|
||||
NamespaceId = "NS-SP",
|
||||
Name = "non-equipment driver",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
});
|
||||
}
|
||||
|
||||
if (otherCluster is not null)
|
||||
{
|
||||
// Equipment-kind namespace + driver in a different cluster.
|
||||
db.Namespaces.Add(new Namespace
|
||||
{
|
||||
NamespaceId = "NS-OTHER",
|
||||
ClusterId = otherCluster,
|
||||
Kind = NamespaceKind.Equipment,
|
||||
NamespaceUri = "urn:zb:other",
|
||||
});
|
||||
db.DriverInstances.Add(new DriverInstance
|
||||
{
|
||||
DriverInstanceId = "DRV-OTHER",
|
||||
ClusterId = otherCluster,
|
||||
NamespaceId = "NS-OTHER",
|
||||
Name = "other-cluster driver",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
});
|
||||
}
|
||||
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static TagInput Input(
|
||||
string tagId,
|
||||
string name,
|
||||
string driverInstanceId,
|
||||
string tagConfig = "{}") =>
|
||||
new(tagId, name, driverInstanceId, DataType: "Float",
|
||||
private static TagInput Input(string tagId, string name, string tagConfig = "{}") =>
|
||||
new(tagId, name, "DRV-EQ", DataType: "Float",
|
||||
AccessLevel: TagAccessLevel.Read, WriteIdempotent: false,
|
||||
PollGroupId: null, TagConfig: tagConfig);
|
||||
|
||||
// ----- CreateTag -----
|
||||
// ----- CreateTag / UpdateTag: retired → Batch-1 refusal -----
|
||||
|
||||
/// <summary>A valid equipment-bound tag persists with EquipmentId set and FolderPath null.</summary>
|
||||
/// <summary>Per-equipment tag creation is retired: the service returns the Batch-1 "moved to Raw tree" refusal.</summary>
|
||||
[Fact]
|
||||
public async Task CreateTag_equipment_bound_persists()
|
||||
public async Task CreateTag_is_retired_returns_batch1_stub()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
|
||||
SeedHierarchy(dbName);
|
||||
|
||||
var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ"));
|
||||
|
||||
result.Ok.ShouldBeTrue();
|
||||
result.Error.ShouldBeNull();
|
||||
|
||||
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||
var tag = db.Tags.Single(t => t.TagId == "TAG-1");
|
||||
tag.EquipmentId.ShouldBe("EQ-1");
|
||||
tag.FolderPath.ShouldBeNull();
|
||||
tag.DriverInstanceId.ShouldBe("DRV-EQ");
|
||||
tag.Name.ShouldBe("speed");
|
||||
tag.DataType.ShouldBe("Float");
|
||||
}
|
||||
|
||||
/// <summary>A tag with invalid TagConfig JSON is blocked.</summary>
|
||||
[Fact]
|
||||
public async Task CreateTag_invalid_json_blocked()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
|
||||
|
||||
var result = await service.CreateTagAsync(
|
||||
"EQ-1", Input("TAG-1", "speed", "DRV-EQ", tagConfig: "{ not json"));
|
||||
var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed"));
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Error.ShouldBe("TagConfig is not valid JSON.");
|
||||
result.Error.ShouldBe(TagAuthoringMovedError);
|
||||
|
||||
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||
db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>A tag with a null TagConfig is blocked with the friendly "not valid JSON" message
|
||||
/// (rather than an unhandled ArgumentNullException) — AdminUI-002.</summary>
|
||||
/// <summary>The refusal is uniform — it does not depend on equipment resolvability or TagConfig validity.</summary>
|
||||
[Fact]
|
||||
public async Task CreateTag_with_null_TagConfig_returns_friendly_error()
|
||||
public async Task CreateTag_refusal_is_uniform_even_for_unknown_equipment_or_bad_json()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
|
||||
SeedHierarchy(dbName);
|
||||
|
||||
var result = await service.CreateTagAsync(
|
||||
"EQ-1", Input("TAG-1", "speed", "DRV-EQ", tagConfig: null!));
|
||||
(await service.CreateTagAsync("EQ-NOPE", Input("TAG-1", "speed"))).Error
|
||||
.ShouldBe(TagAuthoringMovedError);
|
||||
(await service.CreateTagAsync("EQ-1", Input("TAG-2", "speed", tagConfig: "{ not json"))).Error
|
||||
.ShouldBe(TagAuthoringMovedError);
|
||||
}
|
||||
|
||||
/// <summary>Per-equipment tag update is retired: the service returns the Batch-1 "moved to Raw tree" refusal.</summary>
|
||||
[Fact]
|
||||
public async Task UpdateTag_is_retired_returns_batch1_stub()
|
||||
{
|
||||
var (service, _) = Fresh();
|
||||
|
||||
var result = await service.UpdateTagAsync("TAG-1", Input("TAG-1", "renamed"), []);
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Error.ShouldBe("TagConfig is not valid JSON.");
|
||||
|
||||
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||
db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse();
|
||||
result.Error.ShouldBe(TagAuthoringMovedError);
|
||||
}
|
||||
|
||||
/// <summary>A tag with a blank/whitespace TagConfig is blocked with the friendly message — AdminUI-002.</summary>
|
||||
[Fact]
|
||||
public async Task CreateTag_with_blank_TagConfig_returns_friendly_error()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
|
||||
// ----- DeleteTag: still live against a raw Tag row -----
|
||||
|
||||
var result = await service.CreateTagAsync(
|
||||
"EQ-1", Input("TAG-1", "speed", "DRV-EQ", tagConfig: " "));
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Error.ShouldBe("TagConfig is not valid JSON.");
|
||||
|
||||
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||
db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Binding a tag to a driver in a different cluster than the equipment is blocked (#122).</summary>
|
||||
[Fact]
|
||||
public async Task CreateTag_driver_in_other_cluster_blocked()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", otherCluster: "SITE-A");
|
||||
|
||||
var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-OTHER"));
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Error.ShouldNotBeNull();
|
||||
result.Error.ShouldContain("decision #122");
|
||||
result.Error.ShouldContain("DRV-OTHER");
|
||||
result.Error.ShouldContain("SITE-A");
|
||||
result.Error.ShouldContain("MAIN");
|
||||
|
||||
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||
db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Binding a tree tag to a driver in a non-Equipment-kind namespace is blocked.</summary>
|
||||
[Fact]
|
||||
public async Task CreateTag_driver_non_equipment_namespace_blocked()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedNonEquipmentDriver: true);
|
||||
|
||||
var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-SP"));
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Error.ShouldNotBeNull();
|
||||
result.Error.ShouldContain("DRV-SP");
|
||||
result.Error.ShouldContain("Equipment-kind namespace");
|
||||
|
||||
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||
db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Creating a tag with a TagId that already exists is blocked.</summary>
|
||||
[Fact]
|
||||
public async Task CreateTag_duplicate_tagid_blocked()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
|
||||
await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ"));
|
||||
|
||||
var result = await service.CreateTagAsync("EQ-1", Input("TAG-1", "another", "DRV-EQ"));
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Error.ShouldBe("Tag 'TAG-1' already exists.");
|
||||
}
|
||||
|
||||
/// <summary>Creating a tag for an equipment id that does not exist returns a not-found error.</summary>
|
||||
[Fact]
|
||||
public async Task CreateTag_unresolvable_equipment_returns_error()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
|
||||
|
||||
var result = await service.CreateTagAsync("EQ-NOPE", Input("TAG-1", "speed", "DRV-EQ"));
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Error.ShouldNotBeNull();
|
||||
result.Error.ShouldContain("not found");
|
||||
|
||||
using var db = UnsTreeTestDb.CreateNamed(dbName);
|
||||
db.Tags.Any(t => t.TagId == "TAG-1").ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Creating a tag whose Name already exists on the same equipment is blocked.</summary>
|
||||
[Fact]
|
||||
public async Task CreateTag_duplicate_name_on_equipment_blocked()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
|
||||
await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ"));
|
||||
|
||||
var result = await service.CreateTagAsync("EQ-1", Input("TAG-2", "speed", "DRV-EQ"));
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Error.ShouldBe("A tag named 'speed' already exists on this equipment.");
|
||||
}
|
||||
|
||||
// ----- UpdateTag -----
|
||||
|
||||
/// <summary>Updating a tag changes its mutable fields and keeps EquipmentId / FolderPath.</summary>
|
||||
[Fact]
|
||||
public async Task UpdateTag_changes_fields()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
|
||||
await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ"));
|
||||
|
||||
byte[] rv;
|
||||
using (var db = UnsTreeTestDb.CreateNamed(dbName))
|
||||
{
|
||||
rv = db.Tags.Single(t => t.TagId == "TAG-1").RowVersion;
|
||||
}
|
||||
|
||||
var updated = new TagInput("TAG-1", "renamed", "DRV-EQ", DataType: "Int32",
|
||||
AccessLevel: TagAccessLevel.ReadWrite, WriteIdempotent: true,
|
||||
PollGroupId: " ", TagConfig: """{ "register": 40001 }""");
|
||||
|
||||
var result = await service.UpdateTagAsync("TAG-1", updated, rv);
|
||||
|
||||
result.Ok.ShouldBeTrue();
|
||||
result.Error.ShouldBeNull();
|
||||
|
||||
using var verify = UnsTreeTestDb.CreateNamed(dbName);
|
||||
var after = verify.Tags.Single(t => t.TagId == "TAG-1");
|
||||
after.Name.ShouldBe("renamed");
|
||||
after.DataType.ShouldBe("Int32");
|
||||
after.AccessLevel.ShouldBe(TagAccessLevel.ReadWrite);
|
||||
after.WriteIdempotent.ShouldBeTrue();
|
||||
after.PollGroupId.ShouldBeNull(); // whitespace collapses to null
|
||||
after.EquipmentId.ShouldBe("EQ-1");
|
||||
after.FolderPath.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Updating a tag that no longer exists returns the row-gone error.</summary>
|
||||
[Fact]
|
||||
public async Task UpdateTag_missing_row_returns_error()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
|
||||
|
||||
var result = await service.UpdateTagAsync("TAG-nope", Input("TAG-nope", "x", "DRV-EQ"), []);
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Error.ShouldBe("Row no longer exists.");
|
||||
}
|
||||
|
||||
// ----- DeleteTag -----
|
||||
|
||||
/// <summary>Deleting a tag removes the row.</summary>
|
||||
/// <summary>Deleting a raw tag removes the row (DeleteTagAsync remains functional in Batch 1).</summary>
|
||||
[Fact]
|
||||
public async Task DeleteTag_removes_row()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
|
||||
await service.CreateTagAsync("EQ-1", Input("TAG-1", "speed", "DRV-EQ"));
|
||||
|
||||
byte[] rv;
|
||||
using (var db = UnsTreeTestDb.CreateNamed(dbName))
|
||||
{
|
||||
db.Devices.Add(new Device
|
||||
{
|
||||
DeviceId = "DEV-1", DriverInstanceId = "DRV-1", Name = "d", DeviceConfig = "{}",
|
||||
});
|
||||
db.Tags.Add(new Tag
|
||||
{
|
||||
TagId = "TAG-1", DeviceId = "DEV-1", Name = "speed", DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.Read, TagConfig = "{}",
|
||||
});
|
||||
db.SaveChanges();
|
||||
rv = db.Tags.Single(t => t.TagId == "TAG-1").RowVersion;
|
||||
}
|
||||
|
||||
@@ -371,40 +151,25 @@ public sealed class UnsTreeServiceTagTests
|
||||
result.Error.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ----- LoadTagDriversForEquipmentAsync -----
|
||||
// ----- Per-equipment loaders: retired → empty -----
|
||||
|
||||
/// <summary>
|
||||
/// The driver loader returns only Equipment-kind drivers in the equipment's cluster — excluding
|
||||
/// non-Equipment-kind drivers in the same cluster and Equipment-kind drivers in other clusters.
|
||||
/// </summary>
|
||||
/// <summary>The per-equipment tag list is retired (Tag is Raw-only) → always empty.</summary>
|
||||
[Fact]
|
||||
public async Task LoadTagDriversForEquipment_returns_only_equipment_kind_drivers_in_cluster()
|
||||
public async Task LoadTagsForEquipment_returns_empty()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(
|
||||
dbName,
|
||||
equipmentCluster: "MAIN",
|
||||
seedEquipmentDriver: true,
|
||||
seedNonEquipmentDriver: true,
|
||||
otherCluster: "SITE-A");
|
||||
SeedHierarchy(dbName);
|
||||
|
||||
var drivers = await service.LoadTagDriversForEquipmentAsync("EQ-1");
|
||||
|
||||
drivers.Count.ShouldBe(1);
|
||||
drivers[0].DriverInstanceId.ShouldBe("DRV-EQ");
|
||||
drivers[0].Display.ShouldContain("DRV-EQ");
|
||||
drivers[0].Display.ShouldContain("equipment driver");
|
||||
(await service.LoadTagsForEquipmentAsync("EQ-1")).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>An unresolvable equipment yields an empty driver list.</summary>
|
||||
/// <summary>The per-equipment candidate-driver loader is retired (equipment↔driver binding gone) → always empty.</summary>
|
||||
[Fact]
|
||||
public async Task LoadTagDriversForEquipment_unresolvable_equipment_returns_empty()
|
||||
public async Task LoadTagDriversForEquipment_returns_empty()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
SeedHierarchyAndDrivers(dbName, equipmentCluster: "MAIN", seedEquipmentDriver: true);
|
||||
SeedHierarchy(dbName);
|
||||
|
||||
var drivers = await service.LoadTagDriversForEquipmentAsync("EQ-nope");
|
||||
|
||||
drivers.ShouldBeEmpty();
|
||||
(await service.LoadTagDriversForEquipmentAsync("EQ-1")).ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
/// Shared in-memory fixture for <c>UnsTreeService</c> structural tests. Builds an
|
||||
/// <see cref="OtOpcUaConfigDbContext"/> over a named InMemory database and seeds a small,
|
||||
/// deterministic UNS hierarchy: enterprise "zb" across two clusters (MAIN populated,
|
||||
/// SITE-A intentionally empty), plus tags and a virtual tag on one equipment node so
|
||||
/// the per-equipment count joins can be exercised.
|
||||
/// SITE-A intentionally empty), a virtual tag on one equipment node (so the per-equipment
|
||||
/// virtual-tag count join can be exercised), plus a v3 Raw-tree slice
|
||||
/// (Driver→Device→Tag). Under v3, tags are Raw-only — they no longer bind to equipment — so
|
||||
/// the per-equipment driver-tag count is always zero; only the virtual-tag count join remains.
|
||||
/// </summary>
|
||||
internal static class UnsTreeTestDb
|
||||
{
|
||||
@@ -84,12 +86,22 @@ internal static class UnsTreeTestDb
|
||||
MachineCode = "machine_001",
|
||||
});
|
||||
|
||||
// Two driver tags + one virtual tag on the seeded equipment → ChildCount 3.
|
||||
// v3 Raw tree: a device → two raw tags. Tags are Raw-only and no longer bind to equipment,
|
||||
// so they do NOT contribute to any per-equipment count. The owning DriverInstance is left
|
||||
// unseeded on purpose — no structural test reads it, and seeding one would pollute the
|
||||
// per-cluster driver-list assertions in UnsTreeServiceLoadEditTests (EF InMemory does not
|
||||
// enforce the Device→Driver FK).
|
||||
db.Devices.Add(new Device
|
||||
{
|
||||
DeviceId = "DEV-1",
|
||||
DriverInstanceId = "DRV-1",
|
||||
Name = "device-1",
|
||||
DeviceConfig = "{}",
|
||||
});
|
||||
db.Tags.Add(new Tag
|
||||
{
|
||||
TagId = "TAG-1",
|
||||
DriverInstanceId = "DRV-1",
|
||||
EquipmentId = SeededEquipmentId,
|
||||
DeviceId = "DEV-1",
|
||||
Name = "speed",
|
||||
DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
@@ -98,24 +110,14 @@ internal static class UnsTreeTestDb
|
||||
db.Tags.Add(new Tag
|
||||
{
|
||||
TagId = "TAG-2",
|
||||
DriverInstanceId = "DRV-1",
|
||||
EquipmentId = SeededEquipmentId,
|
||||
DeviceId = "DEV-1",
|
||||
Name = "running",
|
||||
DataType = "Boolean",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
TagConfig = "{}",
|
||||
});
|
||||
// A tag with no equipment must be ignored by the count query.
|
||||
db.Tags.Add(new Tag
|
||||
{
|
||||
TagId = "TAG-ORPHAN",
|
||||
DriverInstanceId = "DRV-1",
|
||||
EquipmentId = null,
|
||||
Name = "orphan",
|
||||
DataType = "Int32",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
TagConfig = "{}",
|
||||
});
|
||||
|
||||
// One virtual tag on the seeded equipment → per-equipment virtual-tag ChildCount 1.
|
||||
db.VirtualTags.Add(new VirtualTag
|
||||
{
|
||||
VirtualTagId = "VTAG-1",
|
||||
|
||||
+14
-7
@@ -24,6 +24,14 @@ public sealed class VirtualTagEquipTokenValidationTests
|
||||
private const string EquipBaseScript = "return ctx.GetTag(\"{{equip}}.X\");";
|
||||
private const string PlainScript = "return ctx.GetTag(\"TestMachine_001.X\");";
|
||||
|
||||
// v3 Batch-1: the equipment↔Tag binding was retired (Tag is Raw-only), so the {{equip}} token's
|
||||
// equipment-tag-derived base can never resolve — ValidateEquipTokenAsync now derives from an empty
|
||||
// tag set, so any {{equip}} script is rejected. The token-present/no-base rejection and the
|
||||
// no-token success cases stay live below; the tests that assert a DERIVABLE base (success) or the
|
||||
// divergent-prefix derivation logic are dark until per-equipment tag references return in Batch 3.
|
||||
private const string DarkUntilBatch3 =
|
||||
"v3 Batch-1: {{equip}} equipment-tag-derived base is dark — per-equipment tag references return in Batch 3.";
|
||||
|
||||
private static (UnsTreeService Service, string DbName) Fresh()
|
||||
{
|
||||
var dbName = $"uns-equiptoken-{Guid.NewGuid():N}";
|
||||
@@ -58,11 +66,11 @@ public sealed class VirtualTagEquipTokenValidationTests
|
||||
});
|
||||
if (tagFullName is not null)
|
||||
{
|
||||
// v3: raw tag (no equipment binding). Kept so the dark {{equip}}-base tests still compile.
|
||||
db.Tags.Add(new Tag
|
||||
{
|
||||
TagId = "TAG-1",
|
||||
DriverInstanceId = "DRV-1",
|
||||
EquipmentId = "EQ-1",
|
||||
DeviceId = "DEV-1",
|
||||
Name = "x",
|
||||
DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
@@ -95,7 +103,7 @@ public sealed class VirtualTagEquipTokenValidationTests
|
||||
// ----- Create -----
|
||||
|
||||
/// <summary>{{equip}} script + a driver tag whose FullName gives a base → create succeeds.</summary>
|
||||
[Fact]
|
||||
[Fact(Skip = DarkUntilBatch3)]
|
||||
public async Task Create_equip_token_with_derivable_base_succeeds()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
@@ -142,7 +150,7 @@ public sealed class VirtualTagEquipTokenValidationTests
|
||||
/// {{equip}} script + TWO driver tags whose FullNames have DIFFERENT object prefixes
|
||||
/// (no single base can be derived) → create rejected, error names equipment + token.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Fact(Skip = DarkUntilBatch3)]
|
||||
public async Task Create_equip_token_with_divergent_prefixes_rejected()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
@@ -152,8 +160,7 @@ public sealed class VirtualTagEquipTokenValidationTests
|
||||
db.Tags.Add(new Tag
|
||||
{
|
||||
TagId = "TAG-2",
|
||||
DriverInstanceId = "DRV-1",
|
||||
EquipmentId = "EQ-1",
|
||||
DeviceId = "DEV-1",
|
||||
Name = "y",
|
||||
DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
@@ -176,7 +183,7 @@ public sealed class VirtualTagEquipTokenValidationTests
|
||||
// ----- Update -----
|
||||
|
||||
/// <summary>{{equip}} script + a derivable base → update succeeds.</summary>
|
||||
[Fact]
|
||||
[Fact(Skip = DarkUntilBatch3)]
|
||||
public async Task Update_equip_token_with_derivable_base_succeeds()
|
||||
{
|
||||
var (service, dbName) = Fresh();
|
||||
|
||||
Reference in New Issue
Block a user