104 lines
4.3 KiB
C#
104 lines
4.3 KiB
C#
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<IHostedService>(service);
|
|
services.AddSingleton<IHostedService>(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<SocketException>(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;
|
|
}
|
|
}
|