using Microsoft.EntityFrameworkCore; using Shouldly; using Xunit; 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.Configuration.Tests; [Trait("Category", "Unit")] public sealed class LdapGroupRoleMappingServiceTests : IDisposable { private readonly OtOpcUaConfigDbContext _db; public LdapGroupRoleMappingServiceTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"ldap-grm-{Guid.NewGuid():N}") .Options; _db = new OtOpcUaConfigDbContext(options); } public void Dispose() => _db.Dispose(); private LdapGroupRoleMapping Make(string group, AdminRole role, string? clusterId = null, bool? isSystemWide = null) => new() { LdapGroup = group, Role = role, ClusterId = clusterId, IsSystemWide = isSystemWide ?? (clusterId is null), }; [Fact] public async Task Create_SetsId_AndCreatedAtUtc() { var svc = new LdapGroupRoleMappingService(_db); var row = Make("cn=fleet,dc=x", AdminRole.FleetAdmin); var saved = await svc.CreateAsync(row, CancellationToken.None); saved.Id.ShouldNotBe(Guid.Empty); saved.CreatedAtUtc.ShouldBeGreaterThan(DateTime.UtcNow.AddMinutes(-1)); } [Fact] public async Task Create_Rejects_EmptyLdapGroup() { var svc = new LdapGroupRoleMappingService(_db); var row = Make("", AdminRole.FleetAdmin); await Should.ThrowAsync( () => svc.CreateAsync(row, CancellationToken.None)); } [Fact] public async Task Create_Rejects_SystemWide_With_ClusterId() { var svc = new LdapGroupRoleMappingService(_db); var row = Make("cn=g", AdminRole.ConfigViewer, clusterId: "c1", isSystemWide: true); await Should.ThrowAsync( () => svc.CreateAsync(row, CancellationToken.None)); } [Fact] public async Task Create_Rejects_NonSystemWide_WithoutClusterId() { var svc = new LdapGroupRoleMappingService(_db); var row = Make("cn=g", AdminRole.ConfigViewer, clusterId: null, isSystemWide: false); await Should.ThrowAsync( () => svc.CreateAsync(row, CancellationToken.None)); } [Fact] public async Task GetByGroups_Returns_MatchingGrants_Only() { var svc = new LdapGroupRoleMappingService(_db); await svc.CreateAsync(Make("cn=fleet,dc=x", AdminRole.FleetAdmin), CancellationToken.None); await svc.CreateAsync(Make("cn=editor,dc=x", AdminRole.ConfigEditor), CancellationToken.None); await svc.CreateAsync(Make("cn=viewer,dc=x", AdminRole.ConfigViewer), CancellationToken.None); var results = await svc.GetByGroupsAsync( ["cn=fleet,dc=x", "cn=viewer,dc=x"], CancellationToken.None); results.Count.ShouldBe(2); results.Select(r => r.Role).ShouldBe([AdminRole.FleetAdmin, AdminRole.ConfigViewer], ignoreOrder: true); } [Fact] public async Task GetByGroups_Empty_Input_ReturnsEmpty() { var svc = new LdapGroupRoleMappingService(_db); await svc.CreateAsync(Make("cn=fleet,dc=x", AdminRole.FleetAdmin), CancellationToken.None); var results = await svc.GetByGroupsAsync([], CancellationToken.None); results.ShouldBeEmpty(); } [Fact] public async Task ListAll_Orders_ByGroupThenCluster() { var svc = new LdapGroupRoleMappingService(_db); await svc.CreateAsync(Make("cn=b,dc=x", AdminRole.FleetAdmin), CancellationToken.None); await svc.CreateAsync(Make("cn=a,dc=x", AdminRole.ConfigEditor, clusterId: "c2", isSystemWide: false), CancellationToken.None); await svc.CreateAsync(Make("cn=a,dc=x", AdminRole.ConfigEditor, clusterId: "c1", isSystemWide: false), CancellationToken.None); var results = await svc.ListAllAsync(CancellationToken.None); results[0].LdapGroup.ShouldBe("cn=a,dc=x"); results[0].ClusterId.ShouldBe("c1"); results[1].ClusterId.ShouldBe("c2"); results[2].LdapGroup.ShouldBe("cn=b,dc=x"); } [Fact] public async Task Delete_Removes_Matching_Row() { var svc = new LdapGroupRoleMappingService(_db); var saved = await svc.CreateAsync(Make("cn=fleet,dc=x", AdminRole.FleetAdmin), CancellationToken.None); await svc.DeleteAsync(saved.Id, CancellationToken.None); var after = await svc.ListAllAsync(CancellationToken.None); after.ShouldBeEmpty(); } [Fact] public async Task Delete_Unknown_Id_IsNoOp() { var svc = new LdapGroupRoleMappingService(_db); await svc.DeleteAsync(Guid.NewGuid(), CancellationToken.None); // no exception } }