From a776357184cdd5ac0bdcfaf1393b8bcbe4befa03 Mon Sep 17 00:00:00 2001 From: Stephanie Buadu <47737608+acn-sbuad@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:09:38 +0100 Subject: [PATCH] email notifications endpoint implemented #GCPActive (#299) --- .../Extensions/ServiceCollectionExtensions.cs | 4 +- .../Notification/EmailNotificationSummary.cs | 31 +++ .../EmailNotificationWithResult.cs | 45 ++++ .../Notification/INotificationSummary.cs | 33 +++ .../Notification/INotificationWithResult.cs | 31 +++ .../Models/Notification/NotificationResult.cs | 17 +- .../INotificationSummaryRepository.cs | 15 ++ .../Interfaces/INotificationSummaryService.cs | 18 ++ .../Services/NotificationSummaryService.cs | 94 ++++++++ .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Migration/v0.12/01-setup-functions.sql | 33 +++ .../NotificationSummaryRepository.cs | 93 ++++++++ .../EmailNotificationsController.cs | 63 +++++ .../Mappers/NotificationSummaryMapper.cs | 57 +++++ .../Models/EmailNotificationSummaryExt.cs | 43 ++++ .../Models/EmailNotificationWithResultExt.cs | 36 +++ .../Models/NotificationOrderWithStatusExt.cs | 2 +- .../{ProcessingStatusExt.cs => StatusExt.cs} | 4 +- .../EmailStatusConsumerTests.cs | 4 +- .../PastDueOrdersConsumerTests.cs | 3 +- .../PastDueOrdersRetryConsumerTests.cs | 3 +- .../EmailNotificationsControllerTests.cs | 216 ++++++++++++++++++ .../EmailNotificationsController/GetTests.cs | 134 +++++++++++ .../PostTests.cs | 6 +- .../OrdersController/GetByIdTests.cs | 3 +- .../OrdersController/GetWithStatusById.cs | 3 +- .../Trigger_PastDueOrdersTests.cs | 4 +- .../Trigger_SendEmailNotificationsTests.cs | 4 +- .../Utils/PostgreUtil.cs | 12 + .../NotificationSummaryServiceTests.cs | 96 ++++++++ .../NotificationSummaryMapperTests.cs | 114 +++++++++ 31 files changed, 1193 insertions(+), 29 deletions(-) create mode 100644 src/Altinn.Notifications.Core/Models/Notification/EmailNotificationSummary.cs create mode 100644 src/Altinn.Notifications.Core/Models/Notification/EmailNotificationWithResult.cs create mode 100644 src/Altinn.Notifications.Core/Models/Notification/INotificationSummary.cs create mode 100644 src/Altinn.Notifications.Core/Models/Notification/INotificationWithResult.cs create mode 100644 src/Altinn.Notifications.Core/Repository/Interfaces/INotificationSummaryRepository.cs create mode 100644 src/Altinn.Notifications.Core/Services/Interfaces/INotificationSummaryService.cs create mode 100644 src/Altinn.Notifications.Core/Services/NotificationSummaryService.cs create mode 100644 src/Altinn.Notifications.Persistence/Migration/v0.12/01-setup-functions.sql create mode 100644 src/Altinn.Notifications.Persistence/Repository/NotificationSummaryRepository.cs create mode 100644 src/Altinn.Notifications/Controllers/EmailNotificationsController.cs create mode 100644 src/Altinn.Notifications/Mappers/NotificationSummaryMapper.cs create mode 100644 src/Altinn.Notifications/Models/EmailNotificationSummaryExt.cs create mode 100644 src/Altinn.Notifications/Models/EmailNotificationWithResultExt.cs rename src/Altinn.Notifications/Models/{ProcessingStatusExt.cs => StatusExt.cs} (85%) create mode 100644 test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsController/EmailNotificationsControllerTests.cs create mode 100644 test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsController/GetTests.cs create mode 100644 test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/NotificationSummaryServiceTests.cs create mode 100644 test/Altinn.Notifications.Tests/Notifications/TestingMappers/NotificationSummaryMapperTests.cs diff --git a/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs index 539023ee..b8798d95 100644 --- a/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.Notifications.Core/Extensions/ServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; namespace Altinn.Notifications.Core.Extensions; @@ -32,9 +31,10 @@ public static void AddCoreServices(this IServiceCollection services, IConfigurat .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() - .AddSingleton() .Configure(config.GetSection("KafkaSettings")) .Configure(config.GetSection("NotificationOrderConfig")); } diff --git a/src/Altinn.Notifications.Core/Models/Notification/EmailNotificationSummary.cs b/src/Altinn.Notifications.Core/Models/Notification/EmailNotificationSummary.cs new file mode 100644 index 00000000..48b848b3 --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Notification/EmailNotificationSummary.cs @@ -0,0 +1,31 @@ +namespace Altinn.Notifications.Core.Models.Notification +{ + /// + /// An implementation of for email notifications"/> + /// + public class EmailNotificationSummary : INotificationSummary + { + /// + public Guid OrderId { get; set; } + + /// + public string? SendersReference { get; set; } + + /// + public int Generated { get; internal set; } + + /// + public int Succeeded { get; internal set; } + + /// + public List Notifications { get; set; } = new List(); + + /// + /// Initializes a new instance of the class. + /// + public EmailNotificationSummary(Guid orderId) + { + OrderId = orderId; + } + } +} diff --git a/src/Altinn.Notifications.Core/Models/Notification/EmailNotificationWithResult.cs b/src/Altinn.Notifications.Core/Models/Notification/EmailNotificationWithResult.cs new file mode 100644 index 00000000..23146430 --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Notification/EmailNotificationWithResult.cs @@ -0,0 +1,45 @@ +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.Recipients; + +namespace Altinn.Notifications.Core.Models.Notification +{ + /// + /// An implementation of for email notifications"/> + /// Using the as recipient type and the as result type + /// + public class EmailNotificationWithResult : INotificationWithResult + { + /// + public Guid Id { get; } + + /// + public bool Succeeded { get; internal set; } + + /// + public EmailRecipient Recipient { get; } + + /// + public NotificationResult ResultStatus { get; } + + /// + /// Initializes a new instance of the class. + /// + public EmailNotificationWithResult(Guid id, EmailRecipient recipient, NotificationResult result) + { + Id = id; + Recipient = recipient; + ResultStatus = result; + } + + /// + /// Initializes a new instance of the class. + /// + internal EmailNotificationWithResult(Guid id, bool succeeded, EmailRecipient recipient, NotificationResult result) + { + Id = id; + Succeeded = succeeded; + Recipient = recipient; + ResultStatus = result; + } + } +} diff --git a/src/Altinn.Notifications.Core/Models/Notification/INotificationSummary.cs b/src/Altinn.Notifications.Core/Models/Notification/INotificationSummary.cs new file mode 100644 index 00000000..b5af3d6f --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Notification/INotificationSummary.cs @@ -0,0 +1,33 @@ +namespace Altinn.Notifications.Core.Models.Notification; + +/// +/// An interface representing a summary of the notifications related to an order +/// +public interface INotificationSummary + where TClass : class +{ + /// + /// Gets the notification order id + /// + public Guid OrderId { get; } + + /// + /// Gets the senders reference of the notification order + /// + public string? SendersReference { get; } + + /// + /// Gets the number of generated notifications + /// + public int Generated { get; } + + /// + /// Gets the number of succeeeded notifications + /// + public int Succeeded { get; } + + /// + /// Gets the list of notifications with send result + /// + public List Notifications { get; } +} diff --git a/src/Altinn.Notifications.Core/Models/Notification/INotificationWithResult.cs b/src/Altinn.Notifications.Core/Models/Notification/INotificationWithResult.cs new file mode 100644 index 00000000..0ddfa3bb --- /dev/null +++ b/src/Altinn.Notifications.Core/Models/Notification/INotificationWithResult.cs @@ -0,0 +1,31 @@ +namespace Altinn.Notifications.Core.Models.Notification; + +/// +/// Interface representing a notification object with send result data +/// +/// The class representing the recipient +/// The enum used to describe the send result +public interface INotificationWithResult + where TClass : class + where TEnum : struct, Enum +{ + /// + /// Gets the notification id + /// + public Guid Id { get; } + + /// + /// Gets a boolean indicating if the sending was successful + /// + public bool Succeeded { get; } + + /// + /// Sets the recipient with contact points + /// + public TClass Recipient { get; } + + /// + /// Gets the send result + /// + public NotificationResult ResultStatus { get; } +} diff --git a/src/Altinn.Notifications.Core/Models/Notification/NotificationResult.cs b/src/Altinn.Notifications.Core/Models/Notification/NotificationResult.cs index 9bc8e484..b105be9b 100644 --- a/src/Altinn.Notifications.Core/Models/Notification/NotificationResult.cs +++ b/src/Altinn.Notifications.Core/Models/Notification/NotificationResult.cs @@ -1,6 +1,4 @@ -using Altinn.Notifications.Core.Services.Interfaces; - -namespace Altinn.Notifications.Core.Models.Notification; +namespace Altinn.Notifications.Core.Models.Notification; /// /// A class represednting a notification result @@ -17,6 +15,14 @@ public NotificationResult(TEnum result, DateTime resultTime) Result = result; } + /// + /// Sets the result description + /// + public void SetResultDescription(string? description) + { + ResultDescription = description; + } + /// /// Gets the date and time for when the last result was set. /// @@ -26,4 +32,9 @@ public NotificationResult(TEnum result, DateTime resultTime) /// Gets the send result of the notification /// public TEnum Result { get; } + + /// + /// Gets the description of the send result + /// + public string? ResultDescription { get; private set; } } diff --git a/src/Altinn.Notifications.Core/Repository/Interfaces/INotificationSummaryRepository.cs b/src/Altinn.Notifications.Core/Repository/Interfaces/INotificationSummaryRepository.cs new file mode 100644 index 00000000..c1ef4cdb --- /dev/null +++ b/src/Altinn.Notifications.Core/Repository/Interfaces/INotificationSummaryRepository.cs @@ -0,0 +1,15 @@ +using Altinn.Notifications.Core.Models.Notification; + +namespace Altinn.Notifications.Core.Repository.Interfaces; + +/// +/// Interface describing all repository operations related to notification summaries +/// +public interface INotificationSummaryRepository +{ + /// + /// Retrieves all email notifications for the provided order id in an email notification summary + /// + /// A partial email notification summary object + public Task GetEmailSummary(Guid orderId, string creator); +} diff --git a/src/Altinn.Notifications.Core/Services/Interfaces/INotificationSummaryService.cs b/src/Altinn.Notifications.Core/Services/Interfaces/INotificationSummaryService.cs new file mode 100644 index 00000000..a99c679e --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/Interfaces/INotificationSummaryService.cs @@ -0,0 +1,18 @@ +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Notification; + +namespace Altinn.Notifications.Core.Services.Interfaces +{ + /// + /// Interface describing the notification summary service + /// + public interface INotificationSummaryService + { + /// + /// Gets a summary of all the generated email notifications for the provided order id + /// + /// The order id to find notifications for + /// The creator of the order + public Task<(EmailNotificationSummary? Summary, ServiceError? Error)> GetEmailSummary(Guid orderId, string creator); + } +} diff --git a/src/Altinn.Notifications.Core/Services/NotificationSummaryService.cs b/src/Altinn.Notifications.Core/Services/NotificationSummaryService.cs new file mode 100644 index 00000000..54a9326a --- /dev/null +++ b/src/Altinn.Notifications.Core/Services/NotificationSummaryService.cs @@ -0,0 +1,94 @@ +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Repository.Interfaces; +using Altinn.Notifications.Core.Services.Interfaces; + +namespace Altinn.Notifications.Core.Services +{ + /// + /// Implementation of + /// + public class NotificationSummaryService : INotificationSummaryService + { + private readonly INotificationSummaryRepository _summaryRepository; + private readonly static Dictionary _emailResultDescriptions = new() + { + { EmailNotificationResultType.New, "The email has been created, but has not been picked up for processing yet." }, + { EmailNotificationResultType.Sending, "The email is being processed and will be attempted sent shortly." }, + { EmailNotificationResultType.Succeeded, "The email has been accepted by the third party email service and will be sent shortly." }, + { EmailNotificationResultType.Delivered, "The email was delivered to the recipient. No errors reported, making it likely it was received by the recipient." }, + { EmailNotificationResultType.Failed, "The email was not sent due to an unspecified failure." }, + { EmailNotificationResultType.Failed_RecipientNotIdentified, "The email was not sent because the recipient's email address was not found." }, + { EmailNotificationResultType.Failed_InvalidEmailFormat, "The email was not sent because the recipient’s email address is in an invalid format." } + }; + + private readonly static List _successResults = new() + { + EmailNotificationResultType.Succeeded, + EmailNotificationResultType.Delivered + }; + + /// + /// Initializes a new instance of the class. + /// + public NotificationSummaryService(INotificationSummaryRepository summaryRepository) + { + _summaryRepository = summaryRepository; + } + + /// + public async Task<(EmailNotificationSummary? Summary, ServiceError? Error)> GetEmailSummary(Guid orderId, string creator) + { + EmailNotificationSummary? summary = await _summaryRepository.GetEmailSummary(orderId, creator); + + if (summary == null) + { + return (null, new ServiceError(404)); + } + + if (summary.Notifications.Any()) + { + ProcessNotificationResults(summary); + } + + return (summary, null); + } + + /// + /// Processes the notification results setting counts and descriptions + /// + internal static void ProcessNotificationResults(EmailNotificationSummary summary) + { + summary.Generated = summary.Notifications.Count; + + foreach (EmailNotificationWithResult notification in summary.Notifications) + { + NotificationResult resultStatus = notification.ResultStatus; + if (IsSuccessResult(resultStatus.Result)) + { + notification.Succeeded = true; + ++summary.Succeeded; + } + + resultStatus.SetResultDescription(GetResultDescription(resultStatus.Result)); + } + } + + /// + /// Checks if the is a success result + /// + internal static bool IsSuccessResult(EmailNotificationResultType result) + { + return _successResults.Contains(result); + } + + /// + /// Gets the English description of the " + /// + internal static string GetResultDescription(EmailNotificationResultType result) + { + return _emailResultDescriptions[result]; + } + } +} diff --git a/src/Altinn.Notifications.Persistence/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.Notifications.Persistence/Extensions/ServiceCollectionExtensions.cs index 4b559b48..558ab07c 100644 --- a/src/Altinn.Notifications.Persistence/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.Notifications.Persistence/Extensions/ServiceCollectionExtensions.cs @@ -31,6 +31,7 @@ public static IServiceCollection AddPostgresRepositories(this IServiceCollection return services .AddSingleton() .AddSingleton() + .AddSingleton() .AddNpgsqlDataSource(connectionString, builder => builder.EnableParameterLogging(settings.LogParameters)); } diff --git a/src/Altinn.Notifications.Persistence/Migration/v0.12/01-setup-functions.sql b/src/Altinn.Notifications.Persistence/Migration/v0.12/01-setup-functions.sql new file mode 100644 index 00000000..46787f39 --- /dev/null +++ b/src/Altinn.Notifications.Persistence/Migration/v0.12/01-setup-functions.sql @@ -0,0 +1,33 @@ +CREATE OR REPLACE FUNCTION notifications.getemailsummary( + _alternateorderid uuid, + _creatorname text) + RETURNS TABLE( + sendersreference text, + alternateid uuid, + recipientid text, + toaddress text, + result emailnotificationresulttype, + resulttime timestamptz) + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE + ROWS 1000 + +AS $BODY$ + + BEGIN + RETURN QUERY + SELECT o.sendersreference, n.alternateid, n.recipientid, n.toaddress, n.result, n.resulttime + FROM notifications.emailnotifications n + LEFT JOIN notifications.orders o ON n._orderid = o._id + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + IF NOT FOUND THEN + RETURN QUERY + SELECT o.sendersreference, NULL::uuid, NULL::text, NULL::text, NULL::emailnotificationresulttype, NULL::timestamptz + FROM notifications.orders o + WHERE o.alternateid = _alternateorderid + and o.creatorname = _creatorname; + END IF; + END; +$BODY$; diff --git a/src/Altinn.Notifications.Persistence/Repository/NotificationSummaryRepository.cs b/src/Altinn.Notifications.Persistence/Repository/NotificationSummaryRepository.cs new file mode 100644 index 00000000..bf382295 --- /dev/null +++ b/src/Altinn.Notifications.Persistence/Repository/NotificationSummaryRepository.cs @@ -0,0 +1,93 @@ +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Models.Recipients; +using Altinn.Notifications.Core.Repository.Interfaces; +using Altinn.Notifications.Persistence.Extensions; + +using Microsoft.ApplicationInsights; + +using Npgsql; + +using NpgsqlTypes; + +namespace Altinn.Notifications.Persistence.Repository; + +/// +/// Implementation of notification summary repository logic +/// +public class NotificationSummaryRepository : INotificationSummaryRepository +{ + private readonly NpgsqlDataSource _dataSource; + private readonly TelemetryClient? _telemetryClient; + + private const string _getNotificationsByOrderIdSql = "select * from notifications.getemailsummary($1, $2)"; // (_alternateorderid, creatorname) + + /// + /// Initializes a new instance of the class. + /// + /// The npgsql data source. + /// Telemetry client + public NotificationSummaryRepository(NpgsqlDataSource dataSource, TelemetryClient? telemetryClient = null) + { + _dataSource = dataSource; + _telemetryClient = telemetryClient; + } + + /// + public async Task GetEmailSummary(Guid orderId, string creator) + { + bool matchFound = false; + + List notificationList = new(); + string sendersReference = string.Empty; + + await using NpgsqlCommand pgcom = _dataSource.CreateCommand(_getNotificationsByOrderIdSql); + using TelemetryTracker tracker = new(_telemetryClient, pgcom); + pgcom.Parameters.AddWithValue(NpgsqlDbType.Uuid, orderId); + pgcom.Parameters.AddWithValue(NpgsqlDbType.Text, creator); + + await using (NpgsqlDataReader reader = await pgcom.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + if (!matchFound) + { + matchFound = true; + sendersReference = reader.GetValue("sendersreference"); + } + + Guid notificationId = reader.GetValue("alternateid"); + + if (notificationId != Guid.Empty) + { + notificationList.Add( + new EmailNotificationWithResult( + reader.GetValue("alternateid"), + new EmailRecipient() + { + RecipientId = reader.GetValue("recipientid"), + ToAddress = reader.GetValue("toaddress") + }, + new NotificationResult( + reader.GetValue("result"), + reader.GetValue("resulttime")))); + } + } + } + + tracker.Track(); + + if (!matchFound) + { + return null; + } + + EmailNotificationSummary emailSummary = new(orderId) + { + SendersReference = sendersReference, + Notifications = notificationList + }; + + return emailSummary; + } +} diff --git a/src/Altinn.Notifications/Controllers/EmailNotificationsController.cs b/src/Altinn.Notifications/Controllers/EmailNotificationsController.cs new file mode 100644 index 00000000..9005b58d --- /dev/null +++ b/src/Altinn.Notifications/Controllers/EmailNotificationsController.cs @@ -0,0 +1,63 @@ +using Altinn.Notifications.Configuration; +using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Services.Interfaces; +using Altinn.Notifications.Extensions; +using Altinn.Notifications.Mappers; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +using Swashbuckle.AspNetCore.Annotations; + +namespace Altinn.Notifications.Controllers +{ + /// + /// Controller for all operations related to email notifications + /// + [Authorize(Policy = AuthorizationConstants.POLICY_CREATE_SCOPE_OR_PLATFORM_ACCESS)] + [Route("notifications/api/v1/orders/{id}/notifications/email")] + [ApiController] + [SwaggerResponse(401, "Caller is unauthorized")] + [SwaggerResponse(403, "Caller is not authorized to access the requested resource")] + public class EmailNotificationsController : ControllerBase + { + private readonly INotificationSummaryService _summaryService; + + /// + /// Initializes a new instance of the class. + /// + /// The notifications summary service + public EmailNotificationsController(INotificationSummaryService summaryService) + { + _summaryService = summaryService; + } + + /// + /// Endpoint for retrieving a summary of all email notifications related to an order + /// + /// The order id + /// Sumarized order details and a list containing all email notifications and their send status + [HttpGet] + [Produces("application/json")] + [SwaggerResponse(200, "The notification order was accepted", typeof(EmailNotificationSummaryExt))] + [SwaggerResponse(404, "No notification order mathching the id was found")] + public async Task> Get([FromRoute] Guid id) + { + string? expectedCreator = HttpContext.GetOrg(); + + if (expectedCreator == null) + { + return Forbid(); + } + + var (emailSummary, error) = await _summaryService.GetEmailSummary(id, expectedCreator); + + if (error != null) + { + return StatusCode(error.ErrorCode, error.ErrorMessage); + } + + return Ok(emailSummary?.MapToEmailNotificationSummaryExt()); + } + } +} diff --git a/src/Altinn.Notifications/Mappers/NotificationSummaryMapper.cs b/src/Altinn.Notifications/Mappers/NotificationSummaryMapper.cs new file mode 100644 index 00000000..4f6a11a7 --- /dev/null +++ b/src/Altinn.Notifications/Mappers/NotificationSummaryMapper.cs @@ -0,0 +1,57 @@ +using Altinn.Notifications.Core.Models.Notification; + +namespace Altinn.Notifications.Mappers +{ + /// + /// Mapper for + /// + public static class NotificationSummaryMapper + { + /// + /// Maps a to a + /// + public static EmailNotificationSummaryExt MapToEmailNotificationSummaryExt(this EmailNotificationSummary summary) + { + return new EmailNotificationSummaryExt() + { + OrderId = summary.OrderId, + SendersReference = summary.SendersReference, + Generated = summary.Generated, + Succeeded = summary.Succeeded, + Notifications = summary.Notifications.MapToEmailNotificationWithResultExt(), + }; + } + + /// + /// Maps a list of to a list of + /// + public static List MapToEmailNotificationWithResultExt(this List notifications) + { + List result = notifications.Select(n => n.MapToEmailNotificationWithResultExt()).ToList(); + + return result; + } + + /// + /// Maps a to a + /// + public static EmailNotificationWithResultExt MapToEmailNotificationWithResultExt(this EmailNotificationWithResult notification) + { + return new EmailNotificationWithResultExt() + { + Id = notification.Id, + Succeeded = notification.Succeeded, + Recipient = new() + { + EmailAddress = notification.Recipient.ToAddress + }, + SendStatus = new() + { + Status = notification.ResultStatus.Result.ToString(), + StatusDescription = notification.ResultStatus.ResultDescription, + LastUpdate = notification.ResultStatus.ResultTime + } + }; + } + } +} diff --git a/src/Altinn.Notifications/Models/EmailNotificationSummaryExt.cs b/src/Altinn.Notifications/Models/EmailNotificationSummaryExt.cs new file mode 100644 index 00000000..3131a17e --- /dev/null +++ b/src/Altinn.Notifications/Models/EmailNotificationSummaryExt.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Core.Models.Notification +{ + /// + /// A class representing an email notification summary + /// + /// + /// External representaion to be used in the API. + /// + public class EmailNotificationSummaryExt + { + /// + /// The order id + /// + [JsonPropertyName("orderId")] + public Guid OrderId { get; set; } + + /// + /// The senders reference + /// + [JsonPropertyName("sendersReference")] + public string? SendersReference { get; set; } + + /// + /// The number of generated email notifications + /// + [JsonPropertyName("generated")] + public int Generated { get; set; } + + /// + /// The number of email notifications that were sent successfully + /// + [JsonPropertyName("succeeded")] + public int Succeeded { get; set; } + + /// + /// A list of notifications with send result + /// + [JsonPropertyName("notifications")] + public List Notifications { get; set; } = new List(); + } +} diff --git a/src/Altinn.Notifications/Models/EmailNotificationWithResultExt.cs b/src/Altinn.Notifications/Models/EmailNotificationWithResultExt.cs new file mode 100644 index 00000000..49922d59 --- /dev/null +++ b/src/Altinn.Notifications/Models/EmailNotificationWithResultExt.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +using Altinn.Notifications.Models; + +namespace Altinn.Notifications.Core.Models.Notification +{ + /// + /// EmailNotificationWithResultExt class + /// + public class EmailNotificationWithResultExt + { + /// + /// The notification id + /// + [JsonPropertyName("id")] + public Guid Id { get; set; } + + /// + /// Boolean indicating if the sending of the notification was successful + /// + [JsonPropertyName("succeeded")] + public bool Succeeded { get; set; } + + /// + /// The recipient of the notification + /// + [JsonPropertyName("recipient")] + public RecipientExt Recipient { get; set; } = new(); + + /// + /// The result status of the notification + /// + [JsonPropertyName("sendStatus")] + public StatusExt SendStatus { get; set; } = new(); + } +} diff --git a/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs b/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs index c4eac5fb..dc07c6aa 100644 --- a/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs +++ b/src/Altinn.Notifications/Models/NotificationOrderWithStatusExt.cs @@ -39,7 +39,7 @@ public class NotificationOrderWithStatusExt : IBaseNotificationOrderExt /// Gets or sets the processing status of the notication order /// [JsonPropertyName("processingStatus")] - public ProcessingStatusExt ProcessingStatus { get; set; } = new(); + public StatusExt ProcessingStatus { get; set; } = new(); /// /// Gets or sets the summary of the notifiications statuses diff --git a/src/Altinn.Notifications/Models/ProcessingStatusExt.cs b/src/Altinn.Notifications/Models/StatusExt.cs similarity index 85% rename from src/Altinn.Notifications/Models/ProcessingStatusExt.cs rename to src/Altinn.Notifications/Models/StatusExt.cs index 224282dc..6a4ec24c 100644 --- a/src/Altinn.Notifications/Models/ProcessingStatusExt.cs +++ b/src/Altinn.Notifications/Models/StatusExt.cs @@ -3,12 +3,12 @@ namespace Altinn.Notifications.Models; /// -/// A class representing a summary of status overviews of all notification channels +/// A class representing a status summary /// /// /// External representaion to be used in the API. /// -public class ProcessingStatusExt +public class StatusExt { /// /// Gets or sets the status diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/EmailStatusConsumerTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/EmailStatusConsumerTests.cs index c2140aa6..59af240c 100644 --- a/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/EmailStatusConsumerTests.cs +++ b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/EmailStatusConsumerTests.cs @@ -60,9 +60,7 @@ public async Task DisposeAsync() protected virtual async Task Dispose(bool disposing) { - string sql = $"delete from notifications.orders where sendersreference = '{_sendersRef}'"; - - await PostgreUtil.RunSql(sql); + await PostgreUtil.DeleteOrderFromDb(_sendersRef); await KafkaUtil.DeleteTopicAsync(_statusUpdatedTopicName); } diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersConsumerTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersConsumerTests.cs index 17e6d8f2..0435f42f 100644 --- a/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersConsumerTests.cs +++ b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersConsumerTests.cs @@ -59,8 +59,7 @@ public async void Dispose() protected virtual async Task Dispose(bool disposing) { - string sql = $"delete from notifications.orders where sendersreference = '{_sendersRef}'"; - await PostgreUtil.RunSql(sql); + await PostgreUtil.DeleteOrderFromDb(_sendersRef); await KafkaUtil.DeleteTopicAsync(_pastDueOrdersTopicName); } diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs index 5abe3eea..d4f928b4 100644 --- a/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs +++ b/test/Altinn.Notifications.IntegrationTests/Notifications.Integrations/TestingConsumers/PastDueOrdersRetryConsumerTests.cs @@ -95,8 +95,7 @@ public async void Dispose() protected virtual async Task Dispose(bool disposing) { await KafkaUtil.DeleteTopicAsync(_retryTopicName); - string sql = $"delete from notifications.orders where sendersreference = '{_sendersRef}'"; - await PostgreUtil.RunSql(sql); + await PostgreUtil.DeleteOrderFromDb(_sendersRef); } private static async Task SelectCompletedOrderCount(Guid orderId) diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsController/EmailNotificationsControllerTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsController/EmailNotificationsControllerTests.cs new file mode 100644 index 00000000..68429eb3 --- /dev/null +++ b/test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsController/EmailNotificationsControllerTests.cs @@ -0,0 +1,216 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; + +using Altinn.Common.AccessToken.Services; +using Altinn.Notifications.Configuration; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Services.Interfaces; +using Altinn.Notifications.Tests.Notifications.Mocks.Authentication; +using Altinn.Notifications.Tests.Notifications.Utils; + +using AltinnCore.Authentication.JwtCookie; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Logging; + +using Moq; + +using Xunit; + +namespace Altinn.Notifications.IntegrationTests.Notifications.EmailNotificationsController; + +public class EmailNotificationsControllerTests : IClassFixture> +{ + private readonly string _basePath; + private readonly string _invalidGuidBase; + private readonly IntegrationTestWebApplicationFactory _factory; + + private readonly JsonSerializerOptions _options; + + public EmailNotificationsControllerTests(IntegrationTestWebApplicationFactory factory) + { + _basePath = $"/notifications/api/v1/orders/{Guid.NewGuid()}/notifications/email"; + _invalidGuidBase = "/notifications/api/v1/orders/1337;1=1/notifications/email"; + _factory = factory; + _options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + } + + [Fact] + public async Task Get_MissingBearerToken_Unauthorized() + { + // Arrange + HttpClient client = GetTestClient(); + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, _basePath); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task Get_InvalidScopeInToken_Forbidden() + { + // Arrange + HttpClient client = GetTestClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PrincipalUtil.GetOrgToken("ttd", scope: "altinn:dummmy.scope")); + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, _basePath); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Get_InvalidGuid_BadRequest() + { + HttpClient client = GetTestClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PrincipalUtil.GetOrgToken("ttd", scope: "altinn:serviceowner/notifications.create")); + + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, _invalidGuidBase) + { + Content = new StringContent(string.Empty, Encoding.UTF8, "application/json") + }; + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + string content = await response.Content.ReadAsStringAsync(); + ProblemDetails? actual = JsonSerializer.Deserialize(content, _options); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Equal("One or more validation errors occurred.", actual?.Title); + } + + [Fact] + public async Task Get_ServiceReturnsError_ServerError() + { + // Arrange + Mock serviceMock = new(); + serviceMock.Setup(s => s.GetEmailSummary(It.IsAny(), It.IsAny())) + .ReturnsAsync((null, new ServiceError(500))); + + HttpClient client = GetTestClient(summaryService: serviceMock.Object); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PrincipalUtil.GetOrgToken("ttd", scope: "altinn:serviceowner/notifications.create")); + + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, _basePath); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + serviceMock.VerifyAll(); + } + + [Fact] + public async Task Get_ValidScope_ServiceReturnsNotifications_Ok() + { + // Arrange + Guid id = Guid.NewGuid(); + EmailNotificationSummary output = new(id) + { + SendersReference = "senders-ref", + Generated = 1, + Succeeded = 1, + Notifications = new List() + }; + + Mock serviceMock = new(); + serviceMock.Setup(s => s.GetEmailSummary(It.IsAny(), It.Is(s => s.Equals("ttd")))) + .ReturnsAsync((output, null)); + + HttpClient client = GetTestClient(summaryService: serviceMock.Object); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PrincipalUtil.GetOrgToken("ttd", scope: "altinn:serviceowner/notifications.create")); + + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, _basePath); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + string respoonseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + EmailNotificationSummaryExt? summaryExt = JsonSerializer.Deserialize(respoonseString); + Assert.NotNull(summaryExt); + Assert.Equal(id, summaryExt.OrderId); + + serviceMock.VerifyAll(); + } + + [Fact] + public async Task Get_ValidAccessToken_ServiceReturnsOrder_Accepted() + { + // Arrange + Guid id = Guid.NewGuid(); + EmailNotificationSummary output = new(id) + { + SendersReference = "senders-ref", + Generated = 1, + Succeeded = 1, + Notifications = new List() + }; + + Mock serviceMock = new(); + serviceMock.Setup(s => s.GetEmailSummary(It.IsAny(), It.Is(s => s.Equals("ttd")))) + .ReturnsAsync((output, null)); + + HttpClient client = GetTestClient(summaryService: serviceMock.Object); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PrincipalUtil.GetOrgToken("ttd", scope: "altinn:serviceowner/notifications.create")); + + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, _basePath); + httpRequestMessage.Headers.Add("PlatformAccessToken", PrincipalUtil.GetAccessToken("ttd", "apps-test")); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + string respoonseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + EmailNotificationSummaryExt? summaryExt = JsonSerializer.Deserialize(respoonseString); + Assert.NotNull(summaryExt); + Assert.Equal(id, summaryExt.OrderId); + + serviceMock.VerifyAll(); + } + + private HttpClient GetTestClient(INotificationSummaryService? summaryService = null) + { + if (summaryService == null) + { + var summaryServiceMock = new Mock(); + summaryService = summaryServiceMock.Object; + } + + HttpClient client = _factory.WithWebHostBuilder(builder => + { + IdentityModelEventSource.ShowPII = true; + + builder.ConfigureTestServices(services => + { + services.AddSingleton(summaryService); + + // Set up mock authentication and authorization + services.AddSingleton, JwtCookiePostConfigureOptionsStub>(); + services.AddSingleton(); + }); + }).CreateClient(); + + return client; + } +} diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsController/GetTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsController/GetTests.cs new file mode 100644 index 00000000..f942c09d --- /dev/null +++ b/test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsController/GetTests.cs @@ -0,0 +1,134 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; + +using Altinn.Common.AccessToken.Services; +using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.IntegrationTests.Utils; +using Altinn.Notifications.Tests.Notifications.Mocks.Authentication; +using Altinn.Notifications.Tests.Notifications.Utils; + +using AltinnCore.Authentication.JwtCookie; + +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Logging; + +using Xunit; + +namespace Altinn.Notifications.IntegrationTests.Notifications.EmailNotificationsController +{ + public class GetTests : IClassFixture>, IAsyncLifetime + { + private readonly string _basePath; + private readonly IntegrationTestWebApplicationFactory _factory; + private readonly Task _initializeTask; + private Guid _orderId; + private Guid _notificationId; + + public GetTests(IntegrationTestWebApplicationFactory factory) + { + _basePath = $"/notifications/api/v1/orders"; + _factory = factory; + _initializeTask = InitializeAsync(); + } + + public async Task InitializeAsync() + { + (NotificationOrder persistedOrder, EmailNotification persistedNotification) = await PostgreUtil.PopulateDBWithOrderAndEmailNotification(); + _orderId = persistedOrder.Id; + _notificationId = persistedNotification.Id; + } + + async Task IAsyncLifetime.DisposeAsync() + { + string sql = $"delete from notifications.orders where alternateid= '{_orderId}'"; + + await PostgreUtil.RunSql(sql); + } + + [Fact] + public async Task Get_NonExistingOrder_NotFound() + { + // Arrange + string uri = $"{_basePath}/{Guid.NewGuid()}/notifications/email"; + + HttpClient client = GetTestClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PrincipalUtil.GetOrgToken("ttd", scope: "altinn:serviceowner/notifications.create")); + + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, uri); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Get_OrderIdForAnotherCreator_NotFound() + { + // Arrange + await _initializeTask; + + string uri = $"{_basePath}/{_orderId}/notifications/email"; + + HttpClient client = GetTestClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PrincipalUtil.GetOrgToken("nav", scope: "altinn:serviceowner/notifications.create")); + + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, uri); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Get_ValidOrderId_Ok() + { + // Arrange + await _initializeTask; + + string uri = $"{_basePath}/{_orderId}/notifications/email"; + + HttpClient client = GetTestClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", PrincipalUtil.GetOrgToken("ttd", scope: "altinn:serviceowner/notifications.create")); + + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, uri); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + EmailNotificationSummaryExt? summary = JsonSerializer.Deserialize(responseString); + Assert.True(summary?.Notifications.Count > 0); + Assert.Equal(_orderId, summary?.OrderId); + Assert.Equal(_notificationId, summary?.Notifications[0].Id); + Assert.Equal(1, summary?.Generated); + Assert.Equal(0, summary?.Succeeded); + } + + private HttpClient GetTestClient() + { + HttpClient client = _factory.WithWebHostBuilder(builder => + { + IdentityModelEventSource.ShowPII = true; + + builder.ConfigureTestServices(services => + { + // Set up mock authentication and authorization + services.AddSingleton, JwtCookiePostConfigureOptionsStub>(); + services.AddSingleton(); + }); + }).CreateClient(); + + return client; + } + } +} diff --git a/test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsOrderController/PostTests.cs b/test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsOrderController/PostTests.cs index 2afd82ee..a2112b8d 100644 --- a/test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsOrderController/PostTests.cs +++ b/test/Altinn.Notifications.IntegrationTests/Notifications/EmailNotificationsOrderController/PostTests.cs @@ -5,7 +5,6 @@ using Altinn.Common.AccessToken.Services; using Altinn.Notifications.Controllers; -using Altinn.Notifications.Core.Enums; using Altinn.Notifications.IntegrationTests.Utils; using Altinn.Notifications.Models; using Altinn.Notifications.Tests.Notifications.Mocks.Authentication; @@ -63,7 +62,7 @@ public PostTests(IntegrationTestWebApplicationFactory PopulateDBWithOrderAndEmailNotificat return o; } + public static async Task DeleteOrderFromDb(string sendersRef) + { + string sql = $"delete from notifications.orders where sendersreference = '{sendersRef}'"; + await PostgreUtil.RunSql(sql); + } + + public static async Task DeleteOrderFromDb(Guid orderId) + { + string sql = $"delete from notifications.orders where alternateid = '{orderId}'"; + await PostgreUtil.RunSql(sql); + } + public static async Task RunSqlReturnIntOutput(string query) { NpgsqlDataSource dataSource = (NpgsqlDataSource)ServiceUtil.GetServices(new List() { typeof(NpgsqlDataSource) })[0]!; diff --git a/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/NotificationSummaryServiceTests.cs b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/NotificationSummaryServiceTests.cs new file mode 100644 index 00000000..8c4923cf --- /dev/null +++ b/test/Altinn.Notifications.Tests/Notifications.Core/TestingServices/NotificationSummaryServiceTests.cs @@ -0,0 +1,96 @@ +using System; + +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Services; + +using Xunit; + +namespace Altinn.Notifications.Tests.Notifications.Core.TestingServices +{ + public class NotificationSummaryServiceTests + { + [Theory] + [InlineData(EmailNotificationResultType.New, false)] + [InlineData(EmailNotificationResultType.Sending, false)] + [InlineData(EmailNotificationResultType.Succeeded, true)] + [InlineData(EmailNotificationResultType.Delivered, true)] + [InlineData(EmailNotificationResultType.Failed_RecipientNotIdentified, false)] + [InlineData(EmailNotificationResultType.Failed_InvalidEmailFormat, false)] + public void IsSuccessResult_CheckResultForAllEnums(EmailNotificationResultType result, bool expectedIsSuccess) + { + bool actualIsSuccess = NotificationSummaryService.IsSuccessResult(result); + Assert.Equal(expectedIsSuccess, actualIsSuccess); + } + + [Theory] + [InlineData(EmailNotificationResultType.New, "The email has been created, but has not been picked up for processing yet.")] + [InlineData(EmailNotificationResultType.Sending, "The email is being processed and will be attempted sent shortly.")] + [InlineData(EmailNotificationResultType.Succeeded, "The email has been accepted by the third party email service and will be sent shortly.")] + [InlineData(EmailNotificationResultType.Delivered, "The email was delivered to the recipient. No errors reported, making it likely it was received by the recipient.")] + [InlineData(EmailNotificationResultType.Failed, "The email was not sent due to an unspecified failure.")] + [InlineData(EmailNotificationResultType.Failed_RecipientNotIdentified, "The email was not sent because the recipient's email address was not found.")] + [InlineData(EmailNotificationResultType.Failed_InvalidEmailFormat, "The email was not sent because the recipient’s email address is in an invalid format.")] + public void GetResultDescription_ExpectedDescription(EmailNotificationResultType result, string expected) + { + string actual = NotificationSummaryService.GetResultDescription(result); + Assert.Equal(expected, actual); + } + + [Fact] + public void GetResultDescription_AllResultTypesHaveDescriptions() + { + foreach (EmailNotificationResultType resultType in Enum.GetValues(typeof(EmailNotificationResultType))) + { + string resultDescrption = NotificationSummaryService.GetResultDescription(resultType); + Assert.NotEmpty(resultDescrption); + } + } + + [Fact] + public void ProcessNotificationResults_1_generated_1_successful() + { + // Arrange + EmailNotificationSummary summary = new(Guid.NewGuid()) + { + Notifications = new() + { + new EmailNotificationWithResult( + Guid.NewGuid(), + new Altinn.Notifications.Core.Models.Recipients.EmailRecipient(), + new NotificationResult(EmailNotificationResultType.Succeeded, DateTime.UtcNow)) + } + }; + + // Act + NotificationSummaryService.ProcessNotificationResults(summary); + + // Assert + Assert.Equal(1, summary.Generated); + Assert.Equal(1, summary.Succeeded); + } + + [Fact] + public void ProcessNotificationResults_1_generated_0_successful() + { + // Arrange + EmailNotificationSummary summary = new(Guid.NewGuid()) + { + Notifications = new() + { + new EmailNotificationWithResult( + Guid.NewGuid(), + new Altinn.Notifications.Core.Models.Recipients.EmailRecipient(), + new NotificationResult(EmailNotificationResultType.Failed_RecipientNotIdentified, DateTime.UtcNow)) + } + }; + + // Act + NotificationSummaryService.ProcessNotificationResults(summary); + + // Assert + Assert.Equal(1, summary.Generated); + Assert.Equal(0, summary.Succeeded); + } + } +} diff --git a/test/Altinn.Notifications.Tests/Notifications/TestingMappers/NotificationSummaryMapperTests.cs b/test/Altinn.Notifications.Tests/Notifications/TestingMappers/NotificationSummaryMapperTests.cs new file mode 100644 index 00000000..c58f5332 --- /dev/null +++ b/test/Altinn.Notifications.Tests/Notifications/TestingMappers/NotificationSummaryMapperTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; + +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.Notification; +using Altinn.Notifications.Core.Models.Recipients; +using Altinn.Notifications.Mappers; + +using Xunit; + +namespace Altinn.Notifications.Tests.Notifications.TestingMappers +{ + public class NotificationSummaryMapperTests + { + [Fact] + public void MapToEmailNotificationWithResultExt_EmptyList_AreEquivalent() + { + // Arrange + List input = new(); + + // Act + var actual = input.MapToEmailNotificationWithResultExt(); + + // Assert + Assert.Empty(actual); + } + + [Fact] + public void MapToEmailNotificationWithResultExt_NotificationWithFailedResult_AreEquivalent() + { + // Arrange + Guid id = Guid.NewGuid(); + DateTime timestamp = DateTime.UtcNow; + EmailNotificationWithResultExt expected = new() + { + Id = id, + Succeeded = false, + Recipient = new() + { + EmailAddress = "recipient@domain.com" + }, + SendStatus = new() + { + LastUpdate = timestamp, + Status = "Failed_RecipientNotIdentified", + StatusDescription = "Failed to send. Could not identify recipient." + } + }; + + EmailNotificationWithResult input = new( + id, + false, + new EmailRecipient() + { + RecipientId = "12345678910", + ToAddress = "recipient@domain.com" + }, + new NotificationResult( + EmailNotificationResultType.Failed_RecipientNotIdentified, + timestamp)); + + input.ResultStatus.SetResultDescription("Failed to send. Could not identify recipient."); + + // Act + var actual = input.MapToEmailNotificationWithResultExt(); + + // Assert + Assert.Equivalent(expected, actual, false); + } + + [Fact] + public void MapToEmailNotificationWithResultExt_NotificationWithSuccessResult_AreEquivalent() + { + // Arrange + Guid id = Guid.NewGuid(); + DateTime timestamp = DateTime.UtcNow; + EmailNotificationWithResultExt expected = new() + { + Id = id, + Succeeded = true, + Recipient = new() + { + EmailAddress = "recipient@domain.com" + }, + SendStatus = new() + { + LastUpdate = timestamp, + Status = "Delivered", + StatusDescription = "The email was delivered to the recipient. No errors reported, making it likely it was received by the recipient." + } + }; + + EmailNotificationWithResult input = new( + id, + true, + new EmailRecipient() + { + RecipientId = "12345678910", + ToAddress = "recipient@domain.com" + }, + new NotificationResult( + EmailNotificationResultType.Delivered, + timestamp)); + + input.ResultStatus.SetResultDescription("The email was delivered to the recipient. No errors reported, making it likely it was received by the recipient."); + + // Act + var actual = input.MapToEmailNotificationWithResultExt(); + + // Assert + Assert.Equivalent(expected, actual, false); + } + } +}