fix(opcua): migrate OPC UA server to Opc.Ua SDK 1.5.378 (resolves startup TypeLoadException)
v2-ci / build (push) Failing after 47s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

Opc.Ua.Server was pinned 1.5.374.126 while Client/Configuration were 1.5.378.106, so the
published Host unified Opc.Ua.Core to 1.5.378 (which dropped Opc.Ua.INodeIdFactory that Server
1.5.374 referenced). Every driver-role node (and the fused site nodes) failed to start the OPC
UA server with TypeLoadException, leaving the OPC data plane dead and the site UIs at 503.

Align all OPC UA packages to 1.5.378.106 (bump Server; drop the Opc.Ua.Configuration/Client
VersionOverrides in OpcUaServer + its integration tests) and port the server host to the
1.5.378 async API:
- ApplicationInstance requires an ITelemetryContext ctor (DefaultTelemetry.Create)
- Start/Stop/LoadApplicationConfiguration/Validate -> async; CheckApplicationInstanceCertificate
  -> CheckApplicationInstanceCertificatesAsync
- ImpersonateEventHandler is now (ISession, ImpersonateEventArgs)
- UserNameIdentityToken.DecryptedPassword is now byte[] (UTF-8 decode)
- tests ported (byte[] passwords; async discovery/session/read client API)

Verified: full solution builds, OpcUaServer unit tests 52/52, and in docker-dev all six OPC
endpoints (4840-4845) listen and the site UIs return 302 (were 503). End-to-end OPC behaviour
(read/write/subscribe/security under 1.5.378) still needs a functional client test.
This commit is contained in:
Joseph Doherty
2026-06-04 12:56:18 -04:00
parent c3ae458a95
commit fae960c157
6 changed files with 48 additions and 34 deletions
@@ -7,7 +7,6 @@ using Opc.Ua.Server;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ClientSession = Opc.Ua.Client.Session;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
@@ -85,23 +84,27 @@ public sealed class DualEndpointTests
},
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 },
};
await appConfig.Validate(ApplicationType.Client);
await appConfig.ValidateAsync(ApplicationType.Client, default);
appConfig.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
var endpoint = CoreClientUtils.SelectEndpoint(appConfig, endpointUrl, useSecurity: false);
// 1.5.378: the discovery/session/read client surface moved to async.
var endpoint = await CoreClientUtils.SelectEndpointAsync(
appConfig, endpointUrl, false, DefaultTelemetry.Create(_ => { }), default);
var endpointConfiguration = EndpointConfiguration.Create(appConfig);
var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfiguration);
using var session = await ClientSession.Create(
using var session = await new DefaultSessionFactory(DefaultTelemetry.Create(_ => { })).CreateAsync(
appConfig,
configuredEndpoint,
updateBeforeConnect: false,
checkDomain: false,
sessionName: "DualEndpointTests",
sessionTimeout: 60_000,
identity: new UserIdentity(new AnonymousIdentityToken()),
preferredLocales: null);
preferredLocales: null,
ct: default);
var value = session.ReadValue(VariableIds.Server_ServerArray);
var value = await session.ReadValueAsync(VariableIds.Server_ServerArray, default);
return (string[])value.Value;
}
@@ -11,12 +11,11 @@
<PackageReference Include="xunit.v3"/>
<PackageReference Include="Shouldly"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<!-- Pin to 1.5.374.126 to match the server src (OpcUaServer.csproj). Mixing transitive
Opc.Ua.Core across 1.5.374 / 1.5.378 produces a MissingMethodException at runtime
because ApplicationInstance.Start(ServerBase) (sync) was removed in 1.5.378 in
favour of StartAsync. Stays here until Opc.Ua.Server publishes 1.5.378.x. -->
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" VersionOverride="1.5.374.126"/>
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Configuration" VersionOverride="1.5.374.126"/>
<!-- OPC UA packages aligned at the central 1.5.378.106 (Opc.Ua.Server 1.5.378.x is now
published), so the prior VersionOverride to 1.5.374.126 is removed. The server src moved
ApplicationInstance.Start(ServerBase) → StartAsync (and the rest of the 1.5.378 async API). -->
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client"/>
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Configuration"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions"/>
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
@@ -1,3 +1,4 @@
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Server;
@@ -21,7 +22,7 @@ public sealed class OpcUaApplicationHostImpersonationTests
[Fact]
public void HandleImpersonation_username_success_sets_identity_and_no_validation_error()
{
var token = new UserNameIdentityToken { UserName = "alice", DecryptedPassword = "secret" };
var token = new UserNameIdentityToken { UserName = "alice", DecryptedPassword = Encoding.UTF8.GetBytes("secret") };
var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription());
var authenticator = new RecordingAuthenticator(
OpcUaUserAuthResult.Allow("Alice", new[] { "ReadOnly", "WriteOperate" }));
@@ -38,7 +39,7 @@ public sealed class OpcUaApplicationHostImpersonationTests
[Fact]
public void HandleImpersonation_username_denial_sets_validation_error_and_no_identity()
{
var token = new UserNameIdentityToken { UserName = "mallory", DecryptedPassword = "wrong" };
var token = new UserNameIdentityToken { UserName = "mallory", DecryptedPassword = Encoding.UTF8.GetBytes("wrong") };
var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription());
var authenticator = new RecordingAuthenticator(OpcUaUserAuthResult.Deny("Invalid credentials"));
@@ -68,7 +69,7 @@ public sealed class OpcUaApplicationHostImpersonationTests
[Fact]
public void HandleImpersonation_authenticator_throwing_results_in_rejection()
{
var token = new UserNameIdentityToken { UserName = "bob", DecryptedPassword = "x" };
var token = new UserNameIdentityToken { UserName = "bob", DecryptedPassword = Encoding.UTF8.GetBytes("x") };
var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription());
var authenticator = new ThrowingAuthenticator(new InvalidOperationException("LDAP unreachable"));
@@ -82,7 +83,7 @@ public sealed class OpcUaApplicationHostImpersonationTests
[Fact]
public void HandleImpersonation_null_username_treated_as_empty_string()
{
var token = new UserNameIdentityToken { UserName = null, DecryptedPassword = "abc" };
var token = new UserNameIdentityToken { UserName = null, DecryptedPassword = Encoding.UTF8.GetBytes("abc") };
var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription());
var authenticator = new RecordingAuthenticator(OpcUaUserAuthResult.Deny("no user"));