using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.Roles;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
///
/// Tests for , the shared-Auth
/// seam over the dashboard's
/// LDAP-group → role mapping. Behaviour must match the existing
/// DashboardAuthenticator.MapGroupsToRoles precedence/case rules.
///
public sealed class DashboardGroupRoleMapperTests
{
private static DashboardGroupRoleMapper CreateMapper(Dictionary mapping)
{
GatewayOptions options = new()
{
Dashboard = new DashboardOptions
{
GroupToRole = mapping,
},
};
return new DashboardGroupRoleMapper(Options.Create(options));
}
private static Dictionary StandardMapping() => new(StringComparer.OrdinalIgnoreCase)
{
["GwAdmin"] = DashboardRoles.Admin,
["GwReader"] = DashboardRoles.Viewer,
};
/// Verifies full-DN match, leading-RDN fallback, case-insensitivity, and unmapped → empty.
/// The LDAP group name or distinguished name.
/// The expected role or null if no match.
/// A task that represents the asynchronous operation.
[Theory]
[InlineData("GwAdmin", DashboardRoles.Admin)]
[InlineData("gwadmin", DashboardRoles.Admin)]
[InlineData("ou=GwAdmin,ou=groups,dc=zb,dc=local", DashboardRoles.Admin)]
[InlineData("OtherGroup", null)]
public async Task MapAsync_ResolvesByShortNameAndDistinguishedName(
string ldapGroup,
string? expectedRole)
{
DashboardGroupRoleMapper mapper = CreateMapper(StandardMapping());
GroupRoleMapping result = await mapper.MapAsync([ldapGroup], CancellationToken.None);
if (expectedRole is null)
{
Assert.Empty(result.Roles);
}
else
{
Assert.Equal(expectedRole, Assert.Single(result.Roles));
}
Assert.Null(result.Scope);
}
/// Verifies that admin and viewer roles are both emitted when both groups are present.
/// A task that represents the asynchronous operation.
[Fact]
public async Task MapAsync_AdminPlusViewer_BothRolesEmitted()
{
DashboardGroupRoleMapper mapper = CreateMapper(StandardMapping());
GroupRoleMapping result = await mapper.MapAsync(
["GwAdmin", "GwReader"],
CancellationToken.None);
Assert.Contains(DashboardRoles.Admin, result.Roles);
Assert.Contains(DashboardRoles.Viewer, result.Roles);
}
/// Verifies that an empty GroupToRole map yields no roles.
/// A task that represents the asynchronous operation.
[Fact]
public async Task MapAsync_EmptyMapping_ReturnsNoRoles()
{
DashboardGroupRoleMapper mapper = CreateMapper(new Dictionary(StringComparer.OrdinalIgnoreCase));
GroupRoleMapping result = await mapper.MapAsync(["GwAdmin"], CancellationToken.None);
Assert.Empty(result.Roles);
}
///
/// Canonical roles: an LDAP user in the admin group must resolve
/// to the canonical role value "Administrator" (not the legacy
/// "Admin"), and the reader group to "Viewer". Asserted with
/// string LITERALS — independent of — so a
/// regression on the constant's value is caught here.
///
/// A task that represents the asynchronous operation.
[Fact]
public async Task MapAsync_AdminGroup_ResolvesToCanonicalAdministratorValue()
{
DashboardGroupRoleMapper mapper = CreateMapper(StandardMapping());
GroupRoleMapping adminResult = await mapper.MapAsync(["GwAdmin"], CancellationToken.None);
GroupRoleMapping readerResult = await mapper.MapAsync(["GwReader"], CancellationToken.None);
Assert.Equal("Administrator", Assert.Single(adminResult.Roles));
Assert.Equal("Viewer", Assert.Single(readerResult.Roles));
}
///
/// The canonical admin value ("Administrator") passes the
/// admin-only gate while a "Viewer" fails it — asserted with literals,
/// proving enforcement is bound to the new value and the legacy "Admin"
/// string is no longer what authorizes admin actions.
///
[Fact]
public void AdminOnly_AuthorizesCanonicalAdministratorButNotViewer()
{
IReadOnlyList adminGate = DashboardAuthorizationRequirement.AdminOnly.RequiredRoles;
Assert.Contains("Administrator", adminGate);
Assert.DoesNotContain("Admin", adminGate);
Assert.DoesNotContain("Viewer", adminGate);
}
///
/// Verifies the extracted shared helper is the single source of truth: it
/// produces the same roles the mapper does for the same inputs.
///
/// A task that represents the asynchronous operation.
[Fact]
public async Task SharedHelper_MatchesMapperOutput()
{
Dictionary mapping = StandardMapping();
DashboardGroupRoleMapper mapper = CreateMapper(mapping);
string[] groups = ["ou=GwAdmin,ou=groups,dc=zb,dc=local", "gwreader"];
IReadOnlyList helperRoles = DashboardGroupRoleMapping.MapGroupsToRoles(groups, mapping);
GroupRoleMapping mapperResult = await mapper.MapAsync(groups, CancellationToken.None);
Assert.Equal([.. helperRoles.OrderBy(r => r)], [.. mapperResult.Roles.OrderBy(r => r)]);
}
}