diff --git a/src/Altinn.Notifications.Core/Enums/EmailNotificationResultType.cs b/src/Altinn.Notifications.Core/Enums/EmailNotificationResultType.cs index fa4432a9..be84764b 100644 --- a/src/Altinn.Notifications.Core/Enums/EmailNotificationResultType.cs +++ b/src/Altinn.Notifications.Core/Enums/EmailNotificationResultType.cs @@ -16,7 +16,7 @@ public enum EmailNotificationResultType Sending, /// - /// Email notification sent and delivered + /// Email notification sent /// Succeeded, diff --git a/src/Altinn.Notifications.Core/Models/Notification/SendOperationResult.cs b/src/Altinn.Notifications.Core/Models/Notification/SendOperationResult.cs index 3b2072f6..97a3ce67 100644 --- a/src/Altinn.Notifications.Core/Models/Notification/SendOperationResult.cs +++ b/src/Altinn.Notifications.Core/Models/Notification/SendOperationResult.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.Orders; namespace Altinn.Notifications.Core.Models.Notification; @@ -24,6 +25,21 @@ public class SendOperationResult /// public EmailNotificationResultType? SendResult { get; set; } + /// + /// Json serializes the + /// + public string Serialize() + { + return JsonSerializer.Serialize( + this, + new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }); + } + /// /// Deserialize a json string into the /// diff --git a/src/Altinn.Notifications.Core/Repository/Interfaces/IEmailNotificationRepository.cs b/src/Altinn.Notifications.Core/Repository/Interfaces/IEmailNotificationRepository.cs index e04d88b5..ab69ca75 100644 --- a/src/Altinn.Notifications.Core/Repository/Interfaces/IEmailNotificationRepository.cs +++ b/src/Altinn.Notifications.Core/Repository/Interfaces/IEmailNotificationRepository.cs @@ -22,9 +22,9 @@ public interface IEmailNotificationRepository public Task> GetNewNotifications(); /// - /// Sets result status of an email notification + /// Sets result status of an email notification and update operation id /// - public Task SetResultStatus(Guid notificationId, EmailNotificationResultType status); + public Task SetResultStatus(Guid notificationId, EmailNotificationResultType status, string? operationId); /// /// Retrieves all email recipients for an order diff --git a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs index ac305b7a..5c4ec57c 100644 --- a/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/EmailNotificationService.cs @@ -64,15 +64,15 @@ public async Task SendNotifications() bool success = await _producer.ProduceAsync(_emailQueueTopicName, email.Serialize()); if (!success) { - await _repository.SetResultStatus(email.NotificationId, EmailNotificationResultType.New); + await _repository.SetResultStatus(email.NotificationId, EmailNotificationResultType.New, null); } } } /// - public async Task UpdateStatusNotification(SendOperationResult sendOperationResult) + public async Task UpdateSendStatus(SendOperationResult sendOperationResult) { - await _repository.SetResultStatus(sendOperationResult.NotificationId, (EmailNotificationResultType)sendOperationResult.SendResult!); + await _repository.SetResultStatus(sendOperationResult.NotificationId, (EmailNotificationResultType)sendOperationResult.SendResult!, sendOperationResult.OperationId); } private async Task CreateNotificationForRecipient(Guid orderId, DateTime requestedSendTime, string recipientId, string toAddress, EmailNotificationResultType result) diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs index d695f448..39fcd4de 100644 --- a/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs +++ b/src/Altinn.Notifications.Core/Services/Interfaces/IEmailNotificationService.cs @@ -21,5 +21,5 @@ public interface IEmailNotificationService /// /// Update send status for a notification /// - public Task UpdateStatusNotification(SendOperationResult sendOperationResult); + public Task UpdateSendStatus(SendOperationResult sendOperationResult); } \ No newline at end of file diff --git a/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs index 256dc7de..44b54595 100644 --- a/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.Notifications.Integrations/Extensions/ServiceCollectionExtensions.cs @@ -32,6 +32,7 @@ public static void AddKafkaServices(this IServiceCollection services, IConfigura .AddSingleton() .AddHostedService() .AddHostedService() + .AddHostedService() .Configure(config.GetSection(nameof(KafkaSettings))); } diff --git a/src/Altinn.Notifications.Integrations/Kafka/Consumers/EmailStatusConsumer.cs b/src/Altinn.Notifications.Integrations/Kafka/Consumers/EmailStatusConsumer.cs index a33cd2a4..8c9c5156 100644 --- a/src/Altinn.Notifications.Integrations/Kafka/Consumers/EmailStatusConsumer.cs +++ b/src/Altinn.Notifications.Integrations/Kafka/Consumers/EmailStatusConsumer.cs @@ -47,7 +47,7 @@ private async Task ProcessOrder(string message) return; } - await _emailNotificationsService.UpdateStatusNotification(result); + await _emailNotificationsService.UpdateSendStatus(result); } private async Task RetryOrder(string message) diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.06/01-alter-tables.sql b/src/Altinn.Notifications.Persistence/Migration/v0.06/01-alter-tables.sql index 7b715238..0558fecd 100644 --- a/src/Altinn.Notifications.Persistence/Migration/v0.06/01-alter-tables.sql +++ b/src/Altinn.Notifications.Persistence/Migration/v0.06/01-alter-tables.sql @@ -1,2 +1,2 @@ ALTER TABLE notifications.emailnotifications -ADD COLUMN operationid text; \ No newline at end of file +ADD COLUMN IF NOT EXISTS operationid text; \ 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 6265211d..aec6a832 100644 --- a/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs +++ b/src/Altinn.Notifications.Persistence/Repository/EmailNotificationRepository.cs @@ -75,11 +75,12 @@ public async Task> GetNewNotifications() } /// - public async Task SetResultStatus(Guid notificationId, EmailNotificationResultType status) + public async Task SetResultStatus(Guid notificationId, EmailNotificationResultType status, string? operationId) { await using NpgsqlCommand pgcom = _dataSource.CreateCommand(_setResultStatus); pgcom.Parameters.AddWithValue(NpgsqlDbType.Uuid, notificationId); pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, status.ToString()); + pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, operationId ?? (object)DBNull.Value); await pgcom.ExecuteNonQueryAsync(); } diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/EmailStatusConsumerTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/EmailStatusConsumerTests.cs new file mode 100644 index 00000000..1630bee0 --- /dev/null +++ b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/EmailStatusConsumerTests.cs @@ -0,0 +1,75 @@ +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Integrations.Kafka.Consumers; +using Altinn.Notifications.IntegrationTests.Utils; + +using Microsoft.Extensions.Hosting; + +using Xunit; + +namespace Altinn.Notifications.IntegrationTests.Notifications.Core.Consumers; + +public class EmailStatusConsumerTests : IDisposable +{ + private readonly string _statusUpdatedTopicName = Guid.NewGuid().ToString(); + private readonly string _sendersRef = $"ref-{Guid.NewGuid()}"; + + [Fact] + public async Task RunTask_ConfirmExpectedSideEffects() + { + // Arrange + Dictionary vars = new() + { + { "KafkaSettings__EmailStatusUpdatedTopicName", _statusUpdatedTopicName }, + { "KafkaSettings__Admin__TopicList", $"[\"{_statusUpdatedTopicName}\"]" } + }; + + using EmailStatusConsumer consumerService = (EmailStatusConsumer)ServiceUtil + .GetServices(new List() { typeof(IHostedService) }, vars) + .First(s => s.GetType() == typeof(EmailStatusConsumer))!; + + (NotificationOrder Order, EmailNotification Notification) = await PostgreUtil.PopulateDBWithOrderAndEmailNotification(_sendersRef); + + SendOperationResult sendOperationResult = new() + { + NotificationId = Notification.Id, + OperationId = Guid.NewGuid().ToString(), + SendResult = EmailNotificationResultType.Succeeded + }; + await KafkaUtil.PublishMessageOnTopic(_statusUpdatedTopicName, sendOperationResult.Serialize()); + + + // Act + await consumerService.StartAsync(CancellationToken.None); + await Task.Delay(10000); + await consumerService.StopAsync(CancellationToken.None); + + // Assert + + string emailNotificationStatus = await SelectEmailNotificationStatus(Notification.Id); + Assert.Equal(emailNotificationStatus, EmailNotificationResultType.Succeeded.ToString()); + + } + + public async void Dispose() + { + await Dispose(true); + + GC.SuppressFinalize(this); + } + + protected virtual async Task Dispose(bool disposing) + { + string sql = $"delete from notifications.orders where sendersreference = '{_sendersRef}'"; + + await PostgreUtil.RunSql(sql); + await KafkaUtil.DeleteTopicAsync(_statusUpdatedTopicName); + } + + private async Task SelectEmailNotificationStatus(Guid notificationId) + { + string sql = $"select result from notifications.emailnotifications where alternateid = '{notificationId}'"; + return await PostgreUtil.RunSqlReturnStringOutput(sql); + } +} diff --git a/test/Altinn.Notifications.IntegrationTests/Utils/PostgreUtil.cs b/test/Altinn.Notifications.IntegrationTests/Utils/PostgreUtil.cs index bedccd7d..c85ffb84 100644 --- a/test/Altinn.Notifications.IntegrationTests/Utils/PostgreUtil.cs +++ b/test/Altinn.Notifications.IntegrationTests/Utils/PostgreUtil.cs @@ -102,6 +102,6 @@ public static async Task RunSql(string query) NpgsqlDataSource dataSource = (NpgsqlDataSource)ServiceUtil.GetServices(new List() { typeof(NpgsqlDataSource) })[0]!; await using NpgsqlCommand pgcom = dataSource.CreateCommand(query); - await pgcom.ExecuteNonQueryAsync(); + pgcom.ExecuteNonQuery(); } } \ No newline at end of file diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs index c18a0666..9d19c3ab 100644 --- a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/EmailNotificationServiceTests.cs @@ -56,7 +56,7 @@ public async Task SendNotifications_ProducerReturnsFalse_RepositoryCalledToUpdat .ReturnsAsync(new List() { _email }); repoMock - .Setup(r => r.SetResultStatus(It.IsAny(), It.Is(t => t == EmailNotificationResultType.New))); + .Setup(r => r.SetResultStatus(It.IsAny(), It.Is(t => t == EmailNotificationResultType.New), It.IsAny())); var producerMock = new Mock(); producerMock.Setup(p => p.ProduceAsync(It.Is(s => s.Equals(_emailQueueTopicName)), It.IsAny())) @@ -136,6 +136,32 @@ public async Task CreateEmailNotification_ToAddressDefined_ResultFailedRecipient repoMock.Verify(); } + [Fact] + public async Task UpdateStatusNotification() + { + // Arrange + Guid notificationid = Guid.NewGuid(); + string operationId = Guid.NewGuid().ToString(); + + SendOperationResult sendOperationResult = new() + { + NotificationId = notificationid, + OperationId = operationId, + SendResult= EmailNotificationResultType.Succeeded + }; + + var repoMock = new Mock(); + repoMock.Setup(r => r.SetResultStatus(It.Is(n => n == notificationid), It.Is(e => e == EmailNotificationResultType.Succeeded), It.Is(s => s.Equals(operationId)))); + + var service = GetTestService(repo: repoMock.Object); + + // Act + await service.UpdateSendStatus(sendOperationResult); + // Assert + + repoMock.Verify(); + } + private static EmailNotificationService GetTestService(IEmailNotificationRepository? repo = null, IKafkaProducer? producer = null, Guid? guidOutput = null, DateTime? dateTimeOutput = null) { var guidService = new Mock();