using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging.Abstractions; using Opc.Ua; using Opc.Ua.Client; using Opc.Ua.Configuration; 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; /// /// Source plan Task 60 — closes the audit gap. Boots two real /// instances on loopback, each configured with the other's ApplicationUri in /// . A real OPC UA client connects /// to Node A, reads Server.ServerArray, and asserts both URIs are visible — the /// warm-redundancy discovery contract clients depend on. /// public sealed class DualEndpointTests { private const string NodeAUri = "urn:OtOpcUa.DualEndpoint.NodeA"; private const string NodeBUri = "urn:OtOpcUa.DualEndpoint.NodeB"; [Fact] public async Task Client_reads_both_ApplicationUris_from_NodeA_ServerArray() { var pkiRootA = Path.Combine(Path.GetTempPath(), $"otopcua-pki-a-{Guid.NewGuid():N}"); var pkiRootB = Path.Combine(Path.GetTempPath(), $"otopcua-pki-b-{Guid.NewGuid():N}"); var portA = AllocateFreePort(); var portB = AllocateFreePort(); try { await using var nodeA = await StartNodeAsync(NodeAUri, portA, pkiRootA, peers: new[] { NodeBUri }); await using var nodeB = await StartNodeAsync(NodeBUri, portB, pkiRootB, peers: new[] { NodeAUri }); var serverArray = await ReadServerArrayAsync($"opc.tcp://127.0.0.1:{portA}/OtOpcUa"); serverArray.ShouldContain(NodeAUri); serverArray.ShouldContain(NodeBUri); } finally { if (Directory.Exists(pkiRootA)) Directory.Delete(pkiRootA, recursive: true); if (Directory.Exists(pkiRootB)) Directory.Delete(pkiRootB, recursive: true); } } private static async Task StartNodeAsync( string applicationUri, int port, string pkiRoot, string[] peers) { var options = new OpcUaApplicationHostOptions { ApplicationName = applicationUri, ApplicationUri = applicationUri, OpcUaPort = port, PublicHostname = "127.0.0.1", PkiStoreRoot = pkiRoot, EnabledSecurityProfiles = new List { OpcUaSecurityProfile.None }, AutoAcceptUntrustedClientCertificates = true, PeerApplicationUris = peers, }; var server = new StandardServer(); var host = new OpcUaApplicationHost(options, NullLogger.Instance); await host.StartAsync(server, CancellationToken.None); return host; } private static async Task ReadServerArrayAsync(string endpointUrl) { var appConfig = new ApplicationConfiguration { ApplicationName = "OtOpcUa.DualEndpointClient", ApplicationUri = $"urn:OtOpcUa.DualEndpointClient.{Guid.NewGuid():N}", ApplicationType = ApplicationType.Client, SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier(), AutoAcceptUntrustedCertificates = true, }, ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 }, }; // SDK 1.5.378 deprecates the no-arg ctors of CertificateValidator / DefaultSessionFactory // and the non-telemetry overloads of SelectEndpointAsync. Inject a no-op telemetry context // so the integration test (with TreatWarningsAsErrors=true) doesn't trip the CS0618 wall. var telemetry = DefaultTelemetry.Create(static _ => { }); await appConfig.ValidateAsync(ApplicationType.Client, CancellationToken.None); appConfig.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true; var endpoint = await CoreClientUtils.SelectEndpointAsync( appConfig, endpointUrl, useSecurity: false, discoverTimeout: 15_000, telemetry, CancellationToken.None); var endpointConfiguration = EndpointConfiguration.Create(appConfig); var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfiguration); var factory = new DefaultSessionFactory(telemetry); using var session = (ClientSession)await factory.CreateAsync( appConfig, configuredEndpoint, updateBeforeConnect: false, sessionName: "DualEndpointTests", sessionTimeout: 60_000, identity: new UserIdentity(new AnonymousIdentityToken()), preferredLocales: null, CancellationToken.None); var value = await session.ReadValueAsync(VariableIds.Server_ServerArray, CancellationToken.None); return (string[])value.Value; } private static int AllocateFreePort() { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; listener.Stop(); return port; } }