using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using NSubstitute; using NSubstitute.ExceptionExtensions; using ZB.MOM.WW.ScadaBridge.Commons.Entities.SecuredWrites; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests; /// /// Review 2026-10-21 08/G3: terminal PendingSecuredWrites rows were never purged. /// The Secured Writes list sweep (which already expires overdue Pending rows) now also /// purges decided/executed rows older than SecuredWriteTerminalRetention. /// public class SecuredWriteTerminalPurgeTests : TestKit { private readonly ISecuredWriteRepository _repo = Substitute.For(); private readonly ServiceCollection _services = new ServiceCollection().AddTestConfigurationTransaction(); /// Initializes the harness with an empty secured-write table. public SecuredWriteTerminalPurgeTests() { _services.AddScoped(_ => _repo); _repo.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any?>(), Arg.Any()) .Returns(Array.Empty()); } private void UseOptions(ManagementServiceOptions options) => _services.AddSingleton>(Options.Create(options)); private IActorRef CreateActor() => Sys.ActorOf(Props.Create(() => new ManagementActor( _services.BuildServiceProvider(), NullLogger.Instance))); private static ManagementEnvelope List() => new(new AuthenticatedUser("admin", "Admin", new[] { "Administrator" }, Array.Empty()), new ListSecuredWritesCommand(null, null), Guid.NewGuid().ToString("N")); /// The list sweep purges terminal rows older than the retention window. [Fact] public void List_PurgesTerminalRowsOlderThanRetention() { UseOptions(new ManagementServiceOptions { SecuredWriteTerminalRetention = TimeSpan.FromDays(30), SecuredWriteTerminalPurgeInterval = TimeSpan.Zero, }); DateTime? cutoff = null; _repo.PurgeTerminalAsync(Arg.Do(c => cutoff = c), Arg.Any(), Arg.Any()) .Returns(3); CreateActor().Tell(List()); ExpectMsg(TimeSpan.FromSeconds(5)); Assert.NotNull(cutoff); Assert.InRange(cutoff!.Value, DateTime.UtcNow.AddDays(-30).AddMinutes(-1), DateTime.UtcNow.AddDays(-30).AddMinutes(1)); } /// A non-positive retention disables the purge. [Fact] public void List_WithRetentionDisabled_DoesNotPurge() { UseOptions(new ManagementServiceOptions { SecuredWriteTerminalRetention = TimeSpan.Zero, SecuredWriteTerminalPurgeInterval = TimeSpan.Zero, }); CreateActor().Tell(List()); ExpectMsg(TimeSpan.FromSeconds(5)); _repo.DidNotReceive().PurgeTerminalAsync(Arg.Any(), Arg.Any(), Arg.Any()); } /// A purge failure is best-effort — the list still answers. [Fact] public void List_PurgeFailure_DoesNotFailTheList() { UseOptions(new ManagementServiceOptions { SecuredWriteTerminalPurgeInterval = TimeSpan.Zero }); _repo.PurgeTerminalAsync(Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new InvalidOperationException("db down")); CreateActor().Tell(List()); ExpectMsg(TimeSpan.FromSeconds(5)); } }