f127f45428
v2-ci / unit-tests (push) Has been cancelled
OpcUaApplicationHost left ServerConfiguration.MaxRegistrationInterval at the SDK default (30000) with a null RegistrationEndpoint, so SDK 2.0.1's StandardServer armed its registration timer against opc.tcp://localhost:4840, which on an OtOpcUa node is the server itself (no LDS). It answered BadServiceUnsupported and the SDK retried forever, visible since #874. Registration is now opt-in: ApplyDiscoveryRegistration sets MaxRegistrationInterval = 0 (the value that keeps StandardServer from arming the timer) unless OpcUa:DiscoveryServerUrl is set, in which case the server registers against that URL every OpcUa:DiscoveryRegistrationIntervalMs (default 30000) over SignAndEncrypt/Basic256Sha256, like the SDK's own default endpoint. The validator requires an absolute opc.tcp:// URL and a positive interval when the URL is set. Documented in Configuration.md and OpcUaServer.md. Co-Authored-By: Claude <noreply@anthropic.com>
122 lines
5.0 KiB
C#
122 lines
5.0 KiB
C#
using System.Reflection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Server;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
|
|
|
/// <summary>
|
|
/// lmxopcua#876 — discovery-server (LDS) registration is opt-in. The SDK default (<c>MaxRegistrationInterval</c>
|
|
/// 30000 with a null <c>RegistrationEndpoint</c>) registers every 30 s with <c>opc.tcp://localhost:4840</c>, which on
|
|
/// an OtOpcUa node is the server itself: it answered <c>BadServiceUnsupported</c> and the SDK retried forever.
|
|
/// </summary>
|
|
public sealed class OpcUaApplicationHostDiscoveryRegistrationTests : IDisposable
|
|
{
|
|
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
|
|
|
private readonly string _pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-lds-{Guid.NewGuid():N}");
|
|
|
|
public void Dispose()
|
|
{
|
|
try { if (Directory.Exists(_pkiRoot)) Directory.Delete(_pkiRoot, true); }
|
|
catch { /* best effort */ }
|
|
}
|
|
|
|
[Fact]
|
|
public void Default_options_disable_registration()
|
|
{
|
|
var config = new ServerConfiguration();
|
|
config.MaxRegistrationInterval.ShouldBeGreaterThan(0, "precondition: the SDK default registers");
|
|
|
|
OpcUaApplicationHost.ApplyDiscoveryRegistration(config, new OpcUaApplicationHostOptions());
|
|
|
|
config.MaxRegistrationInterval.ShouldBe(0);
|
|
config.RegistrationEndpoint.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Configured_discovery_server_is_registered_with_at_the_configured_interval()
|
|
{
|
|
var config = new ServerConfiguration();
|
|
|
|
OpcUaApplicationHost.ApplyDiscoveryRegistration(config, new OpcUaApplicationHostOptions
|
|
{
|
|
DiscoveryServerUrl = " opc.tcp://lds-host:4840 ", DiscoveryRegistrationIntervalMs = 45_000,
|
|
});
|
|
|
|
config.MaxRegistrationInterval.ShouldBe(45_000);
|
|
config.RegistrationEndpoint.ShouldNotBeNull();
|
|
config.RegistrationEndpoint!.EndpointUrl.ShouldBe("opc.tcp://lds-host:4840");
|
|
config.RegistrationEndpoint.Server.ApplicationType.ShouldBe(ApplicationType.DiscoveryServer);
|
|
}
|
|
|
|
/// <summary>The real build path: a host started with default options builds a non-registering configuration and
|
|
/// the started SDK server never arms its registration timer.</summary>
|
|
[Fact]
|
|
public async Task Started_host_with_default_options_does_not_register()
|
|
{
|
|
var (host, server) = await StartAsync(o => { });
|
|
try
|
|
{
|
|
var serverConfig = host.ApplicationInstance!.ApplicationConfiguration!.ServerConfiguration!;
|
|
serverConfig.MaxRegistrationInterval.ShouldBe(0);
|
|
serverConfig.RegistrationEndpoint.ShouldBeNull();
|
|
RegistrationTimer(server).ShouldBeNull("the SDK must not arm its RegisterServer2 timer");
|
|
}
|
|
finally
|
|
{
|
|
await host.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Started_host_with_a_discovery_server_registers_with_it()
|
|
{
|
|
var ldsUrl = $"opc.tcp://127.0.0.1:{OpcUaHostStart.AllocateFreePort()}";
|
|
var (host, server) = await StartAsync(o =>
|
|
{
|
|
o.DiscoveryServerUrl = ldsUrl;
|
|
o.DiscoveryRegistrationIntervalMs = 60_000;
|
|
});
|
|
try
|
|
{
|
|
var serverConfig = host.ApplicationInstance!.ApplicationConfiguration!.ServerConfiguration!;
|
|
serverConfig.MaxRegistrationInterval.ShouldBe(60_000);
|
|
serverConfig.RegistrationEndpoint!.EndpointUrl.ShouldBe(ldsUrl);
|
|
RegistrationTimer(server).ShouldNotBeNull("positive control: a configured discovery server arms the timer");
|
|
}
|
|
finally
|
|
{
|
|
await host.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
private async Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> StartAsync(Action<OpcUaApplicationHostOptions> configure) =>
|
|
await OpcUaHostStart.StartAsync(
|
|
port =>
|
|
{
|
|
var options = new OpcUaApplicationHostOptions
|
|
{
|
|
ApplicationName = "OtOpcUa.DiscoveryRegistrationTest",
|
|
ApplicationUri = $"urn:OtOpcUa.DiscoveryRegistrationTest:{Guid.NewGuid():N}",
|
|
OpcUaPort = port,
|
|
PublicHostname = "localhost",
|
|
PkiStoreRoot = _pkiRoot,
|
|
};
|
|
configure(options);
|
|
return new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
|
|
},
|
|
() => new OtOpcUaSdkServer(), Ct);
|
|
|
|
/// <summary>The SDK 2.0.1 <c>StandardServer.m_registrationTimer</c>, created only when
|
|
/// <c>MaxRegistrationInterval > 0</c>.</summary>
|
|
private static object? RegistrationTimer(StandardServer server)
|
|
{
|
|
var field = typeof(StandardServer).GetField("m_registrationTimer", BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?? throw new InvalidOperationException("StandardServer.m_registrationTimer not found — SDK internals changed; re-check #876.");
|
|
return field.GetValue(server);
|
|
}
|
|
}
|