using Microsoft.EntityFrameworkCore; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Admin.Services; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Core.Authorization; namespace ZB.MOM.WW.OtOpcUa.Admin.Tests; [Trait("Category", "Unit")] public sealed class PermissionProbeServiceTests { [Fact] public async Task Probe_Grants_When_ClusterLevelRow_CoversRequiredFlag() { using var ctx = NewContext(); SeedAcl(ctx, gen: 1, cluster: "c1", scopeKind: NodeAclScopeKind.Cluster, scopeId: null, group: "cn=operators", flags: NodePermissions.Browse | NodePermissions.Read); var svc = new PermissionProbeService(ctx); var result = await svc.ProbeAsync( generationId: 1, ldapGroup: "cn=operators", scope: new NodeScope { ClusterId = "c1", NamespaceId = "ns-1", Kind = NodeHierarchyKind.Equipment }, required: NodePermissions.Read, CancellationToken.None); result.Granted.ShouldBeTrue(); result.Matches.Count.ShouldBe(1); result.Matches[0].LdapGroup.ShouldBe("cn=operators"); result.Matches[0].Scope.ShouldBe(NodeAclScopeKind.Cluster); } [Fact] public async Task Probe_Denies_When_NoGroupMatches() { using var ctx = NewContext(); SeedAcl(ctx, 1, "c1", NodeAclScopeKind.Cluster, null, "cn=operators", NodePermissions.Read); var svc = new PermissionProbeService(ctx); var result = await svc.ProbeAsync(1, "cn=random-group", new NodeScope { ClusterId = "c1", Kind = NodeHierarchyKind.Equipment }, NodePermissions.Read, CancellationToken.None); result.Granted.ShouldBeFalse(); result.Matches.ShouldBeEmpty(); result.Effective.ShouldBe(NodePermissions.None); } [Fact] public async Task Probe_Denies_When_Effective_Missing_RequiredFlag() { using var ctx = NewContext(); SeedAcl(ctx, 1, "c1", NodeAclScopeKind.Cluster, null, "cn=operators", NodePermissions.Browse | NodePermissions.Read); var svc = new PermissionProbeService(ctx); var result = await svc.ProbeAsync(1, "cn=operators", new NodeScope { ClusterId = "c1", Kind = NodeHierarchyKind.Equipment }, required: NodePermissions.WriteOperate, CancellationToken.None); result.Granted.ShouldBeFalse(); result.Effective.ShouldBe(NodePermissions.Browse | NodePermissions.Read); } [Fact] public async Task Probe_Ignores_Rows_From_OtherClusters() { using var ctx = NewContext(); SeedAcl(ctx, 1, "c1", NodeAclScopeKind.Cluster, null, "cn=operators", NodePermissions.Read); SeedAcl(ctx, 1, "c2", NodeAclScopeKind.Cluster, null, "cn=operators", NodePermissions.WriteOperate); var svc = new PermissionProbeService(ctx); var c1Result = await svc.ProbeAsync(1, "cn=operators", new NodeScope { ClusterId = "c1", Kind = NodeHierarchyKind.Equipment }, NodePermissions.WriteOperate, CancellationToken.None); c1Result.Granted.ShouldBeFalse("c2's WriteOperate grant must NOT leak into c1's probe"); } [Fact] public async Task Probe_UsesOnlyRows_From_Specified_Generation() { using var ctx = NewContext(); SeedAcl(ctx, gen: 1, cluster: "c1", NodeAclScopeKind.Cluster, null, "cn=operators", NodePermissions.Read); SeedAcl(ctx, gen: 2, cluster: "c1", NodeAclScopeKind.Cluster, null, "cn=operators", NodePermissions.WriteOperate); var svc = new PermissionProbeService(ctx); var gen1 = await svc.ProbeAsync(1, "cn=operators", new NodeScope { ClusterId = "c1", Kind = NodeHierarchyKind.Equipment }, NodePermissions.WriteOperate, CancellationToken.None); var gen2 = await svc.ProbeAsync(2, "cn=operators", new NodeScope { ClusterId = "c1", Kind = NodeHierarchyKind.Equipment }, NodePermissions.WriteOperate, CancellationToken.None); gen1.Granted.ShouldBeFalse(); gen2.Granted.ShouldBeTrue(); } private static void SeedAcl( OtOpcUaConfigDbContext ctx, long gen, string cluster, NodeAclScopeKind scopeKind, string? scopeId, string group, NodePermissions flags) { ctx.NodeAcls.Add(new NodeAcl { NodeAclRowId = Guid.NewGuid(), NodeAclId = $"acl-{Guid.NewGuid():N}"[..16], GenerationId = gen, ClusterId = cluster, LdapGroup = group, ScopeKind = scopeKind, ScopeId = scopeId, PermissionFlags = flags, }); ctx.SaveChanges(); } private static OtOpcUaConfigDbContext NewContext() { var opts = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new OtOpcUaConfigDbContext(opts); } }