using System.Net; using System.Net.Sockets; using Akka.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Authorization; using ZB.MOM.WW.OtOpcUa.Host.OpcUa; using ZB.MOM.WW.OtOpcUa.OpcUaServer; using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security; namespace ZB.MOM.WW.OtOpcUa.Host.Tests.OpcUa; public sealed class OtOpcUaServerHostedServiceBindingFailureTests { [Fact] public async Task Binding_failure_closes_started_endpoint_and_allows_other_host_services_to_start() { using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(15)); using var probe = new TcpListener(IPAddress.Loopback, 0); probe.Start(); var port = ((IPEndPoint)probe.LocalEndpoint).Port; probe.Stop(); var pki = Path.Combine(Path.GetTempPath(), $"otopcua-binding-{Guid.NewGuid():N}"); var deferred = new DeferredServiceLevelPublisher(); deferred.Publish(0); var bindingReached = false; var listenerObserved = false; OtOpcUaServerHostedService? service = null; var factory = new FaultInjectingLoggerFactory(() => { using var connected = new TcpClient(); connected.Connect(IPAddress.Loopback, port); listenerObserved = connected.Connected; bindingReached = true; throw new InvalidOperationException("controlled binding factory failure"); }); service = new OtOpcUaServerHostedService( Options.Create(new OpcUaApplicationHostOptions { ApplicationName = "OtOpcUa.BindingFailure", ApplicationUri = $"urn:OtOpcUa.BindingFailure:{Guid.NewGuid():N}", OpcUaPort = port, PublicHostname = "localhost", PkiStoreRoot = pki, }), new DeferredAddressSpaceSink(), deferred, NullOpcUaUserAuthenticator.Instance, NullHistorianDataSource.Instance, EmptyDataPlaneAuthoritySource.Instance, new ConfigurationBuilder().Build(), () => throw new NotSupportedException("no actor system required"), new ActorRegistry(), null, Options.Create(new AclOptions()), TimeProvider.System, factory); var admin = new AdminBootSentinel(); using var host = new HostBuilder().ConfigureServices(services => { services.AddSingleton(service); services.AddSingleton(admin); }).Build(); try { await host.StartAsync(deadline.Token); listenerObserved.ShouldBeTrue("the SDK endpoint was actually listening before replay"); bindingReached.ShouldBeTrue(); admin.Started.ShouldBeTrue(); using var client = new TcpClient(); await Should.ThrowAsync(async () => await client.ConnectAsync(IPAddress.Loopback, port, deadline.Token)); // After construction failure, subsequent requests are accepted into the unbound cache. deferred.Publish(100); await host.StopAsync(deadline.Token); } finally { await service.DisposeAsync(); if (Directory.Exists(pki)) Directory.Delete(pki, recursive: true); } } private sealed class FaultInjectingLoggerFactory(Action installFault) : ILoggerFactory { public ILogger CreateLogger(string categoryName) { if (categoryName == typeof(SdkServiceLevelPublisher).FullName) installFault(); return NullLogger.Instance; } public void AddProvider(ILoggerProvider provider) { } public void Dispose() { } } private sealed class AdminBootSentinel : IHostedService { public bool Started { get; private set; } public Task StartAsync(CancellationToken cancellationToken) { Started = true; return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } }