using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using NSubstitute.ExceptionExtensions; using ScadaLink.Commons.Entities.Notifications; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Messages.Notification; using ScadaLink.Commons.Types.Enums; namespace ScadaLink.NotificationOutbox.Tests; /// /// Task 13: Tests for the ingest path — building a /// from a , persisting it via /// , and acking the sender. /// public class NotificationOutboxActorIngestTests : TestKit { private readonly INotificationOutboxRepository _repository = Substitute.For(); private IServiceProvider BuildServiceProvider() { var services = new ServiceCollection(); services.AddScoped(_ => _repository); return services.BuildServiceProvider(); } private IActorRef CreateActor() { return Sys.ActorOf(Props.Create(() => new NotificationOutboxActor( BuildServiceProvider(), new NotificationOutboxOptions(), NullLogger.Instance))); } private static NotificationSubmit MakeSubmit(string? notificationId = null) { return new NotificationSubmit( NotificationId: notificationId ?? Guid.NewGuid().ToString(), ListName: "ops-team", Subject: "Tank overflow", Body: "Tank 3 level critical", SourceSiteId: "site-1", SourceInstanceId: "instance-42", SourceScript: "AlarmScript", SiteEnqueuedAt: new DateTimeOffset(2026, 5, 19, 8, 30, 0, TimeSpan.Zero)); } [Fact] public void NotificationSubmit_PersistsMappedNotification_AndAcksAccepted() { _repository.InsertIfNotExistsAsync(Arg.Any(), Arg.Any()) .Returns(true); var submit = MakeSubmit(); var actor = CreateActor(); actor.Tell(submit, TestActor); var ack = ExpectMsg(); Assert.Equal(submit.NotificationId, ack.NotificationId); Assert.True(ack.Accepted); Assert.Null(ack.Error); _repository.Received(1).InsertIfNotExistsAsync( Arg.Is(n => n.NotificationId == submit.NotificationId && n.Type == NotificationType.Email && n.ListName == submit.ListName && n.Subject == submit.Subject && n.Body == submit.Body && n.SourceSiteId == submit.SourceSiteId && n.SourceInstanceId == submit.SourceInstanceId && n.SourceScript == submit.SourceScript && n.SiteEnqueuedAt == submit.SiteEnqueuedAt && n.Status == NotificationStatus.Pending && n.CreatedAt != default), Arg.Any()); } [Fact] public void DuplicateSubmit_RepositoryReturnsFalse_StillAcksAccepted() { _repository.InsertIfNotExistsAsync(Arg.Any(), Arg.Any()) .Returns(false); var submit = MakeSubmit(); var actor = CreateActor(); actor.Tell(submit, TestActor); var ack = ExpectMsg(); Assert.Equal(submit.NotificationId, ack.NotificationId); Assert.True(ack.Accepted); Assert.Null(ack.Error); } [Fact] public void RepositoryThrows_AcksNotAcceptedWithError() { _repository.InsertIfNotExistsAsync(Arg.Any(), Arg.Any()) .ThrowsAsync(new InvalidOperationException("database unavailable")); var submit = MakeSubmit(); var actor = CreateActor(); actor.Tell(submit, TestActor); var ack = ExpectMsg(); Assert.Equal(submit.NotificationId, ack.NotificationId); Assert.False(ack.Accepted); Assert.NotNull(ack.Error); Assert.Contains("database unavailable", ack.Error); } }