test(host): unreachable-directory activation fails bounded; document the SDK impersonation threading contract (03/S2)

This commit is contained in:
Joseph Doherty
2026-07-13 12:00:35 -04:00
parent 0c785415a2
commit 62a49779ac
5 changed files with 81 additions and 1 deletions
@@ -1,12 +1,17 @@
using System.Diagnostics;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Opc.Ua;
using Opc.Ua.Server;
using Shouldly;
using Xunit;
using ZB.MOM.WW.Auth.Abstractions.Roles;
using ZB.MOM.WW.OtOpcUa.Host.OpcUa;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
using LdapTransport = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapTransport;
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
@@ -209,6 +214,51 @@ public sealed class LdapAuthResilienceTests
ldap.Calls.ShouldBe(6, "a disabled circuit never fast-denies");
}
/// <summary>
/// Outage-sim (offline-safe): the REAL <see cref="OtOpcUaLdapAuthService"/> + real shared library
/// pointed at an UNROUTABLE host (a TCP blackhole — the same outage shape as "stop the GLAuth
/// container"), driven through the REAL internal <see cref="OpcUaApplicationHost.HandleImpersonation"/>
/// block-bridge. The activation must fail bounded (well under the SDK's default ~10-20 s stall) with
/// <see cref="StatusCodes.BadIdentityTokenRejected"/> and no granted identity (03/S2).
/// </summary>
[Fact]
public async Task Activation_with_unreachable_directory_fails_within_bound()
{
var options = new LdapOptions
{
Enabled = true,
DevStubMode = false,
Server = "10.255.255.1", // RFC-blackhole: SYN goes nowhere
Port = 3893,
Transport = LdapTransport.None,
AllowInsecure = true,
SearchBase = "dc=zb,dc=local",
ConnectionTimeoutMs = 500,
AuthTimeoutMs = 1000,
};
var ldap = new OtOpcUaLdapAuthService(Options.Create(options), NullLogger<OtOpcUaLdapAuthService>.Instance);
var mapper = new FakeMapper(g => g.ToArray());
var authenticator = new LdapOpcUaUserAuthenticator(
ldap, ScopeFactoryWith(mapper), Options.Create(options),
NullLogger<LdapOpcUaUserAuthenticator>.Instance);
var token = new UserNameIdentityToken { UserName = "alice", DecryptedPassword = Encoding.UTF8.GetBytes("secret") };
var policy = new UserTokenPolicy(UserTokenType.UserName) { PolicyId = "username_basic256sha256" };
var args = new ImpersonateEventArgs(token, policy, new EndpointDescription());
var sw = Stopwatch.StartNew();
await Task.Run(() => OpcUaApplicationHost.HandleImpersonation(
authenticator, args, NullLogger<object>.Instance), TestContext.Current.CancellationToken)
.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
sw.Stop();
args.Identity.ShouldBeNull();
args.IdentityValidationError.ShouldNotBeNull();
args.IdentityValidationError.Code.ShouldBe(Opc.Ua.StatusCodes.BadIdentityTokenRejected);
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(3));
}
private static LdapAuthResult SystemFailure() =>
new(false, null, "u", Array.Empty<string>(), Array.Empty<string>(), "Unexpected authentication error")
{ IsSystemFailure = true };