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
{
///
/// OPC UA client helper for integration tests. Connects to a test server,
/// browses, reads, and subscribes to nodes programmatically.
///
internal class OpcUaTestClient : IDisposable
{
private Session? _session;
///
/// Gets the active OPC UA session used by integration tests once the helper has connected to the bridge.
///
public Session Session => _session ?? throw new InvalidOperationException("Not connected");
///
/// Resolves the namespace index for a given namespace URI (e.g., "urn:TestGalaxy:LmxOpcUa").
///
/// The Galaxy name whose OPC UA namespace should be resolved on the test server.
/// The namespace index assigned by the server for the requested Galaxy namespace.
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;
}
///
/// Creates a NodeId in the LmxOpcUa namespace using the server's actual namespace index.
///
/// The string identifier for the node inside the Galaxy namespace.
/// The Galaxy name whose namespace should be used for the node identifier.
/// A node identifier that targets the requested node on the test server.
public NodeId MakeNodeId(string identifier, string galaxyName = "TestGalaxy")
{
return new NodeId(identifier, GetNamespaceIndex(galaxyName));
}
///
/// Connects the helper to an OPC UA endpoint exposed by the test bridge.
///
/// The OPC UA endpoint URL to connect to.
/// The requested message security mode (default: None).
/// Optional username for authenticated connections.
/// Optional password for authenticated connections.
public async Task ConnectAsync(string endpointUrl,
MessageSecurityMode securityMode = MessageSecurityMode.None,
string? username = null, string? password = null)
{
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;
EndpointDescription endpoint;
if (securityMode != MessageSecurityMode.None)
{
// Ensure client certificate exists for secure connections
var app = new ApplicationInstance
{
ApplicationName = "OpcUaTestClient",
ApplicationType = ApplicationType.Client,
ApplicationConfiguration = config
};
await app.CheckApplicationInstanceCertificate(false, 2048);
// Discover and select endpoint matching the requested mode
endpoint = SelectEndpointByMode(endpointUrl, securityMode);
}
else
{
endpoint = CoreClientUtils.SelectEndpoint(config, endpointUrl, false);
}
var endpointConfig = EndpointConfiguration.Create(config);
var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfig);
UserIdentity identity = username != null
? new UserIdentity(username, password ?? "")
: new UserIdentity();
_session = await Session.Create(
config, configuredEndpoint, false,
"OpcUaTestClient", 30000, identity, null);
}
private static EndpointDescription SelectEndpointByMode(string endpointUrl, MessageSecurityMode mode)
{
using var client = DiscoveryClient.Create(new Uri(endpointUrl));
var endpoints = client.GetEndpoints(null);
foreach (var ep in endpoints)
{
if (ep.SecurityMode == mode && ep.SecurityPolicyUri == SecurityPolicies.Basic256Sha256)
{
ep.EndpointUrl = endpointUrl;
return ep;
}
}
// Fall back to any matching mode
foreach (var ep in endpoints)
{
if (ep.SecurityMode == mode)
{
ep.EndpointUrl = endpointUrl;
return ep;
}
}
throw new InvalidOperationException(
$"No endpoint with security mode {mode} found on {endpointUrl}");
}
///
/// Browse children of a node. Returns list of (DisplayName, NodeId, NodeClass).
///
/// The node whose hierarchical children should be browsed.
/// The child nodes exposed beneath the requested node.
public async Task> 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;
}
///
/// Read a node's value.
///
/// The node whose current value should be read from the server.
/// The OPC UA data value returned by the server.
public DataValue Read(NodeId nodeId)
{
return Session.ReadValue(nodeId);
}
///
/// Read a specific OPC UA attribute from a node.
///
/// The node whose attribute should be read.
/// The OPC UA attribute identifier to read.
/// The attribute value returned by the server.
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];
}
///
/// Write a node's value, optionally using an OPC UA index range for array element writes.
/// Returns the server status code for the write.
///
/// The node whose value should be written.
/// The value to send to the server.
/// An optional OPC UA index range used for array element writes.
/// The server status code returned for the write request.
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];
}
///
/// Create a subscription with a monitored item on the given node.
/// Returns the subscription and monitored item for inspection.
///
/// The node whose value changes should be monitored.
/// The publishing and sampling interval, in milliseconds, for the test subscription.
/// The created subscription and monitored item pair for later assertions and cleanup.
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);
}
///
/// Closes the test session and releases OPC UA client resources.
///
public void Dispose()
{
if (_session != null)
{
try { _session.Close(); }
catch { /* ignore */ }
_session.Dispose();
}
}
}
}