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)); } [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(); } [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(); } [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(); } [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 { public Task> GetByGroupsAsync( IEnumerable ldapGroups, CancellationToken cancellationToken) => Task.FromResult(rows); public Task> ListAllAsync(CancellationToken cancellationToken) => Task.FromResult(rows); public Task CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken) => throw new NotSupportedException(); public Task DeleteAsync(Guid id, CancellationToken cancellationToken) => throw new NotSupportedException(); } }