using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Security; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.Security; namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests; /// /// Lane K review M1: the process-wide served a deleted or /// changed LDAP-group mapping for up to its TTL on the node that made the change. Every /// role-mapping / scope-rule mutation now invalidates it once the change is committed. /// public class RoleMappingCacheInvalidationTests : TestKit { private readonly ISecurityRepository _repo = Substitute.For(); private readonly RoleMappingCache _cache = new(TimeProvider.System); private readonly ServiceCollection _services = new ServiceCollection().AddTestConfigurationTransaction(); private List _mappings = [new("Ops", "Deployer") { Id = 5 }]; /// Initializes the harness with one Deployer mapping for group "Ops". public RoleMappingCacheInvalidationTests() { _repo.GetAllMappingsAsync(Arg.Any()).Returns(_ => _mappings); _repo.GetScopeRulesForMappingAsync(Arg.Any(), Arg.Any()) .Returns(new List()); _repo.GetMappingByIdAsync(5, Arg.Any()).Returns(_ => _mappings.FirstOrDefault()); _services.AddScoped(_ => _repo); _services.AddScoped(_ => Substitute.For()); _services.AddSingleton(_cache); } private IActorRef CreateActor() => Sys.ActorOf(Props.Create(() => new ManagementActor( _services.BuildServiceProvider(), NullLogger.Instance))); private static ManagementEnvelope Admin(object command) => new(new AuthenticatedUser("admin", "Admin", new[] { "Administrator" }, Array.Empty()), command, Guid.NewGuid().ToString("N")); private Task MapOps() => new RoleMapper(_repo, _cache).MapGroupsToRolesAsync(["Ops"]); /// A deleted mapping is visible to the very next mapping on the same cache. /// A task that represents the asynchronous operation. [Fact] public async Task DeleteRoleMapping_IsVisibleToTheNextMapping() { Assert.Contains("Deployer", (await MapOps()).Roles); // primes the snapshot _mappings = []; CreateActor().Tell(Admin(new DeleteRoleMappingCommand(5))); ExpectMsg(TimeSpan.FromSeconds(5)); Assert.Empty((await MapOps()).Roles); } /// Every mapping / scope-rule mutation invalidates the snapshot. /// A task that represents the asynchronous operation. [Fact] public async Task EveryMappingMutation_InvalidatesTheSnapshot() { object[] commands = [ new CreateRoleMappingCommand("Eng", "Designer"), new UpdateRoleMappingCommand(5, "Ops", "Viewer"), new AddScopeRuleCommand(5, 2), new DeleteScopeRuleCommand(9), ]; var actor = CreateActor(); foreach (var command in commands) { await MapOps(); // prime _repo.ClearReceivedCalls(); actor.Tell(Admin(command)); ExpectMsg(TimeSpan.FromSeconds(5)); await MapOps(); await _repo.Received(1).GetAllMappingsAsync(Arg.Any()); // re-read, not served stale } } }