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();
}
}