using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; /// /// Covers follow-up E projection: carries the equipment's /// DriverInstanceId / DeviceId bindings and the resolved DeviceHost (parsed from /// the bound 's schemaless DeviceConfig JSON via the shared /// ). A later task grafts a driver's discovered /// FixedTree onto a zero-tag equipment and partitions a multi-device driver by host using these. /// public sealed class AddressSpaceComposerDeviceHostTests { /// An equipment bound to a driver + a device whose config carries a top-level /// HostAddress resolves all three fields, with the host trimmed + lower-cased. [Fact] public void Compose_resolves_driver_device_and_device_host() { var equipment = new[] { NewEquipment("eq-1", driver: "d1", device: "dev1") }; var devices = new[] { NewDevice("dev1", "d1", "{\"HostAddress\":\"10.0.0.5:8193\"}") }; var node = AddressSpaceComposer.Compose( Array.Empty(), Array.Empty(), equipment, Array.Empty(), Array.Empty(), devices: devices) .EquipmentNodes.ShouldHaveSingleItem(); node.EquipmentId.ShouldBe("eq-1"); node.DriverInstanceId.ShouldBe("d1"); node.DeviceId.ShouldBe("dev1"); node.DeviceHost.ShouldBe("10.0.0.5:8193"); } /// An equipment with no driver and no device → all three new fields null (driver-less, /// no device to resolve a host from). [Fact] public void Compose_equipment_without_driver_or_device_yields_null_bindings() { var equipment = new[] { NewEquipment("eq-1", driver: null, device: null) }; var node = AddressSpaceComposer.Compose( Array.Empty(), Array.Empty(), equipment, Array.Empty(), Array.Empty(), devices: Array.Empty()) .EquipmentNodes.ShouldHaveSingleItem(); node.DriverInstanceId.ShouldBeNull(); node.DeviceId.ShouldBeNull(); node.DeviceHost.ShouldBeNull(); } /// A bound DeviceId with no matching device row, or a device whose config has no /// HostAddress, resolves DeviceHost to null while DeviceId is still carried. [Fact] public void Compose_device_host_is_null_when_unresolvable() { var equipment = new[] { NewEquipment("eq-missing", driver: "d1", device: "dev-missing"), NewEquipment("eq-nohost", driver: "d1", device: "dev-nohost"), }; var devices = new[] { NewDevice("dev-nohost", "d1", "{\"Port\":502}") }; var nodes = AddressSpaceComposer.Compose( Array.Empty(), Array.Empty(), equipment, Array.Empty(), Array.Empty(), devices: devices) .EquipmentNodes; var missing = nodes.Single(n => n.EquipmentId == "eq-missing"); missing.DeviceId.ShouldBe("dev-missing"); missing.DeviceHost.ShouldBeNull(); var noHost = nodes.Single(n => n.EquipmentId == "eq-nohost"); noHost.DeviceId.ShouldBe("dev-nohost"); noHost.DeviceHost.ShouldBeNull(); } /// The shared host extractor normalizes (trim + lower-case) and tolerates every malformed /// shape (blank / non-object / no string HostAddress / blank value / non-JSON) by returning null. [Theory] [InlineData("{\"HostAddress\":\"10.201.31.5:8193\"}", "10.201.31.5:8193")] [InlineData("{\"HostAddress\":\" HOST-A:8193 \"}", "host-a:8193")] // trimmed + lower-cased [InlineData("{\"HostAddress\":\"\"}", null)] // blank value [InlineData("{\"HostAddress\":1234}", null)] // non-string [InlineData("{\"Port\":502}", null)] // absent [InlineData("[]", null)] // non-object root [InlineData("not json", null)] // malformed [InlineData("", null)] // blank public void TryExtractDeviceHost_normalizes_and_tolerates(string? deviceConfig, string? expected) { AddressSpaceComposer.TryExtractDeviceHost(deviceConfig).ShouldBe(expected); } /// The extracted-out shared normalizer (the single source of truth the FixedTree-partition path /// reuses on a driver-discovered device-host folder segment) trims + lower-cases, and is idempotent on an /// already-normalized value — so a segment like " HOST-A:8193 " matches a stored /// "host-a:8193" DeviceHost. [Theory] [InlineData("10.201.31.5:8193", "10.201.31.5:8193")] [InlineData(" HOST-A:8193 ", "host-a:8193")] [InlineData("host-a:8193", "host-a:8193")] // idempotent [InlineData("H1", "h1")] public void NormalizeDeviceHost_trims_and_lowercases(string raw, string expected) { AddressSpaceComposer.NormalizeDeviceHost(raw).ShouldBe(expected); } private static Equipment NewEquipment(string id, string? driver, string? device) => new() { EquipmentId = id, DriverInstanceId = driver, DeviceId = device, UnsLineId = "line-1", Name = id, MachineCode = id.ToUpperInvariant(), }; private static Device NewDevice(string deviceId, string driverInstanceId, string deviceConfig) => new() { DeviceId = deviceId, DriverInstanceId = driverInstanceId, Name = deviceId, DeviceConfig = deviceConfig, }; }