diff --git a/src/Altinn.Notifications.Core/Enums/SmsNotificationResultType.cs b/src/Altinn.Notifications.Core/Enums/SmsNotificationResultType.cs index 03b2239a..0d12645e 100644 --- a/src/Altinn.Notifications.Core/Enums/SmsNotificationResultType.cs +++ b/src/Altinn.Notifications.Core/Enums/SmsNotificationResultType.cs @@ -28,5 +28,10 @@ public enum SmsNotificationResultType /// /// Failed, invalid mobilenumber /// - Failed_InvalidRecipient + Failed_InvalidRecipient, + + /// + /// Recipient mobile number was not identified + /// + Failed_RecipientNotIdentified } diff --git a/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs index 208a8f3f..af20e0c9 100644 --- a/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs @@ -31,10 +31,13 @@ public static void AddCoreServices(this IServiceCollection services, IConfigurat .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .Configure(config.GetSection("KafkaSettings")) diff --git a/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs b/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs new file mode 100644 index 00000000..c182a4f0 --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Recipients/SmsRecipient.cs @@ -0,0 +1,17 @@ +namespace Altinn.Notifications.Core.Models.Recipients; + +/// +/// Class representing an sms recipient +/// +public class SmsRecipient +{ + /// + /// Gets or sets the recipient id + /// + public string? RecipientId { get; set; } = null; + + /// + /// Gets or sets the mobile number + /// + public string MobileNumber { get; set; } = string.Empty; +} diff --git a/src/Altinn.Notifications.Core/Persistence/IEmailNotificationRepository.cs b/src/Altinn.Notifications.Core/Persistence/IEmailNotificationRepository.cs index bed06396..f1054d9c 100644 --- a/src/Altinn.Notifications.Core/Persistence/IEmailNotificationRepository.cs +++ b/src/Altinn.Notifications.Core/Persistence/IEmailNotificationRepository.cs @@ -27,8 +27,8 @@ public interface IEmailNotificationRepository public Task UpdateSendStatus(Guid notificationId, EmailNotificationResultType status, string? operationId = null); /// - /// Retrieves all email recipients for an order + /// Retrieves all processed email recipients for an order /// - /// A list of emails - public Task> GetRecipients(Guid notificationId); + /// A list of email recipients + public Task> GetRecipients(Guid orderId); } diff --git a/src/Altinn.Notifications.Core/Persistence/ISmsNotificationRepository.cs b/src/Altinn.Notifications.Core/Persistence/ISmsNotificationRepository.cs index 0aca03e4..89b4d0b7 100644 --- a/src/Altinn.Notifications.Core/Persistence/ISmsNotificationRepository.cs +++ b/src/Altinn.Notifications.Core/Persistence/ISmsNotificationRepository.cs @@ -1,5 +1,6 @@ using Altinn.Notifications.Core.Models; using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Models.Recipients; namespace Altinn.Notifications.Core.Persistence; @@ -18,4 +19,10 @@ public interface ISmsNotificationRepository /// /// A list of sms public Task> GetNewNotifications(); + + /// + /// Retrieves all processed sms recipients for an order + /// + /// A list of sms recipients + public Task> GetRecipients(Guid orderId); } diff --git a/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs new file mode 100644 index 00000000..dd0c267a --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/EmailOrderProcessingService.cs @@ -0,0 +1,55 @@ +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Address; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Models.Recipients; +using Altinn.Notifications.Core.Persistence; +using Altinn.Notifications.Core.Services.Interfaces; + +namespace Altinn.Notifications.Core.Services; + +/// +/// Implementation of the +/// +public class EmailOrderProcessingService : IEmailOrderProcessingService +{ + private readonly IEmailNotificationRepository _emailNotificationRepository; + private readonly IEmailNotificationService _emailService; + + /// + /// Initializes a new instance of the class. + /// + public EmailOrderProcessingService( + IEmailNotificationRepository emailNotificationRepository, + IEmailNotificationService emailService) + { + _emailNotificationRepository = emailNotificationRepository; + _emailService = emailService; + } + + /// + public async Task ProcessOrder(NotificationOrder order) + { + foreach (Recipient recipient in order.Recipients) + { + await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient); + } + } + + /// + public async Task ProcessOrderRetry(NotificationOrder order) + { + List emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); + foreach (Recipient recipient in order.Recipients) + { + EmailAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Email) as EmailAddressPoint; + + if (!emailRecipients.Exists(er => + er.RecipientId == (string.IsNullOrEmpty(recipient.RecipientId) ? null : recipient.RecipientId) + && er.ToAddress.Equals(addressPoint?.EmailAddress))) + { + await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient); + } + } + } +} diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailOrderProcessingService.cs new file mode 100644 index 00000000..e011af04 --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailOrderProcessingService.cs @@ -0,0 +1,19 @@ +using Altinn.Notifications.Core.Models.Orders; + +namespace Altinn.Notifications.Core.Services.Interfaces; + +/// +/// Interface for the order processing service speficic to email orders +/// +public interface IEmailOrderProcessingService +{ + /// + /// Processes a notification order + /// + public Task ProcessOrder(NotificationOrder order); + + /// + /// Retry processing of an order + /// + public Task ProcessOrderRetry(NotificationOrder order); +} diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs new file mode 100644 index 00000000..09d3e892 --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsNotificationService.cs @@ -0,0 +1,14 @@ +using Altinn.Notifications.Core.Models; + +namespace Altinn.Notifications.Core.Services.Interfaces; + +/// +/// Interface for sms notification service +/// +public interface ISmsNotificationService +{ + /// + /// Creates a new sms notification based on the provided orderId and recipient + /// + public Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient); +} diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/ISmsOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsOrderProcessingService.cs new file mode 100644 index 00000000..535368dc --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/Interfaces/ISmsOrderProcessingService.cs @@ -0,0 +1,19 @@ +using Altinn.Notifications.Core.Models.Orders; + +namespace Altinn.Notifications.Core.Services.Interfaces; + +/// +/// Interface for the order processing service speficic to sms orders +/// +public interface ISmsOrderProcessingService +{ + /// + /// Processes a notification order + /// + public Task ProcessOrder(NotificationOrder order); + + /// + /// Retry processing of an order + /// + public Task ProcessOrderRetry(NotificationOrder order); +} diff --git a/src/Altinn.Notifications.Core/Services/OrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/OrderProcessingService.cs index 0d02aaa9..68888568 100644 --- a/src/Altinn.Notifications.Core/Services/OrderProcessingService.cs +++ b/src/Altinn.Notifications.Core/Services/OrderProcessingService.cs @@ -1,13 +1,12 @@ using System.Diagnostics; + using Altinn.Notifications.Core.Configuration; using Altinn.Notifications.Core.Enums; using Altinn.Notifications.Core.Integrations; -using Altinn.Notifications.Core.Models; -using Altinn.Notifications.Core.Models.Address; using Altinn.Notifications.Core.Models.Orders; -using Altinn.Notifications.Core.Models.Recipients; using Altinn.Notifications.Core.Persistence; using Altinn.Notifications.Core.Services.Interfaces; + using Microsoft.Extensions.Options; namespace Altinn.Notifications.Core.Services; @@ -18,8 +17,8 @@ namespace Altinn.Notifications.Core.Services; public class OrderProcessingService : IOrderProcessingService { private readonly IOrderRepository _orderRepository; - private readonly IEmailNotificationRepository _emailNotificationRepository; - private readonly IEmailNotificationService _emailService; + private readonly IEmailOrderProcessingService _emailProcessingService; + private readonly ISmsOrderProcessingService _smsProcessingService; private readonly IKafkaProducer _producer; private readonly string _pastDueOrdersTopic; @@ -27,15 +26,15 @@ public class OrderProcessingService : IOrderProcessingService /// Initializes a new instance of the class. /// public OrderProcessingService( - IOrderRepository orderRepository, - IEmailNotificationRepository emailNotificationRepository, - IEmailNotificationService emailService, - IKafkaProducer producer, + IOrderRepository orderRepository, + IEmailOrderProcessingService emailProcessingService, + ISmsOrderProcessingService smsProcessingService, + IKafkaProducer producer, IOptions kafkaSettings) { _orderRepository = orderRepository; - _emailNotificationRepository = emailNotificationRepository; - _emailService = emailService; + _emailProcessingService = emailProcessingService; + _smsProcessingService = smsProcessingService; _producer = producer; _pastDueOrdersTopic = kafkaSettings.Value.PastDueOrdersTopicName; } @@ -68,14 +67,14 @@ public async Task ProcessOrder(NotificationOrder order) { NotificationChannel ch = order.NotificationChannel; - foreach (Recipient recipient in order.Recipients) + switch (ch) { - switch (ch) - { - case NotificationChannel.Email: - await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient); - break; - } + case NotificationChannel.Email: + await _emailProcessingService.ProcessOrder(order); + break; + case NotificationChannel.Sms: + await _smsProcessingService.ProcessOrder(order); + break; } await _orderRepository.SetProcessingStatus(order.Id, OrderProcessingStatus.Completed); @@ -86,23 +85,14 @@ public async Task ProcessOrderRetry(NotificationOrder order) { NotificationChannel ch = order.NotificationChannel; - List emailRecipients = await _emailNotificationRepository.GetRecipients(order.Id); - - foreach (Recipient recipient in order.Recipients) + switch (ch) { - switch (ch) - { - case NotificationChannel.Email: - EmailAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Email) as EmailAddressPoint; - - if (!emailRecipients.Exists(er => er.RecipientId == (string.IsNullOrEmpty(recipient.RecipientId) ? null : recipient.RecipientId) - && er.ToAddress.Equals(addressPoint?.EmailAddress))) - { - await _emailService.CreateNotification(order.Id, order.RequestedSendTime, recipient); - } - - break; - } + case NotificationChannel.Email: + await _emailProcessingService.ProcessOrderRetry(order); + break; + case NotificationChannel.Sms: + await _smsProcessingService.ProcessOrderRetry(order); + break; } await _orderRepository.SetProcessingStatus(order.Id, OrderProcessingStatus.Completed); diff --git a/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs new file mode 100644 index 00000000..57e2a718 --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/SmsNotificationService.cs @@ -0,0 +1,61 @@ +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Address; +using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Persistence; +using Altinn.Notifications.Core.Services.Interfaces; + +namespace Altinn.Notifications.Core.Services; + +/// +/// Implementation of +/// +public class SmsNotificationService : ISmsNotificationService +{ + private readonly IGuidService _guid; + private readonly IDateTimeService _dateTime; + private readonly ISmsNotificationRepository _repository; + + /// + /// Initializes a new instance of the class. + /// + public SmsNotificationService( + IGuidService guid, + IDateTimeService dateTime, + ISmsNotificationRepository repository) + { + _guid = guid; + _dateTime = dateTime; + _repository = repository; + } + + /// + public async Task CreateNotification(Guid orderId, DateTime requestedSendTime, Recipient recipient) + { + SmsAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Sms) as SmsAddressPoint; + + if (!string.IsNullOrEmpty(addressPoint?.MobileNumber)) + { + await CreateNotificationForRecipient(orderId, requestedSendTime, recipient.RecipientId, addressPoint!.MobileNumber, SmsNotificationResultType.New); + } + else + { + await CreateNotificationForRecipient(orderId, requestedSendTime, recipient.RecipientId, string.Empty, SmsNotificationResultType.Failed_RecipientNotIdentified); + } + } + + private async Task CreateNotificationForRecipient(Guid orderId, DateTime requestedSendTime, string recipientId, string recipientNumber, SmsNotificationResultType type) + { + var smsNotification = new SmsNotification() + { + Id = _guid.NewGuid(), + OrderId = orderId, + RequestedSendTime = requestedSendTime, + RecipientNumber = recipientNumber, + RecipientId = string.IsNullOrEmpty(recipientId) ? null : recipientId, + SendResult = new(type, _dateTime.UtcNow()) + }; + + await _repository.AddNotification(smsNotification, requestedSendTime.AddHours(1)); + } +} diff --git a/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs new file mode 100644 index 00000000..d5535bea --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/SmsOrderProcessingService.cs @@ -0,0 +1,53 @@ +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Address; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Models.Recipients; +using Altinn.Notifications.Core.Persistence; +using Altinn.Notifications.Core.Services.Interfaces; + +namespace Altinn.Notifications.Core.Services; + +/// +/// Implementation of the +/// +public class SmsOrderProcessingService : ISmsOrderProcessingService +{ + private readonly ISmsNotificationRepository _smsNotificationRepository; + private readonly ISmsNotificationService _smsService; + + /// + /// Initializes a new instance of the class. + /// + public SmsOrderProcessingService(ISmsNotificationRepository smsNotificationRepository, ISmsNotificationService smsService) + { + _smsNotificationRepository = smsNotificationRepository; + _smsService = smsService; + } + + /// + public async Task ProcessOrder(NotificationOrder order) + { + foreach (Recipient recipient in order.Recipients) + { + await _smsService.CreateNotification(order.Id, order.RequestedSendTime, recipient); + } + } + + /// + public async Task ProcessOrderRetry(NotificationOrder order) + { + List smsRecipients = await _smsNotificationRepository.GetRecipients(order.Id); + foreach (Recipient recipient in order.Recipients) + { + SmsAddressPoint? addressPoint = recipient.AddressInfo.Find(a => a.AddressType == AddressType.Sms) as SmsAddressPoint; + + if (!smsRecipients.Exists(sr => + sr.RecipientId == (string.IsNullOrEmpty(recipient.RecipientId) ? null : recipient.RecipientId) + && sr.MobileNumber.Equals(addressPoint?.MobileNumber))) + { + await _smsService.CreateNotification(order.Id, order.RequestedSendTime, recipient); + } + } + } +} diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.19/01-alter-types.sql b/src/Altinn.Notifications.Persistence/Migration/v0.19/01-alter-types.sql new file mode 100644 index 00000000..e4d7ab19 --- /dev/null +++ b/src/Altinn.Notifications.Persistence/Migration/v0.19/01-alter-types.sql @@ -0,0 +1 @@ +ALTER TYPE smsnotificationresulttype ADD VALUE 'Failed_RecipientNotIdentified'; diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.19/02-setup-functions.sql b/src/Altinn.Notifications.Persistence/Migration/v0.19/02-setup-functions.sql new file mode 100644 index 00000000..50546b18 --- /dev/null +++ b/src/Altinn.Notifications.Persistence/Migration/v0.19/02-setup-functions.sql @@ -0,0 +1,17 @@ +CREATE OR REPLACE FUNCTION notifications.getsmsrecipients(_orderid uuid) +RETURNS TABLE( + recipientid text, + mobilenumber text +) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE +__orderid BIGINT := (SELECT _id from notifications.orders + where alternateid = _orderid); +BEGIN +RETURN query + SELECT s.recipientid, s.mobilenumber + FROM notifications.smsnotifications s + WHERE s._orderid = __orderid; +END; +$BODY$; \ No newline at end of file diff --git a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs index 50ab382d..171a1d27 100644 --- a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs @@ -21,7 +21,7 @@ public class EmailNotificationRepository : IEmailNotificationRepository private const string _insertEmailNotificationSql = "call notifications.insertemailnotification($1, $2, $3, $4, $5, $6, $7)"; // (__orderid, _alternateid, _recipientid, _toaddress, _result, _resulttime, _expirytime) private const string _getEmailNotificationsSql = "select * from notifications.getemails_statusnew_updatestatus()"; private const string _updateEmailStatus = "call notifications.updateemailstatus($1, $2, $3)"; // (_alternateid, _result, _operationid) - private const string _getEmailRecipients = "select * from notifications.getemailrecipients($1)"; // (_alternateid) + private const string _getEmailRecipients = "select * from notifications.getemailrecipients($1)"; // (_orderid) /// /// Initializes a new instance of the class. @@ -94,13 +94,13 @@ public async Task UpdateSendStatus(Guid notificationId, EmailNotificationResultT } /// - public async Task> GetRecipients(Guid notificationId) + public async Task> GetRecipients(Guid orderId) { List searchResult = new(); await using NpgsqlCommand pgcom = _dataSource.CreateCommand(_getEmailRecipients); using TelemetryTracker tracker = new(_telemetryClient, pgcom); - pgcom.Parameters.AddWithValue(NpgsqlDbType.Uuid, notificationId); + pgcom.Parameters.AddWithValue(NpgsqlDbType.Uuid, orderId); await using (NpgsqlDataReader reader = await pgcom.ExecuteReaderAsync()) { while (await reader.ReadAsync()) diff --git a/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs b/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs index b4abfb3b..190c324e 100644 --- a/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/SmsNotificationRepository.cs @@ -1,5 +1,6 @@ using Altinn.Notifications.Core.Models; using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Models.Recipients; using Altinn.Notifications.Core.Persistence; using Altinn.Notifications.Persistence.Extensions; @@ -21,6 +22,7 @@ public class SmsNotificationRepository : ISmsNotificationRepository private const string _insertSmsNotificationSql = "call notifications.insertsmsnotification($1, $2, $3, $4, $5, $6, $7)"; // (__orderid, _alternateid, _recipientid, _mobilenumber, _result, _resulttime, _expirytime) private const string _getSmsNotificationsSql = "select * from notifications.getsms_statusnew_updatestatus()"; + private const string _getSmsRecipients = "select * from notifications.getsmsrecipients($1)"; // (_orderid) /// /// Initializes a new instance of the class. @@ -51,6 +53,30 @@ public async Task AddNotification(SmsNotification notification, DateTime expiry) tracker.Track(); } + /// + public async Task> GetRecipients(Guid orderId) + { + List searchResult = new(); + + await using NpgsqlCommand pgcom = _dataSource.CreateCommand(_getSmsRecipients); + using TelemetryTracker tracker = new(_telemetryClient, pgcom); + pgcom.Parameters.AddWithValue(NpgsqlDbType.Uuid, orderId); + await using (NpgsqlDataReader reader = await pgcom.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + searchResult.Add(new SmsRecipient() + { + RecipientId = reader.GetValue("recipientid"), + MobileNumber = reader.GetValue("mobilenumber") + }); + } + } + + tracker.Track(); + return searchResult; + } + /// public async Task> GetNewNotifications() { diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications.Persistence/SmsNotificationRepositoryTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications.Persistence/SmsNotificationRepositoryTests.cs index 7f7de3b4..5b3bfc5a 100644 --- a/test/Altinn.Notifications.IntegrationTests/Notifications.Persistence/SmsNotificationRepositoryTests.cs +++ b/test/Altinn.Notifications.IntegrationTests/Notifications.Persistence/SmsNotificationRepositoryTests.cs @@ -1,6 +1,7 @@ using Altinn.Notifications.Core.Models; using Altinn.Notifications.Core.Models.Notification; using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Models.Recipients; using Altinn.Notifications.Core.Persistence; using Altinn.Notifications.IntegrationTests.Utils; using Altinn.Notifications.Persistence.Repository; @@ -80,4 +81,28 @@ public async Task GetNewNotifications() // Assert Assert.NotEmpty(smsToBeSent.Where(s => s.NotificationId == smsNotification.Id)); } + + [Fact] + public async Task GetRecipients() + { + // Arrange + SmsNotificationRepository repo = (SmsNotificationRepository)ServiceUtil + .GetServices(new List() { typeof(ISmsNotificationRepository) }) + .First(i => i.GetType() == typeof(SmsNotificationRepository)); + + (NotificationOrder order, SmsNotification smsNotification) = await PostgreUtil.PopulateDBWithOrderAndSmsNotification(); + orderIdsToDelete.Add(order.Id); + string expectedNumber = smsNotification.RecipientNumber; + string? expectedRecipientId = smsNotification.RecipientId; + + // Act + List actual = await repo.GetRecipients(order.Id); + + SmsRecipient actualRecipient = actual[0]; + + // Assert + Assert.Single(actual); + Assert.Equal(expectedNumber, actualRecipient.MobileNumber); + Assert.Equal(expectedRecipientId, actualRecipient.RecipientId); + } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs index 69df0b12..91512c21 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs @@ -106,7 +106,7 @@ public async Task CreateEmailNotification_ToAddressDefined_ResultNew() } [Fact] - public async Task CreateEmailNotification_ToAddressDefined_ResultFailedRecipientNotDefined() + public async Task CreateEmailNotification_ToAddressMissing_ResultFailedRecipientNotDefined() { // Arrange Guid id = Guid.NewGuid(); diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs new file mode 100644 index 00000000..591dda68 --- /dev/null +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailOrderProcessingServiceTests.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Address; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Models.Recipients; +using Altinn.Notifications.Core.Persistence; +using Altinn.Notifications.Core.Services; +using Altinn.Notifications.Core.Services.Interfaces; + +using Moq; + +using Xunit; + +namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; + +public class EmailOrderProcessingServiceTests +{ + [Fact] + public async Task ProcessOrder_ServiceCalledOnceForEachRecipient() + { + // Arrange + var order = new NotificationOrder() + { + Id = Guid.NewGuid(), + NotificationChannel = NotificationChannel.Email, + Recipients = new List() + { + new(), + new() + } + }; + + var serviceMock = new Mock(); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny())); + + var service = GetTestService(emailService: serviceMock.Object); + + // Act + await service.ProcessOrder(order); + + // Assert + serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task ProcessOrder_ExpectedInputToService() + { + // Arrange + DateTime requested = DateTime.UtcNow; + Guid orderId = Guid.NewGuid(); + + var order = new NotificationOrder() + { + Id = orderId, + NotificationChannel = NotificationChannel.Email, + RequestedSendTime = requested, + Recipients = new List() + { + new Recipient("skd", new List() { new EmailAddressPoint("test@test.com") }) + } + }; + + Recipient expectedRecipient = new("skd", new List() { new EmailAddressPoint("test@test.com") }); + + var serviceMock = new Mock(); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.Is(d => d.Equals(requested)), It.Is(r => AssertUtils.AreEquivalent(expectedRecipient, r)))); + + var service = GetTestService(emailService: serviceMock.Object); + + // Act + await service.ProcessOrder(order); + + // Assert + serviceMock.VerifyAll(); + } + + [Fact] + public async Task ProcessOrder_ServiceThrowsException_RepositoryNotCalled() + { + // Arrange + var order = new NotificationOrder() + { + NotificationChannel = NotificationChannel.Email, + Recipients = new List() + { + new Recipient() + } + }; + + var serviceMock = new Mock(); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception()); + + var repoMock = new Mock(); + repoMock.Setup(r => r.SetProcessingStatus(It.IsAny(), It.IsAny())); + + var service = GetTestService(emailService: serviceMock.Object); + + // Act + await Assert.ThrowsAsync(async () => await service.ProcessOrder(order)); + + // Assert + serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + repoMock.Verify(r => r.SetProcessingStatus(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task ProcessOrderRetry_ServiceCalledIfRecipientNotInDatabase() + { + // Arrange + var order = new NotificationOrder() + { + Id = Guid.NewGuid(), + NotificationChannel = NotificationChannel.Email, + Recipients = new List() + { + new Recipient(), + new Recipient("skd", new List() { new EmailAddressPoint("test@test.com") }), + new Recipient(new List() { new EmailAddressPoint("test@domain.com") }) + } + }; + + var serviceMock = new Mock(); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny())); + + var emailRepoMock = new Mock(); + emailRepoMock.Setup(e => e.GetRecipients(It.IsAny())).ReturnsAsync(new List() { new EmailRecipient() { RecipientId = "skd", ToAddress = "test@test.com" } }); + + var service = GetTestService(emailRepo: emailRepoMock.Object, emailService: serviceMock.Object); + + // Act + await service.ProcessOrderRetry(order); + + // Assert + serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + emailRepoMock.Verify(e => e.GetRecipients(It.IsAny()), Times.Once); + } + + private static EmailOrderProcessingService GetTestService( + IEmailNotificationRepository? emailRepo = null, + IEmailNotificationService? emailService = null) + { + if (emailRepo == null) + { + var emailRepoMock = new Mock(); + emailRepo = emailRepoMock.Object; + } + + if (emailService == null) + { + var emailServiceMock = new Mock(); + emailService = emailServiceMock.Object; + } + + var smsRepoMock = new Mock(); + var smsServiceMock = new Mock(); + + return new EmailOrderProcessingService(emailRepo, emailService); + } +} diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs index 68ca7c46..7c252c3f 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/OrderProcessingServiceTests.cs @@ -2,17 +2,12 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Altinn.Notifications.Core.Configuration; using Altinn.Notifications.Core.Enums; using Altinn.Notifications.Core.Integrations; -using Altinn.Notifications.Core.Models; -using Altinn.Notifications.Core.Models.Address; using Altinn.Notifications.Core.Models.Orders; -using Altinn.Notifications.Core.Models.Recipients; using Altinn.Notifications.Core.Persistence; using Altinn.Notifications.Core.Services; using Altinn.Notifications.Core.Services.Interfaces; -using Altinn.Notifications.Integrations.Configuration; using Microsoft.Extensions.Options; @@ -20,6 +15,8 @@ using Xunit; +using static Altinn.Authorization.ABAC.Constants.XacmlConstants; + namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; public class OrderProcessingServiceTests @@ -50,164 +47,139 @@ public async Task StartProcessingPastDueOrders_ProducerCalledOnceForEachOrder() } [Fact] - public async Task ProcessOrder_EmailNotificationChannel_ServiceCalledOnceForEachRecipient() + public async Task ProcessOrder_EmailOrder_EmailServiceCalled() { - // Arrange - var order = new NotificationOrder() + // Arrange + NotificationOrder order = new() { - Id = Guid.NewGuid(), NotificationChannel = NotificationChannel.Email, - Recipients = new List() - { - new Recipient(), - new Recipient() - } }; - var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny())); + var emailMockService = new Mock(); + emailMockService.Setup(e => e.ProcessOrder(It.IsAny())); + + var smsMockService = new Mock(); + smsMockService.Setup(s => s.ProcessOrder(It.IsAny())); - var service = GetTestService(emailService: serviceMock.Object); + var orderProcessingService = GetTestService(emailMock: emailMockService.Object, smsMock: smsMockService.Object); // Act - await service.ProcessOrder(order); + await orderProcessingService.ProcessOrder(order); // Assert - serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + emailMockService.Verify(e => e.ProcessOrder(It.IsAny()), Times.Once); + smsMockService.Verify(s => s.ProcessOrder(It.IsAny()), Times.Never); } [Fact] - public async Task ProcessOrder_EmailNotificationChannel_ExpectedInputToService() + public async Task ProcessOrder_SmsOrder_SmsServiceCalled() { - // Arrange - DateTime requested = DateTime.UtcNow; - Guid orderId = Guid.NewGuid(); - - var order = new NotificationOrder() + // Arrange + NotificationOrder order = new() { - Id = orderId, - NotificationChannel = NotificationChannel.Email, - RequestedSendTime = requested, - Recipients = new List() - { - new Recipient("skd", new List() { new EmailAddressPoint("test@test.com") }) - } + NotificationChannel = NotificationChannel.Sms, }; - Recipient expectedRecipient = new("skd", new List() { new EmailAddressPoint("test@test.com") }); - - var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.Is(d => d.Equals(requested)), It.Is(r => AssertUtils.AreEquivalent(expectedRecipient, r)))); + var smsMockService = new Mock(); + smsMockService.Setup(s => s.ProcessOrder(It.IsAny())); - var repoMock = new Mock(); - repoMock.Setup(r => r.SetProcessingStatus(It.Is(s => s.Equals(orderId)), It.Is(s => s == OrderProcessingStatus.Completed))); + var emailMockService = new Mock(); + emailMockService.Setup(e => e.ProcessOrder(It.IsAny())); - var service = GetTestService(repo: repoMock.Object, emailService: serviceMock.Object); + var orderProcessingService = GetTestService(emailMock: emailMockService.Object, smsMock: smsMockService.Object); // Act - await service.ProcessOrder(order); + await orderProcessingService.ProcessOrder(order); // Assert - serviceMock.VerifyAll(); - repoMock.VerifyAll(); + smsMockService.Verify(s => s.ProcessOrder(It.IsAny()), Times.Once); + emailMockService.Verify(e => e.ProcessOrder(It.IsAny()), Times.Never); } [Fact] - public async Task ProcessOrder_EmailNotificationChannel_ServiceThrowsException_RepositoryNotCalled() + public async Task ProcessOrder_SerivceThrowsException_ProcessingStatusIsNotSet() { - // Arrange - var order = new NotificationOrder() + // Arrange + NotificationOrder order = new() { - NotificationChannel = NotificationChannel.Email, - Recipients = new List() - { - new Recipient() - } + NotificationChannel = NotificationChannel.Sms, }; - var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new Exception()); + var smsMockService = new Mock(); + smsMockService.Setup(s => s.ProcessOrder(It.IsAny())).Throws(new Exception()); var repoMock = new Mock(); - repoMock.Setup(r => r.SetProcessingStatus(It.IsAny(), It.IsAny())); + repoMock.Setup(r => r.SetProcessingStatus(It.IsAny(), It.Is(s => s.Equals(OrderProcessingStatus.Completed)))); - var service = GetTestService(repo: repoMock.Object, emailService: serviceMock.Object); + var orderProcessingService = GetTestService(repo: repoMock.Object, smsMock: smsMockService.Object); - // Act - await Assert.ThrowsAsync(async () => await service.ProcessOrder(order)); + // Act + await Assert.ThrowsAsync(async () => await orderProcessingService.ProcessOrder(order)); // Assert - serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - repoMock.Verify(r => r.SetProcessingStatus(It.IsAny(), It.IsAny()), Times.Never); + smsMockService.Verify(s => s.ProcessOrder(It.IsAny()), Times.Once); + repoMock.Verify( + r => r.SetProcessingStatus(It.IsAny(), It.Is(s => s.Equals(OrderProcessingStatus.Completed))), + Times.Never); } [Fact] - public async Task ProcessOrderRetry_EmailNotificationChannel_ServiceCalledIfEmailNotificationNotCreated() + public async Task ProcessOrderRetry_SmsOrder_SmsServiceCalled() { - // Arrange - var order = new NotificationOrder() + // Arrange + NotificationOrder order = new() { - Id = Guid.NewGuid(), - NotificationChannel = NotificationChannel.Email, - Recipients = new List() - { - new Recipient(), - new Recipient("skd", new List() { new EmailAddressPoint("test@test.com") }) - } + NotificationChannel = NotificationChannel.Sms, }; - var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny())); + var smsMockService = new Mock(); + smsMockService.Setup(s => s.ProcessOrderRetry(It.IsAny())); - var emailRepoMock = new Mock(); - emailRepoMock.Setup(e => e.GetRecipients(It.IsAny())).ReturnsAsync(new List() { new EmailRecipient() { RecipientId = "skd", ToAddress = "test@test.com" } }); + var emailMockService = new Mock(); + emailMockService.Setup(e => e.ProcessOrderRetry(It.IsAny())); - var service = GetTestService(emailRepo: emailRepoMock.Object, emailService: serviceMock.Object); + var orderProcessingService = GetTestService(emailMock: emailMockService.Object, smsMock: smsMockService.Object); // Act - await service.ProcessOrderRetry(order); + await orderProcessingService.ProcessOrderRetry(order); // Assert - serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - emailRepoMock.Verify(e => e.GetRecipients(It.IsAny()), Times.Once); + smsMockService.Verify(s => s.ProcessOrderRetry(It.IsAny()), Times.Once); + emailMockService.Verify(e => e.ProcessOrderRetry(It.IsAny()), Times.Never); } [Fact] - public async Task ProcessOrderRetry_EmailNotificationChannel_ServiceThrowsException_OrderRepositoryNotCalled() + public async Task ProcessOrderRetry_SerivceThrowsException_ProcessingStatusIsNotSet() { - // Arrange - var order = new NotificationOrder() + // Arrange + NotificationOrder order = new() { - NotificationChannel = NotificationChannel.Email, - Recipients = new List() - { - new Recipient() - } + NotificationChannel = NotificationChannel.Sms, }; - var serviceMock = new Mock(); - serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new Exception()); + var smsMockService = new Mock(); + smsMockService.Setup(s => s.ProcessOrderRetry(It.IsAny())).Throws(new Exception()); var repoMock = new Mock(); - repoMock.Setup(r => r.SetProcessingStatus(It.IsAny(), It.IsAny())); + repoMock.Setup(r => r.SetProcessingStatus(It.IsAny(), It.Is(s => s.Equals(OrderProcessingStatus.Completed)))); - var emailRepoMock = new Mock(); - emailRepoMock.Setup(e => e.GetRecipients(It.IsAny())).ReturnsAsync(new List() { new EmailRecipient() { RecipientId = "skd", ToAddress = "test@test.com" } }); + var orderProcessingService = GetTestService(repo: repoMock.Object, smsMock: smsMockService.Object); - var service = GetTestService(repo: repoMock.Object, emailRepo: emailRepoMock.Object, emailService: serviceMock.Object); - - // Act - await Assert.ThrowsAsync(async () => await service.ProcessOrderRetry(order)); + // Act + await Assert.ThrowsAsync(async () => await orderProcessingService.ProcessOrderRetry(order)); // Assert - serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - repoMock.Verify(r => r.SetProcessingStatus(It.IsAny(), It.IsAny()), Times.Never); - emailRepoMock.Verify(e => e.GetRecipients(It.IsAny()), Times.Once); + smsMockService.Verify(s => s.ProcessOrderRetry(It.IsAny()), Times.Once); + repoMock.Verify( + r => r.SetProcessingStatus(It.IsAny(), It.Is(s => s.Equals(OrderProcessingStatus.Completed))), + Times.Never); } - private static OrderProcessingService GetTestService(IOrderRepository? repo = null, IEmailNotificationRepository? emailRepo = null, IEmailNotificationService? emailService = null, IKafkaProducer? producer = null) + private static OrderProcessingService GetTestService( + IOrderRepository? repo = null, + IEmailOrderProcessingService? emailMock = null, + ISmsOrderProcessingService? smsMock = null, + IKafkaProducer? producer = null) { if (repo == null) { @@ -215,16 +187,16 @@ private static OrderProcessingService GetTestService(IOrderRepository? repo = nu repo = repoMock.Object; } - if (emailRepo == null) + if (emailMock == null) { - var emailRepoMock = new Mock(); - emailRepo = emailRepoMock.Object; + var emailMockService = new Mock(); + emailMock = emailMockService.Object; } - if (emailService == null) + if (smsMock == null) { - var emailServiceMock = new Mock(); - emailService = emailServiceMock.Object; + var smsMockService = new Mock(); + smsMock = smsMockService.Object; } if (producer == null) @@ -235,6 +207,6 @@ private static OrderProcessingService GetTestService(IOrderRepository? repo = nu var kafkaSettings = new Altinn.Notifications.Core.Configuration.KafkaSettings() { PastDueOrdersTopicName = _pastDueTopicName }; - return new OrderProcessingService(repo, emailRepo, emailService, producer, Options.Create(kafkaSettings)); + return new OrderProcessingService(repo, emailMock, smsMock, producer, Options.Create(kafkaSettings)); } } diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs new file mode 100644 index 00000000..19962323 --- /dev/null +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsNotificationServiceTests.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Address; +using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Persistence; +using Altinn.Notifications.Core.Services; +using Altinn.Notifications.Core.Services.Interfaces; + +using Moq; + +using Xunit; + +namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; + +public class SmsNotificationServiceTests +{ + [Fact] + public async Task CreateNotifications_NewSmsNotification_RepositoryCalledOnce() + { + // Arrange + var repoMock = new Mock(); + var guidService = new Mock(); + guidService + .Setup(g => g.NewGuid()) + .Returns(Guid.NewGuid()); + + var dateTimeService = new Mock(); + dateTimeService + .Setup(d => d.UtcNow()) + .Returns(DateTime.UtcNow); + + var service = GetTestService(repo: repoMock.Object); + + // Act + await service.CreateNotification(Guid.NewGuid(), DateTime.UtcNow, new Recipient("recipientId", new List() { new SmsAddressPoint("999999999") })); + + // Assert + repoMock.Verify(r => r.AddNotification(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task CreateNotification_RecipientNumberIsDefined_ResultNew() + { + // Arrange + Guid id = Guid.NewGuid(); + Guid orderId = Guid.NewGuid(); + DateTime requestedSendTime = DateTime.UtcNow; + DateTime dateTimeOutput = DateTime.UtcNow; + DateTime expectedExpiry = requestedSendTime.AddHours(1); + + SmsNotification expected = new() + { + Id = id, + OrderId = orderId, + RequestedSendTime = requestedSendTime, + RecipientNumber = "+4799999999", + SendResult = new(SmsNotificationResultType.New, dateTimeOutput), + }; + + var repoMock = new Mock(); + repoMock.Setup(r => r.AddNotification(It.Is(e => AssertUtils.AreEquivalent(expected, e)), It.Is(d => d == expectedExpiry))); + + var service = GetTestService(repo: repoMock.Object, guidOutput: id, dateTimeOutput: dateTimeOutput); + + // Act + await service.CreateNotification(orderId, requestedSendTime, new Recipient(new List() { new SmsAddressPoint("+4799999999") })); + + // Assert + repoMock.Verify(r => r.AddNotification(It.Is(e => AssertUtils.AreEquivalent(expected, e)), It.Is(d => d == expectedExpiry)), Times.Once); + } + + [Fact] + public async Task CreateNotification_RecipientNumberMissing_ResultFailedRecipientNotDefined() + { + // Arrange + Guid id = Guid.NewGuid(); + Guid orderId = Guid.NewGuid(); + DateTime requestedSendTime = DateTime.UtcNow; + DateTime dateTimeOutput = DateTime.UtcNow; + DateTime expectedExpiry = dateTimeOutput; + + SmsNotification expected = new() + { + Id = id, + OrderId = orderId, + RequestedSendTime = requestedSendTime, + SendResult = new(SmsNotificationResultType.Failed_RecipientNotIdentified, dateTimeOutput), + }; + + var repoMock = new Mock(); + repoMock.Setup(r => r.AddNotification(It.Is(e => AssertUtils.AreEquivalent(expected, e)), It.Is(d => d == expectedExpiry))); + + var service = GetTestService(repo: repoMock.Object, guidOutput: id, dateTimeOutput: dateTimeOutput); + + // Act + await service.CreateNotification(orderId, requestedSendTime, new Recipient(new List())); + + // Assert + repoMock.Verify(); + } + + private static SmsNotificationService GetTestService(ISmsNotificationRepository? repo = null, Guid? guidOutput = null, DateTime? dateTimeOutput = null) + { + var guidService = new Mock(); + guidService + .Setup(g => g.NewGuid()) + .Returns(guidOutput ?? Guid.NewGuid()); + + var dateTimeService = new Mock(); + dateTimeService + .Setup(d => d.UtcNow()) + .Returns(dateTimeOutput ?? DateTime.UtcNow); + if (repo == null) + { + var repoMock = new Mock(); + repo = repoMock.Object; + } + + return new SmsNotificationService(guidService.Object, dateTimeService.Object, repo); + } +} diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs new file mode 100644 index 00000000..40127650 --- /dev/null +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/SmsOrderProcessingServiceTests.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Address; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Models.Recipients; +using Altinn.Notifications.Core.Persistence; +using Altinn.Notifications.Core.Services; +using Altinn.Notifications.Core.Services.Interfaces; + +using Moq; + +using Xunit; + +namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices; + +public class SmsOrderProcessingServiceTests +{ + [Fact] + public async Task ProcessOrder_ExpectedInputToService() + { + // Arrange + DateTime requested = DateTime.UtcNow; + Guid orderId = Guid.NewGuid(); + + var order = new NotificationOrder() + { + Id = orderId, + NotificationChannel = NotificationChannel.Sms, + RequestedSendTime = requested, + Recipients = new List() + { + new Recipient("end-user", new List() { new SmsAddressPoint("+4799999999") }) + } + }; + + Recipient expectedRecipient = new("end-user", new List() { new SmsAddressPoint("+4799999999") }); + + var serviceMock = new Mock(); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.Is(d => d.Equals(requested)), It.Is(r => AssertUtils.AreEquivalent(expectedRecipient, r)))); + + var service = GetTestService(smsService: serviceMock.Object); + + // Act + await service.ProcessOrder(order); + + // Assert + serviceMock.VerifyAll(); + } + + [Fact] + public async Task ProcessOrder_ServiceCalledOnceForEachRecipient() + { + // Arrange + var order = new NotificationOrder() + { + Id = Guid.NewGuid(), + NotificationChannel = NotificationChannel.Sms, + Recipients = new List() + { + new(), + new() + } + }; + + var serviceMock = new Mock(); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny())); + + var service = GetTestService(smsService: serviceMock.Object); + + // Act + await service.ProcessOrder(order); + + // Assert + serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task ProcessOrderRetry_ServiceCalledIfRecipientNotInDatabase() + { + // Arrange + var order = new NotificationOrder() + { + Id = Guid.NewGuid(), + NotificationChannel = NotificationChannel.Sms, + Recipients = new List() + { + new Recipient(), + new Recipient("end-user", new List() { new SmsAddressPoint("+4799999999") }), + new Recipient(new List() { new SmsAddressPoint("+4749999999") }) + } + }; + + var serviceMock = new Mock(); + serviceMock.Setup(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny())); + + var smsRepoMock = new Mock(); + smsRepoMock.Setup(e => e.GetRecipients(It.IsAny())).ReturnsAsync(new List() { new SmsRecipient() { RecipientId = "end-user", MobileNumber = "+4799999999" } }); + + var service = GetTestService(smsRepo: smsRepoMock.Object, smsService: serviceMock.Object); + + // Act + await service.ProcessOrderRetry(order); + + // Assert + smsRepoMock.Verify(e => e.GetRecipients(It.IsAny()), Times.Once); + serviceMock.Verify(s => s.CreateNotification(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + private static SmsOrderProcessingService GetTestService( + ISmsNotificationRepository? smsRepo = null, + ISmsNotificationService? smsService = null) + { + if (smsRepo == null) + { + var smsRepoMock = new Mock(); + smsRepo = smsRepoMock.Object; + } + + if (smsService == null) + { + var smsServiceMock = new Mock(); + smsService = smsServiceMock.Object; + } + + return new SmsOrderProcessingService(smsRepo, smsService); + } +}