using System.Security.Claims; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ZB.MOM.WW.MxGateway.Server.Configuration; using ZB.MOM.WW.MxGateway.Server.Dashboard; namespace ZB.MOM.WW.MxGateway.IntegrationTests; [Collection(LiveResourcesCollection.Name)] [Trait("Category", "LiveLdap")] public sealed class DashboardLdapLiveTests { [LiveLdapFact] public async Task AuthenticateAsync_AdminInGwAdminGroup_Succeeds() { DashboardAuthenticator authenticator = CreateAuthenticator(); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "admin", "admin123", CancellationToken.None); Assert.True(result.Succeeded); Assert.NotNull(result.Principal); Assert.Equal("admin", result.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value); Assert.Contains(result.Principal.Claims, claim => claim.Type == DashboardAuthenticationDefaults.LdapGroupClaimType && claim.Value.Contains("GwAdmin", StringComparison.OrdinalIgnoreCase)); // IntegrationTests-023: DashboardAuthenticator.CreatePrincipal emits a // ClaimTypes.Role claim derived from MapGroupsToRoles. The seeded // GroupToRole map (GwAdmin -> Admin) means the admin principal must // carry Role=Admin alongside the raw LDAP-group claim. A regression in // MapGroupsToRoles (returning an empty list, missing the RDN fallback) // would silently pass without this assertion. Assert.Contains(result.Principal.Claims, claim => claim.Type == ClaimTypes.Role && claim.Value == DashboardRoles.Admin); } [LiveLdapFact] public async Task AuthenticateAsync_ReadOnlyUserMissingGwAdminGroup_Fails() { DashboardAuthenticator authenticator = CreateAuthenticator(); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "readonly", "readonly123", CancellationToken.None); Assert.False(result.Succeeded); Assert.Null(result.Principal); Assert.DoesNotContain("readonly123", result.FailureMessage, StringComparison.Ordinal); } [LiveLdapFact] public async Task AuthenticateAsync_AdminWithWrongPassword_FailsWithoutLeakingPassword() { // Exercises the LdapException branch: the user exists and the service // account search succeeds, but the candidate bind is rejected. const string wrongPassword = "definitely-not-the-admin-password"; DashboardAuthenticator authenticator = CreateAuthenticator(); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "admin", wrongPassword, CancellationToken.None); Assert.False(result.Succeeded); Assert.Null(result.Principal); Assert.DoesNotContain(wrongPassword, result.FailureMessage, StringComparison.Ordinal); } [LiveLdapFact] public async Task AuthenticateAsync_UnknownUsername_Fails() { // Exercises the `candidate is null` branch: the service-account search // returns no entry, so no candidate bind is attempted. DashboardAuthenticator authenticator = CreateAuthenticator(); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "no-such-user-9f3c1", "irrelevant-password", CancellationToken.None); Assert.False(result.Succeeded); Assert.Null(result.Principal); } [LiveLdapFact] public async Task AuthenticateAsync_ServerUnreachable_FailsWithoutThrowing() { // Exercises the connect-failure path: a closed loopback port produces a // connection error that DashboardAuthenticator must absorb into a Fail // result rather than propagating an exception to the dashboard. DashboardAuthenticator authenticator = new( Options.Create(new GatewayOptions { Ldap = new LdapOptions { // 1 is a reserved port number that no LDAP server listens on. Port = 1, }, }), NullLogger.Instance); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "admin", "admin123", CancellationToken.None); Assert.False(result.Succeeded); Assert.Null(result.Principal); } private static DashboardAuthenticator CreateAuthenticator() { return new DashboardAuthenticator( Options.Create(new GatewayOptions { Dashboard = new DashboardOptions { GroupToRole = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["GwAdmin"] = DashboardRoles.Admin, }, }, }), NullLogger.Instance); } }