From 78cbeb06206f936f6269aaa485f22bae2f0991b2 Mon Sep 17 00:00:00 2001 From: Daniel Skovli Date: Mon, 25 Nov 2024 11:57:50 +0100 Subject: [PATCH] Update CorrespondenceRequest model (#926) --- .../Builder/CorrespondenceRequestBuilder.cs | 9 + .../Builder/ICorrespondenceRequestBuilder.cs | 8 +- .../Correspondence/CorrespondenceClient.cs | 4 +- .../Correspondence/ICorrespondenceClient.cs | 10 +- .../Models/CorrespondenceNotification.cs | 2 +- .../Models/CorrespondenceRequest.cs | 106 +++++++++-- .../Maskinporten/MaskinportenClient.cs | 7 +- src/Altinn.App.Core/Models/JwtToken.cs | 5 + .../Builder/CorrespondenceBuilderTests.cs | 11 +- .../Models/CorrespondenceRequestTests.cs | 173 +++++++++++++++++- .../Features/Maskinporten/TestHelpers.cs | 9 +- .../Models/JwtTokenTests.cs | 62 +++++-- 12 files changed, 352 insertions(+), 54 deletions(-) diff --git a/src/Altinn.App.Core/Features/Correspondence/Builder/CorrespondenceRequestBuilder.cs b/src/Altinn.App.Core/Features/Correspondence/Builder/CorrespondenceRequestBuilder.cs index 517465b12..69a59b05f 100644 --- a/src/Altinn.App.Core/Features/Correspondence/Builder/CorrespondenceRequestBuilder.cs +++ b/src/Altinn.App.Core/Features/Correspondence/Builder/CorrespondenceRequestBuilder.cs @@ -23,6 +23,7 @@ public class CorrespondenceRequestBuilder : ICorrespondenceRequestBuilder private List? _replyOptions; private CorrespondenceNotification? _notification; private bool? _ignoreReservation; + private bool? _isConfirmationNeeded; private List? _existingAttachments; private CorrespondenceRequestBuilder() { } @@ -247,6 +248,13 @@ public ICorrespondenceRequestBuilder WithIgnoreReservation(bool ignoreReservatio return this; } + /// + public ICorrespondenceRequestBuilder WithIsConfirmationNeeded(bool isConfirmationNeeded) + { + _isConfirmationNeeded = isConfirmationNeeded; + return this; + } + /// public ICorrespondenceRequestBuilder WithExistingAttachment(Guid existingAttachment) { @@ -308,6 +316,7 @@ public CorrespondenceRequest Build() Notification = _notification, IgnoreReservation = _ignoreReservation, ExistingAttachments = _existingAttachments, + IsConfirmationNeeded = _isConfirmationNeeded, }; } } diff --git a/src/Altinn.App.Core/Features/Correspondence/Builder/ICorrespondenceRequestBuilder.cs b/src/Altinn.App.Core/Features/Correspondence/Builder/ICorrespondenceRequestBuilder.cs index 3dbdad2e7..df792e6ff 100644 --- a/src/Altinn.App.Core/Features/Correspondence/Builder/ICorrespondenceRequestBuilder.cs +++ b/src/Altinn.App.Core/Features/Correspondence/Builder/ICorrespondenceRequestBuilder.cs @@ -253,9 +253,15 @@ IEnumerable externalReferences /// /// Sets whether the correspondence can override reservation against digital communication in KRR /// - /// A boolean value indicating whether or not reservations can be ignored + /// A boolean value indicating if reservations can be ignored or not ICorrespondenceRequestBuilder WithIgnoreReservation(bool ignoreReservation); + /// + /// Sets whether reading the correspondence needs to be confirmed by the recipient + /// + /// A boolean value indicating if confirmation is needed or not + ICorrespondenceRequestBuilder WithIsConfirmationNeeded(bool isConfirmationNeeded); + /// /// Adds an existing attachment reference to the correspondence /// diff --git a/src/Altinn.App.Core/Features/Correspondence/CorrespondenceClient.cs b/src/Altinn.App.Core/Features/Correspondence/CorrespondenceClient.cs index 7b5be70cc..2c587b63f 100644 --- a/src/Altinn.App.Core/Features/Correspondence/CorrespondenceClient.cs +++ b/src/Altinn.App.Core/Features/Correspondence/CorrespondenceClient.cs @@ -40,7 +40,7 @@ public CorrespondenceClient( _authorisationFactory = new CorrespondenceAuthorisationFactory(serviceProvider); } - private async Task AuthorisationFactory(CorrespondencePayloadBase payload) + private async Task AuthorisationResolver(CorrespondencePayloadBase payload) { if (payload.AccessTokenFactory is null && payload.AuthorisationMethod is null) { @@ -162,7 +162,7 @@ CorrespondencePayloadBase payload ) { _logger.LogDebug("Fetching access token via factory"); - JwtToken accessToken = await AuthorisationFactory(payload); + JwtToken accessToken = await AuthorisationResolver(payload); _logger.LogDebug("Constructing authorized http request for target uri {TargetEndpoint}", uri); HttpRequestMessage request = new(method, uri) { Content = content }; diff --git a/src/Altinn.App.Core/Features/Correspondence/ICorrespondenceClient.cs b/src/Altinn.App.Core/Features/Correspondence/ICorrespondenceClient.cs index 3fc4c55cb..7b50eaf72 100644 --- a/src/Altinn.App.Core/Features/Correspondence/ICorrespondenceClient.cs +++ b/src/Altinn.App.Core/Features/Correspondence/ICorrespondenceClient.cs @@ -8,11 +8,16 @@ namespace Altinn.App.Core.Features.Correspondence; public interface ICorrespondenceClient { /// - /// Sends a correspondence + /// Sends a correspondence. + /// After a successful request, the state of the correspondence order is . + /// This indicates that the request has met all validation requirements and is considered valid, but until the state + /// reaches it has not actually been sent to the recipient. + /// The current status of a correspondence and the associated notifications can be checked via . + /// Alternatively, the correspondence service publishes events which can be subscribed to. + /// For more information, see https://docs.altinn.studio/correspondence/getting-started/developer-guides/events/ /// /// The payload /// An optional cancellation token - /// Task Send( SendCorrespondencePayload payload, CancellationToken cancellationToken = default @@ -23,7 +28,6 @@ Task Send( /// /// The payload /// An optional cancellation token - /// Task GetStatus( GetCorrespondenceStatusPayload payload, CancellationToken cancellationToken = default diff --git a/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceNotification.cs b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceNotification.cs index 4d3ae3f5d..414d0eaaf 100644 --- a/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceNotification.cs +++ b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceNotification.cs @@ -105,7 +105,7 @@ internal void Serialise(MultipartFormDataContent content) AddIfNotNull(content, ReminderSmsBody, "Correspondence.Notification.ReminderSmsBody"); AddIfNotNull(content, NotificationChannel.ToString(), "Correspondence.Notification.NotificationChannel"); AddIfNotNull(content, SendersReference, "Correspondence.Notification.SendersReference"); - AddIfNotNull(content, RequestedSendTime?.ToString("O"), "Correspondence.Notification.RequestedSendTime"); + AddIfNotNull(content, RequestedSendTime, "Correspondence.Notification.RequestedSendTime"); AddIfNotNull( content, ReminderNotificationChannel.ToString(), diff --git a/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceRequest.cs b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceRequest.cs index 69b9636f4..232c8af3e 100644 --- a/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceRequest.cs +++ b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceRequest.cs @@ -18,6 +18,15 @@ internal static void AddRequired(MultipartFormDataContent content, string value, content.Add(new StringContent(value), name); } + internal static void AddRequired(MultipartFormDataContent content, DateTimeOffset value, string name) + { + if (value == default) + throw new CorrespondenceValueException($"Required value is missing: {name}"); + + var normalisedAndFormatted = NormaliseDateTime(value).ToString("O"); + content.Add(new StringContent(normalisedAndFormatted), name); + } + internal static void AddRequired( MultipartFormDataContent content, ReadOnlyMemory data, @@ -37,6 +46,15 @@ internal static void AddIfNotNull(MultipartFormDataContent content, string? valu content.Add(new StringContent(value), name); } + internal static void AddIfNotNull(MultipartFormDataContent content, DateTimeOffset? value, string name) + { + if (value is null) + return; + + var normalisedAndFormatted = NormaliseDateTime(value.Value).ToString("O"); + content.Add(new StringContent(normalisedAndFormatted), name); + } + internal static void AddListItems( MultipartFormDataContent content, IReadOnlyList? items, @@ -153,6 +171,14 @@ internal void ValidateAllProperties(string dataTypeName) ); } } + + /// + /// Removes the portion of a + /// + internal static DateTimeOffset NormaliseDateTime(DateTimeOffset dateTime) + { + return dateTime.AddTicks(-(dateTime.Ticks % TimeSpan.TicksPerSecond)); + } } /// @@ -239,6 +265,11 @@ public sealed record CorrespondenceRequest : MultipartCorrespondenceItem /// public bool? IgnoreReservation { get; init; } + /// + /// Specifies if reading the correspondence needs to be confirmed by the recipient + /// + public bool? IsConfirmationNeeded { get; init; } + /// /// Existing attachments that should be added to the correspondence /// @@ -250,32 +281,20 @@ public sealed record CorrespondenceRequest : MultipartCorrespondenceItem /// The multipart object to serialise into internal void Serialise(MultipartFormDataContent content) { + Validate(); + AddRequired(content, ResourceId, "Correspondence.ResourceId"); AddRequired(content, Sender.Get(OrganisationNumberFormat.International), "Correspondence.Sender"); AddRequired(content, SendersReference, "Correspondence.SendersReference"); - AddRequired(content, AllowSystemDeleteAfter.ToString("O"), "Correspondence.AllowSystemDeleteAfter"); + AddRequired(content, AllowSystemDeleteAfter, "Correspondence.AllowSystemDeleteAfter"); AddIfNotNull(content, MessageSender, "Correspondence.MessageSender"); - AddIfNotNull(content, RequestedPublishTime?.ToString("O"), "Correspondence.RequestedPublishTime"); - AddIfNotNull(content, DueDateTime?.ToString("O"), "Correspondence.DueDateTime"); + AddIfNotNull(content, RequestedPublishTime, "Correspondence.RequestedPublishTime"); + AddIfNotNull(content, DueDateTime, "Correspondence.DueDateTime"); AddIfNotNull(content, IgnoreReservation?.ToString(), "Correspondence.IgnoreReservation"); + AddIfNotNull(content, IsConfirmationNeeded?.ToString(), "Correspondence.IsConfirmationNeeded"); AddDictionaryItems(content, PropertyList, x => x, key => $"Correspondence.PropertyList.{key}"); AddListItems(content, ExistingAttachments, x => x.ToString(), i => $"Correspondence.ExistingAttachments[{i}]"); - AddListItems( - content, - Recipients, - x => - x switch - { - OrganisationOrPersonIdentifier.Organisation org => org.Value.Get( - OrganisationNumberFormat.International - ), - OrganisationOrPersonIdentifier.Person person => person.Value, - _ => throw new CorrespondenceValueException( - $"Unknown OrganisationOrPersonIdentifier type `{x.GetType()}` ({nameof(Recipients)})" - ), - }, - i => $"Recipients[{i}]" - ); + AddListItems(content, Recipients, GetFormattedRecipient, i => $"Recipients[{i}]"); Content.Serialise(content); Notification?.Serialise(content); @@ -292,4 +311,53 @@ internal MultipartFormDataContent Serialise() Serialise(content); return content; } + + /// + /// Validates the state of the request based on some known requirements from the Correspondence API + /// + /// + /// Mostly stuff found here: https://github.com/Altinn/altinn-correspondence/blob/main/src/Altinn.Correspondence.Application/InitializeCorrespondences/InitializeCorrespondencesHandler.cs#L51 + /// + private void Validate() + { + if (Recipients.Count != Recipients.Distinct().Count()) + ValidationError($"Duplicate recipients found in {nameof(Recipients)} list"); + if (IsConfirmationNeeded is true && DueDateTime is null) + ValidationError($"When {nameof(IsConfirmationNeeded)} is set, {nameof(DueDateTime)} is also required"); + + var normalisedAllowSystemDeleteAfter = NormaliseDateTime(AllowSystemDeleteAfter); + if (normalisedAllowSystemDeleteAfter < DateTimeOffset.UtcNow) + ValidationError($"{nameof(AllowSystemDeleteAfter)} cannot be a time in the past"); + if (normalisedAllowSystemDeleteAfter < RequestedPublishTime) + ValidationError($"{nameof(AllowSystemDeleteAfter)} cannot be prior to {nameof(RequestedPublishTime)}"); + + if (DueDateTime is not null) + { + var normalisedDueDate = NormaliseDateTime(DueDateTime.Value); + if (normalisedDueDate < DateTimeOffset.UtcNow) + ValidationError($"{nameof(DueDateTime)} cannot be a time in the past"); + if (normalisedDueDate < RequestedPublishTime) + ValidationError($"{nameof(DueDateTime)} cannot be prior to {nameof(RequestedPublishTime)}"); + if (normalisedAllowSystemDeleteAfter < normalisedDueDate) + ValidationError($"{nameof(AllowSystemDeleteAfter)} cannot be prior to {nameof(DueDateTime)}"); + } + } + + [DoesNotReturn] + private static void ValidationError(string errorMessage) + { + throw new CorrespondenceArgumentException(errorMessage); + } + + private static string GetFormattedRecipient(OrganisationOrPersonIdentifier recipient) + { + return recipient switch + { + OrganisationOrPersonIdentifier.Organisation org => org.Value.Get(OrganisationNumberFormat.International), + OrganisationOrPersonIdentifier.Person person => person.Value.Value, + _ => throw new CorrespondenceValueException( + $"Unknown {nameof(OrganisationOrPersonIdentifier)} type `{recipient.GetType()}`" + ), + }; + } } diff --git a/src/Altinn.App.Core/Features/Maskinporten/MaskinportenClient.cs b/src/Altinn.App.Core/Features/Maskinporten/MaskinportenClient.cs index 638ecacc3..9ee59574e 100644 --- a/src/Altinn.App.Core/Features/Maskinporten/MaskinportenClient.cs +++ b/src/Altinn.App.Core/Features/Maskinporten/MaskinportenClient.cs @@ -278,12 +278,13 @@ private async Task HandleMaskinportenAltinnTokenExchange( using HttpResponseMessage response = await client.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); - string token = await response.Content.ReadAsStringAsync(cancellationToken); + string tokenResponse = await response.Content.ReadAsStringAsync(cancellationToken); + JwtToken token = JwtToken.Parse(tokenResponse); - _logger.LogDebug("Token retrieved successfully"); + _logger.LogDebug("Token retrieved successfully: {Token}", token); _telemetry?.RecordMaskinportenAltinnTokenExchangeRequest(Telemetry.Maskinporten.RequestResult.New); - return JwtToken.Parse(token); + return token; } catch (MaskinportenException) { diff --git a/src/Altinn.App.Core/Models/JwtToken.cs b/src/Altinn.App.Core/Models/JwtToken.cs index f60a092dd..4d4f8eda2 100644 --- a/src/Altinn.App.Core/Models/JwtToken.cs +++ b/src/Altinn.App.Core/Models/JwtToken.cs @@ -29,6 +29,11 @@ namespace Altinn.App.Core.Models; public bool IsExpired(TimeProvider? timeProvider = null) => ExpiresAt < (timeProvider?.GetUtcNow() ?? DateTimeOffset.UtcNow); + /// + /// The issuing authority of the token + /// + public string Issuer => _jwtSecurityToken.Issuer; + /// /// The scope(s) associated with the token /// diff --git a/test/Altinn.App.Core.Tests/Features/Correspondence/Builder/CorrespondenceBuilderTests.cs b/test/Altinn.App.Core.Tests/Features/Correspondence/Builder/CorrespondenceBuilderTests.cs index 2df94bd52..37cd8c760 100644 --- a/test/Altinn.App.Core.Tests/Features/Correspondence/Builder/CorrespondenceBuilderTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Correspondence/Builder/CorrespondenceBuilderTests.cs @@ -67,6 +67,7 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence() dueDateTime = DateTimeOffset.Now.AddDays(30), allowDeleteAfter = DateTimeOffset.Now.AddDays(60), ignoreReservation = true, + isConfirmationNeeded = true, requestedPublishTime = DateTimeOffset.Now.AddSeconds(45), propertyList = new Dictionary { ["prop1"] = "value1", ["prop2"] = "value2" }, content = new @@ -180,6 +181,7 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence() .WithDueDateTime(data.dueDateTime) .WithMessageSender(data.messageSender) .WithIgnoreReservation(data.ignoreReservation) + .WithIsConfirmationNeeded(data.isConfirmationNeeded) .WithRequestedPublishTime(data.requestedPublishTime) .WithPropertyList(data.propertyList) .WithAttachment( @@ -260,6 +262,7 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence() correspondence.DueDateTime.Should().Be(data.dueDateTime); correspondence.AllowSystemDeleteAfter.Should().Be(data.allowDeleteAfter); correspondence.IgnoreReservation.Should().Be(data.ignoreReservation); + correspondence.IsConfirmationNeeded.Should().Be(data.isConfirmationNeeded); correspondence.RequestedPublishTime.Should().Be(data.requestedPublishTime); correspondence.PropertyList.Should().BeEquivalentTo(data.propertyList); correspondence.MessageSender.Should().Be(data.messageSender); @@ -346,7 +349,8 @@ public void Builder_UpdatesAndOverwritesValuesCorrectly() .WithPropertyList(new Dictionary { ["prop1"] = "value1", ["prop2"] = "value2" }) .WithExistingAttachment(Guid.Parse("a3ac4826-5873-4ecb-9fe7-dc4cfccd0afa")) .WithRequestedPublishTime(DateTime.Today) - .WithIgnoreReservation(true); + .WithIgnoreReservation(true) + .WithIsConfirmationNeeded(true); builder.WithResourceId("resourceId-2"); builder.WithSender(TestHelpers.GetOrganisationNumber(2).Get(OrganisationNumberFormat.Local)); @@ -397,6 +401,8 @@ public void Builder_UpdatesAndOverwritesValuesCorrectly() builder.WithExistingAttachment(Guid.Parse("eeb67483-7d6d-40dc-9861-3fc1beff7608")); builder.WithExistingAttachments([Guid.Parse("9a12dfd9-6c70-489c-8b3d-77bb188c64b3")]); builder.WithRequestedPublishTime(DateTime.Today.AddDays(1)); + builder.WithIgnoreReservation(false); + builder.WithIsConfirmationNeeded(false); // Act var correspondence = builder.Build(); @@ -487,7 +493,8 @@ public void Builder_UpdatesAndOverwritesValuesCorrectly() ] ); correspondence.RequestedPublishTime.Should().BeSameDateAs(DateTime.Today.AddDays(1)); - correspondence.IgnoreReservation.Should().BeTrue(); + correspondence.IgnoreReservation.Should().BeFalse(); + correspondence.IsConfirmationNeeded.Should().BeFalse(); } [Fact] diff --git a/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests.cs b/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests.cs index 525660fcc..9ba56b539 100644 --- a/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests.cs @@ -1,4 +1,5 @@ using System.Text; +using Altinn.App.Core.Features.Correspondence.Exceptions; using Altinn.App.Core.Features.Correspondence.Models; using Altinn.App.Core.Models; using FluentAssertions; @@ -21,6 +22,7 @@ public async Task Serialise_ShouldAddCorrectFields() AllowSystemDeleteAfter = DateTimeOffset.UtcNow.AddDays(2), DueDateTime = DateTimeOffset.UtcNow.AddDays(2), IgnoreReservation = true, + IsConfirmationNeeded = true, MessageSender = "message-sender", Recipients = [ @@ -124,6 +126,7 @@ public async Task Serialise_ShouldAddCorrectFields() ["Correspondence.DueDateTime"] = correspondence.DueDateTime, ["Correspondence.MessageSender"] = correspondence.MessageSender, ["Correspondence.IgnoreReservation"] = correspondence.IgnoreReservation, + ["Correspondence.IsConfirmationNeeded"] = correspondence.IsConfirmationNeeded, ["Correspondence.Content.Language"] = correspondence.Content.Language, ["Correspondence.Content.MessageTitle"] = correspondence.Content.Title, ["Correspondence.Content.MessageSummary"] = correspondence.Content.Summary, @@ -270,6 +273,168 @@ public void Serialise_ClashingFilenames_ShouldUseReferenceComparison() processedAttachments[identicalAttachments[2]].Should().Contain("overwritten"); } + [Fact] + public void Serialise_ValidatesUniqueRecipients() + { + // Arrange + var correspondence = new CorrespondenceRequest + { + ResourceId = "resource-id", + Sender = TestHelpers.GetOrganisationNumber(0), + SendersReference = "senders-reference", + AllowSystemDeleteAfter = DateTimeOffset.UtcNow.AddYears(1), + Recipients = + [ + OrganisationOrPersonIdentifier.Create(TestHelpers.GetOrganisationNumber(1)), + OrganisationOrPersonIdentifier.Create(TestHelpers.GetOrganisationNumber(1)), + ], + Content = new CorrespondenceContent + { + Title = "title", + Body = "body", + Summary = "summary", + Language = LanguageCode.Parse("no"), + }, + }; + + // Act + var act = () => correspondence.Serialise(); + + // Assert + act.Should().Throw().WithMessage("Duplicate recipients found *"); + } + + [Fact] + public void Serialise_ValidatesConfirmationAndDueDate() + { + // Arrange + var correspondence = new CorrespondenceRequest + { + ResourceId = "resource-id", + Sender = TestHelpers.GetOrganisationNumber(0), + SendersReference = "senders-reference", + AllowSystemDeleteAfter = DateTimeOffset.UtcNow.AddYears(1), + IsConfirmationNeeded = true, + Recipients = [OrganisationOrPersonIdentifier.Create(TestHelpers.GetOrganisationNumber(1))], + Content = new CorrespondenceContent + { + Title = "title", + Body = "body", + Summary = "summary", + Language = LanguageCode.Parse("no"), + }, + }; + + // Act + var act = () => correspondence.Serialise(); + + // Assert + act.Should().Throw().WithMessage("When*set*required"); + } + + [Fact] + public void Serialise_ValidatesNoDatesInThePast() + { + // Arrange + var baseCorrespondence = new CorrespondenceRequest + { + ResourceId = "resource-id", + Sender = TestHelpers.GetOrganisationNumber(0), + SendersReference = "senders-reference", + AllowSystemDeleteAfter = DateTimeOffset.UtcNow.AddYears(1), + Recipients = [OrganisationOrPersonIdentifier.Create(TestHelpers.GetOrganisationNumber(1))], + Content = new CorrespondenceContent + { + Title = "title", + Body = "body", + Summary = "summary", + Language = LanguageCode.Parse("no"), + }, + }; + + // Act + var act1 = () => + { + var correspondence = baseCorrespondence with { DueDateTime = DateTimeOffset.Now.AddSeconds(-1) }; + correspondence.Serialise(); + }; + var act2 = () => + { + var correspondence = baseCorrespondence with { AllowSystemDeleteAfter = DateTimeOffset.Now.AddSeconds(-1) }; + correspondence.Serialise(); + }; + + // Assert + act1.Should().Throw().WithMessage("*not be*in the past"); + act2.Should().Throw().WithMessage("*not be*in the past"); + } + + [Fact] + public void Serialise_ValidatesNoBeforePublishDate() + { + // Arrange + var baseCorrespondence = new CorrespondenceRequest + { + ResourceId = "resource-id", + Sender = TestHelpers.GetOrganisationNumber(0), + SendersReference = "senders-reference", + RequestedPublishTime = DateTimeOffset.Now.AddDays(2), + AllowSystemDeleteAfter = DateTimeOffset.UtcNow.AddYears(1), + Recipients = [OrganisationOrPersonIdentifier.Create(TestHelpers.GetOrganisationNumber(1))], + Content = new CorrespondenceContent + { + Title = "title", + Body = "body", + Summary = "summary", + Language = LanguageCode.Parse("no"), + }, + }; + + // Act + var act1 = () => + { + var correspondence = baseCorrespondence with { DueDateTime = DateTimeOffset.Now.AddDays(1) }; + correspondence.Serialise(); + }; + var act2 = () => + { + var correspondence = baseCorrespondence with { AllowSystemDeleteAfter = DateTimeOffset.Now.AddDays(1) }; + correspondence.Serialise(); + }; + + // Assert + act1.Should().Throw().WithMessage("*not be prior to*"); + act2.Should().Throw().WithMessage("*not be prior to*"); + } + + [Fact] + public void Serialise_ValidatesDeleteDateAfterDueDate() + { + // Arrange + var correspondence = new CorrespondenceRequest + { + ResourceId = "resource-id", + Sender = TestHelpers.GetOrganisationNumber(0), + SendersReference = "senders-reference", + AllowSystemDeleteAfter = DateTimeOffset.UtcNow.AddDays(2), + DueDateTime = DateTimeOffset.UtcNow.AddDays(3), + Recipients = [OrganisationOrPersonIdentifier.Create(TestHelpers.GetOrganisationNumber(1))], + Content = new CorrespondenceContent + { + Title = "title", + Body = "body", + Summary = "summary", + Language = LanguageCode.Parse("no"), + }, + }; + + // Act + var act = () => correspondence.Serialise(); + + // Assert + act.Should().Throw().WithMessage("*not be prior to*"); + } + private static async Task AssertContent(MultipartFormDataContent content, string dispositionName, object value) { var item = content.GetItem(dispositionName); @@ -278,7 +443,7 @@ private static async Task AssertContent(MultipartFormDataContent content, string item.Should().NotBeNull($"FormDataContent with name `{dispositionName}` was not found"); item!.Headers.ContentDisposition!.Name.Should().NotBeNull(); dispositionName.Should().Be(item.Headers.ContentDisposition.Name!.Trim('\"')); - stringValue.Should().Be(await item.ReadAsStringAsync()); + stringValue.Should().Be(await item.ReadAsStringAsync(), $"`{dispositionName}`"); } private static string FormattedString(object value) @@ -289,8 +454,10 @@ private static string FormattedString(object value) { OrganisationNumber org => org.Get(OrganisationNumberFormat.International), OrganisationOrPersonIdentifier.Organisation org => org.Value.Get(OrganisationNumberFormat.International), - DateTime dateTime => dateTime.ToString("O"), - DateTimeOffset dateTimeOffset => dateTimeOffset.ToString("O"), + DateTime dateTime => MultipartCorrespondenceItem.NormaliseDateTime(dateTime).ToString("O"), + DateTimeOffset dateTimeOffset => MultipartCorrespondenceItem + .NormaliseDateTime(dateTimeOffset) + .ToString("O"), _ => value.ToString() ?? throw new NullReferenceException( $"ToString method call for object `{nameof(value)} ({value.GetType()})` returned null" diff --git a/test/Altinn.App.Core.Tests/Features/Maskinporten/TestHelpers.cs b/test/Altinn.App.Core.Tests/Features/Maskinporten/TestHelpers.cs index edb33d29e..c8f06ff66 100644 --- a/test/Altinn.App.Core.Tests/Features/Maskinporten/TestHelpers.cs +++ b/test/Altinn.App.Core.Tests/Features/Maskinporten/TestHelpers.cs @@ -120,9 +120,12 @@ public static ( (string Header, string Payload, string Signature) Components ) GetEncodedAccessToken() { - const string testTokenHeader = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; - const string testTokenPayload = "eyJzdWIiOiJpdHMtYS1tZSJ9"; - const string testTokenSignature = "wLLw4Timcl9gnQvA93RgREz-6S5y1UfzI_GYVI_XVDA"; + const string testTokenHeader = + "eyJraWQiOiJiZFhMRVduRGpMSGpwRThPZnl5TUp4UlJLbVo3MUxCOHUxeUREbVBpdVQwIiwiYWxnIjoiUlMyNTYifQ"; + const string testTokenPayload = + "eyJzY29wZSI6ImFsdGlubjpzZXJ2aWNlb3duZXIvaW5zdGFuY2VzLnJlYWQiLCJpc3MiOiJodHRwczovL3Rlc3QubWFza2lucG9ydGVuLm5vLyIsImNsaWVudF9hbXIiOiJwcml2YXRlX2tleV9qd3QiLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzMyMjg3NDUxLCJpYXQiOjE3MzIyODczMzEsImNsaWVudF9pZCI6ImQyMjEzMGNmLTMzZjEtNGI2Yy1hMjM4LTVmMjZmZTk1NTRiMyIsImp0aSI6Ik5rMWc1MXFZVlVBWWRmbWVSeWlrdXBCaXdJaVVzSzdOZGxHNFlCSjZFV3MiLCJjb25zdW1lciI6eyJhdXRob3JpdHkiOiJpc282NTIzLWFjdG9yaWQtdXBpcyIsIklEIjoiMDE5Mjo5OTE4MjU4MjcifX0"; + const string testTokenSignature = + "Y-dNpwVXsaYBgCL_bT8EoEnY650KhpwZJW3QN-uvAFq2qxHMTuOEpg0PtZGL4GQtLT57_urHTAtspTG9-y30oOkAEYqggeQ0_TmnXCN17pd4wtPyZLFpYoHJe7ki3-9ITGv2JUuRiRN4gpN92zdsaAvafnEksxG0CjxbpWRCS8XA0Cr3wsKj1Fpd4zLit64iI3OSk_yW0Gfe15QkALnUCQgzJCQhTXlnuSGZgPLuQZcvWfONzdZojkAgxTJJg-hOC-TNNGq2IN8NJhg3GjrGypiB4-niVXyugPyP2MdnxWeZQiuuAsMRbe3mGNTlx9VPDXJIsrtDDYVHrndvL8CHqjHdkOLZxtdTdMOMz1IXi_ZTcTJreqP4ti8J_Fx5u-3AOSVZG0hOOxtONBZgoMul12QoztaOuX65rP4zzZq9Afz07m2XHGg72jbowhtRiJKlf_mn31EK75bmDxZHVlL5s0Crb3VvRu39Xnz4Z8n5-Yn5LqnCYhhvZz_vf8f0U5jv"; const string testToken = testTokenHeader + "." + testTokenPayload + "." + testTokenSignature; return (testToken, (testTokenHeader, testTokenPayload, testTokenSignature)); diff --git a/test/Altinn.App.Core.Tests/Models/JwtTokenTests.cs b/test/Altinn.App.Core.Tests/Models/JwtTokenTests.cs index f14fbfdd7..369061a53 100644 --- a/test/Altinn.App.Core.Tests/Models/JwtTokenTests.cs +++ b/test/Altinn.App.Core.Tests/Models/JwtTokenTests.cs @@ -114,23 +114,51 @@ public void Value_Property_ShouldReturnFullTokenString() tokenString.Should().Be(_validTokens[0]); } - // [Theory] - // [InlineData(true)] - // [InlineData(false)] - // public void ShouldIndicateExpiry(bool expired) - // { - // // Arrange - // var encodedToken = TestHelpers.GetEncodedAccessToken(); - // var jwtToken = JwtToken.Parse(encodedToken.AccessToken); - // var expiry = jwtToken.ExpiresAt; - // var fakeTimeProvider = new FakeTimeProvider(expiry.AddDays(expired ? -1 : 1)); - - // // Act - // var isExpired = jwtToken.IsExpired(fakeTimeProvider); - - // // Assert - // isExpired.Should().Be(expired); - // } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ShouldIndicateExpiry(bool expired) + { + // Arrange + var encodedToken = TestHelpers.GetEncodedAccessToken(); + var jwtToken = JwtToken.Parse(encodedToken.AccessToken); + var expiry = jwtToken.ExpiresAt; + var fakeTimeProvider = new FakeTimeProvider(expiry.AddDays(expired ? 1 : -1)); + + // Act + var isExpired = jwtToken.IsExpired(fakeTimeProvider); + + // Assert + isExpired.Should().Be(expired); + } + + [Fact] + public void ShouldIndicateIssuer() + { + // Arrange + var encodedToken = TestHelpers.GetEncodedAccessToken(); + var jwtToken = JwtToken.Parse(encodedToken.AccessToken); + + // Act + var issuer = jwtToken.Issuer; + + // Assert + issuer.Should().Be("https://test.maskinporten.no/"); + } + + [Fact] + public void ShouldIndicateScopes() + { + // Arrange + var encodedToken = TestHelpers.GetEncodedAccessToken(); + var jwtToken = JwtToken.Parse(encodedToken.AccessToken); + + // Act + var scope = jwtToken.Scope; + + // Assert + scope.Should().Be("altinn:serviceowner/instances.read"); + } [Fact] public void ToString_ShouldMask_AccessToken()