using System.Text.Json; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Commons.Tests; /// /// v3 (B1-WP4) identity — the shared must build a raw tag's RawPath by /// walking RawFolder → Driver → Device → TagGroup ancestry, and /// must fold the endpoint + RawTags into the single driver config. This is the byte-parity authority /// both the deploy artifact and the draft validator compute against. /// public sealed class RawPathResolverTests { private static RawPathResolver Nested() => new( folders: new Dictionary { ["f1"] = (null, "Plant"), ["f2"] = ("f1", "Line1"), }, drivers: new Dictionary { ["drv1"] = ("f2", "Modbus1"), ["drvRoot"] = (null, "S7Root"), }, devices: new Dictionary { ["dev1"] = ("drv1", "PLC-A"), ["devRoot"] = ("drvRoot", "PLC-B"), }, groups: new Dictionary { ["g1"] = (null, "Motors"), ["g2"] = ("g1", "M1"), }); [Fact] public void TryBuildTagPath_walks_folder_driver_device_group_ancestry() { Nested().TryBuildTagPath("dev1", "g2", "Speed") .ShouldBe("Plant/Line1/Modbus1/PLC-A/Motors/M1/Speed"); } [Fact] public void TryBuildTagPath_device_root_tag_has_no_group_segments() { Nested().TryBuildTagPath("dev1", null, "Status") .ShouldBe("Plant/Line1/Modbus1/PLC-A/Status"); } [Fact] public void TryBuildTagPath_cluster_root_driver_has_no_folder_segments() { Nested().TryBuildTagPath("devRoot", null, "Run") .ShouldBe("S7Root/PLC-B/Run"); } [Fact] public void TryBuildTagPath_unknown_device_returns_null() { Nested().TryBuildTagPath("ghost", null, "X").ShouldBeNull(); } [Fact] public void TryBuildDriverPrefix_is_folders_plus_driver_name() { Nested().TryBuildDriverPrefix("drv1").ShouldBe("Plant/Line1/Modbus1"); } [Fact] public void Merge_single_device_folds_endpoint_up_and_injects_rawtags() { var merged = DriverDeviceConfigMerger.Merge( driverConfigJson: "{\"UnitId\":3}", devices: new[] { new DriverDeviceConfigMerger.DeviceRow("PLC-A", "{\"Host\":\"10.0.0.5\",\"Port\":502}") }, rawTags: new[] { new RawTagEntry("Plant/Modbus1/PLC-A/Speed", "{\"address\":\"40001\"}", true, "PLC-A") }); using var doc = JsonDocument.Parse(merged); var root = doc.RootElement; root.GetProperty("UnitId").GetInt32().ShouldBe(3); root.GetProperty("Host").GetString().ShouldBe("10.0.0.5"); // single-device endpoint merged up root.GetProperty("Port").GetInt32().ShouldBe(502); var rawTags = root.GetProperty("RawTags").EnumerateArray().ToList(); rawTags.ShouldHaveSingleItem(); rawTags[0].GetProperty("RawPath").GetString().ShouldBe("Plant/Modbus1/PLC-A/Speed"); var devices = root.GetProperty("Devices").EnumerateArray().ToList(); devices.ShouldHaveSingleItem(); devices[0].GetProperty("DeviceName").GetString().ShouldBe("PLC-A"); } [Fact] public void Merge_multi_device_reconstructs_devices_array_with_names() { var merged = DriverDeviceConfigMerger.Merge( driverConfigJson: "{\"Series\":\"0i\"}", devices: new[] { new DriverDeviceConfigMerger.DeviceRow("PLC-A", "{\"HostAddress\":\"10.0.0.5:8193\"}"), new DriverDeviceConfigMerger.DeviceRow("PLC-B", "{\"HostAddress\":\"10.0.0.6:8193\"}"), }, rawTags: Array.Empty()); using var doc = JsonDocument.Parse(merged); var devices = doc.RootElement.GetProperty("Devices").EnumerateArray().ToList(); devices.Count.ShouldBe(2); devices.Select(d => d.GetProperty("DeviceName").GetString()).ShouldBe(new[] { "PLC-A", "PLC-B" }); devices[0].GetProperty("HostAddress").GetString().ShouldBe("10.0.0.5:8193"); // Multiple devices: no top-level endpoint merge-up (per-device only). doc.RootElement.TryGetProperty("HostAddress", out _).ShouldBeFalse(); } }