using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests; /// /// Gitea #58 live round-trip: a codec-encoded String scalar written to a numeric / Boolean /// node of the infra OPC PLC server (opc.tcp://localhost:50000, see /// infra/docker-compose.yml + infra/opcua/nodes.json) is coerced to the node's /// DataType and reads back as that type. Before the fix the server answered /// BadTypeMismatch (0x80740000). Skips when the server is unreachable. /// [Trait("Category", "RequiresOpcUa")] public class OpcUaWriteCoercionLiveTests { private const string EndpointUrl = "opc.tcp://localhost:50000"; // Writable, non-simulated nodes from infra/opcua/nodes.json (namespace http://microsoft.com/Opc/OpcPlc/). private const string DoubleNode = "nsu=http://microsoft.com/Opc/OpcPlc/;s=Motor.Speed"; private const string BooleanNode = "nsu=http://microsoft.com/Opc/OpcPlc/;s=Motor.Running"; private const string UInt32Node = "nsu=http://microsoft.com/Opc/OpcPlc/;s=Motor.FaultCode"; [SkippableFact] public async Task String_writes_to_typed_nodes_are_coerced_and_read_back_typed() { await using var client = new RealOpcUaClient(); using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30)); try { await client.ConnectAsync(EndpointUrl, new OpcUaConnectionOptions(OperationTimeoutMs: 5000), deadline.Token); } catch (Exception ex) { Skip.If(true, $"OPC UA test server unavailable: {ex.Message}"); return; } // Two distinct values per node so the read-back cannot pass on a stale value. foreach (var (speed, running, fault) in new[] { ("1234.5", "True", "17"), ("42.25", "False", "0") }) { var outcomes = await client.WriteValuesAsync( [(DoubleNode, speed), (BooleanNode, running), (UInt32Node, fault)], deadline.Token); Assert.All(outcomes, o => { Assert.Null(o.Error); Assert.Equal(0u, o.StatusCode); }); var reads = await client.ReadValuesAsync([DoubleNode, BooleanNode, UInt32Node], deadline.Token); Assert.Equal(double.Parse(speed, System.Globalization.CultureInfo.InvariantCulture), Assert.IsType(reads[0].Value)); Assert.Equal(bool.Parse(running), Assert.IsType(reads[1].Value)); Assert.Equal(uint.Parse(fault, System.Globalization.CultureInfo.InvariantCulture), Assert.IsType(reads[2].Value)); } // A value that does not fit the node type is refused client-side with a clear error, // and the node keeps its previous value. var bad = await client.WriteValuesAsync([(UInt32Node, "-1"), (DoubleNode, "abc")], deadline.Token); Assert.All(bad, o => { Assert.Equal(Opc.Ua.StatusCodes.BadTypeMismatch, o.StatusCode); Assert.Contains("Cannot convert", o.Error); }); var after = await client.ReadValuesAsync([UInt32Node, DoubleNode], deadline.Token); Assert.Equal(0u, after[0].Value); Assert.Equal(42.25, after[1].Value); await client.DisconnectAsync(); } }