feat(notification-outbox): add daily terminal-row purge
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using ScadaLink.Commons.Entities.Notifications;
|
||||
using ScadaLink.Commons.Interfaces.Repositories;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
using ScadaLink.NotificationOutbox.Delivery;
|
||||
using ScadaLink.NotificationOutbox.Messages;
|
||||
|
||||
namespace ScadaLink.NotificationOutbox.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Task 16: Tests for the <see cref="NotificationOutboxActor"/> daily purge job — the
|
||||
/// periodic sweep that bulk-deletes terminal notification rows older than
|
||||
/// <see cref="NotificationOutboxOptions.TerminalRetention"/> via
|
||||
/// <see cref="INotificationOutboxRepository.DeleteTerminalOlderThanAsync"/>.
|
||||
/// </summary>
|
||||
public class NotificationOutboxActorPurgeTests : TestKit
|
||||
{
|
||||
private readonly INotificationOutboxRepository _outboxRepository =
|
||||
Substitute.For<INotificationOutboxRepository>();
|
||||
|
||||
private readonly INotificationRepository _notificationRepository =
|
||||
Substitute.For<INotificationRepository>();
|
||||
|
||||
private IServiceProvider BuildServiceProvider()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped(_ => _outboxRepository);
|
||||
services.AddScoped(_ => _notificationRepository);
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the actor with both the dispatch and purge timers set to a long interval so
|
||||
/// neither periodic timer fires during a test — purge ticks are sent manually instead.
|
||||
/// </summary>
|
||||
private IActorRef CreateActor(NotificationOutboxOptions? options = null)
|
||||
{
|
||||
return Sys.ActorOf(Props.Create(() => new NotificationOutboxActor(
|
||||
BuildServiceProvider(),
|
||||
options ?? new NotificationOutboxOptions
|
||||
{
|
||||
DispatchInterval = TimeSpan.FromHours(1),
|
||||
PurgeInterval = TimeSpan.FromHours(1),
|
||||
},
|
||||
NullLogger<NotificationOutboxActor>.Instance,
|
||||
new Dictionary<NotificationType, INotificationDeliveryAdapter>())));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PurgeTick_DeletesTerminalRows_WithCutoffAtUtcNowMinusTerminalRetention()
|
||||
{
|
||||
var retention = TimeSpan.FromDays(30);
|
||||
var options = new NotificationOutboxOptions
|
||||
{
|
||||
DispatchInterval = TimeSpan.FromHours(1),
|
||||
PurgeInterval = TimeSpan.FromHours(1),
|
||||
TerminalRetention = retention,
|
||||
};
|
||||
_outboxRepository
|
||||
.DeleteTerminalOlderThanAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||
.Returns(3);
|
||||
var actor = CreateActor(options);
|
||||
|
||||
var expectedCutoff = DateTimeOffset.UtcNow - retention;
|
||||
actor.Tell(InternalMessages.PurgeTick.Instance);
|
||||
|
||||
AwaitAssert(() =>
|
||||
_outboxRepository.Received(1).DeleteTerminalOlderThanAsync(
|
||||
Arg.Is<DateTimeOffset>(cutoff =>
|
||||
(cutoff - expectedCutoff).Duration() < TimeSpan.FromMinutes(1)),
|
||||
Arg.Any<CancellationToken>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FaultedPurge_DoesNotCrashActor_AndSubsequentPurgeStillRuns()
|
||||
{
|
||||
var calls = 0;
|
||||
_outboxRepository
|
||||
.DeleteTerminalOlderThanAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||
.Returns(_ =>
|
||||
{
|
||||
calls++;
|
||||
// First purge faults; the second returns normally to prove the actor lives on.
|
||||
if (calls == 1)
|
||||
{
|
||||
throw new InvalidOperationException("db down");
|
||||
}
|
||||
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
var actor = CreateActor();
|
||||
|
||||
// First tick: the purge faults internally but must be handled and not kill the actor.
|
||||
actor.Tell(InternalMessages.PurgeTick.Instance);
|
||||
AwaitAssert(() => _outboxRepository.Received(1).DeleteTerminalOlderThanAsync(
|
||||
Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>()));
|
||||
|
||||
// Second tick: if the faulted purge had crashed the actor, this would never run.
|
||||
actor.Tell(InternalMessages.PurgeTick.Instance);
|
||||
AwaitAssert(() => _outboxRepository.Received(2).DeleteTerminalOlderThanAsync(
|
||||
Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user