Files
lmxopcua/tests/ZB.MOM.WW.LmxOpcUa.Tests/Helpers/OpcUaTestClient.cs
2026-03-25 12:57:05 -04:00

230 lines
9.6 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
{
/// <summary>
/// OPC UA client helper for integration tests. Connects to a test server,
/// browses, reads, and subscribes to nodes programmatically.
/// </summary>
internal class OpcUaTestClient : IDisposable
{
private Session? _session;
/// <summary>
/// Gets the active OPC UA session used by integration tests once the helper has connected to the bridge.
/// </summary>
public Session Session => _session ?? throw new InvalidOperationException("Not connected");
/// <summary>
/// Resolves the namespace index for a given namespace URI (e.g., "urn:TestGalaxy:LmxOpcUa").
/// </summary>
/// <param name="galaxyName">The Galaxy name whose OPC UA namespace should be resolved on the test server.</param>
/// <returns>The namespace index assigned by the server for the requested Galaxy namespace.</returns>
public ushort GetNamespaceIndex(string galaxyName = "TestGalaxy")
{
var nsUri = $"urn:{galaxyName}:LmxOpcUa";
var idx = Session.NamespaceUris.GetIndex(nsUri);
if (idx < 0) throw new InvalidOperationException($"Namespace '{nsUri}' not found on server");
return (ushort)idx;
}
/// <summary>
/// Creates a NodeId in the LmxOpcUa namespace using the server's actual namespace index.
/// </summary>
/// <param name="identifier">The string identifier for the node inside the Galaxy namespace.</param>
/// <param name="galaxyName">The Galaxy name whose namespace should be used for the node identifier.</param>
/// <returns>A node identifier that targets the requested node on the test server.</returns>
public NodeId MakeNodeId(string identifier, string galaxyName = "TestGalaxy")
{
return new NodeId(identifier, GetNamespaceIndex(galaxyName));
}
/// <summary>
/// Connects the helper to an OPC UA endpoint exposed by the test bridge.
/// </summary>
/// <param name="endpointUrl">The OPC UA endpoint URL to connect to.</param>
public async Task ConnectAsync(string endpointUrl)
{
var config = new ApplicationConfiguration
{
ApplicationName = "OpcUaTestClient",
ApplicationUri = "urn:localhost:OpcUaTestClient",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(Path.GetTempPath(), "OpcUaTestClient", "pki", "own")
},
TrustedIssuerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(Path.GetTempPath(), "OpcUaTestClient", "pki", "issuer")
},
TrustedPeerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(Path.GetTempPath(), "OpcUaTestClient", "pki", "trusted")
},
RejectedCertificateStore = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(Path.GetTempPath(), "OpcUaTestClient", "pki", "rejected")
},
AutoAcceptUntrustedCertificates = true
},
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 30000 },
TransportQuotas = new TransportQuotas()
};
await config.Validate(ApplicationType.Client);
config.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
var endpoint = CoreClientUtils.SelectEndpoint(config, endpointUrl, false);
var endpointConfig = EndpointConfiguration.Create(config);
var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfig);
_session = await Session.Create(
config, configuredEndpoint, false,
"OpcUaTestClient", 30000, null, null);
}
/// <summary>
/// Browse children of a node. Returns list of (DisplayName, NodeId, NodeClass).
/// </summary>
/// <param name="nodeId">The node whose hierarchical children should be browsed.</param>
/// <returns>The child nodes exposed beneath the requested node.</returns>
public async Task<List<(string Name, NodeId NodeId, NodeClass NodeClass)>> BrowseAsync(NodeId nodeId)
{
var results = new List<(string, NodeId, NodeClass)>();
var browser = new Browser(Session)
{
NodeClassMask = (int)NodeClass.Object | (int)NodeClass.Variable,
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
IncludeSubtypes = true,
BrowseDirection = BrowseDirection.Forward
};
var refs = browser.Browse(nodeId);
foreach (var rd in refs)
{
results.Add((rd.DisplayName.Text, ExpandedNodeId.ToNodeId(rd.NodeId, Session.NamespaceUris), rd.NodeClass));
}
return results;
}
/// <summary>
/// Read a node's value.
/// </summary>
/// <param name="nodeId">The node whose current value should be read from the server.</param>
/// <returns>The OPC UA data value returned by the server.</returns>
public DataValue Read(NodeId nodeId)
{
return Session.ReadValue(nodeId);
}
/// <summary>
/// Read a specific OPC UA attribute from a node.
/// </summary>
/// <param name="nodeId">The node whose attribute should be read.</param>
/// <param name="attributeId">The OPC UA attribute identifier to read.</param>
/// <returns>The attribute value returned by the server.</returns>
public DataValue ReadAttribute(NodeId nodeId, uint attributeId)
{
var nodesToRead = new ReadValueIdCollection
{
new ReadValueId
{
NodeId = nodeId,
AttributeId = attributeId
}
};
Session.Read(
null,
0,
TimestampsToReturn.Neither,
nodesToRead,
out var results,
out _);
return results[0];
}
/// <summary>
/// Write a node's value, optionally using an OPC UA index range for array element writes.
/// Returns the server status code for the write.
/// </summary>
/// <param name="nodeId">The node whose value should be written.</param>
/// <param name="value">The value to send to the server.</param>
/// <param name="indexRange">An optional OPC UA index range used for array element writes.</param>
/// <returns>The server status code returned for the write request.</returns>
public StatusCode Write(NodeId nodeId, object value, string? indexRange = null)
{
var nodesToWrite = new WriteValueCollection
{
new WriteValue
{
NodeId = nodeId,
AttributeId = Attributes.Value,
IndexRange = indexRange,
Value = new DataValue(new Variant(value))
}
};
Session.Write(null, nodesToWrite, out var results, out _);
return results[0];
}
/// <summary>
/// Create a subscription with a monitored item on the given node.
/// Returns the subscription and monitored item for inspection.
/// </summary>
/// <param name="nodeId">The node whose value changes should be monitored.</param>
/// <param name="intervalMs">The publishing and sampling interval, in milliseconds, for the test subscription.</param>
/// <returns>The created subscription and monitored item pair for later assertions and cleanup.</returns>
public async Task<(Subscription Sub, MonitoredItem Item)> SubscribeAsync(
NodeId nodeId, int intervalMs = 250)
{
var subscription = new Subscription(Session.DefaultSubscription)
{
PublishingInterval = intervalMs,
DisplayName = "TestSubscription"
};
var item = new MonitoredItem(subscription.DefaultItem)
{
StartNodeId = nodeId,
DisplayName = nodeId.ToString(),
SamplingInterval = intervalMs
};
subscription.AddItem(item);
Session.AddSubscription(subscription);
await subscription.CreateAsync();
return (subscription, item);
}
/// <summary>
/// Closes the test session and releases OPC UA client resources.
/// </summary>
public void Dispose()
{
if (_session != null)
{
try { _session.Close(); }
catch { /* ignore */ }
_session.Dispose();
}
}
}
}