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.ExternalSystems;
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.ExternalSystemGateway;
using ZB.MOM.WW.ScadaBridge.Security;
namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
///
/// ScadaBridge#65: the central used to be dropped only
/// by its TTL, so an external-system / method / database-connection edit made through the management
/// path was invisible to inbound-method calls on the same node for up to DefinitionCacheTtl.
/// Every such mutation now invalidates it once the change is committed.
///
public class ExternalDefinitionCacheInvalidationTests : TestKit
{
private readonly IExternalSystemRepository _repo = Substitute.For();
// A TTL far longer than the test: only an explicit InvalidateAll can make the cache re-fetch.
private readonly ExternalDefinitionLookupCache _cache = new(TimeSpan.FromHours(1));
private readonly ServiceCollection _services = new ServiceCollection().AddTestConfigurationTransaction();
/// Initializes the harness with one external system, one method and one DB connection.
public ExternalDefinitionCacheInvalidationTests()
{
_repo.GetExternalSystemByIdAsync(1, Arg.Any())
.Returns(_ => new ExternalSystemDefinition("Erp", "http://erp", "None") { Id = 1 });
_repo.GetExternalSystemMethodByIdAsync(2, Arg.Any())
.Returns(_ => new ExternalSystemMethod("Get", "GET", "/x") { Id = 2, ExternalSystemDefinitionId = 1 });
_repo.GetDatabaseConnectionByIdAsync(3, Arg.Any())
.Returns(_ => new DatabaseConnectionDefinition("Db", "Server=x") { Id = 3 });
_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[] { "Designer" }, Array.Empty()),
command, Guid.NewGuid().ToString("N"));
private async Task PrimeAndCountFetches()
{
var fetches = 0;
Task Fetch()
{
fetches++;
return Task.FromResult(new ExternalSystemDefinition("Erp", "http://erp", "None"));
}
await _cache.GetSystemAsync("Erp", Fetch);
await _cache.GetSystemAsync("Erp", Fetch);
Assert.Equal(1, fetches); // second read was served from the cache
return fetches;
}
private async Task IsStillCached()
{
var refetched = false;
await _cache.GetSystemAsync("Erp", () =>
{
refetched = true;
return Task.FromResult(new ExternalSystemDefinition("Erp", "http://erp", "None"));
});
return !refetched;
}
/// Every external-system / method / database-connection mutation drops the cached definitions.
/// A task that represents the asynchronous operation.
[Fact]
public async Task EveryDefinitionMutation_InvalidatesTheCache()
{
object[] commands =
[
new CreateExternalSystemCommand("New", "http://n", "None", null),
new UpdateExternalSystemCommand(1, "Erp", "http://erp2", "None", null),
new DeleteExternalSystemCommand(1),
new CreateExternalSystemMethodCommand(1, "M", "GET", "/m"),
new UpdateExternalSystemMethodCommand(2, Path: "/y"),
new DeleteExternalSystemMethodCommand(2),
new CreateDatabaseConnectionDefCommand("Db2", "Server=y"),
new UpdateDatabaseConnectionDefCommand(3, "Db", "Server=z"),
new DeleteDatabaseConnectionDefCommand(3),
];
var actor = CreateActor();
foreach (var command in commands)
{
_cache.InvalidateAll();
await PrimeAndCountFetches();
Assert.True(await IsStillCached());
actor.Tell(Admin(command));
ExpectMsg(TimeSpan.FromSeconds(5));
Assert.False(await IsStillCached()); // dropped by the edit, so the lookup re-fetched
}
}
}