feat(notification-outbox): add AddNotificationOutbox DI registration

This commit is contained in:
Joseph Doherty
2026-05-19 02:07:29 -04:00
parent 517437b0d9
commit 703cb2d392
8 changed files with 218 additions and 45 deletions

View File

@@ -26,11 +26,19 @@ public class NotificationOutboxActorDispatchTests : TestKit
private readonly INotificationRepository _notificationRepository =
Substitute.For<INotificationRepository>();
private IServiceProvider BuildServiceProvider()
private IServiceProvider BuildServiceProvider(
IEnumerable<INotificationDeliveryAdapter> adapters)
{
var services = new ServiceCollection();
services.AddScoped(_ => _outboxRepository);
services.AddScoped(_ => _notificationRepository);
// The actor resolves the channel adapters from its per-sweep DI scope; register
// each stub adapter under the INotificationDeliveryAdapter service.
foreach (var adapter in adapters)
{
services.AddScoped<INotificationDeliveryAdapter>(_ => adapter);
}
return services.BuildServiceProvider();
}
@@ -67,14 +75,13 @@ public class NotificationOutboxActorDispatchTests : TestKit
}
private IActorRef CreateActor(
IReadOnlyDictionary<NotificationType, INotificationDeliveryAdapter> adapters,
IEnumerable<INotificationDeliveryAdapter> adapters,
NotificationOutboxOptions? options = null)
{
return Sys.ActorOf(Props.Create(() => new NotificationOutboxActor(
BuildServiceProvider(),
BuildServiceProvider(adapters),
options ?? new NotificationOutboxOptions { DispatchInterval = TimeSpan.FromHours(1) },
NullLogger<NotificationOutboxActor>.Instance,
adapters)));
NullLogger<NotificationOutboxActor>.Instance)));
}
private static Notification MakeNotification(
@@ -107,10 +114,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification });
var adapter = new StubAdapter(() => DeliveryOutcome.Success("ops@example.com"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter>
{
[NotificationType.Email] = adapter,
});
var actor = CreateActor([adapter]);
actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -130,10 +134,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification });
var adapter = new StubAdapter(() => DeliveryOutcome.Success("ops@example.com"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter>
{
[NotificationType.Email] = adapter,
});
var actor = CreateActor([adapter]);
actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -158,10 +159,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification });
var adapter = new StubAdapter(() => DeliveryOutcome.Transient("smtp timeout"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter>
{
[NotificationType.Email] = adapter,
});
var actor = CreateActor([adapter]);
actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -187,10 +185,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification });
var adapter = new StubAdapter(() => DeliveryOutcome.Transient("smtp timeout"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter>
{
[NotificationType.Email] = adapter,
});
var actor = CreateActor([adapter]);
actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -212,10 +207,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification });
var adapter = new StubAdapter(() => DeliveryOutcome.Permanent("invalid recipient address"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter>
{
[NotificationType.Email] = adapter,
});
var actor = CreateActor([adapter]);
actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -237,8 +229,8 @@ public class NotificationOutboxActorDispatchTests : TestKit
var notification = MakeNotification();
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification });
// Empty adapter dictionary: no adapter resolves for the notification's type.
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter>());
// No adapters registered: none resolves for the notification's type.
var actor = CreateActor([]);
actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -262,7 +254,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
// failure were not handled, which would leave _dispatching stuck true forever.
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns<IReadOnlyList<Notification>>(_ => throw new InvalidOperationException("db down"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter>());
var actor = CreateActor([]);
// First tick: the pass faults internally but must still clear the in-flight guard.
actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -287,10 +279,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
var adapter = new StubAdapter(
() => DeliveryOutcome.Success("ops@example.com"),
delay: TimeSpan.FromMilliseconds(800));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter>
{
[NotificationType.Email] = adapter,
});
var actor = CreateActor([adapter]);
actor.Tell(InternalMessages.DispatchTick.Instance);
actor.Tell(InternalMessages.DispatchTick.Instance);