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)]); } }