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
}
}
}