using Microsoft.Extensions.Logging.Abstractions; using Opc.Ua; using Opc.Ua.Server; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; /// /// F13c — verifies the impersonation handler routes UserName tokens through /// and translates its result into the SDK's /// shape (granted identity vs. rejection status). /// public sealed class OpcUaApplicationHostImpersonationTests { private static readonly UserTokenPolicy UserNamePolicy = new(UserTokenType.UserName) { PolicyId = "username_basic256sha256" }; private static readonly UserTokenPolicy AnonPolicy = new(UserTokenType.Anonymous) { PolicyId = "anonymous" }; [Fact] public void HandleImpersonation_username_success_sets_identity_and_no_validation_error() { var token = new UserNameIdentityToken { UserName = "alice", DecryptedPassword = "secret" }; var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription()); var authenticator = new RecordingAuthenticator( OpcUaUserAuthResult.Allow("Alice", new[] { "ReadOnly", "WriteOperate" })); OpcUaApplicationHost.HandleImpersonation(authenticator, args, NullLogger.Instance); args.Identity.ShouldNotBeNull(); args.IdentityValidationError.ShouldBeNull(); authenticator.LastUsername.ShouldBe("alice"); authenticator.LastPassword.ShouldBe("secret"); } [Fact] public void HandleImpersonation_username_denial_sets_validation_error_and_no_identity() { var token = new UserNameIdentityToken { UserName = "mallory", DecryptedPassword = "wrong" }; var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription()); var authenticator = new RecordingAuthenticator(OpcUaUserAuthResult.Deny("Invalid credentials")); OpcUaApplicationHost.HandleImpersonation(authenticator, args, NullLogger.Instance); args.Identity.ShouldBeNull(); args.IdentityValidationError.Code.ShouldBe(StatusCodes.BadIdentityTokenRejected); args.IdentityValidationError.LocalizedText.Text.ShouldContain("Invalid credentials"); } [Fact] public void HandleImpersonation_anonymous_token_falls_through_to_sdk_default() { var args = new ImpersonateEventArgs(new AnonymousIdentityToken(), AnonPolicy, new EndpointDescription()); var authenticator = new RecordingAuthenticator(OpcUaUserAuthResult.Allow("x", Array.Empty())); OpcUaApplicationHost.HandleImpersonation(authenticator, args, NullLogger.Instance); // Handler leaves anonymous tokens untouched — no identity, no validation error. args.Identity.ShouldBeNull(); args.IdentityValidationError.ShouldBeNull(); authenticator.LastUsername.ShouldBeNull("anonymous tokens must not hit the authenticator"); } [Fact] public void HandleImpersonation_authenticator_throwing_results_in_rejection() { var token = new UserNameIdentityToken { UserName = "bob", DecryptedPassword = "x" }; var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription()); var authenticator = new ThrowingAuthenticator(new InvalidOperationException("LDAP unreachable")); OpcUaApplicationHost.HandleImpersonation(authenticator, args, NullLogger.Instance); args.Identity.ShouldBeNull(); args.IdentityValidationError.Code.ShouldBe(StatusCodes.BadIdentityTokenRejected); } [Fact] public void HandleImpersonation_null_username_treated_as_empty_string() { var token = new UserNameIdentityToken { UserName = null, DecryptedPassword = "abc" }; var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription()); var authenticator = new RecordingAuthenticator(OpcUaUserAuthResult.Deny("no user")); OpcUaApplicationHost.HandleImpersonation(authenticator, args, NullLogger.Instance); authenticator.LastUsername.ShouldBe(string.Empty); } [Fact] public async Task NullOpcUaUserAuthenticator_always_denies() { var result = await NullOpcUaUserAuthenticator.Instance .AuthenticateUserNameAsync("anyone", "anything", CancellationToken.None); result.Success.ShouldBeFalse(); result.Error.ShouldNotBeNull(); result.Roles.ShouldBeEmpty(); } private sealed class RecordingAuthenticator(OpcUaUserAuthResult outcome) : IOpcUaUserAuthenticator { public string? LastUsername { get; private set; } public string? LastPassword { get; private set; } public Task AuthenticateUserNameAsync(string username, string password, CancellationToken ct) { LastUsername = username; LastPassword = password; return Task.FromResult(outcome); } } private sealed class ThrowingAuthenticator(Exception ex) : IOpcUaUserAuthenticator { public Task AuthenticateUserNameAsync(string username, string password, CancellationToken ct) => Task.FromException(ex); } }