feat(notification-outbox): add AddNotificationOutbox DI registration
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -34,8 +34,7 @@ public class NotificationOutboxActorIngestTests : TestKit
|
||||
return Sys.ActorOf(Props.Create(() => new NotificationOutboxActor(
|
||||
BuildServiceProvider(),
|
||||
new NotificationOutboxOptions(),
|
||||
NullLogger<NotificationOutboxActor>.Instance,
|
||||
new Dictionary<NotificationType, INotificationDeliveryAdapter>())));
|
||||
NullLogger<NotificationOutboxActor>.Instance)));
|
||||
}
|
||||
|
||||
private static NotificationSubmit MakeSubmit(string? notificationId = null)
|
||||
|
||||
@@ -46,8 +46,7 @@ public class NotificationOutboxActorPurgeTests : TestKit
|
||||
DispatchInterval = TimeSpan.FromHours(1),
|
||||
PurgeInterval = TimeSpan.FromHours(1),
|
||||
},
|
||||
NullLogger<NotificationOutboxActor>.Instance,
|
||||
new Dictionary<NotificationType, INotificationDeliveryAdapter>())));
|
||||
NullLogger<NotificationOutboxActor>.Instance)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -36,8 +36,7 @@ public class NotificationOutboxActorQueryTests : TestKit
|
||||
BuildServiceProvider(),
|
||||
// A long dispatch interval keeps the dispatch loop from interfering with these tests.
|
||||
options ?? new NotificationOutboxOptions { DispatchInterval = TimeSpan.FromHours(1) },
|
||||
NullLogger<NotificationOutboxActor>.Instance,
|
||||
new Dictionary<NotificationType, INotificationDeliveryAdapter>())));
|
||||
NullLogger<NotificationOutboxActor>.Instance)));
|
||||
}
|
||||
|
||||
private static Notification MakeNotification(
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Akka.TestKit.Xunit2" />
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
<PackageReference Include="xunit" />
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using ScadaLink.Commons.Interfaces.Repositories;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
using ScadaLink.NotificationOutbox.Delivery;
|
||||
using ScadaLink.NotificationService;
|
||||
|
||||
namespace ScadaLink.NotificationOutbox.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Task 17: Tests for <see cref="ServiceCollectionExtensions.AddNotificationOutbox"/> — the
|
||||
/// DI registration extension for the Notification Outbox component. The extension binds
|
||||
/// <see cref="NotificationOutboxOptions"/> from the <c>ScadaLink:NotificationOutbox</c>
|
||||
/// configuration section and registers the channel delivery adapter(s).
|
||||
///
|
||||
/// The Host wires both <c>AddNotificationService</c> and <c>AddNotificationOutbox</c> on the
|
||||
/// central node; these tests do the same so the
|
||||
/// adapter's SMTP dependencies (<c>Func<ISmtpClientWrapper></c>, <c>OAuth2TokenService</c>,
|
||||
/// <c>NotificationOptions</c>) are satisfied. <see cref="INotificationRepository"/> — which the
|
||||
/// <see cref="EmailNotificationDeliveryAdapter"/> takes directly and is registered scoped by the
|
||||
/// Configuration Database component — is supplied here by a lightweight stub.
|
||||
/// </summary>
|
||||
public class ServiceRegistrationTests
|
||||
{
|
||||
private static ServiceProvider BuildProvider()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Empty configuration: the options binding must still succeed and yield the
|
||||
// documented NotificationOutboxOptions defaults.
|
||||
var configuration = new ConfigurationBuilder().Build();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddLogging();
|
||||
|
||||
// INotificationRepository is registered scoped by the Configuration Database
|
||||
// component in production; a no-op stub stands in for it here.
|
||||
services.AddScoped<INotificationRepository>(_ => Substitute.For<INotificationRepository>());
|
||||
|
||||
services.AddNotificationService();
|
||||
services.AddNotificationOutbox();
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddNotificationOutbox_RegistersNotificationOutboxOptions_WithDefaults()
|
||||
{
|
||||
using var provider = BuildProvider();
|
||||
|
||||
var options = provider.GetRequiredService<IOptions<NotificationOutboxOptions>>().Value;
|
||||
|
||||
Assert.NotNull(options);
|
||||
Assert.Equal(TimeSpan.FromSeconds(10), options.DispatchInterval);
|
||||
Assert.Equal(100, options.DispatchBatchSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddNotificationOutbox_OptionsSection_IsTheNotificationOutboxConfigPath()
|
||||
{
|
||||
Assert.Equal(
|
||||
"ScadaLink:NotificationOutbox",
|
||||
NotificationOutbox.ServiceCollectionExtensions.OptionsSection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddNotificationOutbox_BindsNotificationOutboxOptions_FromConfiguration()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ScadaLink:NotificationOutbox:DispatchBatchSize"] = "250",
|
||||
})
|
||||
.Build();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddLogging();
|
||||
services.AddScoped<INotificationRepository>(_ => Substitute.For<INotificationRepository>());
|
||||
services.AddNotificationService();
|
||||
services.AddNotificationOutbox();
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var options = provider.GetRequiredService<IOptions<NotificationOutboxOptions>>().Value;
|
||||
|
||||
Assert.Equal(250, options.DispatchBatchSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddNotificationOutbox_RegistersEmailDeliveryAdapter()
|
||||
{
|
||||
using var provider = BuildProvider();
|
||||
using var scope = provider.CreateScope();
|
||||
|
||||
var adapter = scope.ServiceProvider.GetRequiredService<EmailNotificationDeliveryAdapter>();
|
||||
|
||||
Assert.NotNull(adapter);
|
||||
Assert.Equal(NotificationType.Email, adapter.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddNotificationOutbox_RegistersEmailAdapter_AsINotificationDeliveryAdapter()
|
||||
{
|
||||
using var provider = BuildProvider();
|
||||
using var scope = provider.CreateScope();
|
||||
|
||||
var adapters = scope.ServiceProvider.GetServices<INotificationDeliveryAdapter>().ToList();
|
||||
|
||||
var email = Assert.Single(adapters);
|
||||
Assert.IsType<EmailNotificationDeliveryAdapter>(email);
|
||||
Assert.Equal(NotificationType.Email, email.Type);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user