using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Host.OpcUa;
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
///
/// F13c — verifies faithfully translates
/// outcomes into OpcUaUserAuthResult and turns LDAP
/// backend exceptions into a denial rather than letting them escape into the SDK.
///
public sealed class LdapOpcUaUserAuthenticatorTests
{
/// Verifies that successful LDAP authentication returns Allow result with user roles.
[Fact]
public async Task Authenticate_LDAP_success_returns_Allow_with_roles()
{
var ldap = new FakeLdap(new LdapAuthResult(true, "Alice", "alice", new[] { "configeditor" }, new[] { "ConfigEditor" }, null));
var sut = new LdapOpcUaUserAuthenticator(ldap, NullLogger.Instance);
var result = await sut.AuthenticateUserNameAsync("alice", "secret", CancellationToken.None);
result.Success.ShouldBeTrue();
result.DisplayName.ShouldBe("Alice");
result.Roles.ShouldBe(new[] { "ConfigEditor" });
}
/// Verifies that LDAP authentication failure returns Deny result with error text.
[Fact]
public async Task Authenticate_LDAP_failure_returns_Deny_with_error_text()
{
var ldap = new FakeLdap(new LdapAuthResult(false, null, "mallory", Array.Empty(), Array.Empty(), "Invalid username or password"));
var sut = new LdapOpcUaUserAuthenticator(ldap, NullLogger.Instance);
var result = await sut.AuthenticateUserNameAsync("mallory", "wrong", CancellationToken.None);
result.Success.ShouldBeFalse();
result.Error.ShouldBe("Invalid username or password");
}
/// Verifies that LDAP exceptions are converted to backend error denial results.
[Fact]
public async Task Authenticate_LDAP_exception_returns_backend_error_denial()
{
var ldap = new FakeLdap(_ => throw new InvalidOperationException("LDAP unreachable"));
var sut = new LdapOpcUaUserAuthenticator(ldap, NullLogger.Instance);
var result = await sut.AuthenticateUserNameAsync("anyone", "x", CancellationToken.None);
result.Success.ShouldBeFalse();
result.Error.ShouldNotBeNull();
result.Error.ShouldContain("backend");
}
/// Verifies that authentication falls back to username when LDAP omits display name.
[Fact]
public async Task Authenticate_falls_back_to_username_when_LDAP_omits_display_name()
{
var ldap = new FakeLdap(new LdapAuthResult(true, null, "alice", Array.Empty(), new[] { "ReadOnly" }, null));
var sut = new LdapOpcUaUserAuthenticator(ldap, NullLogger.Instance);
var result = await sut.AuthenticateUserNameAsync("alice", "x", CancellationToken.None);
result.Success.ShouldBeTrue();
result.DisplayName.ShouldBe("alice");
}
/// Test fake implementation of LDAP authentication service.
private sealed class FakeLdap : ILdapAuthService
{
private readonly Func _handler;
/// Initializes the fake with a fixed result that ignores the username.
/// The result to return for any authentication attempt.
public FakeLdap(LdapAuthResult fixed_) => _handler = _ => fixed_;
/// Initializes the fake with a handler function for custom results.
/// The handler to invoke with the username to produce a result.
public FakeLdap(Func handler) => _handler = handler;
/// Authenticates a user asynchronously via the handler function.
/// The username to authenticate.
/// The password (ignored by the fake).
/// Cancellation token for the operation.
public Task AuthenticateAsync(string username, string password, CancellationToken ct = default)
=> Task.FromResult(_handler(username));
}
}