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