using Microsoft.Extensions.Options; using Shouldly; using Xunit; using ZB.MOM.WW.Auth.Abstractions.Roles; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Configuration.Services; using ZB.MOM.WW.OtOpcUa.Security.Ldap; namespace ZB.MOM.WW.OtOpcUa.Security.Tests; /// /// Proves is a behaviour-preserving wrapper over the /// existing + logic: config /// baseline + system-wide DB grants, cluster-scoped DB rows ignored, unmapped groups dropped, /// and Scope always null. /// public sealed class OtOpcUaGroupRoleMapperTests { private static OtOpcUaGroupRoleMapper Build( IDictionary groupToRole, params LdapGroupRoleMapping[] dbRows) { var options = Microsoft.Extensions.Options.Options.Create(new LdapOptions { GroupToRole = new Dictionary(groupToRole, StringComparer.OrdinalIgnoreCase), }); return new OtOpcUaGroupRoleMapper(options, new FakeMappingService(dbRows)); } /// Verifies that the mapper maps a configured group and drops unmapped groups. /// A task that represents the asynchronous test operation. [Fact] public async Task Maps_config_group_and_drops_unmapped_group() { var mapper = Build(new Dictionary { ["AdminGroup"] = "Administrator" }); var result = await mapper.MapAsync(["AdminGroup", "UnmappedGroup"], CancellationToken.None); result.Roles.ShouldBe(["Administrator"]); result.Scope.ShouldBeNull(); } /// Verifies that a system-wide DB row adds a role on top of the config baseline. /// A task that represents the asynchronous test operation. [Fact] public async Task System_wide_db_row_adds_role_on_top_of_config_baseline() { var mapper = Build( new Dictionary { ["viewers"] = "Viewer" }, new LdapGroupRoleMapping { LdapGroup = "admins", Role = AdminRole.Administrator, IsSystemWide = true }); var result = await mapper.MapAsync(["viewers", "admins"], CancellationToken.None); result.Roles.ShouldContain("Viewer"); result.Roles.ShouldContain("Administrator"); result.Scope.ShouldBeNull(); } /// Verifies that a cluster-scoped DB row is ignored by the mapper. /// A task that represents the asynchronous test operation. [Fact] public async Task Cluster_scoped_db_row_is_ignored() { var mapper = Build( new Dictionary(), new LdapGroupRoleMapping { LdapGroup = "site-a-editors", Role = AdminRole.Designer, IsSystemWide = false, ClusterId = "SITE-A", }); var result = await mapper.MapAsync(["site-a-editors"], CancellationToken.None); result.Roles.ShouldNotContain("Designer"); result.Roles.ShouldBeEmpty(); } /// Verifies that the mapper output matches the expected RoleMapper.Map + Merge result for representative inputs. /// A task that represents the asynchronous test operation. [Fact] public async Task Reproduces_RoleMapper_Map_plus_Merge_for_representative_inputs() { var groupToRole = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["viewers"] = "Viewer", ["editors"] = "Designer", }; var dbRows = new[] { new LdapGroupRoleMapping { LdapGroup = "admins", Role = AdminRole.Administrator, IsSystemWide = true }, new LdapGroupRoleMapping { LdapGroup = "site-a", Role = AdminRole.Designer, IsSystemWide = false, ClusterId = "SITE-A" }, }; var groups = new[] { "viewers", "editors", "admins", "site-a", "noise" }; var mapper = Build(groupToRole, dbRows); // Oracle: exactly what the legacy login path computes today. var baseline = RoleMapper.Map(groups, groupToRole); var expected = RoleMapper.Merge(baseline, dbRows); var result = await mapper.MapAsync(groups, CancellationToken.None); result.Roles.OrderBy(r => r).ShouldBe(expected.OrderBy(r => r)); result.Scope.ShouldBeNull(); } /// In-memory stand-in for the EF-backed DB service; returns the configured rows verbatim. private sealed class FakeMappingService(IReadOnlyList rows) : ILdapGroupRoleMappingService { /// Returns all seeded rows that belong to any of the specified LDAP groups. /// The LDAP groups to look up. /// The cancellation token. /// A task that resolves to the matching role mappings. public Task> GetByGroupsAsync( IEnumerable ldapGroups, CancellationToken cancellationToken) => Task.FromResult(rows); /// Returns all seeded role mapping rows. /// The cancellation token. /// A task that resolves to all role mappings. public Task> ListAllAsync(CancellationToken cancellationToken) => Task.FromResult(rows); /// Not supported in this stub. /// The row to create. /// The cancellation token. /// Never returns; always throws. public Task CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken) => throw new NotSupportedException(); /// Not supported in this stub. /// The identifier of the row to delete. /// The cancellation token. /// Never returns; always throws. public Task DeleteAsync(Guid id, CancellationToken cancellationToken) => throw new NotSupportedException(); } }