using Microsoft.Extensions.Options; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Admin.Security; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Configuration.Services; namespace ZB.MOM.WW.OtOpcUa.Admin.Tests; [Trait("Category", "Unit")] public sealed class AdminRoleGrantResolverTests { /// In-memory — only the read path is exercised. private sealed class FakeMappingService(IReadOnlyList rows) : ILdapGroupRoleMappingService { public Task> GetByGroupsAsync( IEnumerable ldapGroups, CancellationToken cancellationToken) { var set = ldapGroups.ToHashSet(StringComparer.OrdinalIgnoreCase); return Task.FromResult>( rows.Where(r => set.Contains(r.LdapGroup)).ToList()); } 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(); } private static AdminRoleGrantResolver Resolver( IReadOnlyList rows, Dictionary? staticMap = null) { var options = Options.Create(new LdapOptions { GroupToRole = staticMap ?? new Dictionary(StringComparer.OrdinalIgnoreCase), }); return new AdminRoleGrantResolver(new FakeMappingService(rows), options); } private static LdapGroupRoleMapping Row(string group, AdminRole role, bool systemWide, string? clusterId) => new() { Id = Guid.NewGuid(), LdapGroup = group, Role = role, IsSystemWide = systemWide, ClusterId = clusterId, }; [Fact] public async Task No_groups_resolves_to_empty() { var grants = await Resolver([]).ResolveAsync([], CancellationToken.None); grants.IsEmpty.ShouldBeTrue(); } [Fact] public async Task Static_dictionary_grant_is_fleet_wide() { var resolver = Resolver([], new Dictionary(StringComparer.OrdinalIgnoreCase) { ["ReadOnly"] = "ConfigViewer", }); var grants = await resolver.ResolveAsync(["ReadOnly"], CancellationToken.None); grants.FleetRoles.ShouldBe(["ConfigViewer"]); grants.ClusterRoles.ShouldBeEmpty(); } [Fact] public async Task System_wide_db_row_lands_in_fleet_roles() { var resolver = Resolver([Row("cn=admins", AdminRole.FleetAdmin, systemWide: true, clusterId: null)]); var grants = await resolver.ResolveAsync(["cn=admins"], CancellationToken.None); grants.FleetRoles.ShouldBe(["FleetAdmin"]); grants.ClusterRoles.ShouldBeEmpty(); } [Fact] public async Task Cluster_scoped_db_row_lands_in_cluster_roles() { var resolver = Resolver([Row("cn=warsaw", AdminRole.ConfigEditor, systemWide: false, clusterId: "WARSAW")]); var grants = await resolver.ResolveAsync(["cn=warsaw"], CancellationToken.None); grants.FleetRoles.ShouldBeEmpty(); grants.ClusterRoles.Count.ShouldBe(1); grants.ClusterRoles[0].ShouldBe(new ClusterRoleGrant("WARSAW", "ConfigEditor")); } [Fact] public async Task Same_group_can_hold_different_roles_on_different_clusters() { var resolver = Resolver( [ Row("cn=ops", AdminRole.FleetAdmin, systemWide: false, clusterId: "WARSAW"), Row("cn=ops", AdminRole.ConfigViewer, systemWide: false, clusterId: "BERLIN"), ]); var grants = await resolver.ResolveAsync(["cn=ops"], CancellationToken.None); grants.ClusterRoles.ShouldContain(new ClusterRoleGrant("WARSAW", "FleetAdmin")); grants.ClusterRoles.ShouldContain(new ClusterRoleGrant("BERLIN", "ConfigViewer")); grants.ClusterRoles.Count.ShouldBe(2); } [Fact] public async Task Db_grants_stack_additively_on_the_static_bootstrap() { var resolver = Resolver( [Row("cn=ops", AdminRole.ConfigEditor, systemWide: false, clusterId: "WARSAW")], new Dictionary(StringComparer.OrdinalIgnoreCase) { ["cn=ops"] = "ConfigViewer" }); var grants = await resolver.ResolveAsync(["cn=ops"], CancellationToken.None); grants.FleetRoles.ShouldBe(["ConfigViewer"]); grants.ClusterRoles.ShouldBe([new ClusterRoleGrant("WARSAW", "ConfigEditor")]); } [Fact] public async Task Duplicate_cluster_role_pair_is_deduped() { var resolver = Resolver( [ Row("cn=a", AdminRole.ConfigEditor, systemWide: false, clusterId: "WARSAW"), Row("cn=b", AdminRole.ConfigEditor, systemWide: false, clusterId: "WARSAW"), ]); var grants = await resolver.ResolveAsync(["cn=a", "cn=b"], CancellationToken.None); grants.ClusterRoles.ShouldBe([new ClusterRoleGrant("WARSAW", "ConfigEditor")]); } [Fact] public async Task Groups_with_no_mapping_resolve_to_empty() { var grants = await Resolver([]).ResolveAsync(["cn=nobody"], CancellationToken.None); grants.IsEmpty.ShouldBeTrue(); } }