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.Enums; namespace ZB.MOM.WW.OtOpcUa.Admin.Tests; [Trait("Category", "Unit")] public sealed class ValidatedNodeAclAuthoringServiceTests : IDisposable { private readonly OtOpcUaConfigDbContext _db; public ValidatedNodeAclAuthoringServiceTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"val-nodeacl-{Guid.NewGuid():N}") .Options; _db = new OtOpcUaConfigDbContext(options); } public void Dispose() => _db.Dispose(); [Fact] public async Task Grant_Rejects_NonePermissions() { var svc = new ValidatedNodeAclAuthoringService(_db); await Should.ThrowAsync(() => svc.GrantAsync( draftGenerationId: 1, clusterId: "c1", ldapGroup: "cn=ops", scopeKind: NodeAclScopeKind.Cluster, scopeId: null, permissions: NodePermissions.None, notes: null, CancellationToken.None)); } [Fact] public async Task Grant_Rejects_ClusterScope_With_ScopeId() { var svc = new ValidatedNodeAclAuthoringService(_db); await Should.ThrowAsync(() => svc.GrantAsync( 1, "c1", "cn=ops", NodeAclScopeKind.Cluster, scopeId: "not-null-wrong", NodePermissions.Read, null, CancellationToken.None)); } [Fact] public async Task Grant_Rejects_SubClusterScope_Without_ScopeId() { var svc = new ValidatedNodeAclAuthoringService(_db); await Should.ThrowAsync(() => svc.GrantAsync( 1, "c1", "cn=ops", NodeAclScopeKind.Equipment, scopeId: null, NodePermissions.Read, null, CancellationToken.None)); } [Fact] public async Task Grant_Succeeds_When_Valid() { var svc = new ValidatedNodeAclAuthoringService(_db); var row = await svc.GrantAsync( 1, "c1", "cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read | NodePermissions.Browse, "fleet reader", CancellationToken.None); row.LdapGroup.ShouldBe("cn=ops"); row.PermissionFlags.ShouldBe(NodePermissions.Read | NodePermissions.Browse); row.NodeAclId.ShouldNotBeNullOrWhiteSpace(); } [Fact] public async Task Grant_Rejects_DuplicateScopeGroup_Pair() { var svc = new ValidatedNodeAclAuthoringService(_db); await svc.GrantAsync(1, "c1", "cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read, null, CancellationToken.None); await Should.ThrowAsync(() => svc.GrantAsync( 1, "c1", "cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.WriteOperate, null, CancellationToken.None)); } [Fact] public async Task Grant_SameGroup_DifferentScope_IsAllowed() { var svc = new ValidatedNodeAclAuthoringService(_db); await svc.GrantAsync(1, "c1", "cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read, null, CancellationToken.None); var tagRow = await svc.GrantAsync(1, "c1", "cn=ops", NodeAclScopeKind.Tag, scopeId: "tag-xyz", NodePermissions.WriteOperate, null, CancellationToken.None); tagRow.ScopeKind.ShouldBe(NodeAclScopeKind.Tag); } [Fact] public async Task Grant_SameGroupScope_DifferentDraft_IsAllowed() { var svc = new ValidatedNodeAclAuthoringService(_db); await svc.GrantAsync(1, "c1", "cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read, null, CancellationToken.None); var draft2Row = await svc.GrantAsync(2, "c1", "cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read, null, CancellationToken.None); draft2Row.GenerationId.ShouldBe(2); } [Fact] public async Task UpdatePermissions_Rejects_None() { var svc = new ValidatedNodeAclAuthoringService(_db); var row = await svc.GrantAsync(1, "c1", "cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read, null, CancellationToken.None); await Should.ThrowAsync( () => svc.UpdatePermissionsAsync(row.NodeAclRowId, NodePermissions.None, null, CancellationToken.None)); } [Fact] public async Task UpdatePermissions_RoundTrips_NewFlags() { var svc = new ValidatedNodeAclAuthoringService(_db); var row = await svc.GrantAsync(1, "c1", "cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.Read, null, CancellationToken.None); var updated = await svc.UpdatePermissionsAsync(row.NodeAclRowId, NodePermissions.Read | NodePermissions.WriteOperate, "bumped", CancellationToken.None); updated.PermissionFlags.ShouldBe(NodePermissions.Read | NodePermissions.WriteOperate); updated.Notes.ShouldBe("bumped"); } [Fact] public async Task UpdatePermissions_MissingRow_Throws() { var svc = new ValidatedNodeAclAuthoringService(_db); await Should.ThrowAsync( () => svc.UpdatePermissionsAsync(Guid.NewGuid(), NodePermissions.Read, null, CancellationToken.None)); } }