From 6738d87029b54f6d06059c8f22b211f598119ec6 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Fri, 26 Apr 2024 07:55:15 +0200 Subject: [PATCH 01/22] Fix the recursion bug in RemoveAltinnRowId (#618) --- src/Altinn.App.Core/Helpers/ObjectUtils.cs | 2 +- .../Helpers/ObjectUtilsTests.cs | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Helpers/ObjectUtils.cs b/src/Altinn.App.Core/Helpers/ObjectUtils.cs index 0c7dc1292..5662a23fe 100644 --- a/src/Altinn.App.Core/Helpers/ObjectUtils.cs +++ b/src/Altinn.App.Core/Helpers/ObjectUtils.cs @@ -104,7 +104,7 @@ public static void RemoveAltinnRowId(object model) var value = prop.GetValue(model); // continue recursion over all properties - if (value is not null) + if (value?.GetType().IsValueType == false) { RemoveAltinnRowId(value); } diff --git a/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs b/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs index 86baf5f21..bdd9b90a8 100644 --- a/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs @@ -115,6 +115,12 @@ public void TestGuidInitialized() test.DateTime.Should().Be(dateTime); test.NullableDecimal.Should().Be(1.1m); test.Decimal.Should().Be(2.2m); + + ObjectUtils.RemoveAltinnRowId(test); + + test.AltinnRowId.Should().Be(Guid.Empty); + test.Child.AltinnRowId.Should().Be(Guid.Empty); + test.Children.Should().AllSatisfy(c => c.AltinnRowId.Should().Be(Guid.Empty)); } [Fact] @@ -153,6 +159,14 @@ public void TestRemoveAltinnRowId() test.Child.Child.AltinnRowId.Should().Be(Guid.Empty); test.Child.Child.Children.Should().ContainSingle().Which.AltinnRowId.Should().Be(Guid.Empty); test.Child.Child.Children.Should().ContainSingle().Which.Child!.AltinnRowId.Should().Be(Guid.Empty); + + ObjectUtils.InitializeAltinnRowId(test); + + test.AltinnRowId.Should().NotBe(Guid.Empty); + test.Child.AltinnRowId.Should().NotBe(Guid.Empty); + test.Child.Child.AltinnRowId.Should().NotBe(Guid.Empty); + test.Child.Child.Children.Should().ContainSingle().Which.AltinnRowId.Should().NotBe(Guid.Empty); + test.Child.Child.Children.Should().ContainSingle().Which.Child!.AltinnRowId.Should().NotBe(Guid.Empty); } [Fact] @@ -194,6 +208,15 @@ public void TestRemoveAltinnRowIdWithNulls() childArray = test.Child.Child.Children.Should().HaveCount(2).And; childArray.ContainSingle(d => d != null).Which.AltinnRowId.Should().Be(Guid.Empty); childArray.ContainSingle(d => d == null); + + ObjectUtils.InitializeAltinnRowId(test); + + test.AltinnRowId.Should().NotBe(Guid.Empty); + test.Child.AltinnRowId.Should().NotBe(Guid.Empty); + test.Child.Child.AltinnRowId.Should().NotBe(Guid.Empty); + childArray = test.Child.Child.Children.Should().HaveCount(2).And; + childArray.ContainSingle(d => d != null).Which.AltinnRowId.Should().NotBe(Guid.Empty); + childArray.ContainSingle(d => d == null); } [Fact] @@ -235,5 +258,14 @@ public void TestInitializeRowIdWithNulls() childArray = test.Child.Child.Children.Should().HaveCount(2).And; childArray.ContainSingle(d => d != null).Which.AltinnRowId.Should().NotBe(Guid.Empty); childArray.ContainSingle(d => d == null); + + ObjectUtils.RemoveAltinnRowId(test); + + test.AltinnRowId.Should().Be(Guid.Empty); + test.Child.AltinnRowId.Should().Be(Guid.Empty); + test.Child.Child.AltinnRowId.Should().Be(Guid.Empty); + childArray = test.Child.Child.Children.Should().HaveCount(2).And; + childArray.ContainSingle(d => d != null).Which.AltinnRowId.Should().Be(Guid.Empty); + childArray.ContainSingle(d => d == null); } } From 9a75afa100f7635c18f82f5c821e281fb6495e2d Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Mon, 29 Apr 2024 09:59:59 +0200 Subject: [PATCH 02/22] Remove BOM on C# files now that .editorconfig states that BOM should not be used (#620) This causes a massive list of changes when using dotnet format The alternative would be to not specify utf-8 in editor config --- src/Altinn.App.Api/Controllers/ApplicationLanguageController.cs | 2 +- src/Altinn.App.Api/Controllers/DataListsController.cs | 2 +- src/Altinn.App.Api/Controllers/EventsReceiverController.cs | 2 +- src/Altinn.App.Api/Controllers/FileScanController.cs | 2 +- src/Altinn.App.Api/Controllers/StatelessPagesController.cs | 2 +- src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs | 2 +- .../Helpers/RequestHandling/RequestPartValidator.cs | 2 +- src/Altinn.App.Api/Infrastructure/Health/HealthCheck.cs | 2 +- .../Infrastructure/Telemetry/HealthTelemetryFilter.cs | 2 +- src/Altinn.App.Api/Models/DataElementFileScanResult.cs | 2 +- src/Altinn.App.Api/Models/InstanceFileScanResult.cs | 2 +- src/Altinn.App.Core/Configuration/CacheSettings.cs | 2 +- src/Altinn.App.Core/EFormidling/EformidlingConstants.cs | 2 +- src/Altinn.App.Core/EFormidling/EformidlingStartup.cs | 2 +- .../EFormidling/Implementation/EformidlingDeliveryException.cs | 2 +- .../Implementation/EformidlingStatusCheckEventHandler.cs | 2 +- .../Implementation/EformidlingStatusCheckEventHandler2.cs | 2 +- src/Altinn.App.Core/Extensions/DictionaryExtensions.cs | 2 +- src/Altinn.App.Core/Extensions/HttpContextExtensions.cs | 2 +- src/Altinn.App.Core/Extensions/StringExtensions.cs | 2 +- src/Altinn.App.Core/Features/DataLists/DataListsFactory.cs | 2 +- src/Altinn.App.Core/Features/DataLists/DataListsService.cs | 2 +- src/Altinn.App.Core/Features/DataLists/IDataListsService.cs | 2 +- .../Features/DataLists/InstanceDataListsFactory.cs | 2 +- src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs | 2 +- .../Features/DataLists/NullInstanceDataListProvider.cs | 2 +- src/Altinn.App.Core/Features/FeatureFlags.cs | 2 +- .../Features/FileAnalyzis/FileAnalyserFactory.cs | 2 +- src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs | 2 +- .../Features/FileAnalyzis/FileAnalysisService.cs | 2 +- src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs | 2 +- .../Features/FileAnalyzis/IFileAnalyserFactory.cs | 2 +- .../Features/FileAnalyzis/IFileAnalysisService.cs | 2 +- src/Altinn.App.Core/Features/IDataListProvider.cs | 2 +- src/Altinn.App.Core/Features/IEventHandler.cs | 2 +- src/Altinn.App.Core/Features/IInstanceDataListProvider.cs | 2 +- src/Altinn.App.Core/Features/IPdfFormatter.cs | 2 +- src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs | 2 +- src/Altinn.App.Core/Features/Pdf/NullPdfFormatter.cs | 2 +- src/Altinn.App.Core/Features/Validation/IFileValidator.cs | 2 +- src/Altinn.App.Core/Implementation/UserTokenProvider.cs | 2 +- .../Infrastructure/Clients/Events/Subscription.cs | 2 +- .../Infrastructure/Clients/Events/SubscriptionRequest.cs | 2 +- .../Clients/Maskinporten/IX509CertificateProvider.cs | 2 +- .../Infrastructure/Clients/Pdf/PdfGeneratorClient.cs | 2 +- .../Clients/Profile/ProfileClientCachingDecorator.cs | 2 +- .../Infrastructure/Clients/Register/PersonClient.cs | 2 +- src/Altinn.App.Core/Interface/IPersonLookup.cs | 2 +- src/Altinn.App.Core/Interface/IPersonRetriever.cs | 2 +- src/Altinn.App.Core/Interface/IUserTokenProvider.cs | 2 +- src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs | 2 +- src/Altinn.App.Core/Internal/Data/DataService.cs | 2 +- src/Altinn.App.Core/Internal/Data/IDataService.cs | 2 +- src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs | 2 +- src/Altinn.App.Core/Internal/Events/IEventHandlerResolver.cs | 2 +- src/Altinn.App.Core/Internal/Events/IEventSecretCodeProvider.cs | 2 +- .../Internal/Events/KeyVaultSecretCodeProvider.cs | 2 +- .../Internal/Events/SubscriptionValidationHandler.cs | 2 +- src/Altinn.App.Core/Internal/Events/UnhandledEventHandler.cs | 2 +- src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs | 2 +- src/Altinn.App.Core/Internal/Language/IApplicationLanguage.cs | 2 +- .../Internal/Maskinporten/IMaskinportenTokenProvider.cs | 2 +- .../Internal/Maskinporten/MaskinportenExtensions.cs | 2 +- .../Internal/Maskinporten/MaskinportenJwkTokenProvider.cs | 2 +- src/Altinn.App.Core/Internal/Pdf/IPdfGeneratorClient.cs | 2 +- src/Altinn.App.Core/Internal/Pdf/IPdfService.cs | 2 +- src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs | 2 +- src/Altinn.App.Core/Internal/Pdf/PdfGeneratorSettings.cs | 2 +- src/Altinn.App.Core/Internal/Pdf/PdfService.cs | 2 +- .../Internal/Process/EventHandlers/EndEventEventHandler.cs | 2 +- .../Process/EventHandlers/Interfaces/IEndEventEventHandler.cs | 2 +- .../EventHandlers/ProcessTask/AbandonTaskEventHandler.cs | 2 +- .../Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs | 2 +- .../ProcessTask/Interfaces/IAbandonTaskEventHandler.cs | 2 +- .../ProcessTask/Interfaces/IEndTaskEventHandler.cs | 2 +- .../ProcessTask/Interfaces/IStartTaskEventHandler.cs | 2 +- .../Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs | 2 +- .../Process/Interfaces/IProcessEventHandlerDelegator.cs | 2 +- .../Internal/Process/ProcessEventHandlingDelegator.cs | 2 +- .../Internal/Process/ProcessTasks/SigningProcessTask.cs | 2 +- src/Altinn.App.Core/Internal/Registers/IPersonClient.cs | 2 +- .../Internal/Validation/FileValidationService.cs | 2 +- src/Altinn.App.Core/Internal/Validation/FileValidatorFactory.cs | 2 +- .../Internal/Validation/IFileValidationService.cs | 2 +- .../Internal/Validation/IFileValidatorFactory.cs | 2 +- src/Altinn.App.Core/Models/ApplicationLanguage.cs | 2 +- src/Altinn.App.Core/Models/DataList.cs | 2 +- src/Altinn.App.Core/Models/DataListMetadata.cs | 2 +- src/Altinn.App.Core/Models/Notifications/Sms/SmsNotification.cs | 2 +- .../Models/Notifications/Sms/SmsNotificationClientException.cs | 2 +- src/Altinn.App.Core/Models/Pdf/PdfGeneratorCookieOptions.cs | 2 +- src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequest.cs | 2 +- src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequestOptions.cs | 2 +- test/Altinn.App.Api.Tests/Constants/XacmlRequestAttribute.cs | 2 +- test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs | 2 +- .../Controllers/EventsReceiverControllerTests.cs | 2 +- .../Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs | 2 +- test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs | 2 +- test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs | 2 +- test/Altinn.App.Api.Tests/Data/TestData.cs | 2 +- .../EFormidling/EformidlingStatusCheckEventHandlerTests.cs | 2 +- test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs | 2 +- test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs | 2 +- .../Mocks/Event/DummyFailureEventHandler.cs | 2 +- .../Mocks/Event/DummySuccessEventHandler.cs | 2 +- .../Mocks/Event/EventSecretCodeProviderStub.cs | 2 +- test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs | 2 +- test/Altinn.App.Api.Tests/Mocks/JwtTokenMock.cs | 2 +- .../Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs | 2 +- test/Altinn.App.Api.Tests/Models/InstanceFileScanResultTests.cs | 2 +- test/Altinn.App.Api.Tests/Models/XacmlResourceAttributes.cs | 2 +- test/Altinn.App.Api.Tests/Program.cs | 2 +- test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs | 2 +- test/Altinn.App.Core.Tests/DataLists/DataListsFactoryTest.cs | 2 +- .../DataLists/InstanceDataListsFactoryTest.cs | 2 +- .../Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs | 2 +- .../DataLists/NullInstanceDataListProviderTest.cs | 2 +- .../Extensions/DictionaryExtensionsTests.cs | 2 +- test/Altinn.App.Core.Tests/Extensions/ServiceCollectionTests.cs | 2 +- test/Altinn.App.Core.Tests/Extensions/StringExtensionTests.cs | 2 +- .../Notifications/Email/EmailNotificationClientTests.cs | 2 +- .../Features/Notifications/Sms/SmsNotificationClientTests.cs | 2 +- test/Altinn.App.Core.Tests/Helpers/EmbeddedResource.cs | 2 +- test/Altinn.App.Core.Tests/Helpers/SelfLinkHelperTests.cs | 2 +- test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs | 2 +- .../Infrastructure/Clients/EventsSubscriptionClientTests.cs | 2 +- test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs | 2 +- .../Internal/Events/EventHandlerResolverTests.cs | 2 +- .../Internal/Events/UnhandledEventHandlerTests.cs | 2 +- .../Internal/Maskinporten/MaskinportenExtensionsTests.cs | 2 +- .../Internal/Maskinporten/MaskinportenJwkTokenProviderTests.cs | 2 +- test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs | 2 +- .../Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs | 2 +- .../Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs | 2 +- test/Altinn.App.Core.Tests/Mocks/DelegatingHandlerStub.cs | 2 +- test/Altinn.App.Core.Tests/Mocks/RequestInterceptor.cs | 2 +- test/Altinn.App.Core.Tests/Models/AppIdentifierTests.cs | 2 +- 137 files changed, 137 insertions(+), 137 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ApplicationLanguageController.cs b/src/Altinn.App.Api/Controllers/ApplicationLanguageController.cs index 7d8082464..0899fa15a 100644 --- a/src/Altinn.App.Api/Controllers/ApplicationLanguageController.cs +++ b/src/Altinn.App.Api/Controllers/ApplicationLanguageController.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Core.Internal.Language; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Altinn.App.Api/Controllers/DataListsController.cs b/src/Altinn.App.Api/Controllers/DataListsController.cs index 9891ea14c..0c625a448 100644 --- a/src/Altinn.App.Api/Controllers/DataListsController.cs +++ b/src/Altinn.App.Api/Controllers/DataListsController.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.DataLists; +using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Altinn.App.Api/Controllers/EventsReceiverController.cs b/src/Altinn.App.Api/Controllers/EventsReceiverController.cs index fe00713d9..b5fc89b01 100644 --- a/src/Altinn.App.Api/Controllers/EventsReceiverController.cs +++ b/src/Altinn.App.Api/Controllers/EventsReceiverController.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using System.Text.Json; using System.Threading.Tasks; using Altinn.App.Core.Configuration; diff --git a/src/Altinn.App.Api/Controllers/FileScanController.cs b/src/Altinn.App.Api/Controllers/FileScanController.cs index dddc9988e..c93199ecb 100644 --- a/src/Altinn.App.Api/Controllers/FileScanController.cs +++ b/src/Altinn.App.Api/Controllers/FileScanController.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Api.Models; using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; diff --git a/src/Altinn.App.Api/Controllers/StatelessPagesController.cs b/src/Altinn.App.Api/Controllers/StatelessPagesController.cs index ba83c011b..92bf14137 100644 --- a/src/Altinn.App.Api/Controllers/StatelessPagesController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessPagesController.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Core.Features; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; diff --git a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs index 1b6e3ae50..d40657798 100644 --- a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using Altinn.App.Api.Controllers; +using Altinn.App.Api.Controllers; using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Infrastructure.Health; using Altinn.App.Api.Infrastructure.Telemetry; diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs index d297e1d84..c03ea00a8 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs @@ -1,4 +1,4 @@ -using Altinn.Platform.Storage.Interface.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Api.Helpers.RequestHandling { diff --git a/src/Altinn.App.Api/Infrastructure/Health/HealthCheck.cs b/src/Altinn.App.Api/Infrastructure/Health/HealthCheck.cs index 91c3a009a..a34a0d4c7 100644 --- a/src/Altinn.App.Api/Infrastructure/Health/HealthCheck.cs +++ b/src/Altinn.App.Api/Infrastructure/Health/HealthCheck.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Altinn.App.Api.Infrastructure.Health diff --git a/src/Altinn.App.Api/Infrastructure/Telemetry/HealthTelemetryFilter.cs b/src/Altinn.App.Api/Infrastructure/Telemetry/HealthTelemetryFilter.cs index 51a8494bd..aa63dc50a 100644 --- a/src/Altinn.App.Api/Infrastructure/Telemetry/HealthTelemetryFilter.cs +++ b/src/Altinn.App.Api/Infrastructure/Telemetry/HealthTelemetryFilter.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using System.Diagnostics.CodeAnalysis; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; diff --git a/src/Altinn.App.Api/Models/DataElementFileScanResult.cs b/src/Altinn.App.Api/Models/DataElementFileScanResult.cs index ba8ad0b63..78e9b539a 100644 --- a/src/Altinn.App.Api/Models/DataElementFileScanResult.cs +++ b/src/Altinn.App.Api/Models/DataElementFileScanResult.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using System.Text.Json.Serialization; using Altinn.Platform.Storage.Interface.Enums; diff --git a/src/Altinn.App.Api/Models/InstanceFileScanResult.cs b/src/Altinn.App.Api/Models/InstanceFileScanResult.cs index 1fcc8d7fb..00259137f 100644 --- a/src/Altinn.App.Api/Models/InstanceFileScanResult.cs +++ b/src/Altinn.App.Api/Models/InstanceFileScanResult.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using System.Text.Json.Serialization; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Enums; diff --git a/src/Altinn.App.Core/Configuration/CacheSettings.cs b/src/Altinn.App.Core/Configuration/CacheSettings.cs index da30c7a04..599d37d15 100644 --- a/src/Altinn.App.Core/Configuration/CacheSettings.cs +++ b/src/Altinn.App.Core/Configuration/CacheSettings.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Configuration +namespace Altinn.App.Core.Configuration { /// /// Represents caching settings used by the platform services diff --git a/src/Altinn.App.Core/EFormidling/EformidlingConstants.cs b/src/Altinn.App.Core/EFormidling/EformidlingConstants.cs index b614358fe..b04955249 100644 --- a/src/Altinn.App.Core/EFormidling/EformidlingConstants.cs +++ b/src/Altinn.App.Core/EFormidling/EformidlingConstants.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.EFormidling +namespace Altinn.App.Core.EFormidling { /// /// Shared constants within the Eformidling area. diff --git a/src/Altinn.App.Core/EFormidling/EformidlingStartup.cs b/src/Altinn.App.Core/EFormidling/EformidlingStartup.cs index 8349c3daa..d3533caba 100644 --- a/src/Altinn.App.Core/EFormidling/EformidlingStartup.cs +++ b/src/Altinn.App.Core/EFormidling/EformidlingStartup.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Infrastructure.Clients.Events; +using Altinn.App.Core.Infrastructure.Clients.Events; using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Models; using Microsoft.Extensions.Hosting; diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs index 94ffd1a17..7ed19ca88 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.EFormidling.Implementation +namespace Altinn.App.Core.EFormidling.Implementation { /// /// Exception thrown when Eformidling is unable to process the message delivered to diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs index cafab1bb5..00cc4b75c 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Headers; using System.Security.Cryptography.X509Certificates; using Altinn.ApiClients.Maskinporten.Config; diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs index c17bdadad..a8e88452d 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Headers; using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; diff --git a/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs b/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs index af0ae3145..8e5f6e90d 100644 --- a/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs +++ b/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Text; namespace Altinn.App.Core.Extensions diff --git a/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs index 4c4979723..f12393448 100644 --- a/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs +++ b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs @@ -1,4 +1,4 @@ -using System.Net.Http.Headers; +using System.Net.Http.Headers; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; diff --git a/src/Altinn.App.Core/Extensions/StringExtensions.cs b/src/Altinn.App.Core/Extensions/StringExtensions.cs index 326972529..eb96b240a 100644 --- a/src/Altinn.App.Core/Extensions/StringExtensions.cs +++ b/src/Altinn.App.Core/Extensions/StringExtensions.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Extensions +namespace Altinn.App.Core.Extensions { /// /// Extension methods for string diff --git a/src/Altinn.App.Core/Features/DataLists/DataListsFactory.cs b/src/Altinn.App.Core/Features/DataLists/DataListsFactory.cs index aec197bcb..e10f17a66 100644 --- a/src/Altinn.App.Core/Features/DataLists/DataListsFactory.cs +++ b/src/Altinn.App.Core/Features/DataLists/DataListsFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/Altinn.App.Core/Features/DataLists/DataListsService.cs b/src/Altinn.App.Core/Features/DataLists/DataListsService.cs index f65eeebee..aca27afa3 100644 --- a/src/Altinn.App.Core/Features/DataLists/DataListsService.cs +++ b/src/Altinn.App.Core/Features/DataLists/DataListsService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/Altinn.App.Core/Features/DataLists/IDataListsService.cs b/src/Altinn.App.Core/Features/DataLists/IDataListsService.cs index 289c47805..a9738443d 100644 --- a/src/Altinn.App.Core/Features/DataLists/IDataListsService.cs +++ b/src/Altinn.App.Core/Features/DataLists/IDataListsService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs b/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs index 73c05a8ed..5378df447 100644 --- a/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs +++ b/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs b/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs index c14b1f7cf..299bc8190 100644 --- a/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs +++ b/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs b/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs index cb698dfc1..57d48427c 100644 --- a/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs +++ b/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/Altinn.App.Core/Features/FeatureFlags.cs b/src/Altinn.App.Core/Features/FeatureFlags.cs index b1af0e786..402dbc450 100644 --- a/src/Altinn.App.Core/Features/FeatureFlags.cs +++ b/src/Altinn.App.Core/Features/FeatureFlags.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Features; +namespace Altinn.App.Core.Features; /// /// Class representing active feature flag constants. diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs index 6ed150674..6b84b8b13 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Features.FileAnalysis; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Features.FileAnalyzis diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs index 3f9eb34a5..16942a550 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.FileAnalyzis; +using Altinn.App.Core.Features.FileAnalyzis; namespace Altinn.App.Core.Features.FileAnalysis { diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs index f3960395d..2153e4e7d 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Features.FileAnalysis; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Features.FileAnalyzis diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs index e430da273..4b88a2abf 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Features.FileAnalysis +namespace Altinn.App.Core.Features.FileAnalysis { /// /// Interface for doing extended binary file analysing. diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs index 45fef58ae..e67430de6 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Features.FileAnalysis; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Features.FileAnalyzis diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs index bb6769693..2dabbc806 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Features.FileAnalysis; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Features.FileAnalyzis diff --git a/src/Altinn.App.Core/Features/IDataListProvider.cs b/src/Altinn.App.Core/Features/IDataListProvider.cs index f995da084..32f9626ac 100644 --- a/src/Altinn.App.Core/Features/IDataListProvider.cs +++ b/src/Altinn.App.Core/Features/IDataListProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/Altinn.App.Core/Features/IEventHandler.cs b/src/Altinn.App.Core/Features/IEventHandler.cs index beba4d19c..d5b565c94 100644 --- a/src/Altinn.App.Core/Features/IEventHandler.cs +++ b/src/Altinn.App.Core/Features/IEventHandler.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Models; +using Altinn.App.Core.Models; namespace Altinn.App.Core.Features { diff --git a/src/Altinn.App.Core/Features/IInstanceDataListProvider.cs b/src/Altinn.App.Core/Features/IInstanceDataListProvider.cs index 709d1d3c2..79d6600c7 100644 --- a/src/Altinn.App.Core/Features/IInstanceDataListProvider.cs +++ b/src/Altinn.App.Core/Features/IInstanceDataListProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/Altinn.App.Core/Features/IPdfFormatter.cs b/src/Altinn.App.Core/Features/IPdfFormatter.cs index d57650797..5302082d3 100644 --- a/src/Altinn.App.Core/Features/IPdfFormatter.cs +++ b/src/Altinn.App.Core/Features/IPdfFormatter.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Models; +using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Features; diff --git a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs index 4d7e158b9..293a2529c 100644 --- a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs +++ b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; namespace Altinn.App.Core.Features.PageOrder diff --git a/src/Altinn.App.Core/Features/Pdf/NullPdfFormatter.cs b/src/Altinn.App.Core/Features/Pdf/NullPdfFormatter.cs index 5ce427ec8..8eba2ab8e 100644 --- a/src/Altinn.App.Core/Features/Pdf/NullPdfFormatter.cs +++ b/src/Altinn.App.Core/Features/Pdf/NullPdfFormatter.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Models; +using Altinn.App.Core.Models; namespace Altinn.App.Core.Features.Pdf { diff --git a/src/Altinn.App.Core/Features/Validation/IFileValidator.cs b/src/Altinn.App.Core/Features/Validation/IFileValidator.cs index ef65f2f19..14576443a 100644 --- a/src/Altinn.App.Core/Features/Validation/IFileValidator.cs +++ b/src/Altinn.App.Core/Features/Validation/IFileValidator.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Features.FileAnalysis; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; diff --git a/src/Altinn.App.Core/Implementation/UserTokenProvider.cs b/src/Altinn.App.Core/Implementation/UserTokenProvider.cs index 0c9c9dadd..b0f1c9061 100644 --- a/src/Altinn.App.Core/Implementation/UserTokenProvider.cs +++ b/src/Altinn.App.Core/Implementation/UserTokenProvider.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Altinn.App.Core.Configuration; using Altinn.App.Core.Internal.Auth; using AltinnCore.Authentication.Utils; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Events/Subscription.cs b/src/Altinn.App.Core/Infrastructure/Clients/Events/Subscription.cs index 55cfe4b6b..39bac6dc3 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Events/Subscription.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Events/Subscription.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Infrastructure.Clients.Events +namespace Altinn.App.Core.Infrastructure.Clients.Events { /// /// Class that describes an events subscription diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Events/SubscriptionRequest.cs b/src/Altinn.App.Core/Infrastructure/Clients/Events/SubscriptionRequest.cs index a9623aa5e..158a770a2 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Events/SubscriptionRequest.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Events/SubscriptionRequest.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Infrastructure.Clients.Events +namespace Altinn.App.Core.Infrastructure.Clients.Events { /// /// Class that describes the events subscription request model diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Maskinporten/IX509CertificateProvider.cs b/src/Altinn.App.Core/Infrastructure/Clients/Maskinporten/IX509CertificateProvider.cs index 1d83e2115..6da21baca 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Maskinporten/IX509CertificateProvider.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Maskinporten/IX509CertificateProvider.cs @@ -1,4 +1,4 @@ -using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.X509Certificates; namespace Altinn.App.Core.Infrastructure.Clients.Maskinporten { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs index fe85e9aec..c5e873ffe 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using System.Text.Json; using Altinn.App.Core.Configuration; using Altinn.App.Core.Internal.Auth; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs index 242dc33ab..6661d496e 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Configuration; +using Altinn.App.Core.Configuration; using Altinn.App.Core.Internal.Profile; using Altinn.Platform.Profile.Models; using Microsoft.Extensions.Caching.Memory; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs index 505711011..7b375ecef 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text; diff --git a/src/Altinn.App.Core/Interface/IPersonLookup.cs b/src/Altinn.App.Core/Interface/IPersonLookup.cs index bc69357ea..e9676b41b 100644 --- a/src/Altinn.App.Core/Interface/IPersonLookup.cs +++ b/src/Altinn.App.Core/Interface/IPersonLookup.cs @@ -1,4 +1,4 @@ -using Altinn.Platform.Register.Models; +using Altinn.Platform.Register.Models; namespace Altinn.App.Core.Interface { diff --git a/src/Altinn.App.Core/Interface/IPersonRetriever.cs b/src/Altinn.App.Core/Interface/IPersonRetriever.cs index 84e7023f8..2dfc33963 100644 --- a/src/Altinn.App.Core/Interface/IPersonRetriever.cs +++ b/src/Altinn.App.Core/Interface/IPersonRetriever.cs @@ -1,4 +1,4 @@ -using Altinn.Platform.Register.Models; +using Altinn.Platform.Register.Models; namespace Altinn.App.Core.Interface { diff --git a/src/Altinn.App.Core/Interface/IUserTokenProvider.cs b/src/Altinn.App.Core/Interface/IUserTokenProvider.cs index 6297c5118..c11c3f9c6 100644 --- a/src/Altinn.App.Core/Interface/IUserTokenProvider.cs +++ b/src/Altinn.App.Core/Interface/IUserTokenProvider.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface { /// /// Defines the methods required for an implementation of a user JSON Web Token provider. diff --git a/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs b/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs index d6d1d6833..d444ef041 100644 --- a/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs +++ b/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Internal.Auth +namespace Altinn.App.Core.Internal.Auth { /// /// Defines the methods required for an implementation of a user JSON Web Token provider. diff --git a/src/Altinn.App.Core/Internal/Data/DataService.cs b/src/Altinn.App.Core/Internal/Data/DataService.cs index 6101b1db4..645379b48 100644 --- a/src/Altinn.App.Core/Internal/Data/DataService.cs +++ b/src/Altinn.App.Core/Internal/Data/DataService.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; diff --git a/src/Altinn.App.Core/Internal/Data/IDataService.cs b/src/Altinn.App.Core/Internal/Data/IDataService.cs index 5f36908fd..39924ac18 100644 --- a/src/Altinn.App.Core/Internal/Data/IDataService.cs +++ b/src/Altinn.App.Core/Internal/Data/IDataService.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Models; +using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Data diff --git a/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs b/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs index 8c97f98b9..02959f232 100644 --- a/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs +++ b/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features; +using Altinn.App.Core.Features; namespace Altinn.App.Core.Internal.Events { diff --git a/src/Altinn.App.Core/Internal/Events/IEventHandlerResolver.cs b/src/Altinn.App.Core/Internal/Events/IEventHandlerResolver.cs index bbb5d2056..851a1cbde 100644 --- a/src/Altinn.App.Core/Internal/Events/IEventHandlerResolver.cs +++ b/src/Altinn.App.Core/Internal/Events/IEventHandlerResolver.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features; +using Altinn.App.Core.Features; namespace Altinn.App.Core.Internal.Events { diff --git a/src/Altinn.App.Core/Internal/Events/IEventSecretCodeProvider.cs b/src/Altinn.App.Core/Internal/Events/IEventSecretCodeProvider.cs index 6490532d4..a115ab14b 100644 --- a/src/Altinn.App.Core/Internal/Events/IEventSecretCodeProvider.cs +++ b/src/Altinn.App.Core/Internal/Events/IEventSecretCodeProvider.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Internal.Events +namespace Altinn.App.Core.Internal.Events { /// /// Interface for providing a secret code to be used when diff --git a/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs b/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs index 1c639c691..2f801d85a 100644 --- a/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs +++ b/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Internal.Secrets; +using Altinn.App.Core.Internal.Secrets; namespace Altinn.App.Core.Internal.Events { diff --git a/src/Altinn.App.Core/Internal/Events/SubscriptionValidationHandler.cs b/src/Altinn.App.Core/Internal/Events/SubscriptionValidationHandler.cs index 1ab98367d..a4c183d1c 100644 --- a/src/Altinn.App.Core/Internal/Events/SubscriptionValidationHandler.cs +++ b/src/Altinn.App.Core/Internal/Events/SubscriptionValidationHandler.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features; +using Altinn.App.Core.Features; using Altinn.App.Core.Models; namespace Altinn.App.Core.Internal.Events diff --git a/src/Altinn.App.Core/Internal/Events/UnhandledEventHandler.cs b/src/Altinn.App.Core/Internal/Events/UnhandledEventHandler.cs index fc50ebd9a..e0a1cc272 100644 --- a/src/Altinn.App.Core/Internal/Events/UnhandledEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Events/UnhandledEventHandler.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using Altinn.App.Core.Features; using Altinn.App.Core.Models; diff --git a/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs b/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs index e20a4112c..a2cc6128f 100644 --- a/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs +++ b/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using Altinn.App.Core.Configuration; using Altinn.App.Core.Implementation; using Microsoft.Extensions.Logging; diff --git a/src/Altinn.App.Core/Internal/Language/IApplicationLanguage.cs b/src/Altinn.App.Core/Internal/Language/IApplicationLanguage.cs index 02a90f3cc..512183829 100644 --- a/src/Altinn.App.Core/Internal/Language/IApplicationLanguage.cs +++ b/src/Altinn.App.Core/Internal/Language/IApplicationLanguage.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Internal.Language +namespace Altinn.App.Core.Internal.Language { /// /// Interface for retrieving languages supported by the application. diff --git a/src/Altinn.App.Core/Internal/Maskinporten/IMaskinportenTokenProvider.cs b/src/Altinn.App.Core/Internal/Maskinporten/IMaskinportenTokenProvider.cs index 9c7bb3050..7dfaf18f1 100644 --- a/src/Altinn.App.Core/Internal/Maskinporten/IMaskinportenTokenProvider.cs +++ b/src/Altinn.App.Core/Internal/Maskinporten/IMaskinportenTokenProvider.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Internal.Maskinporten; +namespace Altinn.App.Core.Internal.Maskinporten; /// /// Defines the interface required for an implementation of a Maskinporte token provider. diff --git a/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenExtensions.cs b/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenExtensions.cs index 4dfaece9f..4c7e7c6dc 100644 --- a/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenExtensions.cs +++ b/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenExtensions.cs @@ -1,4 +1,4 @@ -using Altinn.ApiClients.Maskinporten.Config; +using Altinn.ApiClients.Maskinporten.Config; using Altinn.ApiClients.Maskinporten.Interfaces; using Altinn.App.Core.Internal.Secrets; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenJwkTokenProvider.cs b/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenJwkTokenProvider.cs index 2062acce2..6eeb7653b 100644 --- a/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenJwkTokenProvider.cs +++ b/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenJwkTokenProvider.cs @@ -1,4 +1,4 @@ -using Altinn.ApiClients.Maskinporten.Config; +using Altinn.ApiClients.Maskinporten.Config; using Altinn.ApiClients.Maskinporten.Interfaces; using Altinn.ApiClients.Maskinporten.Models; using Altinn.App.Core.Internal.Secrets; diff --git a/src/Altinn.App.Core/Internal/Pdf/IPdfGeneratorClient.cs b/src/Altinn.App.Core/Internal/Pdf/IPdfGeneratorClient.cs index f7e8ec3cf..5ebbcf8a8 100644 --- a/src/Altinn.App.Core/Internal/Pdf/IPdfGeneratorClient.cs +++ b/src/Altinn.App.Core/Internal/Pdf/IPdfGeneratorClient.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Internal.Pdf; +namespace Altinn.App.Core.Internal.Pdf; /// /// Defines the required operations on a client of the PDF generator service. diff --git a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs index cbca89190..c50ac756a 100644 --- a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs @@ -1,4 +1,4 @@ -using Altinn.Platform.Storage.Interface.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Pdf { diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs b/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs index 470600c4c..357ffa766 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Altinn.App.Core.Internal.Pdf { diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfGeneratorSettings.cs b/src/Altinn.App.Core/Internal/Pdf/PdfGeneratorSettings.cs index 6683b1d65..5da9f96ae 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfGeneratorSettings.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfGeneratorSettings.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Internal.Pdf; +namespace Altinn.App.Core.Internal.Pdf; /// /// Represents a set of settings and options used by the PDF generator client. diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index f832b6ca4..cb3cd129b 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -1,4 +1,4 @@ -using System.Security.Claims; +using System.Security.Claims; using System.Xml.Serialization; using Altinn.App.Core.Configuration; using Altinn.App.Core.Extensions; diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/EndEventEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/EndEventEventHandler.cs index 17c956670..ea4951aa2 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/EndEventEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/EndEventEventHandler.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs index 979386e54..9b872f7a6 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs @@ -1,4 +1,4 @@ -using Altinn.Platform.Storage.Interface.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.EventHandlers { diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandler.cs index b58bc3c0e..dc70e109a 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandler.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features; +using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs index 35e8b806e..946abc2bd 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features; +using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.App.Core.Internal.Process.ServiceTasks; using Altinn.Platform.Storage.Interface.Models; diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IAbandonTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IAbandonTaskEventHandler.cs index 526520209..0ff3ff7e1 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IAbandonTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IAbandonTaskEventHandler.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IEndTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IEndTaskEventHandler.cs index f2d949df5..9b3efb66f 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IEndTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IEndTaskEventHandler.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IStartTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IStartTaskEventHandler.cs index 32aef1672..d5caebc1d 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IStartTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IStartTaskEventHandler.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs index 4c3cad817..d14440af5 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features; +using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; diff --git a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventHandlerDelegator.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventHandlerDelegator.cs index 0549ec371..0616717c8 100644 --- a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventHandlerDelegator.cs +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventHandlerDelegator.cs @@ -1,4 +1,4 @@ -using Altinn.Platform.Storage.Interface.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process { diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs b/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs index 0dc329310..497041a0b 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Internal.Process.EventHandlers; +using Altinn.App.Core.Internal.Process.EventHandlers; using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Enums; diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs index 99164775b..7968395fd 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs @@ -1,4 +1,4 @@ -using Altinn.Platform.Storage.Interface.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.ProcessTasks { diff --git a/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs b/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs index 6c252ca06..d5a912b84 100644 --- a/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs +++ b/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs @@ -1,4 +1,4 @@ -using Altinn.Platform.Register.Models; +using Altinn.Platform.Register.Models; namespace Altinn.App.Core.Internal.Registers { diff --git a/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs b/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs index f0852a54f..030b65136 100644 --- a/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs +++ b/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Features.FileAnalysis; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; diff --git a/src/Altinn.App.Core/Internal/Validation/FileValidatorFactory.cs b/src/Altinn.App.Core/Internal/Validation/FileValidatorFactory.cs index d31253524..0ff71a295 100644 --- a/src/Altinn.App.Core/Internal/Validation/FileValidatorFactory.cs +++ b/src/Altinn.App.Core/Internal/Validation/FileValidatorFactory.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Features.Validation; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Validation diff --git a/src/Altinn.App.Core/Internal/Validation/IFileValidationService.cs b/src/Altinn.App.Core/Internal/Validation/IFileValidationService.cs index fade531e2..b3bc9ac70 100644 --- a/src/Altinn.App.Core/Internal/Validation/IFileValidationService.cs +++ b/src/Altinn.App.Core/Internal/Validation/IFileValidationService.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Features.FileAnalysis; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; diff --git a/src/Altinn.App.Core/Internal/Validation/IFileValidatorFactory.cs b/src/Altinn.App.Core/Internal/Validation/IFileValidatorFactory.cs index 2cddf268f..a77d2fcf9 100644 --- a/src/Altinn.App.Core/Internal/Validation/IFileValidatorFactory.cs +++ b/src/Altinn.App.Core/Internal/Validation/IFileValidatorFactory.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Features.Validation; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Validation diff --git a/src/Altinn.App.Core/Models/ApplicationLanguage.cs b/src/Altinn.App.Core/Models/ApplicationLanguage.cs index 0582e3e87..2e9635e50 100644 --- a/src/Altinn.App.Core/Models/ApplicationLanguage.cs +++ b/src/Altinn.App.Core/Models/ApplicationLanguage.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Altinn.App.Core.Models { diff --git a/src/Altinn.App.Core/Models/DataList.cs b/src/Altinn.App.Core/Models/DataList.cs index 277a8d6f2..ddf895ebb 100644 --- a/src/Altinn.App.Core/Models/DataList.cs +++ b/src/Altinn.App.Core/Models/DataList.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/Altinn.App.Core/Models/DataListMetadata.cs b/src/Altinn.App.Core/Models/DataListMetadata.cs index d90934177..8be37ed11 100644 --- a/src/Altinn.App.Core/Models/DataListMetadata.cs +++ b/src/Altinn.App.Core/Models/DataListMetadata.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotification.cs b/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotification.cs index 870242f5d..d011e92d1 100644 --- a/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotification.cs +++ b/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotification.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using Altinn.App.Core.Features; namespace Altinn.App.Core.Models.Notifications.Sms; diff --git a/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotificationClientException.cs b/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotificationClientException.cs index 4dc66c4c7..ff37ea28f 100644 --- a/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotificationClientException.cs +++ b/src/Altinn.App.Core/Models/Notifications/Sms/SmsNotificationClientException.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Models.Notifications.Sms; +namespace Altinn.App.Core.Models.Notifications.Sms; /// /// Class representing an exception thrown when a SMS notificcation order could not be created diff --git a/src/Altinn.App.Core/Models/Pdf/PdfGeneratorCookieOptions.cs b/src/Altinn.App.Core/Models/Pdf/PdfGeneratorCookieOptions.cs index baafbd320..5b886c91f 100644 --- a/src/Altinn.App.Core/Models/Pdf/PdfGeneratorCookieOptions.cs +++ b/src/Altinn.App.Core/Models/Pdf/PdfGeneratorCookieOptions.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Models.Pdf; +namespace Altinn.App.Core.Models.Pdf; /// /// This class is created to match the PDF generator cookie options. diff --git a/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequest.cs b/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequest.cs index 0fbcae379..433870d36 100644 --- a/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequest.cs +++ b/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequest.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Models.Pdf; +namespace Altinn.App.Core.Models.Pdf; /// /// This class is created to match the input required to generate a PDF by the PDF generator service. diff --git a/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequestOptions.cs b/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequestOptions.cs index 895a6ceef..184078fe7 100644 --- a/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequestOptions.cs +++ b/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequestOptions.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Models.Pdf; +namespace Altinn.App.Core.Models.Pdf; /// /// This class is created to match the PDF generator options used by the PDF generator. diff --git a/test/Altinn.App.Api.Tests/Constants/XacmlRequestAttribute.cs b/test/Altinn.App.Api.Tests/Constants/XacmlRequestAttribute.cs index 73f388f04..b9c915e79 100644 --- a/test/Altinn.App.Api.Tests/Constants/XacmlRequestAttribute.cs +++ b/test/Altinn.App.Api.Tests/Constants/XacmlRequestAttribute.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Api.Tests.Constants +namespace Altinn.App.Api.Tests.Constants { public static class XacmlRequestAttribute { diff --git a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs index 8ced72693..d0e773da3 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Headers; using Altinn.App.Api.Tests.Data; using Altinn.App.Api.Tests.Utils; diff --git a/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs index 57d315bfe..fa9fdc5cf 100644 --- a/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; diff --git a/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs index 4e9dcaa24..38b4579e5 100644 --- a/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs @@ -1,4 +1,4 @@ -using Altinn.App.Api.Controllers; +using Altinn.App.Api.Controllers; using Altinn.App.Core.Internal.Instances; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; diff --git a/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs index 99df10af5..edd68490a 100644 --- a/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using Altinn.App.Core.Features; using Altinn.App.Core.Models; using FluentAssertions; diff --git a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs index 3c08d4124..2bf7b0bf9 100644 --- a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs +++ b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs @@ -1,4 +1,4 @@ -using System.Net.Http.Headers; +using System.Net.Http.Headers; using Altinn.App.Api.Tests.Data; using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Configuration; diff --git a/test/Altinn.App.Api.Tests/Data/TestData.cs b/test/Altinn.App.Api.Tests/Data/TestData.cs index ed6c2a591..bbe083584 100644 --- a/test/Altinn.App.Api.Tests/Data/TestData.cs +++ b/test/Altinn.App.Api.Tests/Data/TestData.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using Altinn.App.Api.Tests.Mocks; diff --git a/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs b/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs index 21d6f7d59..020c78957 100644 --- a/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs +++ b/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs @@ -1,4 +1,4 @@ -using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.X509Certificates; using Altinn.ApiClients.Maskinporten.Config; using Altinn.ApiClients.Maskinporten.Interfaces; using Altinn.ApiClients.Maskinporten.Services; diff --git a/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs b/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs index d38633438..963ad2205 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using System.Text.Json; using Altinn.App.Api.Tests.Data; using Altinn.App.Api.Tests.Mocks; diff --git a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs index f6d9d633d..c1aa8f9b6 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs @@ -1,4 +1,4 @@ -using System.Security.Claims; +using System.Security.Claims; using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; using Altinn.Platform.Register.Models; diff --git a/test/Altinn.App.Api.Tests/Mocks/Event/DummyFailureEventHandler.cs b/test/Altinn.App.Api.Tests/Mocks/Event/DummyFailureEventHandler.cs index 02cf6bb86..f87147dd2 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Event/DummyFailureEventHandler.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Event/DummyFailureEventHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Altinn.App.Core.Features; using Altinn.App.Core.Models; diff --git a/test/Altinn.App.Api.Tests/Mocks/Event/DummySuccessEventHandler.cs b/test/Altinn.App.Api.Tests/Mocks/Event/DummySuccessEventHandler.cs index acf86eb3e..f27350997 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Event/DummySuccessEventHandler.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Event/DummySuccessEventHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Altinn.App.Core.Features; using Altinn.App.Core.Models; diff --git a/test/Altinn.App.Api.Tests/Mocks/Event/EventSecretCodeProviderStub.cs b/test/Altinn.App.Api.Tests/Mocks/Event/EventSecretCodeProviderStub.cs index 875a45faf..b9dbfd2ba 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Event/EventSecretCodeProviderStub.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Event/EventSecretCodeProviderStub.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Altinn.App.Core.Internal.Events; namespace Altinn.App.Api.Tests.Mocks.Event diff --git a/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs index 696fcdc8b..452c52d13 100644 --- a/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs +++ b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs @@ -1,4 +1,4 @@ -using Altinn.App.Api.Tests.Data; +using Altinn.App.Api.Tests.Data; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Instances; diff --git a/test/Altinn.App.Api.Tests/Mocks/JwtTokenMock.cs b/test/Altinn.App.Api.Tests/Mocks/JwtTokenMock.cs index 8339563ef..93004cd43 100644 --- a/test/Altinn.App.Api.Tests/Mocks/JwtTokenMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/JwtTokenMock.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Security.Claims; diff --git a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs index 7ed51602a..57480e308 100644 --- a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs +++ b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs @@ -1,4 +1,4 @@ -using System.Security.Claims; +using System.Security.Claims; using System.Xml; using Altinn.App.Api.Tests.Constants; using Altinn.App.Api.Tests.Data; diff --git a/test/Altinn.App.Api.Tests/Models/InstanceFileScanResultTests.cs b/test/Altinn.App.Api.Tests/Models/InstanceFileScanResultTests.cs index 1b6fed77c..fb2050370 100644 --- a/test/Altinn.App.Api.Tests/Models/InstanceFileScanResultTests.cs +++ b/test/Altinn.App.Api.Tests/Models/InstanceFileScanResultTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using Altinn.App.Api.Models; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Enums; diff --git a/test/Altinn.App.Api.Tests/Models/XacmlResourceAttributes.cs b/test/Altinn.App.Api.Tests/Models/XacmlResourceAttributes.cs index 143242b98..8054255ab 100644 --- a/test/Altinn.App.Api.Tests/Models/XacmlResourceAttributes.cs +++ b/test/Altinn.App.Api.Tests/Models/XacmlResourceAttributes.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Api.Tests.Models +namespace Altinn.App.Api.Tests.Models { public class XacmlResourceAttributes { diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs index 0e08944b7..9374508cf 100644 --- a/test/Altinn.App.Api.Tests/Program.cs +++ b/test/Altinn.App.Api.Tests/Program.cs @@ -1,4 +1,4 @@ -using Altinn.App.Api.Extensions; +using Altinn.App.Api.Extensions; using Altinn.App.Api.Helpers; using Altinn.App.Api.Tests.Data; using Altinn.App.Api.Tests.Mocks; diff --git a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs index cef035fbe..9ecbec6f2 100644 --- a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs +++ b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs @@ -1,4 +1,4 @@ -using System.Security.Claims; +using System.Security.Claims; using Altinn.App.Api.Tests.Mocks; using AltinnCore.Authentication.Constants; diff --git a/test/Altinn.App.Core.Tests/DataLists/DataListsFactoryTest.cs b/test/Altinn.App.Core.Tests/DataLists/DataListsFactoryTest.cs index 701aafe60..136478c21 100644 --- a/test/Altinn.App.Core.Tests/DataLists/DataListsFactoryTest.cs +++ b/test/Altinn.App.Core.Tests/DataLists/DataListsFactoryTest.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Core.Features; using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Models; diff --git a/test/Altinn.App.Core.Tests/DataLists/InstanceDataListsFactoryTest.cs b/test/Altinn.App.Core.Tests/DataLists/InstanceDataListsFactoryTest.cs index 3cb5c3ac3..d7b27c742 100644 --- a/test/Altinn.App.Core.Tests/DataLists/InstanceDataListsFactoryTest.cs +++ b/test/Altinn.App.Core.Tests/DataLists/InstanceDataListsFactoryTest.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Core.Features; using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Models; diff --git a/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs b/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs index 150f83131..0cda3defc 100644 --- a/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs +++ b/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Core.Features.DataLists; using FluentAssertions; using Xunit; diff --git a/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs b/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs index 2a68b8138..10679fff4 100644 --- a/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs +++ b/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Models; using FluentAssertions; diff --git a/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs b/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs index 1cc1cb89c..3565bcc37 100644 --- a/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs +++ b/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Extensions; +using Altinn.App.Core.Extensions; using Altinn.App.Core.Models; using Microsoft.AspNetCore.Http; using Xunit; diff --git a/test/Altinn.App.Core.Tests/Extensions/ServiceCollectionTests.cs b/test/Altinn.App.Core.Tests/Extensions/ServiceCollectionTests.cs index 4950f7a99..bad7ed84e 100644 --- a/test/Altinn.App.Core.Tests/Extensions/ServiceCollectionTests.cs +++ b/test/Altinn.App.Core.Tests/Extensions/ServiceCollectionTests.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Core.Extensions; using Altinn.App.Core.Infrastructure.Clients.Events; using Altinn.App.Core.Internal.Events; diff --git a/test/Altinn.App.Core.Tests/Extensions/StringExtensionTests.cs b/test/Altinn.App.Core.Tests/Extensions/StringExtensionTests.cs index a43041e30..3954cf77a 100644 --- a/test/Altinn.App.Core.Tests/Extensions/StringExtensionTests.cs +++ b/test/Altinn.App.Core.Tests/Extensions/StringExtensionTests.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Core.Extensions; using Xunit; diff --git a/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs b/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs index 05152f774..d25257ccc 100644 --- a/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Tests.Features.Notifications.Email; +namespace Altinn.App.Core.Tests.Features.Notifications.Email; using System.Net; using System.Net.Http; diff --git a/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs index 37c22936e..465f9c901 100644 --- a/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Tests.Features.Notifications.Sms; +namespace Altinn.App.Core.Tests.Features.Notifications.Sms; using System.Net; using System.Net.Http; diff --git a/test/Altinn.App.Core.Tests/Helpers/EmbeddedResource.cs b/test/Altinn.App.Core.Tests/Helpers/EmbeddedResource.cs index 57f5950f0..f9a7adcad 100644 --- a/test/Altinn.App.Core.Tests/Helpers/EmbeddedResource.cs +++ b/test/Altinn.App.Core.Tests/Helpers/EmbeddedResource.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; namespace Altinn.App.PlatformServices.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Helpers/SelfLinkHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/SelfLinkHelperTests.cs index 056389fd6..01dd4a6e5 100644 --- a/test/Altinn.App.Core.Tests/Helpers/SelfLinkHelperTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/SelfLinkHelperTests.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Helpers; +using Altinn.App.Core.Helpers; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; using Moq; diff --git a/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs b/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs index aff7e3b9e..f463a683e 100644 --- a/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Text; using System.Text.Json; using Altinn.App.Core.Configuration; diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs index b0f694c37..be95297be 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using System.Net; using System.Text.Json; using Altinn.App.Core.Configuration; diff --git a/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs index 385e4c8a1..d32a44adc 100644 --- a/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Models; diff --git a/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs b/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs index 92b6736c8..670b063d8 100644 --- a/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Models; diff --git a/test/Altinn.App.Core.Tests/Internal/Events/UnhandledEventHandlerTests.cs b/test/Altinn.App.Core.Tests/Internal/Events/UnhandledEventHandlerTests.cs index 5af020b0b..c2d011cd7 100644 --- a/test/Altinn.App.Core.Tests/Internal/Events/UnhandledEventHandlerTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Events/UnhandledEventHandlerTests.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Core.Internal.Events; using FluentAssertions; using Xunit; diff --git a/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenExtensionsTests.cs b/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenExtensionsTests.cs index 6bbc479fe..9e8c7d8dd 100644 --- a/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenExtensionsTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenExtensionsTests.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Extensions; +using Altinn.App.Core.Extensions; using Altinn.App.Core.Internal.Maskinporten; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; diff --git a/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenJwkTokenProviderTests.cs b/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenJwkTokenProviderTests.cs index e4bdef6b9..1da1bf1b8 100644 --- a/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenJwkTokenProviderTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenJwkTokenProviderTests.cs @@ -1,4 +1,4 @@ -using Altinn.ApiClients.Maskinporten.Config; +using Altinn.ApiClients.Maskinporten.Config; using Altinn.ApiClients.Maskinporten.Interfaces; using Altinn.ApiClients.Maskinporten.Models; using Altinn.App.Core.Internal.Maskinporten; diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index 43a5eb879..0b0c2cad1 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using Altinn.App.Core.Configuration; using Altinn.App.Core.Infrastructure.Clients.Pdf; using Altinn.App.Core.Internal.App; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs index 2d82e7a03..2d44a63fe 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.App.Core.Models; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs index 43a963dc1..835f30d3b 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Configuration; +using Altinn.App.Core.Configuration; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Internal.Data; diff --git a/test/Altinn.App.Core.Tests/Mocks/DelegatingHandlerStub.cs b/test/Altinn.App.Core.Tests/Mocks/DelegatingHandlerStub.cs index 71a28820e..988f53a13 100644 --- a/test/Altinn.App.Core.Tests/Mocks/DelegatingHandlerStub.cs +++ b/test/Altinn.App.Core.Tests/Mocks/DelegatingHandlerStub.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using System.Net; namespace Altinn.App.PlatformServices.Tests.Mocks diff --git a/test/Altinn.App.Core.Tests/Mocks/RequestInterceptor.cs b/test/Altinn.App.Core.Tests/Mocks/RequestInterceptor.cs index a9ac07f68..13b50429b 100644 --- a/test/Altinn.App.Core.Tests/Mocks/RequestInterceptor.cs +++ b/test/Altinn.App.Core.Tests/Mocks/RequestInterceptor.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; namespace Altinn.App.PlatformServices.Tests.Mocks { diff --git a/test/Altinn.App.Core.Tests/Models/AppIdentifierTests.cs b/test/Altinn.App.Core.Tests/Models/AppIdentifierTests.cs index 08b05e515..30847785b 100644 --- a/test/Altinn.App.Core.Tests/Models/AppIdentifierTests.cs +++ b/test/Altinn.App.Core.Tests/Models/AppIdentifierTests.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using Altinn.App.Core.Models; using FluentAssertions; using Xunit; From bc1a217747430990c2fa4fb9ac8d3a73a0998f8f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 May 2024 09:32:13 +0200 Subject: [PATCH 03/22] chore(deps): update nuget non-major dependencies (#623) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- Directory.Build.props | 2 +- test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj | 4 ++-- test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index fedf89ead..8743ce6d5 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "csharpier": { - "version": "0.28.1", + "version": "0.28.2", "commands": [ "dotnet-csharpier" ] diff --git a/Directory.Build.props b/Directory.Build.props index e6b0172e6..044a5d2d2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index 8f5066e8e..e87183436 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -20,8 +20,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index d755ac39a..b7740b4a2 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -46,8 +46,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From d22ce21f27fcf66c3bda28534d067b933aa39213 Mon Sep 17 00:00:00 2001 From: Johannes Haukland <42615991+HauklandJ@users.noreply.github.com> Date: Thu, 2 May 2024 13:00:38 +0200 Subject: [PATCH 04/22] Signing optional data types (#622) * feat: allow signing of optional models * test: signing optional models * feat: add collections extension instead of using microsoft.identitymodel.tokens * refactor: use string.IsNullOrEmpty for strings, rm unused deps * merge: resolve merge conflict * refactor: rm extension, explicitly handle List null or empty check where needed * refactor: rm null forgiving operator where not needed + fix typo in test --- .../Features/Action/SigningUserAction.cs | 65 +++++++---- .../Internal/Pdf/PdfService.cs | 9 +- .../Features/Action/SigningUserActionTests.cs | 101 +++++++++++++++++- 3 files changed, 148 insertions(+), 27 deletions(-) diff --git a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs index 3971d204e..76817b8d9 100644 --- a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs @@ -18,6 +18,7 @@ namespace Altinn.App.Core.Features.Action; public class SigningUserAction : IUserAction { private readonly IProcessReader _processReader; + private readonly IAppMetadata _appMetadata; private readonly ILogger _logger; private readonly IProfileClient _profileClient; private readonly ISignClient _signClient; @@ -29,25 +30,28 @@ public class SigningUserAction : IUserAction /// The logger /// The profile client /// The sign client + /// The application metadata public SigningUserAction( IProcessReader processReader, ILogger logger, IProfileClient profileClient, - ISignClient signClient + ISignClient signClient, + IAppMetadata appMetadata ) { _logger = logger; _profileClient = profileClient; _signClient = signClient; _processReader = processReader; + _appMetadata = appMetadata; } /// public string Id => "sign"; /// - /// - /// + /// + /// public async Task HandleAction(UserActionContext context) { if (context.UserId == null) @@ -64,20 +68,23 @@ public async Task HandleAction(UserActionContext context) context.Instance.Id, currentTask.Id ); - var dataTypes = - currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.DataTypesToSign ?? new(); - var connectedDataElements = GetDataElementSignatures(context.Instance.Data, dataTypes); - if ( - connectedDataElements.Count > 0 - && currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType != null - ) + var appMetadata = await _appMetadata.GetApplicationMetadata(); + var dataTypeIds = + currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.DataTypesToSign ?? []; + var dataTypesToSign = appMetadata + ?.DataTypes?.Where(d => dataTypeIds.Contains(d.Id, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + if (ShouldSign(currentTask, context.Instance.Data, dataTypesToSign)) { - SignatureContext signatureContext = new SignatureContext( - new InstanceIdentifier(context.Instance), - currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType!, - await GetSignee(context.UserId.Value), - connectedDataElements - ); + var dataElementSignatures = GetDataElementSignatures(context.Instance.Data, dataTypesToSign); + SignatureContext signatureContext = + new( + new InstanceIdentifier(context.Instance), + currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType!, + await GetSignee(context.UserId.Value), + dataElementSignatures + ); await _signClient.SignDataElements(signatureContext); return UserActionResult.SuccessResult(); } @@ -92,17 +99,39 @@ await GetSignee(context.UserId.Value), ); } + private static bool ShouldSign( + ProcessTask currentTask, + List dataElements, + List? dataTypesToSign + ) + { + var signatureIsConfigured = + currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType is not null; + if (dataTypesToSign is null or [] || !signatureIsConfigured) + { + return false; + } + + var dataElementMatchExists = dataElements.Any(de => + dataTypesToSign.Any(dt => string.Equals(dt.Id, de.DataType, StringComparison.OrdinalIgnoreCase)) + ); + var allDataTypesAreOptional = dataTypesToSign.All(d => d.MinCount == 0); + return dataElementMatchExists || allDataTypesAreOptional; + } + private static List GetDataElementSignatures( List dataElements, - List dataTypesToSign + List? dataTypesToSign ) { var connectedDataElements = new List(); + if (dataTypesToSign is null or []) + return connectedDataElements; foreach (var dataType in dataTypesToSign) { connectedDataElements.AddRange( dataElements - .Where(d => d.DataType.Equals(dataType, StringComparison.OrdinalIgnoreCase)) + .Where(d => d.DataType.Equals(dataType.Id, StringComparison.OrdinalIgnoreCase)) .Select(d => new DataElementSignature(d.Id)) ); } diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index cb3cd129b..08f9e5b5b 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -2,21 +2,16 @@ using System.Xml.Serialization; using Altinn.App.Core.Configuration; using Altinn.App.Core.Extensions; -using Altinn.App.Core.Features; using Altinn.App.Core.Helpers.Extensions; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Profile; -using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.Platform.Profile.Models; -using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; -using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json; namespace Altinn.App.Core.Internal.Pdf; @@ -71,7 +66,7 @@ public async Task GenerateAndStorePdf(Instance instance, string taskId, Cancella { string language = GetOverriddenLanguage(); // Avoid a costly call if the language is allready overriden by the user - language = language.IsNullOrEmpty() ? await GetLanguage() : language; + language = string.IsNullOrEmpty(language) ? await GetLanguage() : language; var pdfContent = await GeneratePdfContent(instance, taskId, ct, language); @@ -87,7 +82,7 @@ public async Task GeneratePdf(Instance instance, string taskId, Cancella { var language = GetOverriddenLanguage(); // Avoid a costly call if the language is allready overriden by the user - language = language.IsNullOrEmpty() ? await GetLanguage() : language; + language = string.IsNullOrEmpty(language) ? await GetLanguage() : language; return await GeneratePdfContent(instance, taskId, ct, language); } diff --git a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs index 065c7ea86..7fe0aff91 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs @@ -31,7 +31,59 @@ public async void HandleAction_returns_ok_if_user_is_valid() UserId = 1337, Party = new Party() { SSN = "12345678901" } }; - (var userAction, var signClientMock) = CreateSigningUserAction(userProfile); + var appMetadata = new ApplicationMetadata("org/id") { DataTypes = [new DataType { Id = "model" }] }; + (var userAction, var signClientMock) = CreateSigningUserAction( + applicationMetadataToReturn: appMetadata, + userProfileToReturn: userProfile + ); + var instance = new Instance() + { + Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", + InstanceOwner = new() { PartyId = "5000", }, + Process = new() { CurrentTask = new() { ElementId = "Task2" } }, + Data = new() + { + new() { Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", DataType = "Model" } + } + }; + var userActionContext = new UserActionContext(instance, 1337); + + // Act + var result = await userAction.HandleAction(userActionContext); + + // Assert + SignatureContext expected = new SignatureContext( + new InstanceIdentifier(instance), + "signature", + new Signee() { UserId = "1337", PersonNumber = "12345678901" }, + new DataElementSignature("a499c3ef-e88a-436b-8650-1c43e5037ada") + ); + signClientMock.Verify( + s => s.SignDataElements(It.Is(sc => AssertSigningContextAsExpected(sc, expected))), + Times.Once + ); + result.Should().BeEquivalentTo(UserActionResult.SuccessResult()); + signClientMock.VerifyNoOtherCalls(); + } + + [Fact] + public async void HandleAction_returns_ok_if_no_dataElementSignature_and_optional_datatypes() + { + // Arrange + UserProfile userProfile = new UserProfile() + { + UserId = 1337, + Party = new Party() { SSN = "12345678901" } + }; + var appMetadata = new ApplicationMetadata("org/id") + { + // Optional because MinCount == 0 + DataTypes = [new DataType { Id = "model", MinCount = 0 }] + }; + (var userAction, var signClientMock) = CreateSigningUserAction( + applicationMetadataToReturn: appMetadata, + userProfileToReturn: userProfile + ); var instance = new Instance() { Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", @@ -96,6 +148,47 @@ public async void HandleAction_returns_error_when_UserId_not_set_in_context() signClientMock.VerifyNoOtherCalls(); } + [Fact] + public async void HandleAction_throws_ApplicationConfigException_when_no_dataElementSignature_and_mandatory_datatypes() + { + // Arrange + UserProfile userProfile = new UserProfile() + { + UserId = 1337, + Party = new Party() { SSN = "12345678901" } + }; + var appMetadata = new ApplicationMetadata("org/id") + { + // Mandatory because MinCount != 0 + DataTypes = + [ + new DataType { Id = "not_match", MinCount = 0 }, + new DataType { Id = "not_match_2", MinCount = 1 } + ] + }; + (var userAction, var signClientMock) = CreateSigningUserAction( + applicationMetadataToReturn: appMetadata, + userProfileToReturn: userProfile + ); + var instance = new Instance() + { + Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", + InstanceOwner = new() { PartyId = "5000", }, + Process = new() { CurrentTask = new() { ElementId = "Task2" } }, + Data = new() + { + new() { Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", DataType = "Model" } + } + }; + var userActionContext = new UserActionContext(instance, 1337); + + // Act + await Assert.ThrowsAsync( + async () => await userAction.HandleAction(userActionContext) + ); + signClientMock.VerifyNoOtherCalls(); + } + [Fact] public async void HandleAction_throws_ApplicationConfigException_if_SignatureDataType_is_null() { @@ -130,6 +223,7 @@ await Assert.ThrowsAsync( private static (SigningUserAction SigningUserAction, Mock SignClientMock) CreateSigningUserAction( UserProfile userProfileToReturn = null, + ApplicationMetadata applicationMetadataToReturn = null, PlatformHttpException platformHttpExceptionToThrow = null, string testBpmnfilename = "signing-task-process.bpmn" ) @@ -141,6 +235,8 @@ private static (SigningUserAction SigningUserAction, Mock SignClien var profileClientMock = new Mock(); var signingClientMock = new Mock(); + var appMetadataMock = new Mock(); + appMetadataMock.Setup(m => m.GetApplicationMetadata()).ReturnsAsync(applicationMetadataToReturn); profileClientMock.Setup(p => p.GetUserProfile(It.IsAny())).ReturnsAsync(userProfileToReturn); if (platformHttpExceptionToThrow != null) { @@ -154,7 +250,8 @@ private static (SigningUserAction SigningUserAction, Mock SignClien processReader, new NullLogger(), profileClientMock.Object, - signingClientMock.Object + signingClientMock.Object, + appMetadataMock.Object ), signingClientMock ); From 7e9c2ea91e652deb7766c014d796f057dcdbc750 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Mon, 6 May 2024 11:40:56 +0200 Subject: [PATCH 05/22] Fix a few issues with the new PATCH updates of datamodels (#590) * Split out and improve prepareModelForXmlStorage from InitAltinnRowId * Excecute ShouldSerialize* instead of reading [XmlText] and [BindNever] Expreriment to see if I liked this better. It will require Studio to generate models with the ShouldSerialize* methods to ensure the parent object is nulled when the child is empty. * Use arrow functions for ShouldSerialize in tests * use valueAsNullable instead of valueOrDefault to keep backwards compatibility * Code review fixes * Add a depth counter to prevent infinite recursion (same as json) * Only read public instance properties (infinite recursion was on static fields) * Throw error explaining infinite recursion, but don't recurse into System namespace * Fix dotnet format * Use same recursion fix in RemoveAltinnRowId * Add tests for newline * Make DataClientMock use the same xml serialization logic as production * Ask xmlSerialzier to not change newlines in model properties Fixes: https://github.com/Altinn/app-lib-dotnet/issues/621 * Improve comment --------- Co-authored-by: Martin Othamar --- .../Controllers/ActionsController.cs | 1 + .../Controllers/DataController.cs | 2 + src/Altinn.App.Core/Altinn.App.Core.csproj | 3 +- src/Altinn.App.Core/Helpers/ObjectUtils.cs | 163 ++++++- .../Clients/Storage/DataClient.cs | 8 +- .../Internal/Patch/PatchService.cs | 2 +- .../Common/ProcessTaskInitializer.cs | 1 + .../Controllers/DataController_PatchTests.cs | 59 ++- .../contributer-restriction/models/Skjema.cs | 5 + .../Mocks/DataClientMock.cs | 43 +- .../Helpers/ObjectUtilsTests.cs | 3 + .../ObjectUtils_XmlSerializationTests.cs | 404 ++++++++++++++++++ 12 files changed, 639 insertions(+), 55 deletions(-) create mode 100644 test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs diff --git a/src/Altinn.App.Api/Controllers/ActionsController.cs b/src/Altinn.App.Api/Controllers/ActionsController.cs index badc7a11c..d4aad444c 100644 --- a/src/Altinn.App.Api/Controllers/ActionsController.cs +++ b/src/Altinn.App.Api/Controllers/ActionsController.cs @@ -198,6 +198,7 @@ private async Task SaveChangedModels(Instance instance, Dictionary d.Id.Equals(elementId, StringComparison.OrdinalIgnoreCase)); await _dataClient.UpdateData( diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index 701fa41c8..788d1174c 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -662,6 +662,7 @@ private async Task CreateAppModelData(string org, string app, Inst int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); ObjectUtils.InitializeAltinnRowId(appModel); + ObjectUtils.PrepareModelForXmlStorage(appModel); DataElement dataElement = await _dataClient.InsertFormData( appModel, @@ -897,6 +898,7 @@ private async Task PutFormData( await UpdateDataValuesOnInstance(instance, dataType.Id, serviceModel); ObjectUtils.InitializeAltinnRowId(serviceModel); + ObjectUtils.PrepareModelForXmlStorage(serviceModel); // Save Formdata to database DataElement updatedDataElement = await _dataClient.UpdateData( diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 386bf6b04..00030a741 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -25,7 +25,8 @@ - + + diff --git a/src/Altinn.App.Core/Helpers/ObjectUtils.cs b/src/Altinn.App.Core/Helpers/ObjectUtils.cs index 5662a23fe..a81dc03c4 100644 --- a/src/Altinn.App.Core/Helpers/ObjectUtils.cs +++ b/src/Altinn.App.Core/Helpers/ObjectUtils.cs @@ -1,5 +1,7 @@ using System.Collections; using System.Reflection; +using System.Xml.Serialization; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Altinn.App.Core.Helpers; @@ -9,16 +11,27 @@ namespace Altinn.App.Core.Helpers; public static class ObjectUtils { /// - /// Recursively initialize all properties on the object that are currently null - /// Also ensure that all string properties that are empty are set to null - /// And set empty Guid properties named "AltinnRowId" to a new random guid + /// Set empty Guid properties named "AltinnRowId" to a new random guid /// /// The object to mutate - public static void InitializeAltinnRowId(object model) + /// Remaining recursion depth. To prevent infinite recursion we stop prepeation after this depth. (default matches json serialization) + public static void InitializeAltinnRowId(object model, int depth = 64) { ArgumentNullException.ThrowIfNull(model); + var type = model.GetType(); + if (depth < 0) + { + throw new Exception( + $"Recursion depth exceeded. {type.Name} in {type.Namespace} likely causes infinite recursion." + ); + } + + if (type.Namespace?.StartsWith("System") == true) + { + return; // System.DateTime.Now causes infinite recursion, and we shuldn't recurse into system types anyway. + } - foreach (var prop in model.GetType().GetProperties()) + foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { if (PropertyIsAltinRowGuid(prop)) { @@ -30,52 +43,160 @@ public static void InitializeAltinnRowId(object model) } } else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) + { + var value = prop.GetValue(model); + if (value is not null) + { + foreach (var item in (IList)value) + { + // Recurse into values of a list + if (item is not null) + { + InitializeAltinnRowId(item, depth - 1); + } + } + } + } + // property does not have an index parameter, nor is a value type, thus we should recurse into the property + else if (prop.GetIndexParameters().Length == 0) + { + var value = prop.GetValue(model); + + // continue recursion over all properties that are not null or value types + if (value is not null) + { + InitializeAltinnRowId(value, depth - 1); + } + } + } + } + + /// + /// Xml serialization-deserialization does not preserve all properties, and we sometimes need + /// to know how it looks when it comes back from storage. + /// * Recursively initialize all properties on the object that are currently null + /// * Ensure that all string properties with `[XmlTextAttribute]` that are empty or whitespace are set to null + /// * If a class has `[XmlTextAttribute]` and no value, set the parent property to null (if the other properties has [BindNever] attribute) + /// + /// The object to mutate + /// Remaining recursion depth. To prevent infinite recursion we stop prepeation after this depth. (default matches json serialization) + public static void PrepareModelForXmlStorage(object model, int depth = 64) + { + ArgumentNullException.ThrowIfNull(model); + var type = model.GetType(); + if (depth < 0) + { + throw new Exception( + $"Recursion depth exceeded. {type.Name} in {type.Namespace} likely causes infinite recursion." + ); + } + + if (type.Namespace?.StartsWith("System") == true) + { + return; + } + + var methodInfos = type.GetMethods(); + + // Iterate over properties of the model + foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + // Property has a generic type that is a subtype of List<> + if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) { var value = prop.GetValue(model); if (value is null) { - // Initialize IList with null value + // Initialize IList if it has null value (xml deserialization always retrurn emtpy list, not null) prop.SetValue(model, Activator.CreateInstance(prop.PropertyType)); } else { + // Recurse into values of a list foreach (var item in (IList)value) { - // Recurse into values of a list if (item is not null) { - InitializeAltinnRowId(item); + PrepareModelForXmlStorage(item, depth - 1); } } } } - else if (prop.GetIndexParameters().Length == 0) + else if (prop.GetIndexParameters().Length > 0) + { + // Ignore properties with index parameters + } + else { + // Property does not have an index parameter, thus we should recurse into the property var value = prop.GetValue(model); + if (value is null) + continue; + SetToDefaultIfShouldSerializeFalse(model, prop, methodInfos); - if (value is string s && string.IsNullOrWhiteSpace(s)) + // Set string properties with [XmlText] attribute to null if they are empty or whitespace + if ( + value is string s + && string.IsNullOrWhiteSpace(s) + && prop.GetCustomAttribute() is not null + ) { - // Initialize string with null value (xml serialization does not always preserve "") + // Ensure empty strings are set to null prop.SetValue(model, null); } - // continue recursion over all properties that are not null or value types - if (value?.GetType().IsValueType == false) - { - InitializeAltinnRowId(value); - } + // continue recursion over all properties that are NOT null or value types + PrepareModelForXmlStorage(value, depth - 1); + + SetToDefaultIfShouldSerializeFalse(model, prop, methodInfos); } } } + private static void SetToDefaultIfShouldSerializeFalse(object model, PropertyInfo prop, MethodInfo[] methodInfos) + { + string methodName = $"ShouldSerialize{prop.Name}"; + + var shouldSerializeMethod = methodInfos + .Where(m => m.Name == methodName && m.GetParameters().Length == 0 && m.ReturnType == typeof(bool)) + .SingleElement(); + + if (shouldSerializeMethod?.Invoke(model, null) is false) + { + prop.SetValue(model, default); + } + } + + private static T? SingleElement(this IEnumerable source) + { + using var enumerator = source.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return default(T); + } + var result = enumerator.Current; + return enumerator.MoveNext() ? default(T) : result; + } + /// /// Set all properties named "AltinnRowId" to Guid.Empty /// - public static void RemoveAltinnRowId(object model) + public static void RemoveAltinnRowId(object model, int depth = 64) { ArgumentNullException.ThrowIfNull(model); + if (depth < 0) + { + throw new Exception( + $"Recursion depth exceeded. {model.GetType().Name} in {model.GetType().Namespace} likely causes infinite recursion." + ); + } + var type = model.GetType(); + if (type.Namespace?.StartsWith("System") == true) + { + return; // System.DateTime.Now causes infinite recursion, and we shuldn't recurse into system types anyway. + } - foreach (var prop in model.GetType().GetProperties()) + foreach (var prop in type.GetProperties()) { // Handle guid fields named "AltinnRowId" if (PropertyIsAltinRowGuid(prop)) @@ -93,7 +214,7 @@ public static void RemoveAltinnRowId(object model) // Recurse into values of a list if (item is not null) { - RemoveAltinnRowId(item); + RemoveAltinnRowId(item, depth - 1); } } } @@ -104,9 +225,9 @@ public static void RemoveAltinnRowId(object model) var value = prop.GetValue(model); // continue recursion over all properties - if (value?.GetType().IsValueType == false) + if (value is not null) { - RemoveAltinnRowId(value); + RemoveAltinnRowId(value, depth - 1); } } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index b5adebd71..f9df73d85 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -147,9 +147,13 @@ Guid dataId // to avoid issue introduced with .Net when MS introduced BOM by default // when serializing ref. https://github.com/dotnet/runtime/issues/63585 // Will be fixed with https://github.com/dotnet/runtime/pull/75637 - private static void Serialize(T dataToSerialize, Type type, MemoryStream targetStream) + internal static void Serialize(T dataToSerialize, Type type, Stream targetStream) { - XmlWriterSettings xmlWriterSettings = new XmlWriterSettings() { Encoding = new UTF8Encoding(false) }; + XmlWriterSettings xmlWriterSettings = new XmlWriterSettings() + { + Encoding = new UTF8Encoding(false), + NewLineHandling = NewLineHandling.None, + }; XmlWriter xmlWriter = XmlWriter.Create(targetStream, xmlWriterSettings); XmlSerializer serializer = new(type); diff --git a/src/Altinn.App.Core/Internal/Patch/PatchService.cs b/src/Altinn.App.Core/Internal/Patch/PatchService.cs index 7d9fbcc87..a4484d2db 100644 --- a/src/Altinn.App.Core/Internal/Patch/PatchService.cs +++ b/src/Altinn.App.Core/Internal/Patch/PatchService.cs @@ -108,8 +108,8 @@ public async Task> ApplyPatch( await dataProcessor.ProcessDataWrite(instance, dataElementId, result.Ok, oldModel, language); } - // Ensure that all lists are changed from null to empty list. ObjectUtils.InitializeAltinnRowId(result.Ok); + ObjectUtils.PrepareModelForXmlStorage(result.Ok); var validationIssues = await _validationService.ValidateFormData( instance, diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs index e319dd902..cd2850f72 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs @@ -81,6 +81,7 @@ DataType dataType in applicationMetadata.DataTypes.Where(dt => Type type = _appModel.GetModelType(dataType.AppLogic.ClassRef); ObjectUtils.InitializeAltinnRowId(data); + ObjectUtils.PrepareModelForXmlStorage(data); DataElement createdDataElement = await _dataClient.InsertFormData(instance, dataType.Id, data, type); instance.Data ??= []; diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs index f52ead663..297714f4b 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs @@ -346,6 +346,50 @@ public async Task InsertNonExistingFieldWithoutTest_ReturnsUnprocessableContent( _dataProcessorMock.VerifyNoOtherCalls(); } + [Fact] + public async Task SetXmlTextPropertyToEmtpy_ReturnsCorrectDataModel() + { + _dataProcessorMock + .Setup(p => + p.ProcessDataWrite( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + null + ) + ) + .Returns( + (Instance instance, Guid? dataGuid, object skjema, object? existingData, string language) => + Task.CompletedTask + ); + + var pointer = JsonPointer.Create("melding", "tag-with-attribute"); + var createFirstElementPatch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("null")), + PatchOperation.Add(pointer, JsonNode.Parse("""{"value": "" }""")) + ); + + var (_, _, firstResponse) = await CallPatchApi( + createFirstElementPatch, + null, + HttpStatusCode.OK + ); + + var firstData = firstResponse.NewDataModel.Should().BeOfType().Which; + var emptyValue = firstData.GetProperty("melding").GetProperty("tag-with-attribute"); + emptyValue.ValueKind.Should().Be(JsonValueKind.Null); + + var addValuePatch = new JsonPatch( + PatchOperation.Test(pointer, emptyValue.AsNode()), + PatchOperation.Replace(pointer, JsonNode.Parse("""{"value": "mySecondValue" }""")) + ); + var (_, _, secondResponse) = await CallPatchApi(addValuePatch, null, HttpStatusCode.OK); + var secondData = secondResponse.NewDataModel.Should().BeOfType().Which; + var secondValue = secondData.GetProperty("melding").GetProperty("tag-with-attribute").GetProperty("value"); + secondValue.GetString().Should().Be("mySecondValue"); + } + [Fact] public async Task UpdateContainerWithListProperty_ReturnsCorrectDataModel() { @@ -466,7 +510,8 @@ public async Task SetStringPropertyToEmtpy_ReturnsCorrectDataModel() var firstData = firstResponse.NewDataModel.Should().BeOfType().Which; var firstListItem = firstData.GetProperty("melding").GetProperty("name"); - firstListItem.ValueKind.Should().Be(JsonValueKind.Null); + firstListItem.ValueKind.Should().Be(JsonValueKind.String); + firstListItem.GetString().Should().BeEmpty(); var addValuePatch = new JsonPatch( PatchOperation.Test(pointer, firstListItem.AsNode()), @@ -510,11 +555,11 @@ public async Task SetAttributeTagPropertyToEmtpy_ReturnsCorrectDataModel() var firstData = firstResponse.NewDataModel.Should().BeOfType().Which; var firstListItem = firstData.GetProperty("melding").GetProperty("tag-with-attribute"); - firstListItem.GetProperty("value").ValueKind.Should().Be(JsonValueKind.Null); + firstListItem.ValueKind.Should().Be(JsonValueKind.Null); var addValuePatch = new JsonPatch( - PatchOperation.Test(pointer, firstListItem.AsNode()), - PatchOperation.Replace(pointer.Combine("value"), JsonNode.Parse("null")) + PatchOperation.Test(pointer, JsonNode.Parse("null")), + PatchOperation.Add(pointer.Combine("value"), JsonNode.Parse("null")) ); var (_, _, secondResponse) = await CallPatchApi(addValuePatch, null, HttpStatusCode.OK); var secondData = secondResponse.NewDataModel.Should().BeOfType().Which; @@ -567,12 +612,12 @@ public async Task RowId_GetsAddedAutomatically() { "simple_keyvalues":[ { - "key": "KeyFromClient", - "intValue": 123, + "key": "KeyFromClient", + "intValue": 123, "altinnRowId": "{{rowIdClinet}}" }, { - "key": "KeyFromClientNoRowId", + "key": "KeyFromClientNoRowId", "intValue": 1234 } ] diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs index d7b1f2526..5d299395b 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs @@ -50,6 +50,11 @@ public class Dummy [JsonProperty("tag-with-attribute")] [JsonPropertyName("tag-with-attribute")] public TagWithAttribute? TagWithAttribute { get; set; } + + public bool ShouldSerializeTagWithAttribute() + { + return TagWithAttribute?.value != null; + } } public class TagWithAttribute diff --git a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs index 775b935f0..0354931e4 100644 --- a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs @@ -1,12 +1,14 @@ using System.Text.Json; -using System.Xml.Serialization; using Altinn.App.Api.Tests.Data; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Helpers.Serialization; +using Altinn.App.Core.Infrastructure.Clients.Storage; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using DataElement = Altinn.Platform.Storage.Interface.Models.DataElement; namespace App.IntegrationTests.Mocks.Services @@ -18,9 +20,16 @@ public class DataClientMock : IDataClient private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - public DataClientMock(IAppMetadata appMetadata, IHttpContextAccessor httpContextAccessor) + private readonly ILogger _logger; + + public DataClientMock( + IAppMetadata appMetadata, + IHttpContextAccessor httpContextAccessor, + ILogger logger + ) { _httpContextAccessor = httpContextAccessor; + _logger = logger; _appMetadata = appMetadata; } @@ -148,7 +157,7 @@ Guid instanceGuid return list; } - public Task GetFormData( + public async Task GetFormData( Guid instanceGuid, Type type, string org, @@ -158,24 +167,13 @@ Guid dataId ) { string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId); + using var sourceStream = File.Open(dataPath, FileMode.OpenOrCreate); - XmlSerializer serializer = new(type); - try - { - using FileStream sourceStream = File.Open(dataPath, FileMode.OpenOrCreate); + ModelDeserializer deserializer = new(_logger, type); + var formData = await deserializer.DeserializeAsync(sourceStream, "application/xml"); - var formData = serializer.Deserialize(sourceStream); - return formData != null - ? Task.FromResult(formData) - : throw new Exception("Unable to deserialize form data"); - } - catch - { - var formData = Activator.CreateInstance(type); - if (formData != null) - Task.FromResult(formData); - throw; - } + // var formData = serializer.Deserialize(sourceStream); + return formData ?? throw new Exception("Unable to deserialize form data"); } public async Task InsertFormData( @@ -221,8 +219,7 @@ string dataType using (Stream stream = File.Open(dataPath + @"blob/" + dataGuid, FileMode.Create, FileAccess.ReadWrite)) { - XmlSerializer serializer = new(type); - serializer.Serialize(stream, dataToSerialize); + DataClient.Serialize(dataToSerialize, type, stream); } WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); @@ -245,6 +242,7 @@ public Task UpdateData( Guid dataGuid ) { + ArgumentNullException.ThrowIfNull(dataToSerialize); string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); DataElement? dataElement = GetDataElements(org, app, instanceOwnerPartyId, instanceGuid) @@ -267,8 +265,7 @@ Guid dataGuid ) ) { - XmlSerializer serializer = new(type); - serializer.Serialize(stream, dataToSerialize); + DataClient.Serialize(dataToSerialize, type, stream); } dataElement.LastChanged = DateTime.UtcNow; diff --git a/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs b/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs index bdd9b90a8..52310d3cd 100644 --- a/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs @@ -30,6 +30,7 @@ public void TestSimple() test.Children.Should().BeNull(); ObjectUtils.InitializeAltinnRowId(test); + ObjectUtils.PrepareModelForXmlStorage(test); test.Children.Should().BeEmpty(); } @@ -41,6 +42,7 @@ public void TestSimpleStringInitialized() test.Children.Should().BeNull(); ObjectUtils.InitializeAltinnRowId(test); + ObjectUtils.PrepareModelForXmlStorage(test); test.Children.Should().BeEmpty(); test.StringValue.Should().Be("some"); @@ -80,6 +82,7 @@ public void TestMultipleLevelsInitialized() // Act ObjectUtils.InitializeAltinnRowId(test); + ObjectUtils.PrepareModelForXmlStorage(test); // Assert test.Children.Should().BeEmpty(); diff --git a/test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs b/test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs new file mode 100644 index 000000000..48a936a59 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs @@ -0,0 +1,404 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Xml.Serialization; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Helpers.Serialization; +using Altinn.App.Core.Infrastructure.Clients.Storage; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; +using Xunit.Abstractions; + +// ReSharper disable InconsistentNaming +#pragma warning disable SA1300 // Inconsistent casing on property + +namespace Altinn.App.Core.Tests.Helpers; + +public class ObjectUtils_XmlSerializationTests(ITestOutputHelper _output) +{ + private readonly Mock _loggerMock = new(); + + [XmlRoot(ElementName = "model")] + public class YttersteObjekt + { + [XmlElement("aarets", Order = 1)] + [JsonPropertyName("aarets")] + public NullableDecimalMedORID? DecimalMedOrid { get; set; } + + public bool ShouldSerializeDecimalMedOrid() => DecimalMedOrid?.valueNullable != null; + + [XmlElement("aarets2", Order = 2)] + [JsonPropertyName("aarets2")] + public StringMedORID? StringMedOrid { get; set; } + + public bool ShouldSerializeStringMedOrid() => StringMedOrid?.value != null; + + [XmlElement("aarets3", Order = 3)] + [JsonPropertyName("aarets3")] + public string? NormalString { get; set; } + + public bool ShouldSerializeNormalString() => NormalString != "should not serialize"; + + [XmlElement("aarets4", Order = 4)] + [JsonPropertyName("aarets4")] + public decimal? NullableDecimal { get; set; } + + public bool ShouldSerializeNullableDecimal() + { + return NullableDecimal != null && NullableDecimal != 1234567890; + } + + [XmlElement("aarets5", Order = 5)] + [JsonPropertyName("aarets5")] + public decimal Decimal { get; set; } + + public bool ShouldSerializeDecimal() + { + return Decimal != 1234567890; + } + + [XmlElement("children", Order = 6)] + public List? Children { get; set; } + } + + public class NullableDecimalMedORID + { + [XmlText] + [JsonIgnore] + public decimal value + { + get => valueNullable ?? default; + set { this.valueNullable = value; } + } + + [XmlIgnore] + [JsonPropertyName("value")] + public decimal? valueNullable { get; set; } + + [XmlAttribute("orid")] + [JsonPropertyName("orid")] + [BindNever] + public string orid => "30320"; + } + + public class StringMedORID + { + [XmlText()] + public string? value { get; set; } + + [XmlAttribute("orid")] + [BindNever] + public string orid => "30321"; + } + + public static TheoryData DecimalTests => + new() + { + { 123 }, + { 123.456m }, + { null }, + }; + + public static TheoryData StringTests => + new() + { + // originalValue, storedValue + { null, null }, + { "some", "some" }, + { string.Empty, null }, + { " ", null }, + { " ", null }, + { " a", " a" }, + { " a ", " a " }, + { "a ", "a " }, + { "a", "a" }, + { "a.", "a." }, + { "a.📚", "a.📚" }, + { "\n\n", null }, + { "\n\na", "\n\na" }, + { "a\n\n", "a\n\n" }, + { "a\nb", "a\nb" } + }; + + [Theory] + [MemberData(nameof(StringTests))] + public void TestPrepareForStorage(string? value, string? storedValue) + { + var test = CreateObject(value); + + ObjectUtils.PrepareModelForXmlStorage(test); + + AssertObject(test, value, storedValue); + } + + [Theory] + [MemberData(nameof(StringTests))] + public async Task TestSerializeDeserializeAsStorage(string? value, string? storedValue) + { + var test = CreateObject(value); + + // Serialize and deserialize twice to ensure that all changes in serialization is applied + var testResult = await SerializeDeserialize(test); + testResult = await SerializeDeserialize(testResult); + + AssertObject(testResult, value, storedValue); + } + + private async Task SerializeDeserialize(YttersteObjekt test) + { + // Serialize + using var serializationStream = new MemoryStream(); + DataClient.Serialize(test, typeof(YttersteObjekt), serializationStream); + + serializationStream.Seek(0, SeekOrigin.Begin); + _output.WriteLine(Encoding.UTF8.GetString(serializationStream.ToArray())); + + // Deserialize + ModelDeserializer serializer = new ModelDeserializer(_loggerMock.Object, typeof(YttersteObjekt)); + var deserialized = await serializer.DeserializeAsync(serializationStream, "application/xml"); + var testResult = deserialized.Should().BeOfType().Which; + + return testResult; + } + + [Theory] + [MemberData(nameof(StringTests))] + public void TestSerializeDeserializeAsJson(string? value, string? storedValue) + { + _output.WriteLine($"Direct Json Serialization does not change {value} into {storedValue}"); + + var test = CreateObject(value); + + // Serialize + var json = JsonSerializer.Serialize(test); + _output.WriteLine(json); + + // Deserialize + var testResult = JsonSerializer.Deserialize(json)!; + + if (value is null) + { + // JsonSerialization does not set the parent object aarets2 to null, so we do that manually for all test cases to work + testResult.StringMedOrid.Should().NotBeNull(); + testResult.StringMedOrid!.value.Should().BeNull(); + testResult.StringMedOrid = null; + var child = testResult.Children.Should().ContainSingle().Which; + child.StringMedOrid.Should().NotBeNull(); + child.StringMedOrid!.value.Should().BeNull(); + child.StringMedOrid = null; + } + + AssertObject(testResult, value, value); + } + + private static YttersteObjekt CreateObject(string? value) + { + var test = new YttersteObjekt + { + StringMedOrid = new StringMedORID { value = value }, + NormalString = value, + Children = new List + { + new YttersteObjekt + { + StringMedOrid = new StringMedORID { value = value }, + NormalString = value, + } + } + }; + + test.DecimalMedOrid.Should().BeNull(); + test.StringMedOrid.Should().NotBeNull(); + test.StringMedOrid!.value.Should().Be(value); + test.StringMedOrid.orid.Should().Be("30321"); + var child = test.Children.Should().ContainSingle().Which; + child.DecimalMedOrid.Should().BeNull(); + child.StringMedOrid.Should().NotBeNull(); + child.StringMedOrid!.value.Should().Be(value); + child.StringMedOrid.orid.Should().Be("30321"); + child.NormalString.Should().Be(value); + return test; + } + + private static void AssertObject(YttersteObjekt test, string? normalValue, string? xmlTextValue) + { + test.DecimalMedOrid.Should().BeNull(); + if (xmlTextValue is null) + { + test.StringMedOrid.Should().BeNull(); + } + else + { + test.StringMedOrid.Should().NotBeNull(); + test.StringMedOrid!.value.Should().Be(xmlTextValue); + test.StringMedOrid.orid.Should().Be("30321"); + } + + test.NormalString.Should().Be(normalValue); + var child = test.Children.Should().ContainSingle().Which; + child.DecimalMedOrid.Should().BeNull(); + if (xmlTextValue is null) + { + child.StringMedOrid.Should().BeNull(); + } + else + { + child.StringMedOrid.Should().NotBeNull(); + child.StringMedOrid!.value.Should().Be(xmlTextValue); + child.StringMedOrid.orid.Should().Be("30321"); + } + + child.NormalString.Should().Be(normalValue); + } + + [Theory] + [MemberData(nameof(DecimalTests))] + public void TestPrepareForStorage_Decimal(decimal? value) + { + var test = CreateObject(value); + + ObjectUtils.PrepareModelForXmlStorage(test); + + // prepareForXmlStorage should set all empty strings to null + // but serialization only sets [XmlText] strings to null + AssertObject(test, value); + } + + [Theory] + [MemberData(nameof(DecimalTests))] + public async Task TestSerializeDeserializeAsStorage_Decimal(decimal? value) + { + var test = CreateObject(value); + + // Serialize and deserialize twice to ensure that all changes in serialization is applied + var testResult = await SerializeDeserialize(test); + testResult = await SerializeDeserialize(testResult); + + AssertObject(testResult, value); + } + + [Theory] + [MemberData(nameof(DecimalTests))] + public void TestSerializeDeserializeAsJson_Decimal(decimal? value) + { + var test = CreateObject(value); + + // Serialize + var json = JsonSerializer.Serialize(test); + _output.WriteLine(json); + + // Deserialize + var testResult = JsonSerializer.Deserialize(json)!; + + if (value is null) + { + // JsonSerialization does not set the parent object StringMedOrid to null, so we do that manually for all test cases to work + testResult.DecimalMedOrid.Should().NotBeNull(); + testResult.DecimalMedOrid!.valueNullable.Should().BeNull(); + testResult.DecimalMedOrid = null; + var child = testResult.Children.Should().ContainSingle().Which; + child.DecimalMedOrid.Should().NotBeNull(); + child.DecimalMedOrid!.valueNullable.Should().BeNull(); + child.DecimalMedOrid = null; + } + + AssertObject(testResult, value); + } + + private static YttersteObjekt CreateObject(decimal? value) + { + var test = new YttersteObjekt + { + DecimalMedOrid = new NullableDecimalMedORID { valueNullable = value }, + NullableDecimal = value, + Decimal = value ?? default, + Children = new List + { + new YttersteObjekt + { + DecimalMedOrid = new NullableDecimalMedORID { valueNullable = value }, + NullableDecimal = value, + Decimal = value ?? default, + } + } + }; + + test.StringMedOrid.Should().BeNull(); + test.DecimalMedOrid.Should().NotBeNull(); + test.DecimalMedOrid!.valueNullable.Should().Be(value); + test.DecimalMedOrid.orid.Should().Be("30320"); + test.Decimal.Should().Be(value ?? default); + test.NullableDecimal.Should().Be(value); + var child = test.Children.Should().ContainSingle().Which; + child.StringMedOrid.Should().BeNull(); + child.DecimalMedOrid.Should().NotBeNull(); + child.DecimalMedOrid!.valueNullable.Should().Be(value); + child.DecimalMedOrid.orid.Should().Be("30320"); + child.Decimal.Should().Be(value ?? default); + child.NullableDecimal.Should().Be(value); + return test; + } + + private static void AssertObject(YttersteObjekt test, decimal? value) + { + test.StringMedOrid.Should().BeNull(); + if (value is null) + { + test.DecimalMedOrid.Should().BeNull(); + } + else + { + test.DecimalMedOrid.Should().NotBeNull(); + test.DecimalMedOrid!.valueNullable.Should().Be(value); + test.DecimalMedOrid.orid.Should().Be("30320"); + } + + test.Decimal.Should().Be(value ?? default); + test.NullableDecimal.Should().Be(value); + var child = test.Children.Should().ContainSingle().Which; + child.StringMedOrid.Should().BeNull(); + if (value is null) + { + child.DecimalMedOrid.Should().BeNull(); + } + else + { + child.DecimalMedOrid.Should().NotBeNull(); + child.DecimalMedOrid!.valueNullable.Should().Be(value); + child.DecimalMedOrid.orid.Should().Be("30320"); + } + + child.Decimal.Should().Be(value ?? default); + child.NullableDecimal.Should().Be(value); + } + + [Fact] + public void VerifyShouldSerialize() + { + var test = new YttersteObjekt + { + DecimalMedOrid = new(), + StringMedOrid = new(), + NormalString = "should not serialize", + NullableDecimal = 1234567890, + Decimal = 1234567890, + }; + + test.DecimalMedOrid.Should().NotBeNull(); + test.StringMedOrid.Should().NotBeNull(); + test.NormalString.Should().NotBeNull(); + test.NullableDecimal.Should().NotBeNull(); + test.Decimal.Should().NotBe(0); + + ObjectUtils.PrepareModelForXmlStorage(test); + + test.DecimalMedOrid.Should().BeNull(); + test.StringMedOrid.Should().BeNull(); + test.NormalString.Should().BeNull(); + test.NullableDecimal.Should().BeNull(); + test.Decimal.Should().Be(default); + } +} From 48830d5c907b71424d59f876034ec9efe0b0c75d Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Mon, 6 May 2024 14:14:58 +0200 Subject: [PATCH 06/22] Fix flaky AppInsights DI tests (#632) --- test/Altinn.App.Api.Tests/DITests.cs | 118 +++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 9 deletions(-) diff --git a/test/Altinn.App.Api.Tests/DITests.cs b/test/Altinn.App.Api.Tests/DITests.cs index 8b3b05dc8..b99f001c1 100644 --- a/test/Altinn.App.Api.Tests/DITests.cs +++ b/test/Altinn.App.Api.Tests/DITests.cs @@ -1,5 +1,7 @@ using System.Diagnostics.Tracing; using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -49,8 +51,20 @@ public string EnvironmentName private sealed class AppInsightsListener : EventListener { + private readonly object _lock = new(); private readonly List _eventSources = []; - public readonly List Events = []; + private readonly List _events = []; + + public EventWrittenEventArgs[] Events + { + get + { + lock (_lock) + { + return _events.ToArray(); + } + } + } protected override void OnEventSourceCreated(EventSource eventSource) { @@ -70,7 +84,10 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) return; } - Events.Add(eventData); + lock (_lock) + { + _events.Add(eventData); + } base.OnEventWritten(eventData); } @@ -84,8 +101,35 @@ public override void Dispose() } } + private sealed class TelemetryProcessor(ITelemetryProcessor next) : ITelemetryProcessor + { + private static readonly object _lock = new(); + + private static readonly List _items = new(); + + public static ITelemetry[] Items + { + get + { + lock (_lock) + { + return _items.ToArray(); + } + } + } + + public void Process(ITelemetry item) + { + lock (_lock) + { + _items.Add(item); + } + next.Process(item); + } + } + [Fact] - public void AppInsights_Registers_Correctly() + public async Task AppInsights_Registers_Correctly() { using var listener = new AppInsightsListener(); @@ -102,16 +146,72 @@ public void AppInsights_Registers_Correctly() .Build(); Extensions.ServiceCollectionExtensions.AddAltinnAppServices(services, config, env); + services.AddApplicationInsightsTelemetryProcessor(); + + await using (var sp = services.BuildServiceProvider()) + { + var telemetryConfig = sp.GetRequiredService(); + Assert.NotNull(telemetryConfig); + + var client = sp.GetRequiredService(); + Assert.NotNull(client); + + client.TrackEvent("TestEvent"); + await client.FlushAsync(default); + } - using var sp = services.BuildServiceProvider(); + await Task.Yield(); - var telemetryConfig = sp.GetRequiredService(); - Assert.NotNull(telemetryConfig); + EventLevel[] errorLevels = [EventLevel.Error, EventLevel.Critical]; + var events = listener.Events; + Assert.Empty(events.Where(e => errorLevels.Contains(e.Level))); + + var telemetryItems = TelemetryProcessor.Items; + var customEvents = telemetryItems + .Select(e => e as EventTelemetry) + .Where(e => e?.Name is not null) + .Select(e => e?.Name) + .ToArray(); + Assert.Single(customEvents); + var customEvent = customEvents[0]; + Assert.Equal("TestEvent", customEvent); + } + + [Fact] + public async Task KeyedServices_Produces_Error_Diagnostics() + { + // This test just verifies that we rootcaused the issues re: https://github.com/Altinn/app-lib-dotnet/pull/594 + + using var listener = new AppInsightsListener(); + + var services = new ServiceCollection(); + var env = new FakeWebHostEnvironment { EnvironmentName = "Development" }; + + services.AddSingleton(env); + services.AddSingleton(env); + + var config = new ConfigurationBuilder() + .AddInMemoryCollection( + [new KeyValuePair("ApplicationInsights:InstrumentationKey", "test")] + ) + .Build(); + + // AppInsights SDK currently can't handle keyed services in the container + // Hopefully we can remove all this soon + services.AddKeyedSingleton("test"); + + Extensions.ServiceCollectionExtensions.AddAltinnAppServices(services, config, env); + + await using (var sp = services.BuildServiceProvider()) + { + var client = sp.GetService(); + Assert.Null(client); + } - var client = sp.GetRequiredService(); - Assert.NotNull(client); + await Task.Yield(); EventLevel[] errorLevels = [EventLevel.Error, EventLevel.Critical]; - Assert.Empty(listener.Events.Where(e => errorLevels.Contains(e.Level))); + var events = listener.Events; + Assert.NotEmpty(events.Where(e => errorLevels.Contains(e.Level))); } } From 9a03145d484020a4debf648c507cb45c3392e8a8 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Tue, 7 May 2024 19:02:42 +0200 Subject: [PATCH 07/22] Improve NRT nullability situation (#625) --- .github/workflows/dotnet-test.yml | 10 +++++----- .vscode/extensions.json | 6 ++++++ .vscode/settings.json | 5 ++++- Directory.Build.props | 13 +++++++++++++ src/Altinn.App.Api/Controllers/DataController.cs | 5 +++++ .../Controllers/PartiesController.cs | 6 +++++- .../RequestHandling/DataRestrictionValidation.cs | 5 ++++- src/Altinn.App.Core/Configuration/AppSettings.cs | 13 +++++++++++++ .../Configuration/PlatformSettings.cs | 2 ++ .../Implementation/DefaultEFormidlingService.cs | 5 +++++ .../Extensions/DataProtectionConfiguration.cs | 11 +++++++++-- .../Features/Action/SigningUserAction.cs | 5 ++++- .../Features/DataLists/NullDataListProvider.cs | 2 ++ .../DataLists/NullInstanceDataListProvider.cs | 2 ++ .../Features/PageOrder/DefaultPageOrder.cs | 7 ++++--- .../Helpers/InstantiationHelper.cs | 8 ++++++++ src/Altinn.App.Core/Helpers/ProcessError.cs | 2 ++ .../Helpers/Serialization/ModelDeserializer.cs | 9 +++++++-- src/Altinn.App.Core/Helpers/UserHelper.cs | 4 +++- .../Implementation/AppResourcesSI.cs | 11 ++++++++--- src/Altinn.App.Core/Implementation/PrefillSI.cs | 6 ++++-- .../Clients/Authorization/AuthorizationClient.cs | 2 +- .../Infrastructure/Clients/Storage/DataClient.cs | 3 +++ .../Clients/Storage/InstanceClient.cs | 5 ++++- .../Clients/Storage/InstanceEventClient.cs | 11 +++++++++-- src/Altinn.App.Core/Internal/Pdf/PdfService.cs | 4 +++- .../AltinnSignatureConfiguration.cs | 3 +++ .../Process/Elements/Base/ProcessElement.cs | 3 +++ .../Internal/Process/Elements/Definitions.cs | 2 ++ .../Internal/Process/Elements/Process.cs | 5 +++++ .../Internal/Process/Elements/SequenceFlow.cs | 3 +++ .../Internal/Process/ProcessNavigator.cs | 6 +++++- .../Internal/Sign/SignatureContext.cs | 3 +++ .../Models/ApplicationLanguage.cs | 2 ++ src/Altinn.App.Core/Models/Attachment.cs | 3 +++ src/Altinn.App.Core/Models/AttachmentList.cs | 2 ++ src/Altinn.App.Core/Models/CloudEvent.cs | 5 +++++ src/Altinn.App.Core/Models/LayoutSet.cs | 2 ++ .../Models/Process/ProcessNextRequest.cs | 3 +++ .../Models/Process/ProcessStartRequest.cs | 3 +++ src/Altinn.App.Core/Models/QueryResponse.cs | 2 ++ src/Altinn.App.Core/Models/UserContext.cs | 3 +++ .../Validation/InstantiationValidationResult.cs | 2 ++ src/Directory.Build.props | 5 ++--- .../Altinn.App.Api.Tests.csproj | 3 ++- .../Controllers/StatelessDataControllerTests.cs | 16 ++++++++-------- .../Controllers/TestResources/DummyModel.cs | 2 +- .../Altinn.App.Api.Tests/Mocks/DataClientMock.cs | 2 +- .../Mocks/ProfileClientMock.cs | 4 ++-- .../NullInstanceDataListProviderTest.cs | 2 +- .../Extensions/DictionaryExtensionsTests.cs | 6 +++--- .../Features/Action/SigningUserActionTests.cs | 10 +++++----- .../NullInstantiationProcessTests.cs | 2 +- .../Email/EmailNotificationClientTests.cs | 8 ++++---- .../Sms/SmsNotificationClientTests.cs | 8 ++++---- .../NullInstanceAppOptionsProviderTests.cs | 2 +- .../NullInstantiationValidatorTests.cs | 2 +- .../Helpers/MultiDecisionHelperTests.cs | 9 +++++---- .../Implementation/NullPdfFormatterTests.cs | 2 +- .../Clients/Storage/DataClientTests.cs | 5 ++++- .../Internal/App/AppMedataTest.cs | 16 ++++++++-------- .../Internal/Events/EventHandlerResolverTests.cs | 2 +- .../Internal/Process/ProcessNavigatorTests.cs | 16 ++++++++-------- .../ServiceTasks/EformidlingServiceTaskTests.cs | 1 - .../CommonTests/ExpressionTestCaseRoot.cs | 4 ++++ 65 files changed, 253 insertions(+), 88 deletions(-) create mode 100644 .vscode/extensions.json diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index a31598cc4..bfc632563 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -12,10 +12,10 @@ jobs: analyze: strategy: matrix: - os: [macos-latest,windows-latest,ubuntu-latest] # TODO: Fix test to run on ubuntu-latest also + os: [macos-latest, windows-latest, ubuntu-latest] name: Run dotnet build and test runs-on: ${{ matrix.os}} - env: + env: DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE: false steps: - name: Setup .NET @@ -25,10 +25,10 @@ jobs: 8.0.x - uses: actions/checkout@v4 with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Build run: | - dotnet build AppLibDotnet.sln -v m + dotnet build AppLibDotnet.sln -v m - name: Test run: | - dotnet test AppLibDotnet.sln -v m + dotnet test AppLibDotnet.sln -v m --no-restore --no-build diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..674caf1d5 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "redhat.vscode-yaml", + "ms-dotnettools.csdevkit" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index ce278e953..d2b886b2e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,8 @@ "[csharp]": { "editor.defaultFormatter": "csharpier.csharpier-vscode", "editor.formatOnSave": true - } + }, + "[github-actions-workflow]": { + "editor.defaultFormatter": "redhat.vscode-yaml" + }, } diff --git a/Directory.Build.props b/Directory.Build.props index 044a5d2d2..8ccd36190 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,8 +1,21 @@ + + true + + + + latest + + all runtime; build; native; contentfiles; analyzers + + diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index 788d1174c..3bd87dd4b 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -226,6 +226,11 @@ out StringValues headerValues return BadRequest(await GetErrorDetails(validationIssues)); } + if (streamContent.Headers.ContentType is null) + { + return StatusCode(500, "Content-Type not defined"); + } + fileStream.Seek(0, SeekOrigin.Begin); return await CreateBinaryData( instance, diff --git a/src/Altinn.App.Api/Controllers/PartiesController.cs b/src/Altinn.App.Api/Controllers/PartiesController.cs index 170b521d7..d221d0e1a 100644 --- a/src/Altinn.App.Api/Controllers/PartiesController.cs +++ b/src/Altinn.App.Api/Controllers/PartiesController.cs @@ -85,7 +85,11 @@ public async Task Get(string org, string app, bool allowedToInsta public async Task ValidateInstantiation(string org, string app, [FromQuery] int partyId) { UserContext userContext = await _userHelper.GetUserContext(HttpContext); - UserProfile user = await _profileClient.GetUserProfile(userContext.UserId); + UserProfile? user = await _profileClient.GetUserProfile(userContext.UserId); + if (user is null) + { + return StatusCode(500, "Could not get user profile while validating instantiation"); + } List? partyList = await _authorizationClient.GetPartyList(userContext.UserId); Application application = await _appMetadata.GetApplicationMetadata(); diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs b/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs index e06e0cf35..ce8fc8ab7 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs @@ -154,7 +154,10 @@ public static (bool Success, List Errors) CompliesWithDataRestr /// public static string? GetFileNameFromHeader(StringValues headerValues) { - ContentDispositionHeaderValue contentDisposition = ContentDispositionHeaderValue.Parse(headerValues); + string? headerValue = headerValues; + ArgumentNullException.ThrowIfNull(headerValue, nameof(headerValues)); + + ContentDispositionHeaderValue contentDisposition = ContentDispositionHeaderValue.Parse(headerValue); string? filename = contentDisposition.FileNameStar ?? contentDisposition.FileName; // We actively remove quotes because we don't want them replaced with '_'. diff --git a/src/Altinn.App.Core/Configuration/AppSettings.cs b/src/Altinn.App.Core/Configuration/AppSettings.cs index aaa55d08f..4ada89a59 100644 --- a/src/Altinn.App.Core/Configuration/AppSettings.cs +++ b/src/Altinn.App.Core/Configuration/AppSettings.cs @@ -6,6 +6,7 @@ namespace Altinn.App.Core.Configuration /// /// Class that represents the ServiceRepositorySettings /// + // TODO: IOptions validation so that we know which of these properties are required public class AppSettings { /// @@ -61,8 +62,14 @@ public class AppSettings /// /// Gets or sets the BaseResourceFolderContainer that identifies where in the docker container the runtime can find files needed /// + // TODO: can this be removed? + // Env var being set is ServiceRepositorySettings__BaseResourceFolderContainer, but this prop is not used anywhere +#nullable disable + [Obsolete("This is not used, and will be removed in the next major version")] public string BaseResourceFolderContainer { get; set; } +#nullable restore + /// /// Gets or sets The name of the FormLayout json file Name /// @@ -142,6 +149,7 @@ public class AppSettings /// /// Open Id Connect Well known endpoint /// +#nullable disable public string OpenIdWellKnownEndpoint { get; set; } /// @@ -154,6 +162,8 @@ public class AppSettings /// public string RuntimeCookieName { get; set; } +#nullable restore + /// /// Option to disable csrf check /// @@ -186,8 +196,11 @@ public class AppSettings /// /// Gets or sets the version of the application. /// +#nullable disable public string AppVersion { get; set; } +#nullable restore + /// /// Enable the functionality to load layout in backend and remove data from hidden components before task completion /// diff --git a/src/Altinn.App.Core/Configuration/PlatformSettings.cs b/src/Altinn.App.Core/Configuration/PlatformSettings.cs index 8544e5ed2..ad8f3b1cc 100644 --- a/src/Altinn.App.Core/Configuration/PlatformSettings.cs +++ b/src/Altinn.App.Core/Configuration/PlatformSettings.cs @@ -51,6 +51,8 @@ public class PlatformSettings /// A new subscription key is generated automatically every time an app is deployed to an environment. The new key is then automatically /// added to the environment for the app code during deploy. This will override the value stored in app settings. /// +#nullable disable public string SubscriptionKey { get; set; } +#nullable restore } } diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs index 5a41f6a5d..48f476f79 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs @@ -125,6 +125,11 @@ private async Task ConstructStandardBusinessDocument( Instance instance ) { + if (_appSettings is null) + { + throw new Exception("AppSettings not initialized"); + } + DateTime completedTime = DateTime.UtcNow; Sender digdirSender = new Sender diff --git a/src/Altinn.App.Core/Extensions/DataProtectionConfiguration.cs b/src/Altinn.App.Core/Extensions/DataProtectionConfiguration.cs index b42e70053..9d5bed631 100644 --- a/src/Altinn.App.Core/Extensions/DataProtectionConfiguration.cs +++ b/src/Altinn.App.Core/Extensions/DataProtectionConfiguration.cs @@ -15,14 +15,21 @@ public static class DataProtectionConfiguration /// The service collections public static void ConfigureDataProtection(this IServiceCollection services) { - services.AddDataProtection().PersistKeysToFileSystem(GetKeysDirectory()); + var dir = GetKeysDirectory(); + if (dir is null) + { + throw new DirectoryNotFoundException( + "Could not find a suitable directory for storing DataProtection keys" + ); + } + services.AddDataProtection().PersistKeysToFileSystem(dir); } /// /// Return a directory based on the running operating system. It is possible to override the directory based on the ALTINN_KEYS_DIRECTORY environment variable. /// /// - private static DirectoryInfo GetKeysDirectory() + private static DirectoryInfo? GetKeysDirectory() { var environmentVariable = System.Environment.GetEnvironmentVariable("ALTINN_KEYS_DIRECTORY"); if (!string.IsNullOrWhiteSpace(environmentVariable)) diff --git a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs index 76817b8d9..f49ea4c08 100644 --- a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs @@ -141,7 +141,10 @@ private static List GetDataElementSignatures( private async Task GetSignee(int userId) { - var userProfile = await _profileClient.GetUserProfile(userId); + var userProfile = + await _profileClient.GetUserProfile(userId) + ?? throw new Exception("Could not get user profile while getting signee"); + return new Signee { UserId = userProfile.UserId.ToString(), diff --git a/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs b/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs index 299bc8190..3b484dc4e 100644 --- a/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs +++ b/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs @@ -20,7 +20,9 @@ public class NullDataListProvider : IDataListProvider /// public Task GetDataListAsync(string? language, Dictionary keyValuePairs) { +#nullable disable return Task.FromResult(new DataList() { ListItems = null }); +#nullable restore } } } diff --git a/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs b/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs index 57d48427c..fc928389f 100644 --- a/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs +++ b/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs @@ -24,7 +24,9 @@ public Task GetInstanceDataListAsync( Dictionary keyValuePairs ) { +#nullable disable return Task.FromResult(new DataList() { ListItems = null }); +#nullable restore } } } diff --git a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs index 293a2529c..16f401b44 100644 --- a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs +++ b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs @@ -21,7 +21,7 @@ public DefaultPageOrder(IAppResources resources) } /// - public async Task> GetPageOrder( + public Task> GetPageOrder( AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, string layoutSetId, @@ -40,8 +40,9 @@ object formData { layoutSettings = _resources.GetLayoutSettingsForSet(layoutSetId); } - - return await Task.FromResult(layoutSettings.Pages.Order); +#nullable disable + return Task.FromResult(layoutSettings.Pages.Order); +#nullable restore } } } diff --git a/src/Altinn.App.Core/Helpers/InstantiationHelper.cs b/src/Altinn.App.Core/Helpers/InstantiationHelper.cs index aaca93c39..ba6734f87 100644 --- a/src/Altinn.App.Core/Helpers/InstantiationHelper.cs +++ b/src/Altinn.App.Core/Helpers/InstantiationHelper.cs @@ -51,6 +51,10 @@ public static List FilterPartiesByAllowedPartyTypes( if (canPartyInstantiate && isChildPartyAllowed) { party.ChildParties = new List(); + if (allowedChildParties is null) + { + throw new Exception("List of allowed child parties unexpectedly null"); + } party.ChildParties.AddRange(allowedChildParties); allowed.Add(party); } @@ -58,6 +62,10 @@ public static List FilterPartiesByAllowedPartyTypes( { party.ChildParties = new List(); party.OnlyHierarchyElementWithNoAccess = true; + if (allowedChildParties is null) + { + throw new Exception("List of allowed child parties unexpectedly null"); + } party.ChildParties.AddRange(allowedChildParties); allowed.Add(party); } diff --git a/src/Altinn.App.Core/Helpers/ProcessError.cs b/src/Altinn.App.Core/Helpers/ProcessError.cs index 276c4d89d..5fea7f5ef 100644 --- a/src/Altinn.App.Core/Helpers/ProcessError.cs +++ b/src/Altinn.App.Core/Helpers/ProcessError.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Helpers /// public class ProcessError { +#nullable disable /// /// Gets or sets a machine readable error code or test resource key. /// @@ -14,5 +15,6 @@ public class ProcessError /// Gets or sets a human readable error message. /// public string Text { get; set; } +#nullable restore } } diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs index 48f642fb5..9bc2867cc 100644 --- a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs +++ b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs @@ -90,7 +90,7 @@ public ModelDeserializer(ILogger logger, Type modelType) { Error = null; - string streamContent = null; + string? streamContent = null; try { // In this first try block we assume that the namespace is the same in the model @@ -116,6 +116,11 @@ public ModelDeserializer(ILogger logger, Type modelType) attributes.XmlRoot = new XmlRootAttribute(elementName); attributeOverrides.Add(_modelType, attributes); + if (string.IsNullOrWhiteSpace(streamContent)) + { + throw new Exception("No XML content read from stream"); + } + using XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(streamContent)); XmlSerializer serializer = new XmlSerializer(_modelType, attributeOverrides); @@ -124,7 +129,7 @@ public ModelDeserializer(ILogger logger, Type modelType) catch (InvalidOperationException invalidOperationException) { // One possible fail condition is if the XML has a namespace, but the model does not, or that the namespaces are different. - Error = $"{invalidOperationException.Message} {invalidOperationException?.InnerException.Message}"; + Error = $"{invalidOperationException.Message} {invalidOperationException?.InnerException?.Message}"; return null; } } diff --git a/src/Altinn.App.Core/Helpers/UserHelper.cs b/src/Altinn.App.Core/Helpers/UserHelper.cs index 56deae41a..518751277 100644 --- a/src/Altinn.App.Core/Helpers/UserHelper.cs +++ b/src/Altinn.App.Core/Helpers/UserHelper.cs @@ -68,7 +68,9 @@ public async Task GetUserContext(HttpContext context) } } - UserProfile userProfile = await _profileClient.GetUserProfile(userContext.UserId); + UserProfile userProfile = + await _profileClient.GetUserProfile(userContext.UserId) + ?? throw new Exception("Could not get user profile while getting user context"); userContext.UserParty = userProfile.Party; if (context.Request.Cookies[_settings.GetAltinnPartyCookieName] != null) diff --git a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs index 05404763d..258529d66 100644 --- a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs +++ b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs @@ -249,13 +249,14 @@ public string GetLayouts() public string GetLayoutSets() { string filename = Path.Join(_settings.AppBasePath, _settings.UiFolder, _settings.LayoutSetsFileName); - string filedata = null; + string? filedata = null; if (File.Exists(filename)) { filedata = File.ReadAllText(filename, Encoding.UTF8); } - +#nullable disable return filedata; +#nullable restore } /// @@ -389,13 +390,15 @@ public byte[] GetRuleHandlerForSet(string id) private byte[] ReadFileByte(string fileName) { - byte[] filedata = null; + byte[]? filedata = null; if (File.Exists(fileName)) { filedata = File.ReadAllBytes(fileName); } +#nullable disable return filedata; +#nullable restore } private byte[] ReadFileContentsFromLegalPath(string legalPath, string filePath) @@ -411,7 +414,9 @@ private byte[] ReadFileContentsFromLegalPath(string legalPath, string filePath) return File.ReadAllBytes(fullFileName); } +#nullable disable return null; +#nullable restore } /// diff --git a/src/Altinn.App.Core/Implementation/PrefillSI.cs b/src/Altinn.App.Core/Implementation/PrefillSI.cs index ef3cab8b5..6844898b8 100644 --- a/src/Altinn.App.Core/Implementation/PrefillSI.cs +++ b/src/Altinn.App.Core/Implementation/PrefillSI.cs @@ -86,7 +86,7 @@ public async Task PrefillDataModel( allowOverwrite = allowOverwriteToken.ToObject(); } - Party party = await _altinnPartyClientClient.GetParty(int.Parse(partyId)); + Party? party = await _altinnPartyClientClient.GetParty(int.Parse(partyId)); if (party == null) { string errorMessage = $"Could find party for partyId: {partyId}"; @@ -184,7 +184,7 @@ public async Task PrefillDataModel( /// private void AssignValueToDataModel( string[] keys, - JToken value, + JToken? value, object currentObject, int index = 0, bool continueOnError = false @@ -215,6 +215,8 @@ private void AssignValueToDataModel( { if (propertyValue == null || allowOverwrite) { + ArgumentNullException.ThrowIfNull(value); + // create instance of the property type defined in the datamodel var instance = value.ToObject(property.PropertyType); diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs index 052c52725..3e92758c0 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs @@ -58,7 +58,7 @@ ILogger logger if (!httpClient.DefaultRequestHeaders.Contains(ForwardedForHeaderName)) { - string? clientIpAddress = _httpContextAccessor?.HttpContext?.Request?.Headers?[ForwardedForHeaderName]; + string? clientIpAddress = _httpContextAccessor.HttpContext?.Request?.Headers?[ForwardedForHeaderName]; httpClient.DefaultRequestHeaders.Add(ForwardedForHeaderName, clientIpAddress); } httpClient.DefaultRequestHeaders.Add( diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index f9df73d85..267655bbf 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -183,7 +183,9 @@ Guid dataId } else if (response.StatusCode == HttpStatusCode.NotFound) { +#nullable disable return null; +#nullable restore } throw await PlatformHttpException.CreateAsync(response); @@ -455,6 +457,7 @@ Stream stream string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}"; string token = _userTokenProvider.GetUserToken(); StreamContent content = new StreamContent(stream); + ArgumentNullException.ThrowIfNull(contentType); content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); content.Headers.ContentDisposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment) { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs index dd63b15c5..292e09fa7 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs @@ -99,8 +99,9 @@ public async Task> GetInstances(Dictionary foreach (var queryParameter in queryParams) { - foreach (string value in queryParameter.Value) + foreach (string? value in queryParameter.Value) { + // TODO: remember to escape the value here apiUrl.Append($"&{queryParameter.Key}={value}"); } } @@ -251,7 +252,9 @@ public async Task UpdateReadStatus(int instanceOwnerPartyId, Guid inst _logger.LogError( $"Could not update read status for instance {instanceOwnerPartyId}/{instanceGuid}. Request failed with status code {response.StatusCode}" ); +#nullable disable return null; +#nullable restore } /// diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs index 261be1124..28e3d40c6 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Net.Http.Headers; using System.Text; using Altinn.App.Core.Configuration; @@ -116,8 +117,14 @@ public async Task SaveInstanceEvent(object dataToSerialize, string org, if (response.IsSuccessStatusCode) { string eventData = await response.Content.ReadAsStringAsync(); - InstanceEvent result = JsonConvert.DeserializeObject(eventData)!; - return result.Id.ToString(); + InstanceEvent result = + JsonConvert.DeserializeObject(eventData) + ?? throw new Exception("Failed to deserialize instance event"); + var id = result.Id.ToString(); + Debug.Assert(id is not null, "Nullable.ToString() never returns null"); + // ^ https://github.com/dotnet/runtime/blob/9b088ab8287a77c52ff7c4ed6fa96be6d3eb87f1/src/libraries/System.Private.CoreLib/src/System/Nullable.cs#L67 + // ^ https://github.com/dotnet/runtime/blob/9b088ab8287a77c52ff7c4ed6fa96be6d3eb87f1/src/libraries/System.Private.CoreLib/src/System/Guid.cs#L1124 + return id; } throw await PlatformHttpException.CreateAsync(response); diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index 08f9e5b5b..187fb428e 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -132,7 +132,9 @@ private async Task GetLanguage() if (userId != null) { - UserProfile userProfile = await _profileClient.GetUserProfile((int)userId); + UserProfile userProfile = + await _profileClient.GetUserProfile((int)userId) + ?? throw new Exception("Could not get user profile while getting language"); if (!string.IsNullOrEmpty(userProfile.ProfileSettingPreference?.Language)) { diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnSignatureConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnSignatureConfiguration.cs index 7e94e3bf0..e253f8597 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnSignatureConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnSignatureConfiguration.cs @@ -18,8 +18,11 @@ public class AltinnSignatureConfiguration /// Set what dataTypeId that should be used for storing the signature /// [XmlElement("signatureDataType", Namespace = "http://altinn.no/process")] +#nullable disable public string SignatureDataType { get; set; } +#nullable restore + /// /// Define what signature dataypes this signature should be unique from. Users that have sign any of the signatures in the list will not be able to sign this signature /// diff --git a/src/Altinn.App.Core/Internal/Process/Elements/Base/ProcessElement.cs b/src/Altinn.App.Core/Internal/Process/Elements/Base/ProcessElement.cs index 5cb4b7b7e..6779ccaf3 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/Base/ProcessElement.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/Base/ProcessElement.cs @@ -11,6 +11,7 @@ public abstract class ProcessElement /// Gets or sets the ID of a flow element /// [XmlAttribute("id")] +#nullable disable public string Id { get; set; } /// @@ -31,6 +32,8 @@ public abstract class ProcessElement [XmlElement("outgoing")] public List Outgoing { get; set; } +#nullable restore + /// /// String representation of process element type /// diff --git a/src/Altinn.App.Core/Internal/Process/Elements/Definitions.cs b/src/Altinn.App.Core/Internal/Process/Elements/Definitions.cs index 47fc624e5..17af15f7f 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/Definitions.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/Definitions.cs @@ -13,6 +13,7 @@ public class Definitions /// Gets or sets the ID of the definition /// [XmlAttribute("id")] +#nullable disable public string Id { get; set; } /// @@ -26,5 +27,6 @@ public class Definitions /// [XmlElement("process")] public Process Process { get; set; } +#nullable restore } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/Process.cs b/src/Altinn.App.Core/Internal/Process/Elements/Process.cs index 5a351d06f..4f6fbfe58 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/Process.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/Process.cs @@ -11,8 +11,11 @@ public class Process /// Gets or sets the ID of the process of a workflow /// [XmlAttribute("id")] +#nullable disable public string Id { get; set; } +#nullable restore + /// /// Gets or sets if the process of a workflow is executable or not /// @@ -23,6 +26,7 @@ public class Process /// Gets or sets the start event of the process of a workflow /// [XmlElement("startEvent")] +#nullable disable public List StartEvents { get; set; } /// @@ -48,5 +52,6 @@ public class Process /// [XmlElement("exclusiveGateway")] public List ExclusiveGateway { get; set; } +#nullable restore } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs b/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs index f99a55789..0cc32ef60 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs @@ -11,6 +11,7 @@ public class SequenceFlow /// Gets or sets the ID of a sequence flow /// [XmlAttribute("id")] +#nullable disable public string Id { get; set; } /// @@ -31,6 +32,8 @@ public class SequenceFlow [XmlAttribute("flowtype", Namespace = "http://altinn.no")] public string FlowType { get; set; } +#nullable restore + /// /// Gets or sets the condition expression of a sequence flow /// diff --git a/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs b/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs index ca108fffe..73dc6f4c1 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs @@ -37,7 +37,11 @@ ILogger logger public async Task GetNextTask(Instance instance, string currentElement, string? action) { List directFlowTargets = _processReader.GetNextElements(currentElement); - List filteredNext = await NextFollowAndFilterGateways(instance, directFlowTargets, action); + List filteredNext = await NextFollowAndFilterGateways( + instance, + directFlowTargets as List, + action + ); if (filteredNext.Count == 0) { return null; diff --git a/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs b/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs index a82de509c..d4a0dc33c 100644 --- a/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs +++ b/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs @@ -76,8 +76,11 @@ public class Signee /// /// User id of the user performing the signing /// +#nullable disable public string UserId { get; set; } +#nullable restore + /// /// The SSN of the user performing the signing, set if the signer is a person /// diff --git a/src/Altinn.App.Core/Models/ApplicationLanguage.cs b/src/Altinn.App.Core/Models/ApplicationLanguage.cs index 2e9635e50..6c35b14c2 100644 --- a/src/Altinn.App.Core/Models/ApplicationLanguage.cs +++ b/src/Altinn.App.Core/Models/ApplicationLanguage.cs @@ -12,6 +12,8 @@ public class ApplicationLanguage /// Example: "nb" /// [JsonPropertyName("language")] +#nullable disable public string Language { get; set; } +#nullable restore } } diff --git a/src/Altinn.App.Core/Models/Attachment.cs b/src/Altinn.App.Core/Models/Attachment.cs index cf5e88170..4e27315c3 100644 --- a/src/Altinn.App.Core/Models/Attachment.cs +++ b/src/Altinn.App.Core/Models/Attachment.cs @@ -8,6 +8,7 @@ public class Attachment /// /// The file name /// +#nullable disable public string Name { get; set; } /// @@ -15,6 +16,8 @@ public class Attachment /// public string Id { get; set; } +#nullable restore + /// /// The file size in bytes /// diff --git a/src/Altinn.App.Core/Models/AttachmentList.cs b/src/Altinn.App.Core/Models/AttachmentList.cs index 68ec0d334..c2ae6a888 100644 --- a/src/Altinn.App.Core/Models/AttachmentList.cs +++ b/src/Altinn.App.Core/Models/AttachmentList.cs @@ -8,11 +8,13 @@ public class AttachmentList /// /// The attachment type /// +#nullable disable public string Type { get; set; } /// /// The attachments /// public List Attachments { get; set; } +#nullable restore } } diff --git a/src/Altinn.App.Core/Models/CloudEvent.cs b/src/Altinn.App.Core/Models/CloudEvent.cs index 5fa03e7ef..a1ba92593 100644 --- a/src/Altinn.App.Core/Models/CloudEvent.cs +++ b/src/Altinn.App.Core/Models/CloudEvent.cs @@ -12,6 +12,7 @@ public class CloudEvent /// Gets or sets the id of the event. /// [JsonPropertyName("id")] +#nullable disable public string Id { get; set; } /// @@ -38,6 +39,8 @@ public class CloudEvent [JsonPropertyName("subject")] public string Subject { get; set; } +#nullable restore + /// /// Gets or sets the time of the event. /// @@ -48,6 +51,7 @@ public class CloudEvent /// Gets or sets the alternative subject of the event. /// [JsonPropertyName("alternativesubject")] +#nullable disable public string AlternativeSubject { get; set; } /// @@ -70,5 +74,6 @@ public class CloudEvent /// [JsonPropertyName("contenttype")] public ContentType DataContentType { get; set; } +#nullable restore } } diff --git a/src/Altinn.App.Core/Models/LayoutSet.cs b/src/Altinn.App.Core/Models/LayoutSet.cs index 68836082e..b6193bf6b 100644 --- a/src/Altinn.App.Core/Models/LayoutSet.cs +++ b/src/Altinn.App.Core/Models/LayoutSet.cs @@ -8,6 +8,7 @@ public class LayoutSet /// /// LayoutsetId for layout. This is the foldername /// +#nullable disable public string Id { get; set; } /// @@ -19,5 +20,6 @@ public class LayoutSet /// List of tasks where layuout should be used /// public List Tasks { get; set; } +#nullable restore } } diff --git a/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs b/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs index 9fd88ac2d..e72cc3610 100644 --- a/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs @@ -8,6 +8,7 @@ namespace Altinn.App.Core.Models.Process; /// public class ProcessNextRequest { +#nullable disable /// /// The instance to be moved to the next task /// @@ -18,6 +19,8 @@ public class ProcessNextRequest /// public ClaimsPrincipal User { get; set; } +#nullable restore + /// /// The action that is performed /// diff --git a/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs b/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs index c75b2108e..ecbd13cc5 100644 --- a/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs @@ -8,6 +8,7 @@ namespace Altinn.App.Core.Models.Process; /// public class ProcessStartRequest { +#nullable disable /// /// The instance to be started /// @@ -18,6 +19,8 @@ public class ProcessStartRequest /// public ClaimsPrincipal User { get; set; } +#nullable restore + /// /// The prefill data supplied when starting the process /// diff --git a/src/Altinn.App.Core/Models/QueryResponse.cs b/src/Altinn.App.Core/Models/QueryResponse.cs index 3703b4ed3..4e2bd0b39 100644 --- a/src/Altinn.App.Core/Models/QueryResponse.cs +++ b/src/Altinn.App.Core/Models/QueryResponse.cs @@ -17,6 +17,7 @@ public class QueryResponse /// The current query. /// [JsonProperty(PropertyName = "self")] +#nullable disable public string Self { get; set; } /// @@ -30,5 +31,6 @@ public class QueryResponse /// [JsonProperty(PropertyName = "instances")] public List Instances { get; set; } +#nullable restore } } diff --git a/src/Altinn.App.Core/Models/UserContext.cs b/src/Altinn.App.Core/Models/UserContext.cs index 832a37836..abcb72131 100644 --- a/src/Altinn.App.Core/Models/UserContext.cs +++ b/src/Altinn.App.Core/Models/UserContext.cs @@ -8,6 +8,7 @@ namespace Altinn.App.Core.Models /// public class UserContext { +#nullable disable /// /// Gets or sets the social security number /// @@ -33,6 +34,8 @@ public class UserContext /// public ClaimsPrincipal User { get; set; } +#nullable restore + /// /// Gets or sets the ID of the user /// diff --git a/src/Altinn.App.Core/Models/Validation/InstantiationValidationResult.cs b/src/Altinn.App.Core/Models/Validation/InstantiationValidationResult.cs index 08b034bb8..60185075a 100644 --- a/src/Altinn.App.Core/Models/Validation/InstantiationValidationResult.cs +++ b/src/Altinn.App.Core/Models/Validation/InstantiationValidationResult.cs @@ -12,6 +12,7 @@ public class InstantiationValidationResult /// public bool Valid { get; set; } +#nullable disable /// /// Gets or sets a message /// @@ -21,5 +22,6 @@ public class InstantiationValidationResult /// Gets or sets a list of parties the user represents that can instantiate /// public List ValidParties { get; set; } +#nullable restore } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c751b8cd7..4140f5be8 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - + @@ -10,9 +10,8 @@ preview.0 v true - 12 - + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch).$(BuildNumber) diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index e87183436..c0c86e3a6 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -5,10 +5,11 @@ enable enable false - $(NoWarn);CS1591;CS0618 + $(NoWarn);CS1591;CS0618;CS7022 diff --git a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs index 0a3c2a84a..71c12f373 100644 --- a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs @@ -29,7 +29,7 @@ namespace Altinn.App.Api.Tests.Controllers; public class StatelessDataControllerTests { [Fact] - public async void Get_Returns_BadRequest_when_dataType_is_null() + public async Task Get_Returns_BadRequest_when_dataType_is_null() { // Arrange var altinnAppModelMock = new Mock(); @@ -68,7 +68,7 @@ public async void Get_Returns_BadRequest_when_dataType_is_null() } [Fact] - public async void Get_Returns_BadRequest_when_appResource_classRef_is_null() + public async Task Get_Returns_BadRequest_when_appResource_classRef_is_null() { // Arrange var appModelMock = new Mock(); @@ -136,7 +136,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) } [Fact] - public async void Get_Returns_BadRequest_when_party_header_count_greater_than_one() + public async Task Get_Returns_BadRequest_when_party_header_count_greater_than_one() { // Arrange var factory = new StatelessDataControllerWebApplicationFactory(); @@ -164,7 +164,7 @@ public async void Get_Returns_BadRequest_when_party_header_count_greater_than_on } [Fact] - public async void Get_Returns_Forbidden_when_party_has_no_rights() + public async Task Get_Returns_Forbidden_when_party_has_no_rights() { // Arrange var factory = new StatelessDataControllerWebApplicationFactory(); @@ -190,7 +190,7 @@ public async void Get_Returns_Forbidden_when_party_has_no_rights() } [Fact] - public async void Get_Returns_BadRequest_when_instance_owner_is_empty_party_header() + public async Task Get_Returns_BadRequest_when_instance_owner_is_empty_party_header() { // Arrange var appModelMock = new Mock(); @@ -228,7 +228,7 @@ public async void Get_Returns_BadRequest_when_instance_owner_is_empty_party_head } [Fact] - public async void Get_Returns_BadRequest_when_instance_owner_is_empty_user_in_context() + public async Task Get_Returns_BadRequest_when_instance_owner_is_empty_user_in_context() { // Arrange var appModelMock = new Mock(); @@ -272,7 +272,7 @@ public async void Get_Returns_BadRequest_when_instance_owner_is_empty_user_in_co } [Fact] - public async void Get_Returns_Forbidden_when_returned_descision_is_Deny() + public async Task Get_Returns_Forbidden_when_returned_descision_is_Deny() { // Arrange var appModelMock = new Mock(); @@ -332,7 +332,7 @@ public async void Get_Returns_Forbidden_when_returned_descision_is_Deny() } [Fact] - public async void Get_Returns_OK_with_appModel() + public async Task Get_Returns_OK_with_appModel() { // Arrange var appModelMock = new Mock(); diff --git a/test/Altinn.App.Api.Tests/Controllers/TestResources/DummyModel.cs b/test/Altinn.App.Api.Tests/Controllers/TestResources/DummyModel.cs index 614035373..c855264d3 100644 --- a/test/Altinn.App.Api.Tests/Controllers/TestResources/DummyModel.cs +++ b/test/Altinn.App.Api.Tests/Controllers/TestResources/DummyModel.cs @@ -2,7 +2,7 @@ namespace Altinn.App.Api.Tests.Controllers.TestResources; public class DummyModel { - public string Name { get; set; } + public string? Name { get; set; } public int Age { get; set; } /// diff --git a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs index 0354931e4..8b75f1e35 100644 --- a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs @@ -405,7 +405,7 @@ public async Task InsertBinaryData( string instanceId, string dataType, string contentType, - string filename, + string? filename, Stream stream, string? generatedFromTask = null ) diff --git a/test/Altinn.App.Api.Tests/Mocks/ProfileClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/ProfileClientMock.cs index 7a3210f52..821258f13 100644 --- a/test/Altinn.App.Api.Tests/Mocks/ProfileClientMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/ProfileClientMock.cs @@ -11,10 +11,10 @@ public class ProfileClientMock : IProfileClient private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web) { Converters = { new JsonStringEnumConverter() } }; - public async Task GetUserProfile(int userId) + public async Task GetUserProfile(int userId) { var folder = TestData.GetRegisterProfilePath(); var file = Path.Join(folder, $"{userId}.json"); - return (await JsonSerializer.DeserializeAsync(File.OpenRead(file), _jsonSerializerOptions))!; + return await JsonSerializer.DeserializeAsync(File.OpenRead(file), _jsonSerializerOptions); } } diff --git a/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs b/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs index 10679fff4..330fabebf 100644 --- a/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs +++ b/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs @@ -9,7 +9,7 @@ namespace Altinn.App.PlatformServices.Tests.DataLists public class NullInstanceDataListProviderTest { [Fact] - public async void Constructor_InitializedWithEmptyValues() + public async Task Constructor_InitializedWithEmptyValues() { var provider = new NullInstanceDataListProvider(); diff --git a/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs b/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs index 3565bcc37..b5b65c7bf 100644 --- a/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs +++ b/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs @@ -12,7 +12,7 @@ public void ToNameValueString_OptionParameters_ShouldConvertToHttpHeaderFormat() { var options = new AppOptions { - Parameters = new Dictionary { { "lang", "nb" }, { "level", "1" } }, + Parameters = new Dictionary { { "lang", "nb" }, { "level", "1" } }, }; IHeaderDictionary headers = new HeaderDictionary @@ -26,7 +26,7 @@ public void ToNameValueString_OptionParameters_ShouldConvertToHttpHeaderFormat() [Fact] public void ToNameValueString_OptionParametersWithEmptyValue_ShouldConvertToHttpHeaderFormat() { - var options = new AppOptions { Parameters = new Dictionary() }; + var options = new AppOptions { Parameters = new Dictionary() }; IHeaderDictionary headers = new HeaderDictionary { @@ -54,7 +54,7 @@ public void ToNameValueString_OptionParametersWithSpecialCharaters_IsValidAsHead { var options = new AppOptions { - Parameters = new Dictionary + Parameters = new Dictionary { { "lang", "nb" }, { "level", "1" }, diff --git a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs index 7fe0aff91..f566d8f29 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs @@ -23,7 +23,7 @@ namespace Altinn.App.Core.Tests.Features.Action; public class SigningUserActionTests { [Fact] - public async void HandleAction_returns_ok_if_user_is_valid() + public async Task HandleAction_returns_ok_if_user_is_valid() { // Arrange UserProfile userProfile = new UserProfile() @@ -67,7 +67,7 @@ public async void HandleAction_returns_ok_if_user_is_valid() } [Fact] - public async void HandleAction_returns_ok_if_no_dataElementSignature_and_optional_datatypes() + public async Task HandleAction_returns_ok_if_no_dataElementSignature_and_optional_datatypes() { // Arrange UserProfile userProfile = new UserProfile() @@ -115,7 +115,7 @@ public async void HandleAction_returns_ok_if_no_dataElementSignature_and_optiona } [Fact] - public async void HandleAction_returns_error_when_UserId_not_set_in_context() + public async Task HandleAction_returns_error_when_UserId_not_set_in_context() { // Arrange UserProfile userProfile = new UserProfile() @@ -149,7 +149,7 @@ public async void HandleAction_returns_error_when_UserId_not_set_in_context() } [Fact] - public async void HandleAction_throws_ApplicationConfigException_when_no_dataElementSignature_and_mandatory_datatypes() + public async Task HandleAction_throws_ApplicationConfigException_when_no_dataElementSignature_and_mandatory_datatypes() { // Arrange UserProfile userProfile = new UserProfile() @@ -190,7 +190,7 @@ await Assert.ThrowsAsync( } [Fact] - public async void HandleAction_throws_ApplicationConfigException_if_SignatureDataType_is_null() + public async Task HandleAction_throws_ApplicationConfigException_if_SignatureDataType_is_null() { // Arrange UserProfile userProfile = new UserProfile() diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/NullInstantiationProcessTests.cs b/test/Altinn.App.Core.Tests/Features/DataProcessing/NullInstantiationProcessTests.cs index a8782a2e6..5e7967db7 100644 --- a/test/Altinn.App.Core.Tests/Features/DataProcessing/NullInstantiationProcessTests.cs +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/NullInstantiationProcessTests.cs @@ -10,7 +10,7 @@ namespace Altinn.App.PlatformServices.Tests.Features.DataProcessing; public class NullInstantiationProcessTests { [Fact] - public async void NullInstantiationTest_DataCreation_changes_nothing() + public async Task NullInstantiationTest_DataCreation_changes_nothing() { // Arrange var nullInstantiation = new NullInstantiationProcessor(); diff --git a/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs b/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs index d25257ccc..9cb574cf6 100644 --- a/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs @@ -26,7 +26,7 @@ public class EmailNotificationClientTests [Theory] [InlineData(true)] [InlineData(false)] - public async void Order_VerifyHttpCall(bool includeTelemetryClient) + public async Task Order_VerifyHttpCall(bool includeTelemetryClient) { // Arrange var emailNotification = new EmailNotification @@ -80,7 +80,7 @@ public async void Order_VerifyHttpCall(bool includeTelemetryClient) } [Fact] - public async void Order_ShouldReturnOrderId_OnSuccess() + public async Task Order_ShouldReturnOrderId_OnSuccess() { // Arrange var handlerMock = new Mock(); @@ -120,7 +120,7 @@ public async void Order_ShouldReturnOrderId_OnSuccess() } [Fact] - public async void Order_ShouldThrowEmailNotificationException_OnFailure() + public async Task Order_ShouldThrowEmailNotificationException_OnFailure() { // Arrange var handlerMock = new Mock(); @@ -160,7 +160,7 @@ public async void Order_ShouldThrowEmailNotificationException_OnFailure() } [Fact] - public async void Order_ShouldThrowEmailNotificationException_OnInvalidJsonResponse() + public async Task Order_ShouldThrowEmailNotificationException_OnInvalidJsonResponse() { // Arrange var handlerMock = new Mock(); diff --git a/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs index 465f9c901..92e2af03a 100644 --- a/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs @@ -26,7 +26,7 @@ public class SmsNotificationClientTests [Theory] [InlineData(true)] [InlineData(false)] - public async void Order_VerifyHttpCall(bool includeTelemetryClient) + public async Task Order_VerifyHttpCall(bool includeTelemetryClient) { // Arrange var smsNotification = new SmsNotification @@ -81,7 +81,7 @@ public async void Order_VerifyHttpCall(bool includeTelemetryClient) } [Fact] - public async void Order_ShouldReturnOrderId_OnSuccess() + public async Task Order_ShouldReturnOrderId_OnSuccess() { // Arrange var handlerMock = new Mock(); @@ -122,7 +122,7 @@ public async void Order_ShouldReturnOrderId_OnSuccess() } [Fact] - public async void Order_ShouldThrowSmsNotificationException_OnFailure() + public async Task Order_ShouldThrowSmsNotificationException_OnFailure() { // Arrange var handlerMock = new Mock(); @@ -162,7 +162,7 @@ public async void Order_ShouldThrowSmsNotificationException_OnFailure() } [Fact] - public async void Order_ShouldThrowSmsNotificationException_OnInvalidJsonResponse() + public async Task Order_ShouldThrowSmsNotificationException_OnInvalidJsonResponse() { // Arrange var handlerMock = new Mock(); diff --git a/test/Altinn.App.Core.Tests/Features/Options/NullInstanceAppOptionsProviderTests.cs b/test/Altinn.App.Core.Tests/Features/Options/NullInstanceAppOptionsProviderTests.cs index 1ee359213..9f255728b 100644 --- a/test/Altinn.App.Core.Tests/Features/Options/NullInstanceAppOptionsProviderTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Options/NullInstanceAppOptionsProviderTests.cs @@ -8,7 +8,7 @@ namespace Altinn.App.Core.Tests.Features.Options public class NullInstanceAppOptionsProviderTests { [Fact] - public async void Constructor_InitializedWithEmptyValues() + public async Task Constructor_InitializedWithEmptyValues() { var provider = new NullInstanceAppOptionsProvider(); diff --git a/test/Altinn.App.Core.Tests/Features/Validators/NullInstantiationValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/NullInstantiationValidatorTests.cs index df2e2f507..3d66fb247 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/NullInstantiationValidatorTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/NullInstantiationValidatorTests.cs @@ -9,7 +9,7 @@ namespace Altinn.App.PlatformServices.Tests.Features.Validators; public class NullInstantiationValidatorTests { [Fact] - public async void NullInstantiationTest_Validation_returns_null() + public async Task NullInstantiationTest_Validation_returns_null() { // Arrange var nullInstantiation = new NullInstantiationValidator(); diff --git a/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs index dfddba86d..7dfc40fa0 100644 --- a/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs @@ -112,7 +112,7 @@ public void CreateMultiDecisionRequest_throws_ArgumentNullException_if_user_is_n }; var actions = new List() { "sign", "reject" }; - Action act = () => MultiDecisionHelper.CreateMultiDecisionRequest(null, instance, actions); + Action act = () => MultiDecisionHelper.CreateMultiDecisionRequest(null!, instance, actions); act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'user')"); } @@ -170,7 +170,7 @@ public void ValidateDecisionResult_throws_ArgumentNullException_if_response_is_n { "complete", false }, { "lookup", false } }; - Action act = () => MultiDecisionHelper.ValidatePdpMultiDecision(actions, null, GetClaims("501337")); + Action act = () => MultiDecisionHelper.ValidatePdpMultiDecision(actions, null!, GetClaims("501337")); act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'results')"); } @@ -185,7 +185,7 @@ public void ValidateDecisionResult_throws_ArgumentNullException_if_user_is_null( { "complete", false }, { "lookup", false } }; - Action act = () => MultiDecisionHelper.ValidatePdpMultiDecision(actions, response, null); + Action act = () => MultiDecisionHelper.ValidatePdpMultiDecision(actions, response, null!); act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'user')"); } @@ -234,6 +234,7 @@ private static List GetXacmlJsonRespons(string filename) var xacmlJesonRespons = File.ReadAllText( Path.Join("Helpers", "TestData", "MultiDecisionHelper", filename + ".json") ); - return JsonSerializer.Deserialize>(xacmlJesonRespons); + return JsonSerializer.Deserialize>(xacmlJesonRespons) + ?? throw new Exception("Deserialization failed for XacmlJsonRespons"); } } diff --git a/test/Altinn.App.Core.Tests/Implementation/NullPdfFormatterTests.cs b/test/Altinn.App.Core.Tests/Implementation/NullPdfFormatterTests.cs index c8f53748e..a1f108ace 100644 --- a/test/Altinn.App.Core.Tests/Implementation/NullPdfFormatterTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/NullPdfFormatterTests.cs @@ -10,7 +10,7 @@ namespace Altinn.App.PlatformServices.Tests.Implementation; public class NullPdfFormatterTests { [Fact] - public async void NullPdfFormatter_FormatPdf_returns_Layoutsettings_as_is() + public async Task NullPdfFormatter_FormatPdf_returns_Layoutsettings_as_is() { // Arrange var layoutSettings = new LayoutSettings() diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs index 66e4b679f..110197252 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs @@ -878,8 +878,11 @@ private void AssertHttpRequest( if (expectedFilename is not null) { + Assert.NotNull(actualContentDisposition); + var actualContentDispositionValue = actualContentDisposition.FirstOrDefault(); + Assert.NotNull(actualContentDispositionValue); ContentDispositionHeaderValue - .Parse(actualContentDisposition?.FirstOrDefault()) + .Parse(actualContentDispositionValue) .FileName?.Should() .BeEquivalentTo(expectedFilename); } diff --git a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs index 75c6231cc..50de1f40f 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs @@ -131,7 +131,7 @@ public async Task GetApplicationMetadata_eformidling_desrializes_file_from_disk( } [Fact] - public async void GetApplicationMetadata_second_read_from_cache() + public async Task GetApplicationMetadata_second_read_from_cache() { AppSettings appSettings = GetAppSettings("AppMetadata", "default.applicationmetadata.json"); Mock appFeaturesMock = new Mock(); @@ -464,7 +464,7 @@ public async Task GetApplicationMetadata_deserialize_serialize_unmapped_properti } [Fact] - public async void GetApplicationMetadata_throws_ApplicationConfigException_if_file_not_found() + public async Task GetApplicationMetadata_throws_ApplicationConfigException_if_file_not_found() { AppSettings appSettings = GetAppSettings("AppMetadata", "notfound.applicationmetadata.json"); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); @@ -474,7 +474,7 @@ await Assert.ThrowsAsync( } [Fact] - public async void GetApplicationMetadata_throw_ApplicationConfigException_if_deserialization_fails() + public async Task GetApplicationMetadata_throw_ApplicationConfigException_if_deserialization_fails() { AppSettings appSettings = GetAppSettings("AppMetadata", "invalid.applicationmetadata.json"); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); @@ -484,7 +484,7 @@ await Assert.ThrowsAsync( } [Fact] - public async void GetApplicationMetadata_throws_ApplicationConfigException_if_deserialization_fails_due_to_string_in_int() + public async Task GetApplicationMetadata_throws_ApplicationConfigException_if_deserialization_fails_due_to_string_in_int() { AppSettings appSettings = GetAppSettings("AppMetadata", "invalid-int.applicationmetadata.json"); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); @@ -494,7 +494,7 @@ await Assert.ThrowsAsync( } [Fact] - public async void GetApplicationXACMLPolicy_return_policyfile_as_string() + public async Task GetApplicationXACMLPolicy_return_policyfile_as_string() { AppSettings appSettings = GetAppSettings(subfolder: "AppPolicy", policyFilename: "policy.xml"); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); @@ -505,7 +505,7 @@ public async void GetApplicationXACMLPolicy_return_policyfile_as_string() } [Fact] - public async void GetApplicationXACMLPolicy_throws_FileNotFoundException_if_file_not_found() + public async Task GetApplicationXACMLPolicy_throws_FileNotFoundException_if_file_not_found() { AppSettings appSettings = GetAppSettings(subfolder: "AppPolicy", policyFilename: "notfound.xml"); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); @@ -513,7 +513,7 @@ public async void GetApplicationXACMLPolicy_throws_FileNotFoundException_if_file } [Fact] - public async void GetApplicationBPMNProcess_return_process_as_string() + public async Task GetApplicationBPMNProcess_return_process_as_string() { AppSettings appSettings = GetAppSettings(subfolder: "AppProcess", bpmnFilename: "process.bpmn"); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); @@ -524,7 +524,7 @@ public async void GetApplicationBPMNProcess_return_process_as_string() } [Fact] - public async void GetApplicationBPMNProcess_throws_ApplicationConfigException_if_file_not_found() + public async Task GetApplicationBPMNProcess_throws_ApplicationConfigException_if_file_not_found() { AppSettings appSettings = GetAppSettings(subfolder: "AppProcess", policyFilename: "notfound.xml"); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); diff --git a/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs b/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs index 670b063d8..45cffdcaa 100644 --- a/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs @@ -11,7 +11,7 @@ namespace Altinn.App.PlatformServices.Tests.Internal.Events public class EventHandlerResolverTests { [Fact] - public async void ResolveEventHandler_SubscriptionValidationHandler_ShouldReturnSubscriptionValidationHandler() + public async Task ResolveEventHandler_SubscriptionValidationHandler_ShouldReturnSubscriptionValidationHandler() { var factory = new EventHandlerResolver(new List() { new SubscriptionValidationHandler() }); diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs index 3c7194297..7b48d1f80 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs @@ -15,7 +15,7 @@ namespace Altinn.App.Core.Tests.Internal.Process; public class ProcessNavigatorTests { [Fact] - public async void GetNextTask_returns_next_element_if_no_gateway() + public async Task GetNextTask_returns_next_element_if_no_gateway() { IProcessNavigator processNavigator = SetupProcessNavigator( "simple-linear.bpmn", @@ -40,7 +40,7 @@ public async void GetNextTask_returns_next_element_if_no_gateway() } [Fact] - public async void NextFollowAndFilterGateways_returns_empty_list_if_no_outgoing_flows() + public async Task NextFollowAndFilterGateways_returns_empty_list_if_no_outgoing_flows() { IProcessNavigator processNavigator = SetupProcessNavigator( "simple-linear.bpmn", @@ -51,7 +51,7 @@ public async void NextFollowAndFilterGateways_returns_empty_list_if_no_outgoing_ } [Fact] - public async void GetNextTask_returns_default_if_no_filtering_is_implemented_and_default_set() + public async Task GetNextTask_returns_default_if_no_filtering_is_implemented_and_default_set() { IProcessNavigator processNavigator = SetupProcessNavigator( "simple-gateway-default.bpmn", @@ -80,7 +80,7 @@ public async void GetNextTask_returns_default_if_no_filtering_is_implemented_and } [Fact] - public async void GetNextTask_runs_custom_filter_and_returns_result() + public async Task GetNextTask_runs_custom_filter_and_returns_result() { IProcessNavigator processNavigator = SetupProcessNavigator( "simple-gateway-with-join-gateway.bpmn", @@ -111,7 +111,7 @@ public async void GetNextTask_runs_custom_filter_and_returns_result() } [Fact] - public async void GetNextTask_throws_ProcessException_if_multiple_targets_found() + public async Task GetNextTask_throws_ProcessException_if_multiple_targets_found() { IProcessNavigator processNavigator = SetupProcessNavigator( "simple-gateway-with-join-gateway.bpmn", @@ -128,7 +128,7 @@ public async void GetNextTask_throws_ProcessException_if_multiple_targets_found( } [Fact] - public async void GetNextTask_follows_downstream_gateways() + public async Task GetNextTask_follows_downstream_gateways() { IProcessNavigator processNavigator = SetupProcessNavigator( "simple-gateway-with-join-gateway.bpmn", @@ -150,7 +150,7 @@ public async void GetNextTask_follows_downstream_gateways() } [Fact] - public async void GetNextTask_runs_custom_filter_and_returns_empty_list_if_all_filtered_out() + public async Task GetNextTask_runs_custom_filter_and_returns_empty_list_if_all_filtered_out() { IProcessNavigator processNavigator = SetupProcessNavigator( "simple-gateway-with-join-gateway.bpmn", @@ -170,7 +170,7 @@ public async void GetNextTask_runs_custom_filter_and_returns_empty_list_if_all_f } [Fact] - public async void GetNextTask_returns_empty_list_if_element_has_no_next() + public async Task GetNextTask_returns_empty_list_if_element_has_no_next() { IProcessNavigator processNavigator = SetupProcessNavigator( "simple-gateway-with-join-gateway.bpmn", diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs index 83ebc772b..c974c9277 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs @@ -1,4 +1,3 @@ -#nullable enable using Altinn.App.Core.Configuration; using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Internal.App; diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs index 5a8efb82a..dd09a94ca 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs @@ -84,6 +84,10 @@ public ComponentContext ToContext(LayoutModel model) public static ComponentContextForTestSpec FromContext(ComponentContext context) { + ArgumentNullException.ThrowIfNull(context.Component); + ArgumentNullException.ThrowIfNull(context.Component.Id); + ArgumentNullException.ThrowIfNull(context.Component.PageId); + return new ComponentContextForTestSpec { ComponentId = context.Component.Id, From e191264f7b56c951516c0496aad0ea04773f41a6 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Wed, 8 May 2024 07:29:24 +0200 Subject: [PATCH 08/22] Fix issue where URL encoding was missing when building query string for GetInstances (#635) --- .../Clients/Storage/InstanceClient.cs | 14 +++----------- .../Implementation/InstanceClientTests.cs | 6 +++--- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs index 292e09fa7..81297460a 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs @@ -10,6 +10,7 @@ using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Utils; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -95,22 +96,13 @@ public async Task GetInstance(Instance instance) /// public async Task> GetInstances(Dictionary queryParams) { - StringBuilder apiUrl = new($"instances?"); - - foreach (var queryParameter in queryParams) - { - foreach (string? value in queryParameter.Value) - { - // TODO: remember to escape the value here - apiUrl.Append($"&{queryParameter.Key}={value}"); - } - } + var apiUrl = QueryHelpers.AddQueryString("instances", queryParams); string token = JwtTokenUtil.GetTokenFromContext( _httpContextAccessor.HttpContext, _settings.RuntimeCookieName ); - QueryResponse queryResponse = await QueryInstances(token, apiUrl.ToString()); + QueryResponse queryResponse = await QueryInstances(token, apiUrl); if (queryResponse.Count == 0) { diff --git a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs index 4afa1da12..8191aa0f8 100644 --- a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs @@ -437,7 +437,7 @@ public async Task QueryInstances_QueryResponseContainsNext() Count = 1, Instances = new List { new Instance { Id = $"{1337}/{Guid.NewGuid()}" } }, Next = - "https://platform.altinn.no/storage/api/instances?&appId=ttd/apps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false&continuationtoken=abcd" + "https://platform.altinn.no/storage/api/instances?appId=ttd%2Fapps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false&continuationtoken=abcd" }; QueryResponse queryResponse2 = @@ -448,9 +448,9 @@ public async Task QueryInstances_QueryResponseContainsNext() }; string urlPart1 = - "instances?&appId=ttd/apps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false"; + "instances?appId=ttd%2Fapps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false"; string urlPart2 = - "https://platform.altinn.no/storage/api/instances?&appId=ttd/apps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false&continuationtoken=abcd"; + "https://platform.altinn.no/storage/api/instances?appId=ttd%2Fapps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false&continuationtoken=abcd"; HttpResponseMessage httpResponseMessage1 = new HttpResponseMessage { From 141a4866bc72b7e50dd7dae273dec570cc954f74 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Wed, 8 May 2024 08:52:04 +0200 Subject: [PATCH 09/22] Add utility to make it simpler to assert on http responses. (#634) * Add utility to make it simpler to assert on http responses. This has to be a method on the custom WebApplicationFactory to access the _outputHelper * Update test/Altinn.App.Api.Tests/Utils/JsonUtils.cs --- .../Controllers/DataController_PutTests.cs | 54 +++++++------- .../CustomWebApplicationFactory.cs | 41 +++++++++++ test/Altinn.App.Api.Tests/Utils/JsonUtils.cs | 72 +++++++++++++++++++ .../Utils/ReflectionUtils.cs | 21 ++++++ 4 files changed, 158 insertions(+), 30 deletions(-) create mode 100644 test/Altinn.App.Api.Tests/Utils/JsonUtils.cs create mode 100644 test/Altinn.App.Api.Tests/Utils/ReflectionUtils.cs diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs index 7cf5b0a2e..f9d491cf0 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs @@ -16,9 +16,6 @@ namespace Altinn.App.Api.Tests.Controllers; public class DataController_PutTests : ApiTestBase, IClassFixture> { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; - private readonly Mock _dataProcessor = new(); public DataController_PutTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) @@ -46,9 +43,7 @@ public async Task PutDataElement_TestSinglePartUpdate_ReturnsOk() $"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", null ); - var createResponseContent = await createResponse.Content.ReadAsStringAsync(); - createResponse.StatusCode.Should().Be(HttpStatusCode.Created); - var createResponseParsed = JsonSerializer.Deserialize(createResponseContent, _jsonSerializerOptions)!; + var createResponseParsed = await VerifyStatusAndDeserialize(createResponse, HttpStatusCode.Created); var instanceId = createResponseParsed.Id; // Create data element (not sure why it isn't created when the instance is created, autoCreate is true) @@ -61,12 +56,11 @@ public async Task PutDataElement_TestSinglePartUpdate_ReturnsOk() $"/{org}/{app}/instances/{instanceId}/data?dataType=default", createDataElementContent ); - var createDataElementResponseContent = await createDataElementResponse.Content.ReadAsStringAsync(); - createDataElementResponse.StatusCode.Should().Be(HttpStatusCode.Created); - var createDataElementResponseParsed = JsonSerializer.Deserialize( - createDataElementResponseContent, - _jsonSerializerOptions - )!; + + var createDataElementResponseParsed = await VerifyStatusAndDeserialize( + createDataElementResponse, + HttpStatusCode.Created + ); var dataGuid = createDataElementResponseParsed.Id; // Update data element @@ -83,9 +77,10 @@ public async Task PutDataElement_TestSinglePartUpdate_ReturnsOk() // Verify stored data var readDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); - readDataElementResponse.StatusCode.Should().Be(HttpStatusCode.OK); - var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); - var readDataElementResponseParsed = JsonSerializer.Deserialize(readDataElementResponseContent)!; + var readDataElementResponseParsed = await VerifyStatusAndDeserialize( + readDataElementResponse, + HttpStatusCode.OK + ); readDataElementResponseParsed.Melding!.Name.Should().Be("Ola Olsen"); _dataProcessor.Verify( @@ -152,9 +147,7 @@ public async Task PutDataElement_TestMultiPartUpdateWithCustomDataProcessor_Retu $"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", null ); - var createResponseContent = await createResponse.Content.ReadAsStringAsync(); - createResponse.StatusCode.Should().Be(HttpStatusCode.Created); - var createResponseParsed = JsonSerializer.Deserialize(createResponseContent, _jsonSerializerOptions)!; + var createResponseParsed = await VerifyStatusAndDeserialize(createResponse, HttpStatusCode.Created); var instanceId = createResponseParsed.Id; // Create data element (not sure why it isn't created when the instance is created, autoCreate is true) @@ -167,11 +160,9 @@ public async Task PutDataElement_TestMultiPartUpdateWithCustomDataProcessor_Retu $"/{org}/{app}/instances/{instanceId}/data?dataType=default", createDataElementContent ); - var createDataElementResponseContent = await createDataElementResponse.Content.ReadAsStringAsync(); - createDataElementResponse.StatusCode.Should().Be(HttpStatusCode.Created); - var createDataElementResponseParsed = JsonSerializer.Deserialize( - createDataElementResponseContent, - _jsonSerializerOptions + var createDataElementResponseParsed = await VerifyStatusAndDeserialize( + createDataElementResponse, + HttpStatusCode.Created )!; var dataGuid = createDataElementResponseParsed.Id; @@ -179,10 +170,10 @@ public async Task PutDataElement_TestMultiPartUpdateWithCustomDataProcessor_Retu var firstReadDataElementResponse = await client.GetAsync( $"/{org}/{app}/instances/{instanceId}/data/{dataGuid}" ); - var firstReadDataElementResponseContent = await firstReadDataElementResponse.Content.ReadAsStringAsync(); - var firstReadDataElementResponseParsed = JsonSerializer.Deserialize( - firstReadDataElementResponseContent - )!; + var firstReadDataElementResponseParsed = await VerifyStatusAndDeserialize( + firstReadDataElementResponse, + HttpStatusCode.OK + ); firstReadDataElementResponseParsed.Melding!.Name.Should().Be("Ivar"); firstReadDataElementResponseParsed.Melding.Toggle.Should().BeFalse(); @@ -196,12 +187,15 @@ public async Task PutDataElement_TestMultiPartUpdateWithCustomDataProcessor_Retu $"/{org}/{app}/instances/{instanceId}/data/{dataGuid}", updateDataElementContent ); - response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Should().HaveStatusCode(HttpStatusCode.OK); // Verify stored data var readDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); - var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); - var readDataElementResponseParsed = JsonSerializer.Deserialize(readDataElementResponseContent)!; + + var readDataElementResponseParsed = await VerifyStatusAndDeserialize( + readDataElementResponse, + HttpStatusCode.OK + )!; readDataElementResponseParsed.Melding!.Name.Should().Be("Ola Olsen"); readDataElementResponseParsed.Melding.Toggle.Should().BeTrue(); diff --git a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs index 2bf7b0bf9..5c8c64def 100644 --- a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs +++ b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs @@ -1,7 +1,10 @@ +using System.Net; using System.Net.Http.Headers; +using System.Text.Json; using Altinn.App.Api.Tests.Data; using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Configuration; +using FluentAssertions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; @@ -15,6 +18,12 @@ namespace Altinn.App.Api.Tests; public class ApiTestBase { + protected static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; + protected readonly ITestOutputHelper _outputHelper; private readonly WebApplicationFactory _factory; @@ -116,6 +125,38 @@ private void ConfigureFakeHttpClientHandler(IServiceCollection services) )); } + /// + /// Set this in your test class constructor + /// + protected async Task VerifyStatusAndDeserialize( + HttpResponseMessage response, + HttpStatusCode expectedStatusCode + ) + { + // Verify status code + response.Should().HaveStatusCode(expectedStatusCode); + + // Deserialize content and log everything if it fails + var content = await response.Content.ReadAsStringAsync(); + try + { + return JsonSerializer.Deserialize(content, _jsonSerializerOptions) + ?? throw new JsonException("Content was \"null\""); + } + catch (Exception) + { + _outputHelper.WriteLine(string.Empty); + _outputHelper.WriteLine(string.Empty); + _outputHelper.WriteLine( + $"Failed to deserialize content of {response.RequestMessage?.Method} request to {response.RequestMessage?.RequestUri} as {ReflectionUtils.GetTypeNameWithGenericArguments()}:" + ); + + _outputHelper.WriteLine(JsonUtils.IndentJson(content)); + _outputHelper.WriteLine(string.Empty); + throw; + } + } + /// /// Set this in your test class constructor to make the same overrides for all tests. /// diff --git a/test/Altinn.App.Api.Tests/Utils/JsonUtils.cs b/test/Altinn.App.Api.Tests/Utils/JsonUtils.cs new file mode 100644 index 000000000..27e6894a4 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Utils/JsonUtils.cs @@ -0,0 +1,72 @@ +namespace Altinn.App.Api.Tests.Utils; + +using System.Text; +using System.Text.Json; +using System.Text.Unicode; + +public static class JsonUtils +{ + public static string IndentJson(string json) + { + try + { + var bytes = Encoding.UTF8.GetBytes(json); + var parser = new Utf8JsonReader(bytes); + using var outStream = new MemoryStream(bytes.Length * 2); + using var writer = new Utf8JsonWriter((outStream), new JsonWriterOptions { Indented = true }); + + while (parser.Read()) + { + switch (parser.TokenType) + { + case JsonTokenType.StartObject: + writer.WriteStartObject(); + break; + case JsonTokenType.EndObject: + writer.WriteEndObject(); + break; + case JsonTokenType.StartArray: + writer.WriteStartArray(); + break; + case JsonTokenType.EndArray: + writer.WriteEndArray(); + break; + case JsonTokenType.PropertyName: + writer.WritePropertyName(parser.GetString()!); + break; + case JsonTokenType.String: + writer.WriteStringValue(parser.GetString()); + break; + case JsonTokenType.Number: + if (parser.TryGetInt64(out long longValue)) + { + writer.WriteNumberValue(longValue); + } + else if (parser.TryGetDouble(out double doubleValue)) + { + writer.WriteNumberValue(doubleValue); + } + break; + case JsonTokenType.True: + writer.WriteBooleanValue(true); + break; + case JsonTokenType.False: + writer.WriteBooleanValue(false); + break; + case JsonTokenType.Null: + writer.WriteNullValue(); + break; + } + } + + writer.Flush(); + outStream.Position = 0; + + return Encoding.UTF8.GetString(outStream.ToArray()); + } + catch (JsonException) + { + return json; + } + } +} diff --git a/test/Altinn.App.Api.Tests/Utils/ReflectionUtils.cs b/test/Altinn.App.Api.Tests/Utils/ReflectionUtils.cs new file mode 100644 index 000000000..6f013f396 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Utils/ReflectionUtils.cs @@ -0,0 +1,21 @@ +namespace Altinn.App.Api.Tests.Utils; + +public class ReflectionUtils +{ + public static string GetTypeNameWithGenericArguments() + { + return GetTypeNameWithGenericArguments(typeof(T)); + } + + private static string GetTypeNameWithGenericArguments(Type type) + { + if (!type.IsGenericType) + { + return type.Name; + } + + var genericArguments = type.GetGenericArguments(); + var genericArgumentsString = string.Join(", ", genericArguments.Select(GetTypeNameWithGenericArguments)); + return $"{type.Name.Split('`')[0]}<{genericArgumentsString}>"; // Remove the `1, `2, etc. from the type name + } +} From 066b021271105c15a6b46a167a87d9e4e5597adc Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Wed, 8 May 2024 09:20:39 +0200 Subject: [PATCH 10/22] Fix broken build because of warnings in tests (#637) --- .../Controllers/ApplicationMetadataControllerTests.cs | 2 -- .../Controllers/DataController_PatchTests.cs | 2 +- .../Controllers/InstancesController_PostNewInstance.cs | 3 --- .../Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs | 2 +- .../Controllers/ValidateController_ValidateInstanceTests.cs | 2 +- 5 files changed, 3 insertions(+), 8 deletions(-) diff --git a/test/Altinn.App.Api.Tests/Controllers/ApplicationMetadataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ApplicationMetadataControllerTests.cs index e36813a7e..3be5e17cd 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ApplicationMetadataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ApplicationMetadataControllerTests.cs @@ -12,8 +12,6 @@ namespace Altinn.App.Api.Tests.Controllers; public class ApplicationMetadataControllerTests : ApiTestBase, IClassFixture> { - private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); - private readonly Mock _appMetadataMock = new(); public ApplicationMetadataControllerTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs index 297714f4b..af874136a 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs @@ -26,7 +26,7 @@ namespace Altinn.App.Api.Tests.Controllers; public class DataControllerPatchTests : ApiTestBase, IClassFixture> { - private static readonly JsonSerializerOptions _jsonSerializerOptions = + private static new readonly JsonSerializerOptions _jsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs index 37e76f397..1cad512aa 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs @@ -16,9 +16,6 @@ namespace Altinn.App.Api.Tests.Controllers; public class InstancesController_PostNewInstanceTests : ApiTestBase, IClassFixture> { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; - private readonly Mock _dataProcessor = new(); public InstancesController_PostNewInstanceTests( diff --git a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs index d8ca4980f..66d7bd62c 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs @@ -27,7 +27,7 @@ public class ProcessControllerTests : ApiTestBase, IClassFixture Date: Tue, 14 May 2024 12:47:17 +0200 Subject: [PATCH 11/22] chore(deps): update nuget non-major dependencies (main) (#639) --- src/Altinn.App.Api/Altinn.App.Api.csproj | 2 +- src/Altinn.App.Core/Altinn.App.Core.csproj | 6 +-- .../Altinn.App.Api.Tests/OpenApi/swagger.json | 39 ++++++------------- .../Altinn.App.Api.Tests/OpenApi/swagger.yaml | 30 +++++--------- .../Altinn.App.Core.Tests.csproj | 2 +- 5 files changed, 27 insertions(+), 52 deletions(-) diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index a66f13d85..80c867601 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 00030a741..6d1b313b3 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -12,12 +12,12 @@ - + - + - + diff --git a/test/Altinn.App.Api.Tests/OpenApi/swagger.json b/test/Altinn.App.Api.Tests/OpenApi/swagger.json index 1bb779d1d..c57941e5a 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/swagger.json +++ b/test/Altinn.App.Api.Tests/OpenApi/swagger.json @@ -5324,20 +5324,6 @@ }, "additionalProperties": false }, - "JsonPointer": { - "type": "object", - "properties": { - "segments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PointerSegment" - }, - "nullable": true, - "readOnly": true - } - }, - "additionalProperties": false - }, "Logo": { "type": "object", "properties": { @@ -5424,10 +5410,20 @@ "$ref": "#/components/schemas/OperationType" }, "from": { - "$ref": "#/components/schemas/JsonPointer" + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "readOnly": true }, "path": { - "$ref": "#/components/schemas/JsonPointer" + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "readOnly": true }, "value": { "$ref": "#/components/schemas/JsonNode" @@ -5435,17 +5431,6 @@ }, "additionalProperties": false }, - "PointerSegment": { - "type": "object", - "properties": { - "value": { - "type": "string", - "nullable": true, - "readOnly": true - } - }, - "additionalProperties": false - }, "ProblemDetails": { "type": "object", "properties": { diff --git a/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml b/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml index 3192ba1dd..394eaa1c9 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml +++ b/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml @@ -3370,16 +3370,6 @@ components: $ref: '#/components/schemas/PatchOperation' nullable: true additionalProperties: false - JsonPointer: - type: object - properties: - segments: - type: array - items: - $ref: '#/components/schemas/PointerSegment' - nullable: true - readOnly: true - additionalProperties: false Logo: type: object properties: @@ -3442,19 +3432,19 @@ components: op: $ref: '#/components/schemas/OperationType' from: - $ref: '#/components/schemas/JsonPointer' + type: array + items: + type: string + nullable: true + readOnly: true path: - $ref: '#/components/schemas/JsonPointer' - value: - $ref: '#/components/schemas/JsonNode' - additionalProperties: false - PointerSegment: - type: object - properties: - value: - type: string + type: array + items: + type: string nullable: true readOnly: true + value: + $ref: '#/components/schemas/JsonNode' additionalProperties: false ProblemDetails: type: object diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index b7740b4a2..a360e5532 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -43,7 +43,7 @@ - + From d228eafb9693c2b3ebe3a72e2acf9a385c88f9ea Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Tue, 14 May 2024 13:11:44 +0200 Subject: [PATCH 12/22] Cleanup analyzers and StyleCop config, enable builtint .NET static analysis (#641) --- .editorconfig | 13 +- Altinn3.ruleset | 177 ------------- Directory.Build.props | 4 + Settings.StyleCop | 237 ------------------ src/Altinn.App.Api/Altinn.App.Api.csproj | 19 -- .../Controllers/DataController.cs | 2 +- .../Controllers/DataTagsController.cs | 3 +- .../Controllers/InstancesController.cs | 4 +- .../Controllers/ProcessController.cs | 2 +- .../RequestHandling/MultipartRequestReader.cs | 2 +- ...eryTokenIfAuthCookieAuthorizationFilter.cs | 10 +- .../EformidlingStatusCheckEventHandler.cs | 10 +- .../EformidlingStatusCheckEventHandler2.cs | 10 +- .../ConfigurationBuilderExtensions.cs | 2 +- .../Features/Options/AppOptionsFactory.cs | 2 +- .../Options/InstanceAppOptionsFactory.cs | 2 +- src/Altinn.App.Core/Helpers/AppTextHelper.cs | 6 +- src/Altinn.App.Core/Helpers/DataHelper.cs | 2 +- .../Helpers/DataModel/DataModel.cs | 8 +- src/Altinn.App.Core/Helpers/MimeTypeMap.cs | 18 +- .../Helpers/RemoveBomExtentions.cs | 2 +- src/Altinn.App.Core/Helpers/SelfLinkHelper.cs | 4 +- .../Implementation/PrefillSI.cs | 8 +- .../Clients/Storage/DataClient.cs | 2 +- .../Internal/Events/EventHandlerResolver.cs | 2 +- .../Internal/Expressions/LayoutEvaluator.cs | 2 +- .../Expressions/LayoutEvaluatorState.cs | 2 +- .../Internal/Pdf/PdfService.cs | 8 +- src/Altinn.App.Core/Models/AppIdentifier.cs | 5 +- .../Models/Expressions/ComponentContext.cs | 2 +- .../Models/Layout/LayoutModel.cs | 2 +- stylecop.json | 69 ----- .../Altinn.App.Api.Tests.csproj | 6 - .../ValidateControllerValidateDataTests.cs | 2 +- .../Altinn.App.Core.Tests.csproj | 15 -- .../GlobalSuppressions.cs | 13 - .../Helpers/JsonDataModel.cs | 8 +- .../ObjectUtils_XmlSerializationTests.cs | 1 - .../Implementation/InstanceClientTests.cs | 25 +- .../FullTests/Test1/RunTest1.cs | 2 +- .../LayoutExpressions/TestDataModel.cs | 66 ++--- test/Directory.Build.props | 15 ++ 42 files changed, 140 insertions(+), 654 deletions(-) delete mode 100644 Altinn3.ruleset delete mode 100644 Settings.StyleCop delete mode 100644 stylecop.json delete mode 100644 test/Altinn.App.Core.Tests/GlobalSuppressions.cs create mode 100644 test/Directory.Build.props diff --git a/.editorconfig b/.editorconfig index 88adceb6a..5fb6a86b8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -73,7 +73,6 @@ max_line_length = 160 end_of_line = crlf dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -[*.cs] csharp_using_directive_placement = outside_namespace:silent csharp_prefer_simple_using_statement = true:suggestion csharp_prefer_braces = true:silent @@ -91,6 +90,18 @@ csharp_style_expression_bodied_local_functions = false:silent csharp_indent_labels = one_less_than_current csharp_style_prefer_primary_constructors = false:suggestion +# CA1848: Use the LoggerMessage delegates +dotnet_diagnostic.CA1848.severity = none + +# CA1727: Use PascalCase for named placeholders +dotnet_diagnostic.CA1727.severity = suggestion + +# CA2254: Template should be a static expression +dotnet_diagnostic.CA2254.severity = none + +# CA1822: Mark members as static +dotnet_diagnostic.CA1822.severity = suggestion + [Program.cs] dotnet_diagnostic.CA1050.severity = none dotnet_diagnostic.S1118.severity = none diff --git a/Altinn3.ruleset b/Altinn3.ruleset deleted file mode 100644 index 144dabf85..000000000 --- a/Altinn3.ruleset +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Directory.Build.props b/Directory.Build.props index 8ccd36190..7f5799428 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,6 +5,10 @@ latest + true + Minimum + strict + diff --git a/Settings.StyleCop b/Settings.StyleCop deleted file mode 100644 index fd124ef02..000000000 --- a/Settings.StyleCop +++ /dev/null @@ -1,237 +0,0 @@ - - - - preprocessor, pre-processor - shortlived, short-lived - - - altinn - arbeidsgiveravgift - aspx - BankID - brreg - Buypass - Commfides - compat - Compat.browser - Creuna - css - dequeue - Dequeue - deserializing - Determinator - enum - en-US - formset - Functoid - ID-Porten - js - leveranse - linq - msdn - oppgave - orid - participant - Porten - psa - referer - reportee - sone - ssn - subform - subforms - virksomhet - Winnovative - xfd - xsd - Guid - Api - OAuth - Auth - mpcId - mpc - Sdp - Difi - Difis - Rijndael - eq - orderby - Oppgaveregister - Seres - reportees - - 10000 - - - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - True - - - - - - - False - - - - - - - - - - False - - - - - False - - - - - - a1 - as - at - d - db - dn - do - dr - ds - dt - e - e2 - er - f - fs - go - id - if - in - ip - is - js - li - my - no - ns - on - or - pi - pv - sa - sb - se - si - so - sp - tc - to - tr - ui - un - wf - ws - x - y - - - - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - - - - - - False - - - - - - - - - - False - - - - - - - \ No newline at end of file diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index 80c867601..c3f17fbab 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -26,29 +26,10 @@ - - - all - runtime; build; native; contentfiles; analyzers - - - stylecop.json - - - - - ..\..\Altinn3.ruleset - - - - true - $(NoWarn);1591 - - diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index 3bd87dd4b..d1931c270 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -586,7 +586,7 @@ [FromRoute] Guid dataGuid } } - private ActionResult ExceptionResponse(Exception exception, string message) + private ObjectResult ExceptionResponse(Exception exception, string message) { _logger.LogError(exception, message); diff --git a/src/Altinn.App.Api/Controllers/DataTagsController.cs b/src/Altinn.App.Api/Controllers/DataTagsController.cs index 2de8025fa..49b3cabcb 100644 --- a/src/Altinn.App.Api/Controllers/DataTagsController.cs +++ b/src/Altinn.App.Api/Controllers/DataTagsController.cs @@ -168,9 +168,8 @@ [FromRoute] string tag return NotFound("Unable to find data element based on the given parameters."); } - if (dataElement.Tags.Contains(tag)) + if (dataElement.Tags.Remove(tag)) { - dataElement.Tags.Remove(tag); await _dataClient.Update(instance, dataElement); } diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index 257e04eb9..a32dfa55e 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -194,7 +194,7 @@ [FromQuery] int? instanceOwnerPartyId MultipartRequestReader parsedRequest = new MultipartRequestReader(Request); await parsedRequest.Read(); - if (parsedRequest.Errors.Any()) + if (parsedRequest.Errors.Count != 0) { return BadRequest($"Error when reading content: {JsonConvert.SerializeObject(parsedRequest.Errors)}"); } @@ -793,7 +793,7 @@ int instanceOwnerPartyId List activeInstances = await _instanceClient.GetInstances(queryParams); - if (!activeInstances.Any()) + if (activeInstances.Count == 0) { return Ok(new List()); } diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 0d4828b5b..190ed80c5 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -631,7 +631,7 @@ private async Task ConvertAndAuthorizeActions(Instance instance return appProcessState; } - private ActionResult ExceptionResponse(Exception exception, string message) + private ObjectResult ExceptionResponse(Exception exception, string message) { _logger.LogError(exception, message); diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs b/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs index 224d46b1c..e9d53242a 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs @@ -30,7 +30,7 @@ public bool IsMultipart get { return !string.IsNullOrEmpty(request.ContentType) - && request.ContentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0; + && request.ContentType.Contains("multipart/", StringComparison.OrdinalIgnoreCase); } } diff --git a/src/Altinn.App.Api/Infrastructure/Filters/ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter.cs b/src/Altinn.App.Api/Infrastructure/Filters/ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter.cs index fe6023452..6f305dc93 100644 --- a/src/Altinn.App.Api/Infrastructure/Filters/ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter.cs +++ b/src/Altinn.App.Api/Infrastructure/Filters/ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter.cs @@ -27,10 +27,7 @@ public ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter( IOptionsMonitor settings ) { - if (antiforgery == null) - { - throw new ArgumentNullException(nameof(antiforgery)); - } + ArgumentNullException.ThrowIfNull(antiforgery); _antiforgery = antiforgery; _settings = settings.CurrentValue; @@ -64,10 +61,7 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext context) /// True if validation is needed. protected virtual bool ShouldValidate(AuthorizationFilterContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + ArgumentNullException.ThrowIfNull(context); string method = context.HttpContext.Request.Method; if ( diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs index 00cc4b75c..6a704f3ec 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs @@ -223,8 +223,10 @@ private async Task GetStatusesForShipment(string shipmentId) private static bool MessageDeliveredToKS(Statuses statuses) { - return statuses.Content.FirstOrDefault(s => s.Status.ToLower() == "levert" || s.Status.ToLower() == "lest") - != null; + return statuses.Content.FirstOrDefault(s => + s.Status.Equals("levert", StringComparison.OrdinalIgnoreCase) + || s.Status.Equals("lest", StringComparison.OrdinalIgnoreCase) + ) != null; } private static bool MessageTimedOutToKS(Statuses statuses, out string errorMessage) @@ -244,7 +246,9 @@ private static (bool Error, string ErrorMessage) CheckErrorStatus(Statuses statu bool isError = false; string errorMessage = string.Empty; - var status = statuses.Content.FirstOrDefault(s => s.Status.ToLower() == errorStatus); + var status = statuses.Content.FirstOrDefault(s => + s.Status.Equals(errorStatus, StringComparison.OrdinalIgnoreCase) + ); if (status != null) { isError = true; diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs index a8e88452d..b5b23f5c6 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs @@ -197,8 +197,10 @@ private async Task GetStatusesForShipment(string shipmentId) private static bool MessageDeliveredToKS(Statuses statuses) { - return statuses.Content.FirstOrDefault(s => s.Status.ToLower() == "levert" || s.Status.ToLower() == "lest") - != null; + return statuses.Content.FirstOrDefault(s => + s.Status.Equals("levert", StringComparison.OrdinalIgnoreCase) + || s.Status.Equals("lest", StringComparison.OrdinalIgnoreCase) + ) != null; } private static bool MessageTimedOutToKS(Statuses statuses, out string errorMessage) @@ -218,7 +220,9 @@ private static (bool Error, string ErrorMessage) CheckErrorStatus(Statuses statu bool isError = false; string errorMessage = string.Empty; - var status = statuses.Content.FirstOrDefault(s => s.Status.ToLower() == errorStatus); + var status = statuses.Content.FirstOrDefault(s => + s.Status.Equals(errorStatus, StringComparison.OrdinalIgnoreCase) + ); if (status != null) { isError = true; diff --git a/src/Altinn.App.Core/Extensions/ConfigurationBuilderExtensions.cs b/src/Altinn.App.Core/Extensions/ConfigurationBuilderExtensions.cs index fb6fe10a4..9d0301c85 100644 --- a/src/Altinn.App.Core/Extensions/ConfigurationBuilderExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ConfigurationBuilderExtensions.cs @@ -34,7 +34,7 @@ public static void LoadAppConfig(this IConfigurationBuilder builder, string[]? a // Add values from environment and command line arguments last, to override values from other sources. builder.AddEnvironmentVariables(); - builder.AddCommandLine(args ?? new string[0]); + builder.AddCommandLine(args ?? []); } } } diff --git a/src/Altinn.App.Core/Features/Options/AppOptionsFactory.cs b/src/Altinn.App.Core/Features/Options/AppOptionsFactory.cs index 84338d417..107ff7cea 100644 --- a/src/Altinn.App.Core/Features/Options/AppOptionsFactory.cs +++ b/src/Altinn.App.Core/Features/Options/AppOptionsFactory.cs @@ -29,7 +29,7 @@ public IAppOptionsProvider GetOptionsProvider(string optionsId) foreach (var appOptionProvider in AppOptionsProviders) { - if (appOptionProvider.Id.ToLower() != optionsId.ToLower()) + if (!appOptionProvider.Id.Equals(optionsId, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/src/Altinn.App.Core/Features/Options/InstanceAppOptionsFactory.cs b/src/Altinn.App.Core/Features/Options/InstanceAppOptionsFactory.cs index d796ef8f1..a859f8ed0 100644 --- a/src/Altinn.App.Core/Features/Options/InstanceAppOptionsFactory.cs +++ b/src/Altinn.App.Core/Features/Options/InstanceAppOptionsFactory.cs @@ -25,7 +25,7 @@ public IInstanceAppOptionsProvider GetOptionsProvider(string optionsId) { foreach (var instanceAppOptionProvider in InstanceAppOptionsProviders) { - if (instanceAppOptionProvider.Id.ToLower() != optionsId.ToLower()) + if (!instanceAppOptionProvider.Id.Equals(optionsId, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/src/Altinn.App.Core/Helpers/AppTextHelper.cs b/src/Altinn.App.Core/Helpers/AppTextHelper.cs index f8939e862..8d8642846 100644 --- a/src/Altinn.App.Core/Helpers/AppTextHelper.cs +++ b/src/Altinn.App.Core/Helpers/AppTextHelper.cs @@ -21,11 +21,11 @@ string languageId ) { string text = key; - if (serviceText != null && serviceText.ContainsKey(key)) + if (serviceText != null && serviceText.TryGetValue(key, out var serviceTextMap)) { - if (serviceText[key].ContainsKey(languageId)) + if (serviceTextMap.TryGetValue(languageId, out var lookupText)) { - text = serviceText[key][languageId]; + text = lookupText; } if (textParams != null && textParams.Count > 0) diff --git a/src/Altinn.App.Core/Helpers/DataHelper.cs b/src/Altinn.App.Core/Helpers/DataHelper.cs index 20bf0314a..916e4c88a 100644 --- a/src/Altinn.App.Core/Helpers/DataHelper.cs +++ b/src/Altinn.App.Core/Helpers/DataHelper.cs @@ -133,7 +133,7 @@ object data string key = entry.Key; string? value = entry.Value; - if (originalDictionary.ContainsKey(key) && originalDictionary[key] != value) + if (originalDictionary.TryGetValue(key, out string? originalValue) && originalValue != value) { updatedValues.Add(key, value); } diff --git a/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs b/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs index eb01db83f..6dff9697b 100644 --- a/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs +++ b/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs @@ -98,7 +98,7 @@ public string[] GetResolvedKeys(string key) { if (_serviceModel is null) { - return new string[0]; + return []; } var keyParts = key.Split('.'); @@ -144,12 +144,12 @@ private string[] GetResolvedKeysRecursive( { if (currentModel is null) { - return new string[0]; + return []; } if (currentIndex == keyParts.Length) { - return new[] { currentKey }; + return [currentKey]; } var (key, groupIndex) = ParseKeyPart(keyParts[currentIndex]); @@ -157,7 +157,7 @@ private string[] GetResolvedKeysRecursive( var childModel = prop?.GetValue(currentModel); if (childModel is null) { - return new string[0]; + return []; } if (childModel is not string && childModel is System.Collections.IEnumerable childModelList) diff --git a/src/Altinn.App.Core/Helpers/MimeTypeMap.cs b/src/Altinn.App.Core/Helpers/MimeTypeMap.cs index 12ed19300..10f572a69 100644 --- a/src/Altinn.App.Core/Helpers/MimeTypeMap.cs +++ b/src/Altinn.App.Core/Helpers/MimeTypeMap.cs @@ -1,3 +1,4 @@ +using System.Collections.Frozen; using Altinn.App.Core.Models; namespace Altinn.App.Core.Helpers @@ -7,11 +8,11 @@ namespace Altinn.App.Core.Helpers /// public static class MimeTypeMap { - private static readonly Lazy> _mappings = new Lazy>( - BuildMappings - ); + private static readonly Lazy> _mappings = new Lazy< + FrozenDictionary + >(BuildMappings); - private static IDictionary BuildMappings() + private static FrozenDictionary BuildMappings() { var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase) { @@ -620,7 +621,7 @@ private static IDictionary BuildMappings() #endregion }; - return mappings; + return mappings.ToFrozenDictionary(); } /// @@ -630,12 +631,9 @@ private static IDictionary BuildMappings() /// The mime type public static MimeType GetMimeType(string extension) { - if (extension == null) - { - throw new ArgumentNullException(nameof(extension)); - } + ArgumentNullException.ThrowIfNull(extension); - if (!extension.StartsWith(".")) + if (!extension.StartsWith('.')) { extension = "." + extension; } diff --git a/src/Altinn.App.Core/Helpers/RemoveBomExtentions.cs b/src/Altinn.App.Core/Helpers/RemoveBomExtentions.cs index 1f4c903a8..b2c96aafe 100644 --- a/src/Altinn.App.Core/Helpers/RemoveBomExtentions.cs +++ b/src/Altinn.App.Core/Helpers/RemoveBomExtentions.cs @@ -2,7 +2,7 @@ namespace Altinn.App.Core.Helpers; internal static class RemoveBomExtentions { - private static readonly byte[] _utf8bom = new byte[] { 0xEF, 0xBB, 0xBF }; + private static readonly byte[] _utf8bom = [0xEF, 0xBB, 0xBF]; internal static ReadOnlySpan RemoveBom(this byte[] bytes) { diff --git a/src/Altinn.App.Core/Helpers/SelfLinkHelper.cs b/src/Altinn.App.Core/Helpers/SelfLinkHelper.cs index 396e3a4e2..2dbcde16a 100644 --- a/src/Altinn.App.Core/Helpers/SelfLinkHelper.cs +++ b/src/Altinn.App.Core/Helpers/SelfLinkHelper.cs @@ -24,7 +24,7 @@ public static void SetInstanceAppSelfLinks(Instance instance, HttpRequest reques int start = selfLink.IndexOf("/instances"); if (start > 0) { - selfLink = selfLink.Substring(0, start) + "/instances"; + selfLink = string.Concat(selfLink.AsSpan(0, start), "/instances"); } selfLink += $"/{instance.Id}"; @@ -69,7 +69,7 @@ HttpRequest request int start = selfLink.IndexOf("/instances"); if (start > 0) { - selfLink = selfLink.Substring(0, start) + "/instances"; + selfLink = string.Concat(selfLink.AsSpan(0, start), "/instances"); } selfLink += $"/{instanceOwnerPartyId}/{instanceGuid.ToString()}"; diff --git a/src/Altinn.App.Core/Implementation/PrefillSI.cs b/src/Altinn.App.Core/Implementation/PrefillSI.cs index 6844898b8..f4e60aae9 100644 --- a/src/Altinn.App.Core/Implementation/PrefillSI.cs +++ b/src/Altinn.App.Core/Implementation/PrefillSI.cs @@ -110,7 +110,7 @@ public async Task PrefillDataModel( JObject userProfileJsonObject = JObject.FromObject(userProfile); _logger.LogInformation($"Started prefill from {USER_PROFILE_KEY}"); LoopThroughDictionaryAndAssignValuesToDataModel( - SwapKeyValuesForPrefil(userProfileDict), + SwapKeyValuesForPrefill(userProfileDict), userProfileJsonObject, dataModel ); @@ -138,7 +138,7 @@ public async Task PrefillDataModel( JObject orgJsonObject = JObject.FromObject(org); _logger.LogInformation($"Started prefill from {ER_KEY}"); LoopThroughDictionaryAndAssignValuesToDataModel( - SwapKeyValuesForPrefil(enhetsregisterPrefill), + SwapKeyValuesForPrefill(enhetsregisterPrefill), orgJsonObject, dataModel ); @@ -165,7 +165,7 @@ public async Task PrefillDataModel( JObject personJsonObject = JObject.FromObject(person); _logger.LogInformation($"Started prefill from {DSF_KEY}"); LoopThroughDictionaryAndAssignValuesToDataModel( - SwapKeyValuesForPrefil(folkeregisterPrefill), + SwapKeyValuesForPrefill(folkeregisterPrefill), personJsonObject, dataModel ); @@ -288,7 +288,7 @@ private void LoopThroughDictionaryAndAssignValuesToDataModel( } } - private Dictionary SwapKeyValuesForPrefil(Dictionary externalPrefil) + private Dictionary SwapKeyValuesForPrefill(Dictionary externalPrefil) { return externalPrefil.ToDictionary(x => x.Value, x => x.Key); } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index 267655bbf..ce2679768 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -101,7 +101,7 @@ Type type _logger.Log( LogLevel.Error, - "unable to save form data for instance{0} due to response {1}", + "unable to save form data for instance {InstanceId} due to response {StatusCode}", instance.Id, response.StatusCode ); diff --git a/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs b/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs index 02959f232..aaae48cee 100644 --- a/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs +++ b/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs @@ -23,7 +23,7 @@ public IEventHandler ResolveEventHandler(string eventType) foreach (var handler in _eventHandlers) { - if (handler.EventType.ToLower() != eventType.ToLower()) + if (!handler.EventType.Equals(eventType, StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs index b8a96c1bf..d40942805 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs @@ -77,7 +77,7 @@ context.Component is RepeatingGroupComponent repGroup { foreach (var index in Enumerable.Range(0, context.RowLength.Value).Reverse()) { - var rowIndices = context.RowIndices?.Append(index).ToArray() ?? new[] { index }; + var rowIndices = context.RowIndices?.Append(index).ToArray() ?? [index]; var indexedBinding = state.AddInidicies(repGroup.DataModelBindings["group"], rowIndices); if (context.HiddenRows.Contains(index)) { diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs index 5544bf19f..a249e2c4e 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs @@ -347,7 +347,7 @@ context.Component is RepeatingGroupComponent repGroup var hiddenRows = new List(); foreach (var index in Enumerable.Range(0, context.RowLength.Value)) { - var rowIndices = context.RowIndices?.Append(index).ToArray() ?? new[] { index }; + var rowIndices = context.RowIndices?.Append(index).ToArray() ?? [index]; var childContexts = context.ChildContexts.Where(c => c.RowIndices?.Last() == index); var rowContext = new ComponentContext(context.Component, rowIndices, null, childContexts); var rowHidden = ExpressionEvaluator.EvaluateBooleanExpression(this, rowContext, "hiddenRow", false); diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index 187fb428e..17968c818 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -68,7 +68,7 @@ public async Task GenerateAndStorePdf(Instance instance, string taskId, Cancella // Avoid a costly call if the language is allready overriden by the user language = string.IsNullOrEmpty(language) ? await GetLanguage() : language; - var pdfContent = await GeneratePdfContent(instance, taskId, ct, language); + var pdfContent = await GeneratePdfContent(instance, taskId, language, ct); var appIdentifier = new AppIdentifier(instance); @@ -84,14 +84,14 @@ public async Task GeneratePdf(Instance instance, string taskId, Cancella // Avoid a costly call if the language is allready overriden by the user language = string.IsNullOrEmpty(language) ? await GetLanguage() : language; - return await GeneratePdfContent(instance, taskId, ct, language); + return await GeneratePdfContent(instance, taskId, language, ct); } private async Task GeneratePdfContent( Instance instance, string taskId, - CancellationToken ct, - string language + string language, + CancellationToken ct ) { var baseUrl = _generalSettings.FormattedExternalAppBaseUrl(new AppIdentifier(instance)); diff --git a/src/Altinn.App.Core/Models/AppIdentifier.cs b/src/Altinn.App.Core/Models/AppIdentifier.cs index 7d90c74df..5bb3af0e1 100644 --- a/src/Altinn.App.Core/Models/AppIdentifier.cs +++ b/src/Altinn.App.Core/Models/AppIdentifier.cs @@ -117,10 +117,7 @@ public override int GetHashCode() /// A new instance of public static AppIdentifier CreateFromUrl(string url) { - if (url == null) - { - throw new ArgumentNullException(nameof(url)); - } + ArgumentNullException.ThrowIfNull(url); if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) { diff --git a/src/Altinn.App.Core/Models/Expressions/ComponentContext.cs b/src/Altinn.App.Core/Models/Expressions/ComponentContext.cs index aaad82733..2cabed33f 100644 --- a/src/Altinn.App.Core/Models/Expressions/ComponentContext.cs +++ b/src/Altinn.App.Core/Models/Expressions/ComponentContext.cs @@ -72,7 +72,7 @@ public IEnumerable Decendants get { var stack = new Stack(ChildContexts); - while (stack.Any()) + while (stack.Count != 0) { var node = stack.Pop(); yield return node; diff --git a/src/Altinn.App.Core/Models/Layout/LayoutModel.cs b/src/Altinn.App.Core/Models/Layout/LayoutModel.cs index 6f57631f2..3ecb38466 100644 --- a/src/Altinn.App.Core/Models/Layout/LayoutModel.cs +++ b/src/Altinn.App.Core/Models/Layout/LayoutModel.cs @@ -48,7 +48,7 @@ public IEnumerable GetComponents() { // Use a stack in order to implement a depth first search var nodes = new Stack(Pages.Values); - while (nodes.Any()) + while (nodes.Count != 0) { var node = nodes.Pop(); yield return node; diff --git a/stylecop.json b/stylecop.json deleted file mode 100644 index 4eef0f17a..000000000 --- a/stylecop.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - // ACTION REQUIRED: This file was automatically added to your project, but it - // will not take effect until additional steps are taken to enable it. See the - // following page for additional information: - // - // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md - - "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", - "settings": { - "documentationRules": { - "companyName": "PlaceholderCompany" - }, - "orderingRules": { - "usingDirectivesPlacement": "outsideNamespace", - "systemUsingDirectivesFirst": true, - "blankLinesBetweenUsingGroups": "allow" - }, - "namingRules": { - "allowCommonHungarianPrefixes": true, - "allowedHungarianPrefixes": [ - "as", - "d", - "db", - "dn", - "do", - "dr", - "ds", - "dt", - "e", - "e2", - "er", - "f", - "fs", - "go", - "id", - "if", - "in", - "ip", - "is", - "js", - "li", - "my", - "no", - "ns", - "on", - "or", - "pi", - "pv", - "sa", - "sb", - "se", - "si", - "so", - "sp", - "tc", - "to", - "tr", - "ui", - "un", - "wf", - "ws", - "x", - "y", - "j", - "js" - ] - } - } -} diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index c0c86e3a6..d472686d2 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -5,12 +5,6 @@ enable enable false - $(NoWarn);CS1591;CS0618;CS7022 - diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs index 6b289572a..662a971ab 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs @@ -146,7 +146,7 @@ public IEnumerator GetEnumerator() List testData = new List(); foreach (var d in _data) { - testData.Add(new object[] { d }); + testData.Add([d]); } return testData.GetEnumerator(); diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index a360e5532..bf3475ae0 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -57,16 +57,6 @@ - - - all - runtime; build; native; contentfiles; analyzers - - - stylecop.json - - - @@ -109,9 +99,4 @@ - - ..\..\Altinn3.ruleset - - - diff --git a/test/Altinn.App.Core.Tests/GlobalSuppressions.cs b/test/Altinn.App.Core.Tests/GlobalSuppressions.cs deleted file mode 100644 index f825eb5b4..000000000 --- a/test/Altinn.App.Core.Tests/GlobalSuppressions.cs +++ /dev/null @@ -1,13 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage( - "StyleCop.CSharp.DocumentationRules", - "SA1600:Elements should be documented", - Justification = "Test description should be in the name of the test.", - Scope = "module" -)] diff --git a/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs b/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs index b3d73f877..e5f63e48f 100644 --- a/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs +++ b/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs @@ -169,7 +169,7 @@ public string[] GetResolvedKeys(string key) { if (_modelRoot is null) { - return new string[0]; + return []; } var keyParts = key.Split('.'); @@ -185,12 +185,12 @@ private string[] GetResolvedKeysRecursive( { if (currentModel is null) { - return new string[0]; + return []; } if (currentIndex == keyParts.Length) { - return new[] { currentKey }; + return [currentKey]; } var (key, groupIndex) = DataModel.ParseKeyPart(keyParts[currentIndex]); @@ -199,7 +199,7 @@ currentModel is not JsonObject || !currentModel.AsObject().TryGetPropertyValue(key, out JsonNode? childModel) ) { - return new string[0]; + return []; } if (childModel is JsonArray childArray) diff --git a/test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs b/test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs index 48a936a59..696540bf5 100644 --- a/test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs @@ -13,7 +13,6 @@ using Xunit.Abstractions; // ReSharper disable InconsistentNaming -#pragma warning disable SA1300 // Inconsistent casing on property namespace Altinn.App.Core.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs index 8191aa0f8..6dee3de07 100644 --- a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs @@ -52,7 +52,7 @@ public async Task AddCompleteConfirmation_SuccessfulCallToStorage() Content = new StringContent(JsonConvert.SerializeObject(instance), Encoding.UTF8, "application/json"), }; - InitializeMocks(new HttpResponseMessage[] { httpResponseMessage }, new string[] { "complete" }); + InitializeMocks([httpResponseMessage], ["complete"]); HttpClient httpClient = new HttpClient(handlerMock.Object); @@ -81,7 +81,7 @@ public async Task AddCompleteConfirmation_StorageReturnsNonSuccess_ThrowsPlatfor Content = new StringContent("Error message", Encoding.UTF8, "application/json"), }; - InitializeMocks(new HttpResponseMessage[] { httpResponseMessage }, new string[] { "complete" }); + InitializeMocks([httpResponseMessage], ["complete"]); HttpClient httpClient = new HttpClient(handlerMock.Object); @@ -121,7 +121,7 @@ public async Task UpdateReadStatus_StorageReturnsNonSuccess_LogsErrorAppContinue Content = new StringContent("Error message", Encoding.UTF8, "application/json"), }; - InitializeMocks(new HttpResponseMessage[] { httpResponseMessage }, new string[] { "read" }); + InitializeMocks([httpResponseMessage], ["read"]); HttpClient httpClient = new HttpClient(handlerMock.Object); @@ -163,7 +163,7 @@ public async Task UpdateReadStatus_StorageReturnsSuccess() Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), }; - InitializeMocks(new HttpResponseMessage[] { httpResponseMessage }, new string[] { "read" }); + InitializeMocks([httpResponseMessage], ["read"]); HttpClient httpClient = new HttpClient(handlerMock.Object); @@ -201,7 +201,7 @@ public async Task UpdateSubtatus_StorageReturnsSuccess() Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), }; - InitializeMocks(new HttpResponseMessage[] { httpResponseMessage }, new string[] { "substatus" }); + InitializeMocks([httpResponseMessage], ["substatus"]); HttpClient httpClient = new HttpClient(handlerMock.Object); @@ -236,7 +236,7 @@ public async Task UpdateSubtatus_StorageReturnsNonSuccess_ThrowsPlatformHttpExce Content = new StringContent("Error message", Encoding.UTF8, "application/json"), }; - InitializeMocks(new HttpResponseMessage[] { httpResponseMessage }, new string[] { "substatus" }); + InitializeMocks([httpResponseMessage], ["substatus"]); HttpClient httpClient = new HttpClient(handlerMock.Object); @@ -285,7 +285,7 @@ public async Task DeleteInstance_StorageReturnsSuccess() Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), }; - InitializeMocks(new HttpResponseMessage[] { httpResponseMessage }, new string[] { "1337" }); + InitializeMocks([httpResponseMessage], ["1337"]); HttpClient httpClient = new HttpClient(handlerMock.Object); @@ -317,7 +317,7 @@ public async Task DeleteInstance_StorageReturnsNonSuccess_ThrowsPlatformHttpExce Content = new StringContent("Error message", Encoding.UTF8, "application/json"), }; - InitializeMocks(new HttpResponseMessage[] { httpResponseMessage }, new string[] { "1337" }); + InitializeMocks([httpResponseMessage], ["1337"]); HttpClient httpClient = new HttpClient(handlerMock.Object); @@ -359,7 +359,7 @@ public async Task UpdatePresentationTexts_StorageReturnsNonSuccess_ThrowsPlatfor Content = new StringContent("Error message", Encoding.UTF8, "application/json"), }; - InitializeMocks(new HttpResponseMessage[] { httpResponseMessage }, new string[] { "1337" }); + InitializeMocks([httpResponseMessage], ["1337"]); HttpClient httpClient = new HttpClient(handlerMock.Object); @@ -408,7 +408,7 @@ public async Task UpdatePresentationTexts_SuccessfulCallToStorage() Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), }; - InitializeMocks(new HttpResponseMessage[] { httpResponseMessage }, new string[] { "presentationtexts" }); + InitializeMocks([httpResponseMessage], ["presentationtexts"]); HttpClient httpClient = new HttpClient(handlerMock.Object); @@ -472,10 +472,7 @@ public async Task QueryInstances_QueryResponseContainsNext() ), }; - InitializeMocks( - new HttpResponseMessage[] { httpResponseMessage1, httpResponseMessage2 }, - new string[] { urlPart1, urlPart2 } - ); + InitializeMocks([httpResponseMessage1, httpResponseMessage2], [urlPart1, urlPart2]); HttpClient httpClient = new HttpClient(handlerMock.Object); diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test1/RunTest1.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test1/RunTest1.cs index 046e408f9..6ccab17a2 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test1/RunTest1.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test1/RunTest1.cs @@ -47,7 +47,7 @@ public async Task RemoveData_WhenPageExpressionIsTrue() "Test1" ); var hidden = LayoutEvaluator.GetHiddenFieldsForRemoval(state); - hidden.Should().BeEquivalentTo(new string[] { "some.data.binding2" }); + hidden.Should().BeEquivalentTo(["some.data.binding2"]); } [Fact] diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs index 6f118a3df..97a2ac37f 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs @@ -20,7 +20,7 @@ public void TestSimpleGet() var modelHelper = new DataModel(model); modelHelper.GetModelData("does.not.exist", default).Should().BeNull(); modelHelper.GetModelData("name.value", default).Should().Be(model.Name.Value); - modelHelper.GetModelData("name.value", new int[] { 1, 2, 3 }).Should().Be(model.Name.Value); + modelHelper.GetModelData("name.value", [1, 2, 3]).Should().Be(model.Name.Value); } [Fact] @@ -74,22 +74,22 @@ public void RecursiveLookup() IDataModelAccessor modelHelper = new DataModel(model); modelHelper.GetModelData("friends.name.value", default).Should().BeNull(); modelHelper.GetModelData("friends[0].name.value", default).Should().Be("Donald Duck"); - modelHelper.GetModelData("friends.name.value", new int[] { 0 }).Should().Be("Donald Duck"); + modelHelper.GetModelData("friends.name.value", [0]).Should().Be("Donald Duck"); modelHelper.GetModelData("friends[0].age", default).Should().Be(123); - modelHelper.GetModelData("friends.age", new int[] { 0 }).Should().Be(123); + modelHelper.GetModelData("friends.age", [0]).Should().Be(123); modelHelper.GetModelData("friends[1].name.value", default).Should().Be("Dolly Duck"); - modelHelper.GetModelData("friends.name.value", new int[] { 1 }).Should().Be("Dolly Duck"); + modelHelper.GetModelData("friends.name.value", [1]).Should().Be("Dolly Duck"); // Run the same tests with JsonDataModel var doc = JsonSerializer.Deserialize(JsonSerializer.Serialize(model)); modelHelper = new JsonDataModel(doc); modelHelper.GetModelData("friends.name.value", default).Should().BeNull(); modelHelper.GetModelData("friends[0].name.value", default).Should().Be("Donald Duck"); - modelHelper.GetModelData("friends.name.value", new int[] { 0 }).Should().Be("Donald Duck"); + modelHelper.GetModelData("friends.name.value", [0]).Should().Be("Donald Duck"); modelHelper.GetModelData("friends[0].age", default).Should().Be(123); - modelHelper.GetModelData("friends.age", new int[] { 0 }).Should().Be(123); + modelHelper.GetModelData("friends.age", [0]).Should().Be(123); modelHelper.GetModelData("friends[1].name.value", default).Should().Be("Dolly Duck"); - modelHelper.GetModelData("friends.name.value", new int[] { 1 }).Should().Be("Dolly Duck"); + modelHelper.GetModelData("friends.name.value", [1]).Should().Be("Dolly Duck"); } [Fact] @@ -134,37 +134,37 @@ public void DoubleRecursiveLookup() IDataModelAccessor modelHelper = new DataModel(model); modelHelper.GetModelData("friends[1].friends[0].name.value", default).Should().Be("Onkel Skrue"); - modelHelper.GetModelData("friends[1].friends.name.value", new int[] { 0, 0 }).Should().BeNull(); + modelHelper.GetModelData("friends[1].friends.name.value", [0, 0]).Should().BeNull(); modelHelper - .GetModelData("friends[1].friends.name.value", new int[] { 1, 0 }) + .GetModelData("friends[1].friends.name.value", [1, 0]) .Should() .BeNull("context indexes should not be used after literal index is used"); - modelHelper.GetModelData("friends[1].friends.name.value", new int[] { 1 }).Should().BeNull(); - modelHelper.GetModelData("friends.friends[0].name.value", new int[] { 1, 4, 5, 7 }).Should().Be("Onkel Skrue"); + modelHelper.GetModelData("friends[1].friends.name.value", [1]).Should().BeNull(); + modelHelper.GetModelData("friends.friends[0].name.value", [1, 4, 5, 7]).Should().Be("Onkel Skrue"); modelHelper.GetModelDataCount("friends[1].friends", Array.Empty()).Should().Be(1); - modelHelper.GetModelDataCount("friends.friends", new int[] { 1 }).Should().Be(1); - modelHelper.GetModelDataCount("friends[1].friends.friends", new int[] { 1, 0, 0 }).Should().BeNull(); - modelHelper.GetModelDataCount("friends[1].friends[0].friends", new int[] { 1, 0, 0 }).Should().Be(2); - modelHelper.GetModelDataCount("friends.friends.friends", new int[] { 1, 0, 0 }).Should().Be(2); - modelHelper.GetModelDataCount("friends.friends", new int[] { 1 }).Should().Be(1); + modelHelper.GetModelDataCount("friends.friends", [1]).Should().Be(1); + modelHelper.GetModelDataCount("friends[1].friends.friends", [1, 0, 0]).Should().BeNull(); + modelHelper.GetModelDataCount("friends[1].friends[0].friends", [1, 0, 0]).Should().Be(2); + modelHelper.GetModelDataCount("friends.friends.friends", [1, 0, 0]).Should().Be(2); + modelHelper.GetModelDataCount("friends.friends", [1]).Should().Be(1); // Run the same tests with JsonDataModel var doc = JsonSerializer.Deserialize(JsonSerializer.Serialize(model)); modelHelper = new JsonDataModel(doc); modelHelper.GetModelData("friends[1].friends[0].name.value", default).Should().Be("Onkel Skrue"); - modelHelper.GetModelData("friends[1].friends.name.value", new int[] { 0, 0 }).Should().BeNull(); + modelHelper.GetModelData("friends[1].friends.name.value", [0, 0]).Should().BeNull(); modelHelper - .GetModelData("friends[1].friends.name.value", new int[] { 1, 0 }) + .GetModelData("friends[1].friends.name.value", [1, 0]) .Should() .BeNull("context indexes should not be used after literal index is used"); - modelHelper.GetModelData("friends[1].friends.name.value", new int[] { 1 }).Should().BeNull(); - modelHelper.GetModelData("friends.friends[0].name.value", new int[] { 1, 4, 5, 7 }).Should().Be("Onkel Skrue"); + modelHelper.GetModelData("friends[1].friends.name.value", [1]).Should().BeNull(); + modelHelper.GetModelData("friends.friends[0].name.value", [1, 4, 5, 7]).Should().Be("Onkel Skrue"); modelHelper.GetModelDataCount("friends[1].friends", Array.Empty()).Should().Be(1); - modelHelper.GetModelDataCount("friends.friends", new int[] { 1 }).Should().Be(1); - modelHelper.GetModelDataCount("friends[1].friends.friends", new int[] { 1, 0, 0 }).Should().BeNull(); - modelHelper.GetModelDataCount("friends[1].friends[0].friends", new int[] { 1, 0, 0 }).Should().Be(2); - modelHelper.GetModelDataCount("friends.friends.friends", new int[] { 1, 0, 0 }).Should().Be(2); - modelHelper.GetModelDataCount("friends.friends", new int[] { 1 }).Should().Be(1); + modelHelper.GetModelDataCount("friends.friends", [1]).Should().Be(1); + modelHelper.GetModelDataCount("friends[1].friends.friends", [1, 0, 0]).Should().BeNull(); + modelHelper.GetModelDataCount("friends[1].friends[0].friends", [1, 0, 0]).Should().Be(2); + modelHelper.GetModelDataCount("friends.friends.friends", [1, 0, 0]).Should().Be(2); + modelHelper.GetModelDataCount("friends.friends", [1]).Should().Be(1); } [Fact] @@ -316,13 +316,13 @@ public void TestErrorCases() modelHelper.GetModelData("friends[3]").Should().BeNull(); modelHelper - .Invoking(m => m.AddIndicies("tull.sd", new int[] { 2 })) + .Invoking(m => m.AddIndicies("tull.sd", [2])) .Should() .Throw() .WithMessage("Unknown model property tull in*"); modelHelper - .Invoking(m => m.AddIndicies("id[4]", new int[] { 6 })) + .Invoking(m => m.AddIndicies("id[4]", [6])) .Should() .Throw() .WithMessage("Index on non indexable property"); @@ -340,7 +340,7 @@ public void TestEdgeCaseWithNonGenericEnumerableForCoverage() } ); modelHelper - .Invoking(m => m.AddIndicies("friends", new int[] { 0 })) + .Invoking(m => m.AddIndicies("friends", [0])) .Should() .Throw() .WithMessage("DataModels must have generic IEnumerable<> implementation for list"); @@ -358,16 +358,16 @@ public void TestAddIndicies() ); // Plain add indicies - modelHelper.AddIndicies("friends.friends", new int[] { 0, 1 }).Should().Be("friends[0].friends[1]"); + modelHelper.AddIndicies("friends.friends", [0, 1]).Should().Be("friends[0].friends[1]"); // Ignore extra indicies - modelHelper.AddIndicies("friends.friends", new int[] { 0, 1, 4, 6 }).Should().Be("friends[0].friends[1]"); + modelHelper.AddIndicies("friends.friends", [0, 1, 4, 6]).Should().Be("friends[0].friends[1]"); // Don't add indicies if they are specified in input - modelHelper.AddIndicies("friends[3]", new int[] { 0 }).Should().Be("friends[3]"); + modelHelper.AddIndicies("friends[3]", [0]).Should().Be("friends[3]"); // First index is ignored if it is explicit - modelHelper.AddIndicies("friends[0].friends", new int[] { 2, 3 }).Should().Be("friends[0].friends[3]"); + modelHelper.AddIndicies("friends[0].friends", [2, 3]).Should().Be("friends[0].friends[3]"); } [Fact] @@ -377,7 +377,7 @@ public void AddIndicies_WhenGivenIndexOnNonIndexableProperty_ThrowsError() // Throws because id is not indexable modelHelper - .Invoking(m => m.AddIndicies("id[0]", new int[] { 1, 2, 3 })) + .Invoking(m => m.AddIndicies("id[0]", [1, 2, 3])) .Should() .Throw() .WithMessage("Index on non indexable property"); diff --git a/test/Directory.Build.props b/test/Directory.Build.props new file mode 100644 index 000000000..c4a3a4e9b --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,15 @@ + + + + + + Default + $(NoWarn);CS1591;CS0618;CS7022;CA1707 + + + + From 69b9b92213f0dfcf0a49340584781d2ec1cc203e Mon Sep 17 00:00:00 2001 From: Daniel Skovli Date: Thu, 16 May 2024 09:59:56 +0200 Subject: [PATCH 13/22] Delete signatures if task process is reversed (cancelled) (#617) * Adds `TaskId` requirement to all signature requests * Flows TaskId through to Json->Http * Revert "Flows TaskId through to Json->Http" This reverts commit 20481cb40f0b1152e7ed1ece0db77c42de527605. * Revert "Adds `TaskId` requirement to all signature requests" This reverts commit 1f36c9370d6e4b9490a783f4ea692c886ce6faad. * Moves `RemoveDataElementsGeneratedFromTask` to `ProcessEngine.Next` From previous `ProcessTaskFinalizer.Finalize` * Moves `RemoveDataElementsGeneratedFromTask` to `ProcessTaskInitializer` class Still invoked from `ProcessEngine.Next` * Cleanup * Fixes tests * Deletes dataElement references from `Data` object, fixes enumeration to allow modification * Doctag in interface declaration * CSharpier formatting * Update src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs Co-authored-by: Martin Othamar * Conforms to log message formatting requirements * Formatting * Groups actionHandler with actionResult in a more logical way * Implements `IProcessTaskCleaner` and `SignRequest.GeneratedFromTask` * Fixes tests * Removes unused IProcessTaskInitializer injection * Removes IProcessTaskInitializer xml param --------- Co-authored-by: Martin Othamar --- src/Altinn.App.Api/Altinn.App.Api.csproj | 2 +- src/Altinn.App.Core/Altinn.App.Core.csproj | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Features/Action/SigningUserAction.cs | 1 + .../Clients/Storage/SignClient.cs | 3 +- .../Internal/Process/ProcessEngine.cs | 12 ++++- .../Common/Interfaces/IProcessTaskCleaner.cs | 16 ++++++ .../ProcessTasks/Common/ProcessTaskCleaner.cs | 53 +++++++++++++++++++ .../Common/ProcessTaskFinalizer.cs | 25 --------- .../Common/ProcessTaskInitializer.cs | 1 + .../Internal/Sign/SignatureContext.cs | 11 ++++ .../Features/Action/SigningUserActionTests.cs | 2 + .../Clients/Storage/SignClientTests.cs | 5 +- .../Internal/Process/ProcessEngineTest.cs | 8 +++ .../Common/ProcessTaskFinalizerTests.cs | 12 ----- 15 files changed, 111 insertions(+), 43 deletions(-) create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskCleaner.cs create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index c3f17fbab..71094da52 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 6d1b313b3..1538d2d1f 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 7ec481fc2..bc081d09c 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -294,6 +294,7 @@ private static void AddProcessServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs index f49ea4c08..86444a228 100644 --- a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs @@ -81,6 +81,7 @@ public async Task HandleAction(UserActionContext context) SignatureContext signatureContext = new( new InstanceIdentifier(context.Instance), + currentTask.Id, currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType!, await GetSignee(context.UserId.Value), dataElementSignatures diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs index 6ca5016e6..bb5c3a2dd 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs @@ -66,7 +66,8 @@ private static JsonContent BuildSignRequest(SignatureContext signatureContext) OrganisationNumber = signatureContext.Signee.OrganisationNumber }, SignatureDocumentDataType = signatureContext.SignatureDataTypeId, - DataElementSignatures = new() + DataElementSignatures = new(), + GeneratedFromTask = signatureContext.GeneratedFromTask }; foreach (var dataElementSignature in signatureContext.DataElementSignatures) { diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index e8e42f775..d7c32e15a 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -5,6 +5,7 @@ using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.UserAction; @@ -25,6 +26,7 @@ public class ProcessEngine : IProcessEngine private readonly IProcessEventHandlerDelegator _processEventHandlerDelegator; private readonly IProcessEventDispatcher _processEventDispatcher; private readonly UserActionService _userActionService; + private readonly IProcessTaskCleaner _processTaskCleaner; /// /// Initializes a new instance of the class @@ -32,8 +34,9 @@ public class ProcessEngine : IProcessEngine /// Process reader service /// The profile service /// The process navigator - /// + /// The process events delegator /// The process event dispatcher + /// The process task cleaner /// The action handler factory public ProcessEngine( IProcessReader processReader, @@ -41,6 +44,7 @@ public ProcessEngine( IProcessNavigator processNavigator, IProcessEventHandlerDelegator processEventsDelegator, IProcessEventDispatcher processEventDispatcher, + IProcessTaskCleaner processTaskCleaner, UserActionService userActionService ) { @@ -49,6 +53,7 @@ UserActionService userActionService _processNavigator = processNavigator; _processEventHandlerDelegator = processEventsDelegator; _processEventDispatcher = processEventDispatcher; + _processTaskCleaner = processTaskCleaner; _userActionService = userActionService; } @@ -127,8 +132,11 @@ public async Task Next(ProcessNextRequest request) }; } - int? userId = request.User.GetUserIdAsInt(); + // Removes existing/stale data elements previously generated from this task + // TODO: Move this logic to ProcessTaskInitializer.Initialize once the authentication model supports a service/app user with the appropriate scopes + await _processTaskCleaner.RemoveAllDataElementsGeneratedFromTask(instance, currentElementId); + int? userId = request.User.GetUserIdAsInt(); IUserAction? actionHandler = _userActionService.GetActionHandler(request.Action); UserActionResult actionResult = actionHandler is null diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskCleaner.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskCleaner.cs new file mode 100644 index 000000000..4a6d92994 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskCleaner.cs @@ -0,0 +1,16 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +/// Contains common logic to clean up process data +/// +public interface IProcessTaskCleaner +{ + /// + /// Removes ALL data elements generated from a specific task + /// + /// The instance to to act on + /// The ID of the task which generated the data elements + Task RemoveAllDataElementsGeneratedFromTask(Instance instance, string taskId); +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs new file mode 100644 index 000000000..f78cbedbb --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs @@ -0,0 +1,53 @@ +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +internal sealed class ProcessTaskCleaner : IProcessTaskCleaner +{ + private readonly ILogger _logger; + private readonly IDataClient _dataClient; + + public ProcessTaskCleaner(ILogger logger, IDataClient dataClient) + { + _logger = logger; + _dataClient = dataClient; + } + + /// + public async Task RemoveAllDataElementsGeneratedFromTask(Instance instance, string taskId) + { + AppIdentifier appIdentifier = new(instance.AppId); + InstanceIdentifier instanceIdentifier = new(instance); + var dataElements = + instance + .Data?.Where(de => + de.References?.Exists(r => r.ValueType == ReferenceType.Task && r.Value == taskId) is true + ) + .ToList() ?? []; + + _logger.LogInformation("Found {Count} stale data element(s) to delete", dataElements.Count); + + foreach (var dataElement in dataElements) + { + _logger.LogWarning( + "Deleting stale data element for task {TaskId}: {BlobStoragePath}", + taskId, + dataElement.BlobStoragePath + ); + await _dataClient.DeleteData( + appIdentifier.Org, + appIdentifier.App, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + Guid.Parse(dataElement.Id), + false + ); + + instance.Data?.Remove(dataElement); + } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs index 4a2d5354c..7d1eb7d28 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs @@ -6,7 +6,6 @@ using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Models; -using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Options; @@ -54,33 +53,9 @@ public async Task Finalize(string taskId, Instance instance) ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); List connectedDataTypes = applicationMetadata.DataTypes.FindAll(dt => dt.TaskId == taskId); - await RemoveDataElementsGeneratedFromTask(instance, taskId); - await RunRemoveFieldsInModelOnTaskComplete(instance, connectedDataTypes); } - private async Task RemoveDataElementsGeneratedFromTask(Instance instance, string taskId) - { - AppIdentifier appIdentifier = new(instance.AppId); - InstanceIdentifier instanceIdentifier = new(instance); - foreach ( - DataElement dataElement in instance.Data?.Where(de => - de.References != null - && de.References.Exists(r => r.ValueType == ReferenceType.Task && r.Value == taskId) - ) ?? Enumerable.Empty() - ) - { - await _dataClient.DeleteData( - appIdentifier.Org, - appIdentifier.App, - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - Guid.Parse(dataElement.Id), - false - ); - } - } - private async Task RunRemoveFieldsInModelOnTaskComplete(Instance instance, List dataTypesToLock) { ArgumentNullException.ThrowIfNull(instance.Data); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs index cd2850f72..e124f26bc 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs @@ -6,6 +6,7 @@ using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Prefill; using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; diff --git a/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs b/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs index d4a0dc33c..f65ef6b8f 100644 --- a/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs +++ b/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs @@ -11,17 +11,20 @@ public class SignatureContext /// Create a new signing context for one data element /// /// Identifier for the instance containing the data elements to sign + /// The id of the task connected to this signature /// The id of the DataType where the signature should be stored /// The signee /// The data element to sign public SignatureContext( InstanceIdentifier instanceIdentifier, + string generatedFromTask, string signatureDataTypeId, Signee signee, params DataElementSignature[] dataElementSignature ) { InstanceIdentifier = instanceIdentifier; + GeneratedFromTask = generatedFromTask; SignatureDataTypeId = signatureDataTypeId; DataElementSignatures.AddRange(dataElementSignature); Signee = signee; @@ -31,17 +34,20 @@ params DataElementSignature[] dataElementSignature /// Create a new signing context for multiple data elements /// /// Identifier for the instance containing the data elements to sign + /// The id of the task connected to this signature /// The id of the DataType where the signature should be stored /// The signee /// The data elements to sign public SignatureContext( InstanceIdentifier instanceIdentifier, + string generatedFromTask, string signatureDataTypeId, Signee signee, List dataElementSignatures ) { InstanceIdentifier = instanceIdentifier; + GeneratedFromTask = generatedFromTask; SignatureDataTypeId = signatureDataTypeId; DataElementSignatures = dataElementSignatures; Signee = signee; @@ -66,6 +72,11 @@ List dataElementSignatures /// The user performing the signing /// public Signee Signee { get; } + + /// + /// The task which should be linked to this signature + /// + public string GeneratedFromTask { get; } } /// diff --git a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs index f566d8f29..d2e304a0a 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs @@ -54,6 +54,7 @@ public async Task HandleAction_returns_ok_if_user_is_valid() // Assert SignatureContext expected = new SignatureContext( new InstanceIdentifier(instance), + instance.Process.CurrentTask.ElementId, "signature", new Signee() { UserId = "1337", PersonNumber = "12345678901" }, new DataElementSignature("a499c3ef-e88a-436b-8650-1c43e5037ada") @@ -102,6 +103,7 @@ public async Task HandleAction_returns_ok_if_no_dataElementSignature_and_optiona // Assert SignatureContext expected = new SignatureContext( new InstanceIdentifier(instance), + instance.Process.CurrentTask.ElementId, "signature", new Signee() { UserId = "1337", PersonNumber = "12345678901" }, new DataElementSignature("a499c3ef-e88a-436b-8650-1c43e5037ada") diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs index bf412ec55..218ff9f3a 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs @@ -51,6 +51,7 @@ public async Task SignDataElements_sends_request_to_platform() var dataElementId = Guid.NewGuid().ToString(); var signatureContext = new SignatureContext( instanceIdentifier, + "TheTask", "sign-data-type", new Signee() { UserId = "1337", PersonNumber = "0101011337" }, new DataElementSignature(dataElementId) @@ -63,7 +64,8 @@ public async Task SignDataElements_sends_request_to_platform() { new() { DataElementId = dataElementId, Signed = true } }, - SignatureDocumentDataType = "sign-data-type" + SignatureDocumentDataType = "sign-data-type", + GeneratedFromTask = "TheTask" }; await signClient.SignDataElements(signatureContext); @@ -103,6 +105,7 @@ public async Task SignDataElements_throws_PlatformHttpException_if_platform_retu var dataElementId = Guid.NewGuid().ToString(); var signatureContext = new SignatureContext( instanceIdentifier, + "TheTask", "sign-data-type", new Signee() { UserId = "1337", PersonNumber = "0101011337" }, new DataElementSignature(dataElementId) diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index c8f2025e8..5f3d14efc 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -4,6 +4,7 @@ using Altinn.App.Core.Features.Action; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.UserAction; @@ -26,6 +27,7 @@ public class ProcessEngineTest : IDisposable private readonly Mock _processNavigatorMock; private readonly Mock _processEventHandlingDelegatorMock; private readonly Mock _processEventDispatcherMock; + private readonly Mock _processTaskCleanerMock; public ProcessEngineTest() { @@ -34,6 +36,7 @@ public ProcessEngineTest() _processNavigatorMock = new(); _processEventHandlingDelegatorMock = new(); _processEventDispatcherMock = new(); + _processTaskCleanerMock = new(); } [Fact] @@ -444,6 +447,10 @@ public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() _processReaderMock.Verify(r => r.IsEndEvent("Task_2"), Times.Once); _processReaderMock.Verify(r => r.IsProcessTask("Task_2"), Times.Once); _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "Task_1", null), Times.Once); + _processTaskCleanerMock.Verify( + x => x.RemoveAllDataElementsGeneratedFromTask(It.IsAny(), It.IsAny()), + Times.Once + ); var expectedInstanceEvents = new List() { @@ -974,6 +981,7 @@ private ProcessEngine GetProcessEngine( _processNavigatorMock.Object, _processEventHandlingDelegatorMock.Object, _processEventDispatcherMock.Object, + _processTaskCleanerMock.Object, new UserActionService(userActions ?? []) ); } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs index 835f30d3b..2d9e04978 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs @@ -76,18 +76,6 @@ public async Task Finalize_WithValidInputs_ShouldCallCorrectMethods() // Assert _appMetadataMock.Verify(x => x.GetApplicationMetadata(), Times.Once); - _dataClientMock.Verify( - x => - x.DeleteData( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny() - ), - Times.AtLeastOnce - ); } private static Instance CreateInstance() From 290f815faf2e3e481fe8010b9cf0d08be5fccad1 Mon Sep 17 00:00:00 2001 From: Johannes Haukland <42615991+HauklandJ@users.noreply.github.com> Date: Wed, 22 May 2024 12:54:52 +0200 Subject: [PATCH 14/22] Add OpenTelemetry configuration and instrumentation to replace AppInsights and prometheus-net (#589) Add OTEL and instrument. --------- Co-authored-by: HauklandJ Co-authored-by: Martin Othamar --- src/Altinn.App.Api/Altinn.App.Api.csproj | 7 +- .../Extensions/ServiceCollectionExtensions.cs | 195 +++++++++- .../WebApplicationBuilderExtensions.cs | 24 +- .../Extensions/WebHostBuilderExtensions.cs | 13 +- .../TelemetryEnrichingMiddleware.cs | 142 ++++++++ src/Altinn.App.Core/Altinn.App.Core.csproj | 5 +- .../Configuration/MetricsSettings.cs | 1 + .../Extensions/ServiceCollectionExtensions.cs | 12 - .../Features/DataLists/DataListsService.cs | 15 +- .../FileAnalyzis/FileAnalysisService.cs | 5 +- .../Email/EmailNotificationClient.cs | 39 +- .../Sms/SmsNotificationClient.cs | 40 +-- .../Features/Notifications/Telemetry.cs | 30 -- .../Features/Options/AppOptionsService.cs | 7 +- .../Features/Telemetry.AppOptionsService.cs | 16 + .../Features/Telemetry.ApplicationLanguage.cs | 8 + .../Telemetry.ApplicationMetadata.Client.cs | 20 ++ .../Telemetry.ApplicationMetadata.Service.cs | 66 ++++ .../Features/Telemetry.Authentication.cs | 9 + .../Telemetry.Authorization.Client.cs | 76 ++++ .../Telemetry.Authorization.Service.cs | 87 +++++ .../Features/Telemetry.Data.cs | 50 +++ .../Features/Telemetry.DataClient.cs | 129 +++++++ .../Features/Telemetry.DataList.cs | 16 + .../Features/Telemetry.ErClient.cs | 13 + .../Features/Telemetry.EventsClient.cs | 14 + .../Features/Telemetry.FileAnalysis.cs | 8 + .../Features/Telemetry.FileValidation.cs | 8 + .../Features/Telemetry.Instances.cs | 135 +++++++ .../Features/Telemetry.Notifications.cs | 79 +++++ .../Features/Telemetry.PartyClient.cs | 15 + .../Features/Telemetry.PdfService.cs | 23 ++ .../Features/Telemetry.Prefill.Service.cs | 21 ++ .../Features/Telemetry.ProcessClient.cs | 17 + .../Features/Telemetry.ProcessReader.cs | 51 +++ .../Features/Telemetry.Processes.cs | 65 ++++ .../Features/Telemetry.ProfileClient.cs | 13 + .../Features/Telemetry.Validation.cs | 72 ++++ src/Altinn.App.Core/Features/Telemetry.cs | 206 +++++++++++ .../Features/TelemetryActivityExtensions.cs | 251 +++++++++++++ .../Implementation/AppResourcesSI.cs | 29 +- .../Implementation/PrefillSI.cs | 9 +- .../Authorization/AuthorizationClient.cs | 11 +- .../Clients/Events/EventsClient.cs | 8 +- .../Clients/Profile/ProfileClient.cs | 8 +- .../Clients/Register/AltinnPartyClient.cs | 9 +- .../Clients/Register/RegisterERClient.cs | 8 +- .../Clients/Storage/DataClient.cs | 28 +- .../Clients/Storage/InstanceClient.cs | 44 ++- .../Storage/InstanceClientMetricsDecorator.cs | 140 -------- .../Clients/Storage/ProcessClient.cs | 8 +- .../Internal/App/AppMetadata.cs | 13 +- .../Internal/Auth/AuthorizationService.cs | 13 +- .../Internal/Language/ApplicationLanguage.cs | 11 +- .../Internal/Patch/PatchService.cs | 16 +- .../Internal/Pdf/PdfService.cs | 10 +- .../Internal/Process/ProcessEngine.cs | 19 +- .../Process/ProcessEngineMetricsDecorator.cs | 92 ----- .../Internal/Process/ProcessReader.cs | 24 +- .../Validation/FileValidationService.cs | 6 +- .../Internal/Validation/ValidationService.cs | 83 +++-- .../TelemetryConfigurationTests.cs} | 170 ++++++++- .../Altinn.App.Core.Tests.csproj | 1 + ...entTests.Order_VerifyHttpCall.verified.txt | 26 ++ .../Email/EmailNotificationClientTests.cs | 105 ++++-- ...entTests.Order_VerifyHttpCall.verified.txt | 26 ++ .../Sms/SmsNotificationClientTests.cs | 106 ++++-- .../Implementation/AppResourcesSITests.cs | 74 +++- ...uestContainOrganisationNumber.verified.txt | 9 + .../Implementation/EventsClientTest.cs | 48 +-- ...ation_SuccessfulCallToStorage.verified.txt | 25 ++ .../Implementation/InstanceClientTests.cs | 46 ++- ...ionary_with_one_action_denied.verified.txt | 14 + .../Authorization/AuthorizationClientTests.cs | 27 +- ...odProduceValidPlatformRequest.verified.txt | 14 + .../Clients/Storage/DataClientTests.cs | 14 +- .../InstanceClientMetricsDecoratorTests.cs | 335 ------------------ ...ta_desrializes_file_from_disk.verified.txt | 9 + .../Internal/App/AppMedataTest.cs | 22 +- ...list_from_AuthorizationClient.verified.txt | 14 + .../Auth/AuthorizationServiceTests.cs | 7 +- .../PatchServiceTests.Test_Ok.verified.txt | 28 ++ .../Internal/Patch/PatchServiceTests.cs | 15 +- ...viceTests.GenerateAndStorePdf.verified.txt | 17 + .../Internal/Pdf/PdfServiceTests.cs | 8 +- .../ProcessEngineMetricsDecoratorTests.cs | 319 ----------------- ...ocess_and_moves_to_first_task.verified.txt | 17 + .../Internal/Process/ProcessEngineTest.cs | 12 +- ...ue_when_element_is_StartEvent.verified.txt | 17 + .../Internal/Process/ProcessReaderTests.cs | 9 +- .../Process/TestUtils/ProcessTestUtils.cs | 10 +- .../Mocks/TelemetrySink.cs | 188 ++++++++++ .../TestHelpers/PrometheusTestHelper.cs | 16 - 93 files changed, 3063 insertions(+), 1277 deletions(-) create mode 100644 src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs delete mode 100644 src/Altinn.App.Core/Features/Notifications/Telemetry.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.AppOptionsService.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.ApplicationLanguage.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Client.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Service.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.Authentication.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.Authorization.Client.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.Authorization.Service.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.Data.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.DataClient.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.DataList.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.ErClient.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.EventsClient.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.FileAnalysis.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.FileValidation.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.Instances.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.Notifications.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.PartyClient.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.PdfService.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.Prefill.Service.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.ProcessClient.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.ProcessReader.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.Processes.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.ProfileClient.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.Validation.cs create mode 100644 src/Altinn.App.Core/Features/Telemetry.cs create mode 100644 src/Altinn.App.Core/Features/TelemetryActivityExtensions.cs delete mode 100644 src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClientMetricsDecorator.cs delete mode 100644 src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs rename test/Altinn.App.Api.Tests/{DITests.cs => Telemetry/TelemetryConfigurationTests.cs} (51%) create mode 100644 test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.Order_VerifyHttpCall.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.Order_VerifyHttpCall.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Implementation/EventsClientTest.AddEvent_RegisterEventWithInstanceOwnerOrganisation_CloudEventInRequestContainOrganisationNumber.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.AddCompleteConfirmation_SuccessfulCallToStorage.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.AuthorizeActions_returns_dictionary_with_one_action_denied.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.InsertBinaryData_MethodProduceValidPlatformRequest.verified.txt delete mode 100644 test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.GetApplicationMetadata_desrializes_file_from_disk.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.GetPartyList_returns_party_list_from_AuthorizationClient.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.Test_Ok.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.GenerateAndStorePdf.verified.txt delete mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.IsStartEvent_returns_true_when_element_is_StartEvent.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Mocks/TelemetrySink.cs delete mode 100644 test/Altinn.App.Core.Tests/TestHelpers/PrometheusTestHelper.cs diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index 71094da52..333548902 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -24,8 +24,13 @@ + + + + + + - diff --git a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs index d40657798..dde2c8e0f 100644 --- a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,19 +1,24 @@ using Altinn.App.Api.Controllers; +using Altinn.App.Api.Helpers; using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Infrastructure.Health; using Altinn.App.Api.Infrastructure.Telemetry; -using Altinn.App.Core.Configuration; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.Common.PEP.Authorization; using Altinn.Common.PEP.Clients; using AltinnCore.Authentication.JwtCookie; +using Azure.Monitor.OpenTelemetry.Exporter; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.FeatureManagement; using Microsoft.IdentityModel.Tokens; -using Prometheus; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; namespace Altinn.App.Api.Extensions { @@ -58,7 +63,18 @@ IWebHostEnvironment env services.AddAppServices(config, env); services.ConfigureDataProtection(); - AddApplicationInsights(services, config, env); + var useOpenTelemetrySetting = config.GetValue("AppSettings:UseOpenTelemetry"); + + // Use Application Insights as default, opt in to use Open Telemetry + if (useOpenTelemetrySetting is true) + { + AddOpenTelemetry(services, config, env); + } + else + { + AddApplicationInsights(services, config, env); + } + AddAuthenticationScheme(services, config, env); AddAuthorizationPolicies(services); AddAntiforgery(services); @@ -74,7 +90,6 @@ IWebHostEnvironment env services.AddHttpClient(); services.AddSingleton(); - services.AddMetricsServer(config); } /// @@ -89,12 +104,7 @@ internal static void AddApplicationInsights( IWebHostEnvironment env ) { - string? applicationInsightsKey = env.IsDevelopment() - ? config["ApplicationInsights:InstrumentationKey"] - : Environment.GetEnvironmentVariable("ApplicationInsights__InstrumentationKey"); - string? applicationInsightsConnectionString = env.IsDevelopment() - ? config["ApplicationInsights:ConnectionString"] - : Environment.GetEnvironmentVariable("ApplicationInsights__ConnectionString"); + var (applicationInsightsKey, applicationInsightsConnectionString) = GetAppInsightsConfig(config, env); if ( !string.IsNullOrEmpty(applicationInsightsKey) @@ -123,6 +133,132 @@ IWebHostEnvironment env } } + private static void AddOpenTelemetry( + IServiceCollection services, + IConfiguration config, + IWebHostEnvironment env + ) + { + var appId = StartupHelper.GetApplicationId().Split("/")[1]; + var appVersion = config.GetSection("AppSettings").GetValue("AppVersion"); + if (string.IsNullOrWhiteSpace(appVersion)) + { + appVersion = "Local"; + } + services.AddHostedService(); + services.AddSingleton(); + + var appInsightsConnectionString = GetAppInsightsConnectionStringForOtel(config, env); + + services + .AddOpenTelemetry() + .ConfigureResource(r => + r.AddService( + serviceName: appId, + serviceVersion: appVersion, + serviceInstanceId: Environment.MachineName + ) + ) + .WithTracing(builder => + { + builder = builder + .AddSource(appId) + .AddHttpClientInstrumentation(opts => + { + opts.RecordException = true; + }) + .AddAspNetCoreInstrumentation(opts => + { + opts.RecordException = true; + }); + + if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) + { + builder = builder.AddAzureMonitorTraceExporter(options => + { + options.ConnectionString = appInsightsConnectionString; + }); + } + else + { + builder = builder.AddOtlpExporter(); + } + }) + .WithMetrics(builder => + { + builder = builder + .AddMeter(appId) + .AddRuntimeInstrumentation() + .AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation(); + + if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) + { + builder = builder.AddAzureMonitorMetricExporter(options => + { + options.ConnectionString = appInsightsConnectionString; + }); + } + else + { + builder = builder.AddOtlpExporter(); + } + }); + + services.AddLogging(logging => + { + logging.AddOpenTelemetry(options => + { + options.IncludeFormattedMessage = true; + + if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) + { + options.AddAzureMonitorLogExporter(options => + { + options.ConnectionString = appInsightsConnectionString; + }); + } + else + { + options.AddOtlpExporter(); + } + }); + }); + } + + private sealed class TelemetryInitialization( + ILogger logger, + Telemetry telemetry, + MeterProvider meterProvider + ) : IHostedService + { + public Task StartAsync(CancellationToken cancellationToken) + { + // This codepath for initialization is here only because it makes it a lot easier to + // query the metrics from Prometheus using 'increase' without the appearance of a "missed" sample. + // 'increase' in Prometheus will not interpret 'none' -> 1 as a delta/increase, + // so when querying the increase within a range, there may be 1 less sample than expected. + // So here we let the metrics be initialized to 0, + // and then run collection/flush on the OTel MeterProvider to make sure they are exported. + // The first time we then increment the metric, it will count as a change from 0 -> 1 + telemetry.Init(); + try + { + if (!meterProvider.ForceFlush(10_000)) + { + logger.LogWarning("Failed to flush metrics after 10 seconds"); + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to flush metrics"); + } + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + private static void AddAuthorizationPolicies(IServiceCollection services) { services.AddAuthorization(options => @@ -194,17 +330,40 @@ private static void AddAntiforgery(IServiceCollection services) services.TryAddSingleton(); } - private static void AddMetricsServer(this IServiceCollection services, IConfiguration config) + private static (string? Key, string? ConnectionString) GetAppInsightsConfig( + IConfiguration config, + IHostEnvironment env + ) { - var metricsSettings = config.GetSection("MetricsSettings").Get() ?? new MetricsSettings(); - if (metricsSettings.Enabled) + var isDevelopment = env.IsDevelopment(); + string? key = isDevelopment + ? config["ApplicationInsights:InstrumentationKey"] + : Environment.GetEnvironmentVariable("ApplicationInsights__InstrumentationKey"); + string? connectionString = isDevelopment + ? config["ApplicationInsights:ConnectionString"] + : Environment.GetEnvironmentVariable("ApplicationInsights__ConnectionString"); + + return (key, connectionString); + } + + private static string? GetAppInsightsConnectionStringForOtel(IConfiguration config, IHostEnvironment env) + { + var (key, connString) = GetAppInsightsConfig(config, env); + if (string.IsNullOrWhiteSpace(connString)) { - ushort port = metricsSettings.Port; - services.AddMetricServer(options => - { - options.Port = port; - }); + connString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING"); + } + if (!string.IsNullOrWhiteSpace(connString)) + { + return connString; } + + if (!Guid.TryParse(key, out _)) + { + return null; + } + + return $"InstrumentationKey={key}"; } } } diff --git a/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs index 9f148acb9..d90763d50 100644 --- a/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs +++ b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs @@ -1,7 +1,5 @@ -using System.Reflection; using Altinn.App.Api.Helpers; -using Altinn.App.Core.Configuration; -using Prometheus; +using Altinn.App.Api.Infrastructure.Middleware; namespace Altinn.App.Api.Extensions; @@ -21,16 +19,14 @@ public static IApplicationBuilder UseAltinnAppCommonConfiguration(this IApplicat if (app is WebApplication webApp && webApp.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); - webApp.UseAltinnPrometheus(appId); } - app.UseHttpMetrics(); - app.UseMetricServer(); app.UseDefaultSecurityHeaders(); app.UseRouting(); app.UseStaticFiles('/' + appId); app.UseAuthentication(); app.UseAuthorization(); + app.UseTelemetryEnricher(); app.UseEndpoints(endpoints => { @@ -39,20 +35,4 @@ public static IApplicationBuilder UseAltinnAppCommonConfiguration(this IApplicat app.UseHealthChecks("/health"); return app; } - - private static void UseAltinnPrometheus(this WebApplication webApp, string appId) - { - var metricsSettings = - webApp.Configuration.GetSection("MetricsSettings")?.Get() ?? new MetricsSettings(); - if (!metricsSettings.Enabled) - { - return; - } - - webApp.UseHttpMetrics(); - var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; - Metrics.DefaultRegistry.SetStaticLabels( - new Dictionary() { { "application_id", appId }, { "nuget_package_version", version } } - ); - } } diff --git a/src/Altinn.App.Api/Extensions/WebHostBuilderExtensions.cs b/src/Altinn.App.Api/Extensions/WebHostBuilderExtensions.cs index a80179f67..1bc5b0681 100644 --- a/src/Altinn.App.Api/Extensions/WebHostBuilderExtensions.cs +++ b/src/Altinn.App.Api/Extensions/WebHostBuilderExtensions.cs @@ -1,4 +1,3 @@ -#nullable disable using Altinn.App.Core.Extensions; namespace Altinn.App.Api.Extensions; @@ -16,8 +15,18 @@ public static class WebHostBuilderExtensions public static void ConfigureAppWebHost(this IWebHostBuilder builder, string[] args) { builder.ConfigureAppConfiguration( - (_, configBuilder) => + (context, configBuilder) => { + var config = new List>(); + + if (context.HostingEnvironment.IsDevelopment()) + { + config.Add(new("OTEL_TRACES_SAMPLER", "always_on")); + config.Add(new("OTEL_METRIC_EXPORT_INTERVAL", "10000")); + config.Add(new("OTEL_METRIC_EXPORT_TIMEOUT", "8000")); + } + + configBuilder.AddInMemoryCollection(config); configBuilder.LoadAppConfig(args); } ); diff --git a/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs b/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs new file mode 100644 index 000000000..f5641ff95 --- /dev/null +++ b/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs @@ -0,0 +1,142 @@ +using System.Collections.Frozen; +using System.Diagnostics; +using System.Security.Claims; +using Altinn.App.Core.Features; +using AltinnCore.Authentication.Constants; +using Microsoft.AspNetCore.Http.Features; + +namespace Altinn.App.Api.Infrastructure.Middleware; + +/// +/// Middleware for adding telemetry to the request. +/// +public class TelemetryEnrichingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private static readonly FrozenDictionary> ClaimActions; + + static TelemetryEnrichingMiddleware() + { + var actions = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { AltinnCoreClaimTypes.UserName, static (claim, activity) => activity.SetUsername(claim.Value) }, + { + AltinnCoreClaimTypes.UserId, + static (claim, activity) => + { + if (int.TryParse(claim.Value, out var result)) + { + activity.SetUserId(result); + } + } + }, + { + AltinnCoreClaimTypes.PartyID, + static (claim, activity) => + { + if (int.TryParse(claim.Value, out var result)) + { + activity.SetUserPartyId(result); + } + } + }, + { + AltinnCoreClaimTypes.AuthenticationLevel, + static (claim, activity) => + { + if (int.TryParse(claim.Value, out var result)) + { + activity.SetAuthenticationLevel(result); + } + } + } + }; + + ClaimActions = actions.ToFrozenDictionary(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The next middleware in the pipeline. + /// The logger instance. + public TelemetryEnrichingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + /// + /// Invokes the middleware to process the HTTP context. + /// + /// The HTTP context. + public async Task InvokeAsync(HttpContext context) + { + var trace = context.Features.Get(); + if (trace is null) + { + await _next(context); + return; + } + + try + { + var activity = trace.Activity; + + foreach (var claim in context.User.Claims) + { + if (ClaimActions.TryGetValue(claim.Type, out var action)) + { + action(claim, activity); + } + } + + // Set telemetry tags with route values if available. + if ( + context.Request.RouteValues.TryGetValue("instanceOwnerPartyId", out var instanceOwnerPartyId) + && instanceOwnerPartyId != null + && int.TryParse(instanceOwnerPartyId.ToString(), out var instanceOwnerPartyIdInt) + ) + { + activity.SetInstanceOwnerPartyId(instanceOwnerPartyIdInt); + } + + var routeValues = context.Request.RouteValues; + if ( + routeValues.TryGetValue("instanceGuid", out var instanceGuidObj) && instanceGuidObj is Guid instanceGuid + ) + { + activity.SetInstanceId(instanceGuid); + } + + if (routeValues.TryGetValue("dataGuid", out var dataGuidObj) && dataGuidObj is Guid dataGuid) + { + activity.SetDataElementId(dataGuid); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while enriching telemetry."); + } + + await _next(context); + } +} + +/// +/// Extension methods for adding the to the application pipeline. +/// +public static class TelemetryEnrichingMiddlewareExtensions +{ + /// + /// Adds the to the application's request pipeline. + /// + /// The application builder. + public static IApplicationBuilder UseTelemetryEnricher(this IApplicationBuilder app) + { + return app.UseMiddleware( + app.ApplicationServices.GetRequiredService>() + ); + } +} diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 1538d2d1f..565d61ad0 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -20,13 +20,14 @@ - - + + + diff --git a/src/Altinn.App.Core/Configuration/MetricsSettings.cs b/src/Altinn.App.Core/Configuration/MetricsSettings.cs index 1ce2b395a..0639afb28 100644 --- a/src/Altinn.App.Core/Configuration/MetricsSettings.cs +++ b/src/Altinn.App.Core/Configuration/MetricsSettings.cs @@ -3,6 +3,7 @@ namespace Altinn.App.Core.Configuration; /// /// Metric settings for Altinn Apps /// +[Obsolete("MetricSettings will no longer be supported in version 9.")] public class MetricsSettings { /// diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index bc081d09c..9fd678b58 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -183,7 +183,6 @@ IWebHostEnvironment env AddProcessServices(services); AddFileAnalyserServices(services); AddFileValidatorServices(services); - AddMetricsDecorators(services, configuration); if (!env.IsDevelopment()) { @@ -330,16 +329,5 @@ private static void AddFileValidatorServices(IServiceCollection services) services.TryAddTransient(); services.TryAddTransient(); } - - private static void AddMetricsDecorators(IServiceCollection services, IConfiguration configuration) - { - MetricsSettings metricsSettings = - configuration.GetSection("MetricsSettings")?.Get() ?? new MetricsSettings(); - if (metricsSettings.Enabled) - { - services.Decorate(); - services.Decorate(); - } - } } } diff --git a/src/Altinn.App.Core/Features/DataLists/DataListsService.cs b/src/Altinn.App.Core/Features/DataLists/DataListsService.cs index aca27afa3..a6c94d754 100644 --- a/src/Altinn.App.Core/Features/DataLists/DataListsService.cs +++ b/src/Altinn.App.Core/Features/DataLists/DataListsService.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Altinn.App.Core.Models; namespace Altinn.App.Core.Features.DataLists @@ -14,14 +9,20 @@ public class DataListsService : IDataListsService { private readonly DataListsFactory _dataListsFactory; private readonly InstanceDataListsFactory _instanceDataListsFactory; + private readonly Telemetry? _telemetry; /// /// Initializes a new instance of the class. /// - public DataListsService(DataListsFactory dataListsFactory, InstanceDataListsFactory instanceDataListsFactory) + public DataListsService( + DataListsFactory dataListsFactory, + InstanceDataListsFactory instanceDataListsFactory, + Telemetry? telemetry = null + ) { _dataListsFactory = dataListsFactory; _instanceDataListsFactory = instanceDataListsFactory; + _telemetry = telemetry; } /// @@ -31,6 +32,7 @@ public async Task GetDataListAsync( Dictionary keyValuePairs ) { + using var activity = _telemetry?.StartDataListActivity(); return await _dataListsFactory.GetDataListProvider(dataListId).GetDataListAsync(language, keyValuePairs); } @@ -42,6 +44,7 @@ public async Task GetDataListAsync( Dictionary keyValuePairs ) { + using var activity = _telemetry?.StartDataListActivity(instanceIdentifier); return await _instanceDataListsFactory .GetDataListProvider(dataListId) .GetInstanceDataListAsync(instanceIdentifier, language, keyValuePairs); diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs index 2153e4e7d..6ba5999a6 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs @@ -9,13 +9,15 @@ namespace Altinn.App.Core.Features.FileAnalyzis public class FileAnalysisService : IFileAnalysisService { private readonly IFileAnalyserFactory _fileAnalyserFactory; + private readonly Telemetry? _telemetry; /// /// Initializes a new instance of the class. /// - public FileAnalysisService(IFileAnalyserFactory fileAnalyserFactory) + public FileAnalysisService(IFileAnalyserFactory fileAnalyserFactory, Telemetry? telemetry = null) { _fileAnalyserFactory = fileAnalyserFactory; + _telemetry = telemetry; } /// @@ -27,6 +29,7 @@ public async Task> Analyse( string? filename = null ) { + using var activity = _telemetry?.StartAnalyseActivity(); List fileAnalysers = _fileAnalyserFactory .GetFileAnalysers(dataType.EnabledFileAnalysers) .ToList(); diff --git a/src/Altinn.App.Core/Features/Notifications/Email/EmailNotificationClient.cs b/src/Altinn.App.Core/Features/Notifications/Email/EmailNotificationClient.cs index f2b6c7ad7..9e8ee46e7 100644 --- a/src/Altinn.App.Core/Features/Notifications/Email/EmailNotificationClient.cs +++ b/src/Altinn.App.Core/Features/Notifications/Email/EmailNotificationClient.cs @@ -5,7 +5,6 @@ using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models.Notifications.Email; using Altinn.Common.AccessTokenClient.Services; -using Microsoft.ApplicationInsights; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -13,12 +12,14 @@ namespace Altinn.App.Core.Features.Notifications.Email; internal sealed class EmailNotificationClient : IEmailNotificationClient { + private static readonly Telemetry.Notifications.OrderType _orderType = Telemetry.Notifications.OrderType.Email; + private readonly ILogger _logger; private readonly HttpClient _httpClient; private readonly IAppMetadata _appMetadata; private readonly PlatformSettings _platformSettings; private readonly IAccessTokenGenerator _accessTokenGenerator; - private readonly TelemetryClient? _telemetryClient; + private readonly Telemetry? _telemetry; public EmailNotificationClient( ILogger logger, @@ -26,7 +27,7 @@ public EmailNotificationClient( IOptions platformSettings, IAppMetadata appMetadata, IAccessTokenGenerator accessTokenGenerator, - TelemetryClient? telemetryClient = null + Telemetry? telemetry = null ) { _logger = logger; @@ -34,22 +35,15 @@ public EmailNotificationClient( _httpClient = httpClient; _appMetadata = appMetadata; _accessTokenGenerator = accessTokenGenerator; - _telemetryClient = telemetryClient; + _telemetry = telemetry; } public async Task Order(EmailNotification emailNotification, CancellationToken ct) { - DateTime startDateTime = default; - long startTimestamp = default; - if (_telemetryClient is not null) - { - startDateTime = DateTime.UtcNow; - startTimestamp = Stopwatch.GetTimestamp(); - } + using var activity = _telemetry?.StartNotificationOrderActivity(_orderType); HttpResponseMessage? httpResponseMessage = null; string? httpContent = null; - Exception? exception = null; try { var application = await _appMetadata.GetApplicationMetadata(); @@ -76,7 +70,7 @@ public async Task Order(EmailNotification emailNotification, if (orderResponse is null) throw new JsonException("Couldn't deserialize email notification order response."); - Telemetry.OrderCount.WithLabels(Telemetry.Types.Email, Telemetry.Result.Success).Inc(); + _telemetry?.RecordNotificationOrder(_orderType, Telemetry.Notifications.OrderResult.Success); } else { @@ -86,8 +80,6 @@ public async Task Order(EmailNotification emailNotification, } catch (Exception e) { - exception = e; - Telemetry.OrderCount.WithLabels(Telemetry.Types.Email, Telemetry.Result.Error).Inc(); var ex = new EmailNotificationException( $"Something went wrong when processing the email order", httpResponseMessage, @@ -95,25 +87,14 @@ public async Task Order(EmailNotification emailNotification, e ); _logger.LogError(ex, "Error when processing email notification order"); + + _telemetry?.RecordNotificationOrder(_orderType, Telemetry.Notifications.OrderResult.Error); + throw ex; } finally { httpResponseMessage?.Dispose(); - if (_telemetryClient is not null) - { - var stopTimestamp = Stopwatch.GetTimestamp(); - var elapsed = Stopwatch.GetElapsedTime(startTimestamp, stopTimestamp); - - _telemetryClient.TrackDependency( - Telemetry.Dependency.TypeName, - Telemetry.Dependency.Name, - null, - startDateTime, - elapsed, - exception is null - ); - } } } } diff --git a/src/Altinn.App.Core/Features/Notifications/Sms/SmsNotificationClient.cs b/src/Altinn.App.Core/Features/Notifications/Sms/SmsNotificationClient.cs index e3d17cafb..5d7d4b862 100644 --- a/src/Altinn.App.Core/Features/Notifications/Sms/SmsNotificationClient.cs +++ b/src/Altinn.App.Core/Features/Notifications/Sms/SmsNotificationClient.cs @@ -5,7 +5,6 @@ using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models.Notifications.Sms; using Altinn.Common.AccessTokenClient.Services; -using Microsoft.ApplicationInsights; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -13,12 +12,14 @@ namespace Altinn.App.Core.Features.Notifications.Sms; internal sealed class SmsNotificationClient : ISmsNotificationClient { + private static readonly Telemetry.Notifications.OrderType _orderType = Telemetry.Notifications.OrderType.Sms; + private readonly ILogger _logger; private readonly HttpClient _httpClient; private readonly PlatformSettings _platformSettings; private readonly IAppMetadata _appMetadata; private readonly IAccessTokenGenerator _accessTokenGenerator; - private readonly TelemetryClient? _telemetryClient; + private readonly Telemetry? _telemetry; public SmsNotificationClient( ILogger logger, @@ -26,7 +27,7 @@ public SmsNotificationClient( IOptions platformSettings, IAppMetadata appMetadata, IAccessTokenGenerator accessTokenGenerator, - TelemetryClient? telemetryClient = null + Telemetry? telemetry = null ) { _logger = logger; @@ -34,23 +35,15 @@ public SmsNotificationClient( _platformSettings = platformSettings.Value; _appMetadata = appMetadata; _accessTokenGenerator = accessTokenGenerator; - _telemetryClient = telemetryClient; + _telemetry = telemetry; } public async Task Order(SmsNotification smsNotification, CancellationToken ct) { - DateTime startDateTime = default; - long startTimestamp = default; - if (_telemetryClient is not null) - { - startDateTime = DateTime.UtcNow; - startTimestamp = Stopwatch.GetTimestamp(); - } + using var activity = _telemetry?.StartNotificationOrderActivity(_orderType); HttpResponseMessage? httpResponseMessage = null; string? httpContent = null; - Exception? exception = null; - try { Models.ApplicationMetadata? application = await _appMetadata.GetApplicationMetadata(); @@ -79,7 +72,7 @@ public async Task Order(SmsNotification smsNotific if (orderResponse is null) throw new JsonException("Couldn't deserialize SMS notification order response"); - Telemetry.OrderCount.WithLabels(Telemetry.Types.Sms, Telemetry.Result.Success).Inc(); + _telemetry?.RecordNotificationOrder(_orderType, Telemetry.Notifications.OrderResult.Success); return orderResponse; } else @@ -89,8 +82,6 @@ public async Task Order(SmsNotification smsNotific } catch (Exception e) { - exception = e; - Telemetry.OrderCount.WithLabels(Telemetry.Types.Sms, Telemetry.Result.Error).Inc(); var ex = new SmsNotificationException( $"Something went wrong when processing the SMS notification order", httpResponseMessage, @@ -98,25 +89,14 @@ public async Task Order(SmsNotification smsNotific e ); _logger.LogError(ex, "Error when processing SMS notification order"); + + _telemetry?.RecordNotificationOrder(_orderType, Telemetry.Notifications.OrderResult.Error); + throw ex; } finally { httpResponseMessage?.Dispose(); - if (_telemetryClient is not null) - { - var stopTimestamp = Stopwatch.GetTimestamp(); - var elapsed = Stopwatch.GetElapsedTime(startTimestamp, stopTimestamp); - - _telemetryClient.TrackDependency( - Telemetry.Dependency.TypeName, - Telemetry.Dependency.Name, - null, - startDateTime, - elapsed, - exception is null - ); - } } } } diff --git a/src/Altinn.App.Core/Features/Notifications/Telemetry.cs b/src/Altinn.App.Core/Features/Notifications/Telemetry.cs deleted file mode 100644 index ed79fb6fc..000000000 --- a/src/Altinn.App.Core/Features/Notifications/Telemetry.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Prometheus; - -namespace Altinn.App.Core.Features.Notifications; - -internal static class Telemetry -{ - internal static readonly Counter OrderCount = Metrics.CreateCounter( - "altinn_app_notification_order_request_count", - "Number of notification order requests.", - labelNames: ["type", "result"] - ); - - internal static class Types - { - internal static readonly string Sms = "sms"; - internal static readonly string Email = "email"; - } - - internal static class Result - { - internal static readonly string Success = "success"; - internal static readonly string Error = "error"; - } - - internal static class Dependency - { - internal static readonly string TypeName = "Altinn.Notifications"; - internal static readonly string Name = "OrderNotification"; - } -} diff --git a/src/Altinn.App.Core/Features/Options/AppOptionsService.cs b/src/Altinn.App.Core/Features/Options/AppOptionsService.cs index cd568d01b..20b20f084 100644 --- a/src/Altinn.App.Core/Features/Options/AppOptionsService.cs +++ b/src/Altinn.App.Core/Features/Options/AppOptionsService.cs @@ -9,17 +9,20 @@ public class AppOptionsService : IAppOptionsService { private readonly AppOptionsFactory _appOpptionsFactory; private readonly InstanceAppOptionsFactory _instanceAppOptionsFactory; + private readonly Telemetry? _telemetry; /// /// Initializes a new instance of the class. /// public AppOptionsService( AppOptionsFactory appOptionsFactory, - InstanceAppOptionsFactory instanceAppOptionsFactory + InstanceAppOptionsFactory instanceAppOptionsFactory, + Telemetry? telemetry = null ) { _appOpptionsFactory = appOptionsFactory; _instanceAppOptionsFactory = instanceAppOptionsFactory; + _telemetry = telemetry; } /// @@ -29,6 +32,7 @@ public async Task GetOptionsAsync( Dictionary keyValuePairs ) { + using var activity = _telemetry?.StartGetOptionsActivity(); return await _appOpptionsFactory.GetOptionsProvider(optionId).GetAppOptionsAsync(language, keyValuePairs); } @@ -40,6 +44,7 @@ public async Task GetOptionsAsync( Dictionary keyValuePairs ) { + using var activity = _telemetry?.StartGetOptionsActivity(instanceIdentifier); return await _instanceAppOptionsFactory .GetOptionsProvider(optionId) .GetInstanceAppOptionsAsync(instanceIdentifier, language, keyValuePairs); diff --git a/src/Altinn.App.Core/Features/Telemetry.AppOptionsService.cs b/src/Altinn.App.Core/Features/Telemetry.AppOptionsService.cs new file mode 100644 index 000000000..3d498ad81 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.AppOptionsService.cs @@ -0,0 +1,16 @@ +using System.Diagnostics; +using Altinn.App.Core.Models; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartGetOptionsActivity() => ActivitySource.StartActivity("AppOptionsService.GetOptions"); + + internal Activity? StartGetOptionsActivity(InstanceIdentifier instanceIdentifier) + { + var activity = ActivitySource.StartActivity("AppOptionsService.GetOptions"); + activity?.SetInstanceId(instanceIdentifier.InstanceGuid); + return activity; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.ApplicationLanguage.cs b/src/Altinn.App.Core/Features/Telemetry.ApplicationLanguage.cs new file mode 100644 index 000000000..01403ca4b --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.ApplicationLanguage.cs @@ -0,0 +1,8 @@ +using System.Diagnostics; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartGetApplicationLanguageActivity() => ActivitySource.StartActivity("ApplicationLanguage.Get"); +} diff --git a/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Client.cs b/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Client.cs new file mode 100644 index 000000000..94e0c9ef4 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Client.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; +using static Altinn.App.Core.Features.Telemetry.ApplicationMetadataClient; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartGetApplicationMetadataActivity() => ActivitySource.StartActivity($"{_prefix}.Get"); + + internal Activity? StartGetApplicationXACMLPolicyActivity() => + ActivitySource.StartActivity($"{_prefix}.GetXACMLPolicy"); + + internal Activity? StartGetApplicationBPMNProcessActivity() => + ActivitySource.StartActivity($"{_prefix}.GetBPMNProcess"); + + internal static class ApplicationMetadataClient + { + internal const string _prefix = "ApplicationMetadata.Client"; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Service.cs b/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Service.cs new file mode 100644 index 000000000..aa03c1ed5 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Service.cs @@ -0,0 +1,66 @@ +using System.Diagnostics; +using static Altinn.App.Core.Features.Telemetry.ApplicationMetadataService; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartGetTextActivity() => ActivitySource.StartActivity($"{_prefix}.GetText"); + + internal Activity? StartGetApplicationActivity() => ActivitySource.StartActivity($"{_prefix}.GetApplication"); + + internal Activity? StartGetModelJsonSchemaActivity() => + ActivitySource.StartActivity($"{_prefix}.GetModelJsonSchema"); + + internal Activity? StartGetPrefillJsonActivity() => ActivitySource.StartActivity($"{_prefix}.GetPrefillJson"); + + internal Activity? StartGetLayoutsActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayouts"); + + internal Activity? StartGetLayoutSetActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayoutSet"); + + internal Activity? StartGetLayoutSetsActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayoutSets"); + + internal Activity? StartGetLayoutsForSetActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayoutsForSet"); + + internal Activity? StartGetLayoutSetsForTaskActivity() => + ActivitySource.StartActivity($"{_prefix}.GetLayoutSetsForTask"); + + internal Activity? StartGetLayoutSettingsActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayoutSettings"); + + internal Activity? StartGetLayoutSettingsStringActivity() => + ActivitySource.StartActivity($"{_prefix}.GetLayoutSettingsString"); + + internal Activity? StartGetLayoutSettingsForSetActivity() => + ActivitySource.StartActivity($"{_prefix}.GetLayoutSettingsForSet"); + + internal Activity? StartGetLayoutSettingsStringForSetActivity() => + ActivitySource.StartActivity($"{_prefix}.GetLayoutSettingsStringForSet"); + + internal Activity? StartGetTextsActivity() => ActivitySource.StartActivity($"{_prefix}.GetTexts"); + + internal Activity? StartGetRuleConfigurationForSetActivity() => + ActivitySource.StartActivity($"{_prefix}.GetRuleConfigurationForSet"); + + internal Activity? StartGetRuleHandlerForSetActivity() => + ActivitySource.StartActivity($"{_prefix}.GetRuleHandlerForSet"); + + internal Activity? StartGetFooterActivity() => ActivitySource.StartActivity($"{_prefix}.GetFooter"); + + internal Activity? StartGetValidationConfigurationActivity() => + ActivitySource.StartActivity($"{_prefix}.GetValidationConfiguration"); + + internal Activity? StartGetLayoutModelActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayoutModel"); + + internal Activity? StartGetClassRefActivity() => ActivitySource.StartActivity($"{_prefix}.GetClassRef"); + + internal Activity? StartClientGetApplicationXACMLPolicyActivity() => + ActivitySource.StartActivity($"{_prefix}.GetXACMLPolicy"); + + internal Activity? StartClientGetApplicationBPMNProcessActivity() => + ActivitySource.StartActivity($"{_prefix}.GetBPMNProcess"); + + internal static class ApplicationMetadataService + { + internal const string _prefix = "ApplicationMetadata.Service"; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.Authentication.cs b/src/Altinn.App.Core/Features/Telemetry.Authentication.cs new file mode 100644 index 000000000..e09f097c2 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.Authentication.cs @@ -0,0 +1,9 @@ +using System.Diagnostics; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartRefreshAuthenticationTokenActivity() => + ActivitySource.StartActivity($"Authentication.Refresh"); +} diff --git a/src/Altinn.App.Core/Features/Telemetry.Authorization.Client.cs b/src/Altinn.App.Core/Features/Telemetry.Authorization.Client.cs new file mode 100644 index 000000000..2abfd9e3d --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.Authorization.Client.cs @@ -0,0 +1,76 @@ +using System.Diagnostics; +using Altinn.App.Core.Internal.Process.Authorization; +using Altinn.App.Core.Models; +using static Altinn.App.Core.Features.Telemetry.AuthorizationClient; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartClientGetPartyListActivity(int userId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.GetPartyList"); + activity?.SetUserId(userId); + + return activity; + } + + internal Activity? StartClientValidateSelectedPartyActivity(int userId, int partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.ValidateSelectedParty"); + { + activity?.SetUserId(userId); + activity?.SetInstanceOwnerPartyId(partyId); + } + return activity; + } + + internal Activity? StartClientAuthorizeActionActivity( + InstanceIdentifier instanceIdentifier, + string action, + string? taskId = null + ) + { + var activity = ActivitySource.StartActivity($"{_prefix}.AuthorizeAction"); + + activity?.SetInstanceId(instanceIdentifier.InstanceGuid); + activity?.SetInstanceOwnerPartyId(instanceIdentifier.InstanceOwnerPartyId); + activity?.SetTag(InternalLabels.AuthorizationAction, action); + activity?.SetTaskId(taskId); + + return activity; + } + + internal Activity? StartClientAuthorizeActionsActivity(Platform.Storage.Interface.Models.Instance instance) + { + var activity = ActivitySource.StartActivity($"{_prefix}.AuthorizeActions"); + + activity?.SetInstanceId(instance); + + return activity; + } + + internal Activity? StartClientIsAuthorizerActivity( + IUserActionAuthorizerProvider authorizer, + string? taskId, + string action + ) + { + var activity = ActivitySource.StartActivity($"{_prefix}.IsAuthorizerForTaskAndAction"); + if (activity is not null) + { + activity.SetTaskId(taskId); + if (authorizer.TaskId is not null) + activity.SetTag(InternalLabels.AuthorizerTaskId, authorizer.TaskId); + if (authorizer.Action is not null) + activity.SetTag(InternalLabels.AuthorizerAction, authorizer.Action); + activity.SetTag(InternalLabels.AuthorizationAction, action); + } + return activity; + } + + internal static class AuthorizationClient + { + internal const string _prefix = "Authorization.Client"; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.Authorization.Service.cs b/src/Altinn.App.Core/Features/Telemetry.Authorization.Service.cs new file mode 100644 index 000000000..eae417d33 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.Authorization.Service.cs @@ -0,0 +1,87 @@ +using System.Diagnostics; +using Altinn.App.Core.Internal.Process.Authorization; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Models; +using static Altinn.App.Core.Features.Telemetry.AuthorizationService; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartGetPartyListActivity(int userId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.GetPartyList"); + activity?.SetUserId(userId); + return activity; + } + + internal Activity? StartValidateSelectedPartyActivity(int userId, int partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.ValidateSelectedParty"); + activity?.SetUserId(userId); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal Activity? StartAuthorizeActionActivity( + InstanceIdentifier instanceIdentifier, + string action, + string? taskId = null + ) + { + var activity = ActivitySource.StartActivity($"{_prefix}.AuthorizeAction"); + + activity?.SetInstanceId(instanceIdentifier.InstanceGuid); + activity?.SetInstanceOwnerPartyId(instanceIdentifier.InstanceOwnerPartyId); + activity?.SetTag(InternalLabels.AuthorizationAction, action); + activity?.SetTaskId(taskId); + return activity; + } + + internal Activity? StartAuthorizeActionsActivity( + Platform.Storage.Interface.Models.Instance instance, + List actions + ) + { + var activity = ActivitySource.StartActivity($"{_prefix}.AuthorizeActions"); + if (activity is not null) + { + activity.SetInstanceId(instance); + var now = DateTimeOffset.UtcNow; + ActivityTagsCollection tags = new([new("actions.count", actions.Count),]); + for (int i = 0; i < actions.Count; i++) + { + var action = actions[i]; + tags.Add(new($"actions.{i}.value", action.Value)); + tags.Add(new($"actions.{i}.type", action.ActionType.ToString())); + } + + activity.AddEvent(new ActivityEvent("actions", now, tags)); + } + return activity; + } + + internal Activity? StartIsAuthorizerActivity( + IUserActionAuthorizerProvider authorizer, + string? taskId, + string action + ) + { + var activity = ActivitySource.StartActivity($"{_prefix}.IsAuthorizerForTaskAndAction"); + if (activity is not null) + { + activity.SetTaskId(taskId); + if (authorizer.TaskId is not null) + activity.SetTag(InternalLabels.AuthorizerTaskId, authorizer.TaskId); + if (authorizer.Action is not null) + activity.SetTag(InternalLabels.AuthorizerAction, authorizer.Action); + activity.SetTag(InternalLabels.AuthorizationAction, action); + } + return activity; + } + + internal static class AuthorizationService + { + internal const string _prefix = "Authorization.Service"; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.Data.cs b/src/Altinn.App.Core/Features/Telemetry.Data.cs new file mode 100644 index 000000000..06219c636 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.Data.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using Altinn.Platform.Storage.Interface.Models; +using NetEscapades.EnumGenerators; +using static Altinn.App.Core.Features.Telemetry.Data; +using Tag = System.Collections.Generic.KeyValuePair; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + private void InitData(InitContext context) + { + InitMetricCounter( + context, + MetricNameDataPatched, + init: static m => + { + m.Add(0, new Tag(InternalLabels.Result, PatchResult.Success.ToStringFast())); + m.Add(0, new Tag(InternalLabels.Result, PatchResult.Error.ToStringFast())); + } + ); + } + + internal void DataPatched(PatchResult result) => + _counters[MetricNameDataPatched].Add(1, new Tag(InternalLabels.Result, result.ToStringFast())); + + internal Activity? StartDataPatchActivity(Instance instance) + { + var activity = ActivitySource.StartActivity($"{_prefix}.Patch"); + activity?.SetInstanceId(instance); + return activity; + } + + internal static class Data + { + internal const string _prefix = "Data"; + internal static readonly string MetricNameDataPatched = Metrics.CreateLibName("data_patched"); + + [EnumExtensions] + internal enum PatchResult + { + [Display(Name = "success")] + Success, + + [Display(Name = "error")] + Error + } + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.DataClient.cs b/src/Altinn.App.Core/Features/Telemetry.DataClient.cs new file mode 100644 index 000000000..a827d6c5e --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.DataClient.cs @@ -0,0 +1,129 @@ +using System.Diagnostics; +using Altinn.Platform.Storage.Interface.Models; +using static Altinn.App.Core.Features.Telemetry.DataClient; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartInsertFormDataActivity(Instance? instance) + { + var activity = ActivitySource.StartActivity($"{_prefix}.InsertFormData"); + activity?.SetInstanceId(instance); + return activity; + } + + internal Activity? StartInsertFormDataActivity(Guid? instanceId, int? partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.InsertFormData"); + activity?.SetInstanceId(instanceId); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal Activity? StartUpdateDataActivity(Guid? instanceId, int? partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.UpdateData"); + activity?.SetInstanceId(instanceId); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal Activity? StartGetBinaryDataActivity(Guid? instanceId, int? partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.GetBinaryData"); + activity?.SetInstanceId(instanceId); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal Activity? StartGetBinaryDataListActivity(Guid? instanceId, int? partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.GetBinaryDataList"); + activity?.SetInstanceId(instanceId); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal Activity? StartDeleteBinaryDataActivity(Guid? instanceId, int? partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.DeleteBinaryData"); + activity?.SetInstanceId(instanceId); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal Activity? StartInsertBinaryDataActivity(Guid? instanceId, int? partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.InsertBinaryData"); + activity?.SetInstanceId(instanceId); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal Activity? StartInsertBinaryDataActivity(string? instanceId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.InsertBinaryData"); + activity?.SetInstanceId(instanceId); + return activity; + } + + internal Activity? StartUpdateBinaryDataActivity(Guid? instanceId, int? partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.UpdateBinaryData"); + activity?.SetInstanceId(instanceId); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal Activity? StartUpdateBinaryDataActivity(string? instanceId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.UpdateBinaryData"); + activity?.SetInstanceId(instanceId); + return activity; + } + + internal Activity? StartUpdateDataActivity(Instance? instance) + { + var activity = ActivitySource.StartActivity($"{_prefix}.UpdateData"); + activity?.SetInstanceId(instance); + return activity; + } + + internal Activity? StartDeleteDataActivity(Guid? instanceId, int? partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.DeleteData"); + activity?.SetInstanceId(instanceId); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal Activity? StartGetFormDataActivity(Guid? instanceId, int? partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.GetFormData"); + activity?.SetInstanceId(instanceId); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal Activity? StartLockDataElementActivity(string? instanceId, Guid? dataGuid) + { + var activity = ActivitySource.StartActivity($"{_prefix}.LockDataElement"); + activity?.SetInstanceId(instanceId); + activity?.SetDataElementId(dataGuid); + return activity; + } + + internal Activity? StartUnlockDataElementActivity(string? instanceId, Guid? dataGuid) + { + var activity = ActivitySource.StartActivity($"{_prefix}.UnlockDataElement"); + activity?.SetInstanceId(instanceId); + activity?.SetDataElementId(dataGuid); + return activity; + } + + internal static class DataClient + { + internal const string _prefix = "DataClient"; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.DataList.cs b/src/Altinn.App.Core/Features/Telemetry.DataList.cs new file mode 100644 index 000000000..9173f742f --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.DataList.cs @@ -0,0 +1,16 @@ +using System.Diagnostics; +using Altinn.App.Core.Models; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartDataListActivity() => ActivitySource.StartActivity("DataList.Get"); + + internal Activity? StartDataListActivity(InstanceIdentifier instanceIdentifier) + { + var activity = ActivitySource.StartActivity("DataList.GetWithId"); + activity?.SetInstanceId(instanceIdentifier.InstanceGuid); + return activity; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.ErClient.cs b/src/Altinn.App.Core/Features/Telemetry.ErClient.cs new file mode 100644 index 000000000..1fa464600 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.ErClient.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartGetOrganizationActivity(string? orgNr) + { + var activity = ActivitySource.StartActivity("RegisterERClient.GetOrganization"); + activity?.SetOrganisationNumber(orgNr); + return activity; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.EventsClient.cs b/src/Altinn.App.Core/Features/Telemetry.EventsClient.cs new file mode 100644 index 000000000..4be75e61e --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.EventsClient.cs @@ -0,0 +1,14 @@ +using System.Diagnostics; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartAddEventActivity(Instance instance) + { + var activity = ActivitySource.StartActivity("EventClient.GetAsyncWithId"); + activity?.SetInstanceId(instance); + return activity; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.FileAnalysis.cs b/src/Altinn.App.Core/Features/Telemetry.FileAnalysis.cs new file mode 100644 index 000000000..1d799fc16 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.FileAnalysis.cs @@ -0,0 +1,8 @@ +using System.Diagnostics; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartAnalyseActivity() => ActivitySource.StartActivity("FileAnalysis.Analyse"); +} diff --git a/src/Altinn.App.Core/Features/Telemetry.FileValidation.cs b/src/Altinn.App.Core/Features/Telemetry.FileValidation.cs new file mode 100644 index 000000000..dd0e83719 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.FileValidation.cs @@ -0,0 +1,8 @@ +using System.Diagnostics; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartFileValidateActivity() => ActivitySource.StartActivity("FileValidatorService.Validate"); +} diff --git a/src/Altinn.App.Core/Features/Telemetry.Instances.cs b/src/Altinn.App.Core/Features/Telemetry.Instances.cs new file mode 100644 index 000000000..757b93fb3 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.Instances.cs @@ -0,0 +1,135 @@ +using System.Diagnostics; +using Altinn.Platform.Storage.Interface.Models; +using static Altinn.App.Core.Features.Telemetry.Instances; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + private void InitInstances(InitContext context) + { + InitMetricCounter(context, MetricNameInstancesCreated, init: static m => m.Add(0)); + InitMetricCounter(context, MetricNameInstancesCompleted, init: static m => m.Add(0)); + InitMetricCounter(context, MetricNameInstancesDeleted, init: static m => m.Add(0)); + + InitMetricHistogram(context, MetricNameInstancesDuration); + } + + internal void InstanceCreated(Instance instance) => _counters[MetricNameInstancesCreated].Add(1); + + internal void InstanceCompleted(Instance instance) + { + _counters[MetricNameInstancesCompleted].Add(1); + + if (instance.Created is not null) + { + var duration = DateTime.UtcNow - instance.Created.Value; + _histograms[MetricNameInstancesDuration].Record(duration.TotalSeconds); + } + } + + internal void InstanceDeleted(Instance instance) + { + _counters[MetricNameInstancesDeleted].Add(1); + + if (instance.Created is not null) + { + var duration = DateTime.UtcNow - instance.Created.Value; + _histograms[MetricNameInstancesDuration].Record(duration.TotalSeconds); + } + } + + internal Activity? StartGetInstanceByGuidActivity(Guid? instanceGuid = null) + { + var activity = ActivitySource.StartActivity($"{_prefix}.GetInstanceByGuid"); + activity?.SetInstanceId(instanceGuid); + return activity; + } + + internal Activity? StartGetInstanceByInstanceActivity(Guid? instanceGuid = null) + { + var activity = ActivitySource.StartActivity($"{_prefix}.GetInstanceByInstance"); + activity?.SetInstanceId(instanceGuid); + return activity; + } + + internal Activity? StartGetInstancesActivity(Guid? instanceGuid = null) + { + var activity = ActivitySource.StartActivity($"{_prefix}.GetInstances"); + activity?.SetInstanceId(instanceGuid); + return activity; + } + + internal Activity? StartQueryInstancesActivity() => ActivitySource.StartActivity($"{_prefix}.Query"); + + internal Activity? StartCreateInstanceActivity() + { + var activity = ActivitySource.StartActivity($"{_prefix}.Create"); + return activity; + } + + internal Activity? StartDeleteInstanceActivity(Guid instanceGuid, int instanceOwnerPartyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.Delete"); + activity?.SetInstanceId(instanceGuid); + activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); + return activity; + } + + internal Activity? StartUpdateProcessActivity(Instance instance) + { + var activity = ActivitySource.StartActivity($"{_prefix}.UpdateProcess"); + activity?.SetInstanceId(instance); + return activity; + } + + internal Activity? StartCompleteConfirmationActivity(Guid instanceGuid, int instanceOwnerPartyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.CompleteConfirmation"); + activity?.SetInstanceId(instanceGuid); + activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); + return activity; + } + + internal Activity? StartUpdateReadStatusActivity(Guid instanceGuid, int instanceOwnerPartyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.UpdateReadStatus"); + activity?.SetInstanceId(instanceGuid); + activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); + return activity; + } + + internal Activity? StartUpdateSubStatusActivity(Guid instanceGuid, int instanceOwnerPartyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.UpdateSubStatus"); + activity?.SetInstanceId(instanceGuid); + activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); + return activity; + } + + internal Activity? StartUpdatePresentationTextActivity(Guid instanceGuid, int instanceOwnerPartyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.UpdatePresentationText"); + activity?.SetInstanceId(instanceGuid); + activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); + return activity; + } + + internal Activity? StartUpdateDataValuesActivity(Guid instanceGuid, int instanceOwnerPartyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.UpdateDataValues"); + activity?.SetInstanceId(instanceGuid); + activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); + return activity; + } + + internal static class Instances + { + internal const string _prefix = "Instance"; + + internal static readonly string MetricNameInstancesCreated = Metrics.CreateLibName("instances_created"); + internal static readonly string MetricNameInstancesCompleted = Metrics.CreateLibName("instances_completed"); + internal static readonly string MetricNameInstancesDeleted = Metrics.CreateLibName("instances_deleted"); + internal static readonly string MetricNameInstancesDuration = Metrics.CreateLibName("instances_duration"); + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.Notifications.cs b/src/Altinn.App.Core/Features/Telemetry.Notifications.cs new file mode 100644 index 000000000..bdd928665 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.Notifications.cs @@ -0,0 +1,79 @@ +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using NetEscapades.EnumGenerators; +using static Altinn.App.Core.Features.Telemetry.Notifications; +using Tag = System.Collections.Generic.KeyValuePair; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + /// + /// Prometheus' increase and rate functions do not register the first value as an increase, but rather as the registration.
+ /// This means that going from none (non-existant) to 1 on a counter will register as an increase of 0.
+ /// In order to workaround this, we initialize to 0 for all metrics here.
+ /// Github issue can be found here. + ///
+ /// + private void InitNotifications(InitContext context) + { + InitMetricCounter( + context, + MetricNameOrder, + init: static m => + { + foreach (var type in OrderTypeExtensions.GetValues()) + { + foreach (var result in OrderResultExtensions.GetValues()) + { + m.Add( + 0, + new Tag(InternalLabels.Type, type.ToStringFast()), + new Tag(InternalLabels.Result, result.ToStringFast()) + ); + } + } + } + ); + } + + internal Activity? StartNotificationOrderActivity(OrderType type) + { + var activity = ActivitySource.StartActivity("Notifications.Order"); + activity?.SetTag(InternalLabels.Type, type.ToStringFast()); + return activity; + } + + internal void RecordNotificationOrder(OrderType type, OrderResult result) => + _counters[MetricNameOrder] + .Add( + 1, + new Tag(InternalLabels.Type, type.ToStringFast()), + new Tag(InternalLabels.Result, result.ToStringFast()) + ); + + internal static class Notifications + { + internal static readonly string MetricNameOrder = Metrics.CreateLibName("notification_orders"); + + [EnumExtensions] + internal enum OrderResult + { + [Display(Name = "success")] + Success, + + [Display(Name = "error")] + Error + } + + [EnumExtensions] + internal enum OrderType + { + [Display(Name = "sms")] + Sms, + + [Display(Name = "email")] + Email, + } + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.PartyClient.cs b/src/Altinn.App.Core/Features/Telemetry.PartyClient.cs new file mode 100644 index 000000000..f406b0178 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.PartyClient.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartGetPartyActivity(int? partyId) + { + var activity = ActivitySource.StartActivity("PartyClient.GetParty"); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal Activity? StartLookupPartyActivity() => ActivitySource.StartActivity("PartyClient.LookupParty"); +} diff --git a/src/Altinn.App.Core/Features/Telemetry.PdfService.cs b/src/Altinn.App.Core/Features/Telemetry.PdfService.cs new file mode 100644 index 000000000..8f4767c07 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.PdfService.cs @@ -0,0 +1,23 @@ +using System.Diagnostics; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartGenerateAndStorePdfActivity(Instance? instance, string? taskId) + { + var activity = ActivitySource.StartActivity("PdfService.GenerateAndStorePdf"); + activity?.SetInstanceId(instance); + activity?.SetTaskId(taskId); + return activity; + } + + internal Activity? StartGeneratePdfActivity(Instance? instance, string? taskId) + { + var activity = ActivitySource.StartActivity("PdfService.GeneratePdf"); + activity?.SetInstanceId(instance); + activity?.SetTaskId(taskId); + return activity; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.Prefill.Service.cs b/src/Altinn.App.Core/Features/Telemetry.Prefill.Service.cs new file mode 100644 index 000000000..f2f3b00ec --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.Prefill.Service.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; +using static Altinn.App.Core.Features.Telemetry.PrefillService; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartPrefillDataModelActivity() => ActivitySource.StartActivity($"{_prefix}.PrefillDataModel"); + + internal Activity? StartPrefillDataModelActivity(string? partyId) + { + var activity = ActivitySource.StartActivity($"{_prefix}.PrefillDataModelWithId"); + activity?.SetInstanceOwnerPartyId(partyId); + return activity; + } + + internal static class PrefillService + { + internal const string _prefix = "PrefillService"; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.ProcessClient.cs b/src/Altinn.App.Core/Features/Telemetry.ProcessClient.cs new file mode 100644 index 000000000..c31059a0f --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.ProcessClient.cs @@ -0,0 +1,17 @@ +using System.Diagnostics; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartGetProcessDefinitionActivity() => + ActivitySource.StartActivity("ProcessClient.GetProcessDefinition"); + + internal Activity? StartGetProcessHistoryActivity(string? instanceId, string? instanceOwnerPartyId) + { + var activity = ActivitySource.StartActivity("ProcessClient.GetProcessHistory"); + activity?.SetInstanceId(instanceId); + activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); + return activity; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.ProcessReader.cs b/src/Altinn.App.Core/Features/Telemetry.ProcessReader.cs new file mode 100644 index 000000000..799cac777 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.ProcessReader.cs @@ -0,0 +1,51 @@ +using System.Diagnostics; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartGetStartEventsActivity() => ActivitySource.StartActivity("ProcessReader.GetStartEvents"); + + internal Activity? StartGetStartEventIdsActivity() => + ActivitySource.StartActivity("ProcessReader.GetStartEventIds"); + + internal Activity? StartIsStartEventActivity() => ActivitySource.StartActivity("ProcessReader.IsStartEvent"); + + internal Activity? StartGetProcessTasksActivity() => ActivitySource.StartActivity("ProcessReader.GetProcessTasks"); + + internal Activity? StartGetProcessTaskIdsActivity() => + ActivitySource.StartActivity("ProcessReader.GetProcessTaskIds"); + + internal Activity? StartIsProcessTaskActivity() => ActivitySource.StartActivity("ProcessReader.IsProcessTask"); + + internal Activity? StartGetExclusiveGatewaysActivity() => + ActivitySource.StartActivity("ProcessReader.GetExclusiveGateways"); + + internal Activity? StartGetExclusiveGatewayIdsActivity() => + ActivitySource.StartActivity("ProcessReader.GetExclusiveGatewayIds"); + + internal Activity? StartGetEndEventsActivity() => ActivitySource.StartActivity("ProcessReader.GetEndEvents"); + + internal Activity? StartGetEndEventIdsActivity() => ActivitySource.StartActivity("ProcessReader.GetEndEventIds"); + + internal Activity? StartIsEndEventActivity() => ActivitySource.StartActivity("ProcessReader.IsEndEvent"); + + internal Activity? StartGetSequenceFlowsActivity() => + ActivitySource.StartActivity("ProcessReader.GetSequenceFlows"); + + internal Activity? StartGetSequenceFlowIdsActivity() => + ActivitySource.StartActivity("ProcessReader.GetSequenceFlowIds"); + + internal Activity? StartGetFlowElementActivity() => ActivitySource.StartActivity("ProcessReader.GetFlowElement"); + + internal Activity? StartGetNextElementsActivity() => ActivitySource.StartActivity("ProcessReader.GetNextElements"); + + internal Activity? StartGetOutgoingSequenceFlowsActivity() => + ActivitySource.StartActivity("ProcessReader.GetOutgoingSequenceFlows"); + + internal Activity? StartGetAllFlowElementsActivity() => + ActivitySource.StartActivity("ProcessReader.GetAllFlowElements"); + + internal Activity? StartGetAltinnTaskExtensionActivity() => + ActivitySource.StartActivity("ProcessReader.GetAltinnTaskExtension"); +} diff --git a/src/Altinn.App.Core/Features/Telemetry.Processes.cs b/src/Altinn.App.Core/Features/Telemetry.Processes.cs new file mode 100644 index 000000000..a27b0bd3a --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.Processes.cs @@ -0,0 +1,65 @@ +using System.Diagnostics; +using Altinn.App.Core.Models.Process; +using Altinn.Platform.Storage.Interface.Models; +using static Altinn.App.Core.Features.Telemetry.Processes; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + private void InitProcesses(InitContext context) + { + InitMetricCounter(context, MetricNameProcessesStarted, static m => m.Add(0)); + InitMetricCounter(context, MetricNameProcessesEnded, static m => m.Add(0)); + + InitMetricHistogram(context, MetricNameProcessesDuration); + } + + internal void ProcessStarted() + { + _counters[MetricNameProcessesStarted].Add(1); + } + + internal void ProcessEnded(ProcessStateChange processChange) + { + if (processChange?.NewProcessState?.Started is null || processChange?.NewProcessState?.Ended is null) + { + return; + } + var state = processChange.NewProcessState; + + _counters[MetricNameProcessesEnded].Add(1); + var duration = state.Ended.Value - state.Started.Value; + _histograms[MetricNameProcessesDuration].Record(duration.TotalSeconds); + } + + internal Activity? StartProcessStartActivity(Instance instance) + { + var activity = ActivitySource.StartActivity($"{_prefix}.Start"); + activity?.SetInstanceId(instance); + return activity; + } + + internal Activity? StartProcessNextActivity(Instance instance) + { + var activity = ActivitySource.StartActivity($"{_prefix}.Next"); + activity?.SetInstanceId(instance); + return activity; + } + + internal Activity? StartProcessEndActivity(Instance instance) + { + var activity = ActivitySource.StartActivity($"{_prefix}.End"); + activity?.SetInstanceId(instance); + return activity; + } + + internal static class Processes + { + internal const string _prefix = "Process"; + + internal static readonly string MetricNameProcessesStarted = Metrics.CreateLibName("processes_started"); + internal static readonly string MetricNameProcessesEnded = Metrics.CreateLibName("processes_ended"); + internal static readonly string MetricNameProcessesDuration = Metrics.CreateLibName("processes_duration"); + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.ProfileClient.cs b/src/Altinn.App.Core/Features/Telemetry.ProfileClient.cs new file mode 100644 index 000000000..10d1e50d4 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.ProfileClient.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartGetUserProfileActivity(int? userId) + { + var activity = ActivitySource.StartActivity("ProfileClient.GetUserProfile"); + activity?.SetUserId(userId); + return activity; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.Validation.cs b/src/Altinn.App.Core/Features/Telemetry.Validation.cs new file mode 100644 index 000000000..173728d26 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.Validation.cs @@ -0,0 +1,72 @@ +using System.Diagnostics; +using Altinn.Platform.Storage.Interface.Models; +using static Altinn.App.Core.Features.Telemetry.Validation; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + private void InitValidation(InitContext context) { } + + internal Activity? StartValidateInstanceAtTaskActivity(Instance instance, string taskId) + { + ArgumentException.ThrowIfNullOrWhiteSpace(taskId); + + var activity = ActivitySource.StartActivity($"{_prefix}.ValidateInstanceAtTask"); + activity?.SetTaskId(taskId); + activity?.SetInstanceId(instance); + return activity; + } + + internal Activity? StartRunTaskValidatorActivity(ITaskValidator validator) + { + var activity = ActivitySource.StartActivity($"{_prefix}.RunTaskValidator"); + + activity?.SetTag(InternalLabels.ValidatorType, validator.GetType().Name); + activity?.SetTag(InternalLabels.ValidatorSource, validator.ValidationSource); + + return activity; + } + + internal Activity? StartValidateDataElementActivity(Instance instance, DataElement dataElement) + { + var activity = ActivitySource.StartActivity($"{_prefix}.ValidateDataElement"); + activity?.SetInstanceId(instance); + activity?.SetDataElementId(dataElement); + return activity; + } + + internal Activity? StartRunDataElementValidatorActivity(IDataElementValidator validator) + { + var activity = ActivitySource.StartActivity($"{_prefix}.RunDataElementValidator"); + + activity?.SetTag(InternalLabels.ValidatorType, validator.GetType().Name); + activity?.SetTag(InternalLabels.ValidatorSource, validator.ValidationSource); + + return activity; + } + + internal Activity? StartValidateFormDataActivity(Instance instance, DataElement dataElement) + { + var activity = ActivitySource.StartActivity($"{_prefix}.ValidateFormData"); + + activity?.SetInstanceId(instance); + activity?.SetDataElementId(dataElement); + return activity; + } + + internal Activity? StartRunFormDataValidatorActivity(IFormDataValidator validator) + { + var activity = ActivitySource.StartActivity($"{_prefix}.RunFormDataValidator"); + + activity?.SetTag(InternalLabels.ValidatorType, validator.GetType().Name); + activity?.SetTag(InternalLabels.ValidatorSource, validator.ValidationSource); + + return activity; + } + + internal static class Validation + { + internal const string _prefix = "Validation"; + } +} diff --git a/src/Altinn.App.Core/Features/Telemetry.cs b/src/Altinn.App.Core/Features/Telemetry.cs new file mode 100644 index 000000000..110a01cb6 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry.cs @@ -0,0 +1,206 @@ +using System.Collections.Frozen; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Models; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Features; + +/// +/// Used for creating traces and metrics for the app. +/// +/// NOTE: this class holds all labels, metrics and trace datastructures for OTel based instrumentation. +/// There are a couple of reasons to do this +/// * Decouple metric lifetime from the objects that use them (since they are often scoped/transient) +/// * Create a logical boundary to emphasize that telemetry and label names are considered public contract, subject to semver same as the rest of the code +/// * Reason being that users may refer to these names in alerts, dashboards, saved queries etc +/// * Minimize cluttering of "business logic" with instrumentation code +/// +/// Watch out for high cardinality when choosing tags. Most timeseries and tracing databases +/// do not handle high cardinality well. +public sealed partial class Telemetry : IDisposable +{ + internal bool IsDisposed; + internal bool IsInitialized; + private readonly object _lock = new(); + + /// + /// Gets the ActivitySource for the app. + /// Using this, you can create traces that are transported to the OpenTelemetry collector. + /// + public ActivitySource ActivitySource { get; } + + /// + /// Gets the Meter for the app. + /// Using this, you can create metrics that are transported to the OpenTelemetry collector. + /// + public Meter Meter { get; } + + private FrozenDictionary> _counters; + private FrozenDictionary> _histograms; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public Telemetry(AppIdentifier appIdentifier, IOptions appSettings) + { + var appId = appIdentifier.App; + var appVersion = appSettings.Value.AppVersion; + ActivitySource = new ActivitySource(appId, appVersion); + Meter = new Meter(appId, appVersion); + + _counters = FrozenDictionary>.Empty; + _histograms = FrozenDictionary>.Empty; + + Init(); + } + + internal void Init() + { + lock (_lock) + { + if (IsInitialized) + return; + IsInitialized = true; + + var counters = new Dictionary>(); + var histograms = new Dictionary>(); + var context = new InitContext(counters, histograms); + + InitData(context); + InitInstances(context); + InitNotifications(context); + InitProcesses(context); + InitValidation(context); + + // NOTE: This Telemetry class is registered as a singleton + // Metrics could be kept in fields of the respective objects that use them for instrumentation + // but most of these objects have scoped or transient lifetime, which would be inefficient. + // So instead they are kept in frozen dicts here and looked up as they are incremented. + // Another option would be to keep them as plain fields here + _counters = counters.ToFrozenDictionary(); + _histograms = histograms.ToFrozenDictionary(); + } + } + + private readonly record struct InitContext( + Dictionary> Counters, + Dictionary> Histograms + ); + + /// + /// Utility methods for creating metrics for an app. + /// + public static class Metrics + { + internal static readonly string Prefix = "altinn_app_lib"; + internal static readonly string PrefixCustom = "altinn_app"; + + /// + /// Creates a name for a metric with the prefix "altinn_app". + /// + /// Name of the metric, naming-convention is 'snake_case' + /// Full metric name + public static string CreateName(string name) => $"{PrefixCustom}_{name}"; + + internal static string CreateLibName(string name) => $"{Prefix}_{name}"; + } + + /// + /// Labels used to tag traces for observability. + /// + public static class Labels + { + /// + /// Label for the party ID of the instance owner. + /// + public static readonly string InstanceOwnerPartyId = "instance.owner_party_id"; + + /// + /// Label for the guid that identifies the instance. + /// + public static readonly string InstanceGuid = "instance.guid"; + + /// + /// Label for the guid that identifies the data. + /// + public static readonly string DataGuid = "data.guid"; + + /// + /// Label for the ID of the task. + /// + public static readonly string TaskId = "task.id"; + + /// + /// Label for the name of the user. + /// + public const string UserName = "user.name"; + + /// + /// Label for the ID of the user. + /// + public const string UserId = "user.id"; + + /// + /// Label for the ID of the party. + /// + public const string UserPartyId = "user.party_id"; + + /// + /// Label for the authentication level of the user. + /// + public const string UserAuthenticationLevel = "user.authentication_level"; + + /// + /// Label for the organisation number. + /// + public const string OrganisationNumber = "organisation.number"; + } + + internal static class InternalLabels + { + internal const string Result = "result"; + internal const string Type = "type"; + internal const string AuthorizationAction = "authorization.action"; + internal const string AuthorizerAction = "authorization.authorizer.action"; + internal const string AuthorizerTaskId = "authorization.authorizer.task_id"; + internal const string ValidatorType = "validator.type"; + internal const string ValidatorSource = "validator.source"; + } + + private void InitMetricCounter(InitContext context, string name, Action> init) + { + // NOTE: There is an initialization function here mostly to zero-init counters. + // This is useful in a prometheus-setting due to the 'increase' operator being a bit strange: + // * none -> 1 does not count as an increase + // * 0 -> 1 does count as an increase + var counter = Meter.CreateCounter(name, unit: null, description: null); + context.Counters.Add(name, counter); + init(counter); + } + + private void InitMetricHistogram(InitContext context, string name) + { + var histogram = Meter.CreateHistogram(name, unit: null, description: null); + context.Histograms.Add(name, histogram); + } + + /// + /// Disposes the Telemetry object. + /// + public void Dispose() + { + lock (_lock) + { + if (IsDisposed) + return; + + IsDisposed = true; + ActivitySource?.Dispose(); + Meter?.Dispose(); + } + } +} diff --git a/src/Altinn.App.Core/Features/TelemetryActivityExtensions.cs b/src/Altinn.App.Core/Features/TelemetryActivityExtensions.cs new file mode 100644 index 000000000..ad8079ea9 --- /dev/null +++ b/src/Altinn.App.Core/Features/TelemetryActivityExtensions.cs @@ -0,0 +1,251 @@ +using System.Diagnostics; +using Altinn.Platform.Storage.Interface.Models; +using OpenTelemetry.Trace; +using Labels = Altinn.App.Core.Features.Telemetry.Labels; + +namespace Altinn.App.Core.Features; + +/// +/// Extensions for instrumentation APIs +/// +public static class TelemetryActivityExtensions +{ + /// + /// Sets the user ID as a tag/attribute on the activity/span + /// + /// Activity + /// User ID + /// Activity + public static Activity SetUserId(this Activity activity, int? userId) + { + if (userId is not null) + { + activity.SetTag(Labels.UserId, userId.Value); + } + return activity; + } + + /// + /// Sets the user party ID as a tag/attribute on the activity/span + /// + /// Activity + /// User party ID + /// Activity + public static Activity SetUserPartyId(this Activity activity, int? userPartyId) + { + if (userPartyId is not null) + { + activity.SetTag(Labels.UserPartyId, userPartyId.Value); + } + return activity; + } + + /// + /// Sets the username as a tag/attribute on the activity/span + /// + /// Activity + /// Username + /// Activity + public static Activity SetUsername(this Activity activity, string? username) + { + if (!string.IsNullOrWhiteSpace(username)) + { + activity.SetTag(Labels.UserName, username); + } + return activity; + } + + /// + /// Sets the user authentication level as a tag/attribute on the activity/span + /// + /// Activity + /// Authentication level + /// Activity + public static Activity SetAuthenticationLevel(this Activity activity, int? authenticationLevel) + { + if (authenticationLevel is not null) + { + activity.SetTag(Labels.UserAuthenticationLevel, authenticationLevel.Value); + } + return activity; + } + + /// + /// Sets the Instance GUID as a tag/attribute on the activity/span + /// + /// Activity + /// Instance + /// Activity + public static Activity SetInstanceId(this Activity activity, Instance? instance) + { + if (instance?.Id is not null) + { + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + activity.SetTag(Labels.InstanceGuid, instanceGuid); + } + return activity; + } + + /// + /// Sets the Instance GUID as a tag/attribute on the activity/span + /// + /// Activity + /// Instance ID + /// Activity + public static Activity SetInstanceId(this Activity activity, string? instanceId) + { + if (!string.IsNullOrWhiteSpace(instanceId)) + { + Guid instanceGuid = Guid.Parse(instanceId.Split("/")[1]); + activity.SetTag(Labels.InstanceGuid, instanceGuid); + } + return activity; + } + + /// + /// Sets the Instance GUID as a tag/attribute on the activity/span + /// + /// Activity + /// Instance GUID + /// Activity + public static Activity SetInstanceId(this Activity activity, Guid? instanceGuid) + { + if (instanceGuid is not null) + { + activity.SetTag(Labels.InstanceGuid, instanceGuid.Value); + } + + return activity; + } + + /// + /// Sets the Instance owner Party ID as a tag/attribute on the activity/span + /// + /// Activity + /// Instance owner Party ID + /// Activity + public static Activity SetInstanceOwnerPartyId(this Activity activity, int? instanceOwnerPartyId) + { + if (instanceOwnerPartyId is not null) + { + activity.SetTag(Labels.InstanceOwnerPartyId, instanceOwnerPartyId.Value); + } + + return activity; + } + + /// + /// Sets the Instance owner Party ID as a tag/attribute on the activity/span + /// + /// Activity + /// Instance owner Party ID + /// Activity + public static Activity SetInstanceOwnerPartyId(this Activity activity, string? instanceOwnerPartyId) + { + if (!string.IsNullOrWhiteSpace(instanceOwnerPartyId)) + { + activity.SetTag(Labels.InstanceOwnerPartyId, instanceOwnerPartyId); + } + + return activity; + } + + /// + /// Sets the Data GUID as a tag/attribute on the activity/span + /// + /// Activity + /// Data element + /// Activity + public static Activity SetDataElementId(this Activity activity, DataElement? dataElement) + { + if (dataElement?.Id is not null) + { + Guid dataGuid = Guid.Parse(dataElement.Id); + activity.SetTag(Labels.DataGuid, dataGuid); + } + + return activity; + } + + /// + /// Sets the Data GUID as a tag/attribute on the activity/span + /// + /// Activity + /// Data element ID + /// Activity + public static Activity SetDataElementId(this Activity activity, string? dataElementId) + { + if (!string.IsNullOrWhiteSpace(dataElementId)) + { + Guid dataGuid = Guid.Parse(dataElementId); + activity.SetTag(Labels.DataGuid, dataGuid); + } + + return activity; + } + + /// + /// Sets the Data GUID as a tag/attribute on the activity/span + /// + /// Activity + /// Data element ID + /// Activity + public static Activity SetDataElementId(this Activity activity, Guid? dataElementId) + { + if (dataElementId is not null) + { + activity.SetTag(Labels.DataGuid, dataElementId.Value); + } + + return activity; + } + + /// + /// Sets the Process Task ID as a tag/attribute on the activity/span + /// + /// Activity + /// Task ID + /// Activity + public static Activity SetTaskId(this Activity activity, string? taskId) + { + if (!string.IsNullOrWhiteSpace(taskId)) + { + activity.SetTag(Labels.TaskId, taskId); + } + + return activity; + } + + /// + /// Sets the Process Task ID as a tag/attribute on the activity/span + /// + /// Activity + /// Organisation number + /// Activity + public static Activity SetOrganisationNumber(this Activity activity, string? organisationNumber) + { + if (!string.IsNullOrWhiteSpace(organisationNumber)) + { + activity.SetTag(Labels.OrganisationNumber, organisationNumber); + } + + return activity; + } + + /// + /// Used to record an exception on the activity. + /// Should be used when + /// * Calling into user code fails + /// * ? + /// Typically, whenever we record an exception, + /// two spans in a trace will be marked as errored: the root span and this span. + /// + /// Activity + /// Exception + /// Error message + internal static void Errored(this Activity activity, Exception? exception = null, string? error = null) + { + activity.SetStatus(ActivityStatusCode.Error, error); + activity.RecordException(exception); + } +} diff --git a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs index 258529d66..6c0a3d6b4 100644 --- a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs +++ b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs @@ -1,6 +1,7 @@ using System.Text; using System.Text.Json; using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; @@ -31,6 +32,7 @@ public class AppResourcesSI : IAppResources private readonly IAppMetadata _appMetadata; private readonly IWebHostEnvironment _hostingEnvironment; private readonly ILogger _logger; + private readonly Telemetry? _telemetry; /// /// Initializes a new instance of the class. @@ -39,22 +41,26 @@ public class AppResourcesSI : IAppResources /// App metadata service /// The hosting environment /// A logger from the built in logger factory. + /// Telemetry for traces and metrics. public AppResourcesSI( IOptions settings, IAppMetadata appMetadata, IWebHostEnvironment hostingEnvironment, - ILogger logger + ILogger logger, + Telemetry? telemetry = null ) { _settings = settings.Value; _appMetadata = appMetadata; _hostingEnvironment = hostingEnvironment; _logger = logger; + _telemetry = telemetry; } /// public byte[] GetText(string org, string app, string textResource) { + using var activity = _telemetry?.StartGetTextActivity(); return ReadFileContentsFromLegalPath( _settings.AppBasePath + _settings.ConfigurationFolder + _settings.TextFolder, textResource @@ -64,6 +70,7 @@ public byte[] GetText(string org, string app, string textResource) /// public async Task GetTexts(string org, string app, string language) { + using var activity = _telemetry?.StartGetTextsActivity(); string pathTextsFolder = _settings.AppBasePath + _settings.ConfigurationFolder + _settings.TextFolder; string fullFileName = Path.Join(pathTextsFolder, $"resource.{language}.json"); @@ -93,6 +100,7 @@ await System.Text.Json.JsonSerializer.DeserializeAsync( /// public Application GetApplication() { + using var activity = _telemetry?.StartGetApplicationActivity(); try { ApplicationMetadata applicationMetadata = _appMetadata.GetApplicationMetadata().Result; @@ -113,6 +121,7 @@ public Application GetApplication() /// public string? GetApplicationXACMLPolicy() { + using var activity = _telemetry?.StartClientGetApplicationXACMLPolicyActivity(); try { return _appMetadata.GetApplicationXACMLPolicy().Result; @@ -127,6 +136,7 @@ public Application GetApplication() /// public string? GetApplicationBPMNProcess() { + using var activity = _telemetry?.StartClientGetApplicationBPMNProcessActivity(); try { return _appMetadata.GetApplicationBPMNProcess().Result; @@ -141,6 +151,7 @@ public Application GetApplication() /// public string GetModelJsonSchema(string modelId) { + using var activity = _telemetry?.StartGetModelJsonSchemaActivity(); string legalPath = $"{_settings.AppBasePath}{_settings.ModelsFolder}"; string filename = $"{legalPath}{modelId}.{_settings.JsonSchemaFileName}"; PathHelper.EnsureLegalPath(legalPath, filename); @@ -153,6 +164,7 @@ public string GetModelJsonSchema(string modelId) /// public string? GetPrefillJson(string dataModelName = "ServiceModel") { + using var activity = _telemetry?.StartGetPrefillJsonActivity(); string legalPath = _settings.AppBasePath + _settings.ModelsFolder; string filename = legalPath + dataModelName + ".prefill.json"; PathHelper.EnsureLegalPath(legalPath, filename); @@ -169,6 +181,7 @@ public string GetModelJsonSchema(string modelId) /// public string? GetLayoutSettingsString() { + using var activity = _telemetry?.StartGetLayoutSettingsStringActivity(); string filename = Path.Join( _settings.AppBasePath, _settings.UiFolder, @@ -186,6 +199,7 @@ public string GetModelJsonSchema(string modelId) /// public LayoutSettings GetLayoutSettings() { + using var activity = _telemetry?.StartGetLayoutSettingsActivity(); string filename = Path.Join( _settings.AppBasePath, _settings.UiFolder, @@ -204,6 +218,7 @@ public LayoutSettings GetLayoutSettings() /// public string GetClassRefForLogicDataType(string dataType) { + using var activity = _telemetry?.StartGetClassRefActivity(); Application application = GetApplication(); string classRef = string.Empty; @@ -220,6 +235,7 @@ public string GetClassRefForLogicDataType(string dataType) /// public string GetLayouts() { + using var activity = _telemetry?.StartGetLayoutsActivity(); Dictionary layouts = new Dictionary(); // Get FormLayout.json if it exists and return it (for backwards compatibility) @@ -248,6 +264,7 @@ public string GetLayouts() /// public string GetLayoutSets() { + using var activity = _telemetry?.StartGetLayoutSetsActivity(); string filename = Path.Join(_settings.AppBasePath, _settings.UiFolder, _settings.LayoutSetsFileName); string? filedata = null; if (File.Exists(filename)) @@ -262,6 +279,7 @@ public string GetLayoutSets() /// public LayoutSets? GetLayoutSet() { + using var activity = _telemetry?.StartGetLayoutSetActivity(); string? layoutSetsString = GetLayoutSets(); if (layoutSetsString is not null) { @@ -277,6 +295,7 @@ public string GetLayoutSets() /// public LayoutSet? GetLayoutSetForTask(string taskId) { + using var activity = _telemetry?.StartGetLayoutSetsForTaskActivity(); var sets = GetLayoutSet(); return sets?.Sets?.FirstOrDefault(s => s?.Tasks?.Contains(taskId) ?? false); } @@ -284,6 +303,7 @@ public string GetLayoutSets() /// public string GetLayoutsForSet(string layoutSetId) { + using var activity = _telemetry?.StartGetLayoutsForSetActivity(); Dictionary layouts = new Dictionary(); string layoutsPath = _settings.AppBasePath + _settings.UiFolder + layoutSetId + "/layouts/"; @@ -303,6 +323,7 @@ public string GetLayoutsForSet(string layoutSetId) /// public LayoutModel GetLayoutModel(string? layoutSetId = null) { + using var activity = _telemetry?.StartGetLayoutModelActivity(); string folder = Path.Join(_settings.AppBasePath, _settings.UiFolder, layoutSetId, "layouts"); var order = GetLayoutSettingsForSet(layoutSetId)?.Pages?.Order; if (order is null) @@ -331,6 +352,7 @@ public LayoutModel GetLayoutModel(string? layoutSetId = null) /// public string? GetLayoutSettingsStringForSet(string layoutSetId) { + using var activity = _telemetry?.StartGetLayoutSettingsStringForSetActivity(); string filename = Path.Join( _settings.AppBasePath, _settings.UiFolder, @@ -349,6 +371,7 @@ public LayoutModel GetLayoutModel(string? layoutSetId = null) /// public LayoutSettings? GetLayoutSettingsForSet(string? layoutSetId) { + using var activity = _telemetry?.StartGetLayoutSettingsForSetActivity(); string filename = Path.Join( _settings.AppBasePath, _settings.UiFolder, @@ -369,6 +392,7 @@ public LayoutModel GetLayoutModel(string? layoutSetId = null) /// public byte[] GetRuleConfigurationForSet(string id) { + using var activity = _telemetry?.StartGetRuleConfigurationForSetActivity(); string legalPath = Path.Join(_settings.AppBasePath, _settings.UiFolder); string filename = Path.Join(legalPath, id, _settings.RuleConfigurationJSONFileName); @@ -380,6 +404,7 @@ public byte[] GetRuleConfigurationForSet(string id) /// public byte[] GetRuleHandlerForSet(string id) { + using var activity = _telemetry?.StartGetRuleHandlerForSetActivity(); string legalPath = Path.Join(_settings.AppBasePath, _settings.UiFolder); string filename = Path.Join(legalPath, id, _settings.RuleHandlerFileName); @@ -422,6 +447,7 @@ private byte[] ReadFileContentsFromLegalPath(string legalPath, string filePath) /// public async Task GetFooter() { + using var activity = _telemetry?.StartGetFooterActivity(); string filename = Path.Join(_settings.AppBasePath, _settings.UiFolder, _settings.FooterFileName); string? filedata = null; if (File.Exists(filename)) @@ -435,6 +461,7 @@ private byte[] ReadFileContentsFromLegalPath(string legalPath, string filePath) /// public string? GetValidationConfiguration(string modelId) { + using var activity = _telemetry?.StartGetValidationConfigurationActivity(); string legalPath = $"{_settings.AppBasePath}{_settings.ModelsFolder}"; string filename = $"{legalPath}{modelId}.{_settings.ValidationConfigurationFileName}"; PathHelper.EnsureLegalPath(legalPath, filename); diff --git a/src/Altinn.App.Core/Implementation/PrefillSI.cs b/src/Altinn.App.Core/Implementation/PrefillSI.cs index f4e60aae9..27269b4cd 100644 --- a/src/Altinn.App.Core/Implementation/PrefillSI.cs +++ b/src/Altinn.App.Core/Implementation/PrefillSI.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Prefill; @@ -20,6 +21,7 @@ public class PrefillSI : IPrefill private readonly IAppResources _appResourcesService; private readonly IAltinnPartyClient _altinnPartyClientClient; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly Telemetry? _telemetry; private static readonly string ER_KEY = "ER"; private static readonly string DSF_KEY = "DSF"; private static readonly string USER_PROFILE_KEY = "UserProfile"; @@ -34,12 +36,14 @@ public class PrefillSI : IPrefill /// The app's resource service /// The register client /// A service with access to the http context. + /// Telemetry for traces and metrics. public PrefillSI( ILogger logger, IProfileClient profileClient, IAppResources appResourcesService, IAltinnPartyClient altinnPartyClientClient, - IHttpContextAccessor httpContextAccessor + IHttpContextAccessor httpContextAccessor, + Telemetry? telemetry = null ) { _logger = logger; @@ -47,6 +51,7 @@ IHttpContextAccessor httpContextAccessor _appResourcesService = appResourcesService; _altinnPartyClientClient = altinnPartyClientClient; _httpContextAccessor = httpContextAccessor; + _telemetry = telemetry; } /// @@ -56,6 +61,7 @@ public void PrefillDataModel( bool continueOnError = false ) { + using var activity = _telemetry?.StartPrefillDataModelActivity(); LoopThroughDictionaryAndAssignValuesToDataModel(externalPrefill, null, dataModel, continueOnError); } @@ -67,6 +73,7 @@ public async Task PrefillDataModel( Dictionary? externalPrefill = null ) { + using var activity = _telemetry?.StartPrefillDataModelActivity(partyId); // Prefill from external input. Only available during instansiation if (externalPrefill != null && externalPrefill.Count > 0) { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs index 3e92758c0..7224c17c0 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs @@ -4,6 +4,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; @@ -30,6 +31,7 @@ public class AuthorizationClient : IAuthorizationClient private readonly HttpClient _client; private readonly IPDP _pdp; private readonly ILogger _logger; + private readonly Telemetry? _telemetry; private const string ForwardedForHeaderName = "x-forwarded-for"; /// @@ -41,19 +43,22 @@ public class AuthorizationClient : IAuthorizationClient /// The application settings. /// /// the handler for logger service + /// Telemetry for traces and metrics. public AuthorizationClient( IOptions platformSettings, IHttpContextAccessor httpContextAccessor, HttpClient httpClient, IOptionsMonitor settings, IPDP pdp, - ILogger logger + ILogger logger, + Telemetry? telemetry = null ) { _httpContextAccessor = httpContextAccessor; _settings = settings.CurrentValue; _pdp = pdp; _logger = logger; + _telemetry = telemetry; httpClient.BaseAddress = new Uri(platformSettings.Value.ApiAuthorizationEndpoint); if (!httpClient.DefaultRequestHeaders.Contains(ForwardedForHeaderName)) @@ -72,6 +77,7 @@ ILogger logger /// public async Task?> GetPartyList(int userId) { + using var activity = _telemetry?.StartClientGetPartyListActivity(userId); List? partyList = null; string apiUrl = $"parties?userid={userId}"; string token = JwtTokenUtil.GetTokenFromContext( @@ -99,6 +105,7 @@ ILogger logger /// public async Task ValidateSelectedParty(int userId, int partyId) { + using var activity = _telemetry?.StartClientValidateSelectedPartyActivity(userId, partyId); bool? result; string apiUrl = $"parties/{partyId}/validate?userid={userId}"; string token = JwtTokenUtil.GetTokenFromContext( @@ -133,6 +140,7 @@ public async Task AuthorizeAction( string? taskId = null ) { + using var activity = _telemetry?.StartClientAuthorizeActionActivity(instanceIdentifier, action, taskId); XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest( appIdentifier.Org, appIdentifier.App, @@ -163,6 +171,7 @@ public async Task> AuthorizeActions( List actions ) { + using var activity = _telemetry?.StartClientAuthorizeActionsActivity(instance); XacmlJsonRequestRoot request = MultiDecisionHelper.CreateMultiDecisionRequest(user, instance, actions); XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); if (response?.Response == null) diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs index 19a86ead4..f7054284e 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs @@ -4,6 +4,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Events; @@ -25,6 +26,7 @@ public class EventsClient : IEventsClient private readonly AppSettings _settings; private readonly GeneralSettings _generalSettings; private readonly HttpClient _client; + private readonly Telemetry? _telemetry; private readonly IAccessTokenGenerator _accessTokenGenerator; private readonly IAppMetadata _appMetadata; @@ -38,6 +40,7 @@ public class EventsClient : IEventsClient /// The app metadata service /// The application settings. /// The general settings of the application. + /// Telemetry for metrics and traces. public EventsClient( IOptions platformSettings, IHttpContextAccessor httpContextAccessor, @@ -45,7 +48,8 @@ public EventsClient( IAccessTokenGenerator accessTokenGenerator, IAppMetadata appMetadata, IOptionsMonitor settings, - IOptions generalSettings + IOptions generalSettings, + Telemetry? telemetry = null ) { _httpContextAccessor = httpContextAccessor; @@ -60,11 +64,13 @@ IOptions generalSettings ); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); _client = httpClient; + _telemetry = telemetry; } /// public async Task AddEvent(string eventType, Instance instance) { + using var activity = _telemetry?.StartAddEventActivity(instance); string? alternativeSubject = null; if (!string.IsNullOrWhiteSpace(instance.InstanceOwner.OrganisationNumber)) { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs index a3c36dbcc..d2375fe87 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs @@ -2,6 +2,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Profile; @@ -26,6 +27,7 @@ public class ProfileClient : IProfileClient private readonly HttpClient _client; private readonly IAppMetadata _appMetadata; private readonly IAccessTokenGenerator _accessTokenGenerator; + private readonly Telemetry? _telemetry; /// /// Initializes a new instance of the class @@ -37,6 +39,7 @@ public class ProfileClient : IProfileClient /// A HttpClient provided by the HttpClientFactory. /// An instance of the IAppMetadata service. /// An instance of the AccessTokenGenerator service. + /// Telemetry for traces and metrics. public ProfileClient( IOptions platformSettings, ILogger logger, @@ -44,7 +47,8 @@ public ProfileClient( IOptionsMonitor settings, HttpClient httpClient, IAppMetadata appMetadata, - IAccessTokenGenerator accessTokenGenerator + IAccessTokenGenerator accessTokenGenerator, + Telemetry? telemetry = null ) { _logger = logger; @@ -59,11 +63,13 @@ IAccessTokenGenerator accessTokenGenerator _client = httpClient; _appMetadata = appMetadata; _accessTokenGenerator = accessTokenGenerator; + _telemetry = telemetry; } /// public async Task GetUserProfile(int userId) { + using var activity = _telemetry?.StartGetUserProfileActivity(userId); UserProfile? userProfile = null; string endpointUrl = $"users/{userId}"; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs index 86d322a10..93c3e0e63 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs @@ -3,6 +3,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Registers; @@ -27,6 +28,7 @@ public class AltinnPartyClient : IAltinnPartyClient private readonly HttpClient _client; private readonly IAppMetadata _appMetadata; private readonly IAccessTokenGenerator _accessTokenGenerator; + private readonly Telemetry? _telemetry; /// /// Initializes a new instance of the class @@ -38,6 +40,7 @@ public class AltinnPartyClient : IAltinnPartyClient /// The http client /// The app metadata service /// The platform access token generator + /// Telemetry for metrics and traces. public AltinnPartyClient( IOptions platformSettings, ILogger logger, @@ -45,7 +48,8 @@ public AltinnPartyClient( IOptionsMonitor settings, HttpClient httpClient, IAppMetadata appMetadata, - IAccessTokenGenerator accessTokenGenerator + IAccessTokenGenerator accessTokenGenerator, + Telemetry? telemetry = null ) { _logger = logger; @@ -60,11 +64,13 @@ IAccessTokenGenerator accessTokenGenerator _client = httpClient; _appMetadata = appMetadata; _accessTokenGenerator = accessTokenGenerator; + _telemetry = telemetry; } /// public async Task GetParty(int partyId) { + using var activity = _telemetry?.StartGetPartyActivity(partyId); Party? party = null; string endpointUrl = $"parties/{partyId}"; @@ -101,6 +107,7 @@ IAccessTokenGenerator accessTokenGenerator /// public async Task LookupParty(PartyLookup partyLookup) { + using var activity = _telemetry?.StartLookupPartyActivity(); Party party; string endpointUrl = "parties/lookup"; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs index 5b0ab96fa..872be9f62 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs @@ -2,6 +2,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Registers; @@ -25,6 +26,7 @@ public class RegisterERClient : IOrganizationClient private readonly AppSettings _settings; private readonly HttpClient _client; private readonly IAppMetadata _appMetadata; + private readonly Telemetry? _telemetry; private readonly IAccessTokenGenerator _accessTokenGenerator; /// @@ -37,6 +39,7 @@ public class RegisterERClient : IOrganizationClient /// The http client /// The platform access token generator /// The app metadata service + /// Telemetry for metrics and traces. public RegisterERClient( IOptions platformSettings, ILogger logger, @@ -44,7 +47,8 @@ public RegisterERClient( IOptionsMonitor settings, HttpClient httpClient, IAccessTokenGenerator accessTokenGenerator, - IAppMetadata appMetadata + IAppMetadata appMetadata, + Telemetry? telemetry = null ) { _logger = logger; @@ -59,11 +63,13 @@ IAppMetadata appMetadata _client = httpClient; _accessTokenGenerator = accessTokenGenerator; _appMetadata = appMetadata; + _telemetry = telemetry; } /// public async Task GetOrganization(string OrgNr) { + using var activity = _telemetry?.StartGetOrganizationActivity(OrgNr); Organization? organization = null; string endpointUrl = $"organizations/{OrgNr}"; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index ce2679768..16682e316 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -7,6 +7,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; using Altinn.App.Core.Internal.Auth; @@ -28,6 +29,7 @@ public class DataClient : IDataClient private readonly PlatformSettings _platformSettings; private readonly ILogger _logger; private readonly IUserTokenProvider _userTokenProvider; + private readonly Telemetry? _telemetry; private readonly HttpClient _client; /// @@ -37,11 +39,13 @@ public class DataClient : IDataClient /// the logger /// A HttpClient from the built in HttpClient factory. /// Service to obtain json web token + /// Telemetry for traces and metrics. public DataClient( IOptions platformSettings, ILogger logger, HttpClient httpClient, - IUserTokenProvider userTokenProvider + IUserTokenProvider userTokenProvider, + Telemetry? telemetry = null ) { _platformSettings = platformSettings.Value; @@ -53,6 +57,7 @@ IUserTokenProvider userTokenProvider httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); _client = httpClient; _userTokenProvider = userTokenProvider; + _telemetry = telemetry; } /// @@ -66,8 +71,8 @@ public async Task InsertFormData( string dataType ) { - Instance instance = new Instance { Id = $"{instanceOwnerPartyId}/{instanceGuid}", }; - + using var activity = _telemetry?.StartInsertFormDataActivity(instanceGuid, instanceOwnerPartyId); + Instance instance = new() { Id = $"{instanceOwnerPartyId}/{instanceGuid}", }; return await InsertFormData(instance, dataType, dataToSerialize, type); } @@ -79,6 +84,7 @@ public async Task InsertFormData( Type type ) { + using var activity = _telemetry?.StartInsertFormDataActivity(instance); string apiUrl = $"instances/{instance.Id}/data?dataType={dataType}"; string token = _userTokenProvider.GetUserToken(); DataElement dataElement; @@ -119,6 +125,7 @@ public async Task UpdateData( Guid dataId ) { + using var activity = _telemetry?.StartUpdateDataActivity(instanceGuid, instanceOwnerPartyId); string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; string token = _userTokenProvider.GetUserToken(); @@ -170,6 +177,7 @@ public async Task GetBinaryData( Guid dataId ) { + using var activity = _telemetry?.StartGetBinaryDataActivity(instanceGuid, instanceOwnerPartyId); string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; @@ -201,6 +209,7 @@ public async Task GetFormData( Guid dataId ) { + using var activity = _telemetry?.StartGetFormDataActivity(instanceGuid, instanceOwnerPartyId); string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; string token = _userTokenProvider.GetUserToken(); @@ -235,6 +244,7 @@ public async Task> GetBinaryDataList( Guid instanceGuid ) { + using var activity = _telemetry?.StartGetBinaryDataListActivity(instanceGuid, instanceOwnerPartyId); string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}/dataelements"; string token = _userTokenProvider.GetUserToken(); @@ -304,6 +314,7 @@ public async Task DeleteBinaryData( Guid dataGuid ) { + using var activity = _telemetry?.StartDeleteBinaryDataActivity(instanceGuid, instanceOwnerPartyId); return await DeleteData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, false); } @@ -317,6 +328,7 @@ public async Task DeleteData( bool delay ) { + using var activity = _telemetry?.StartDeleteDataActivity(instanceGuid, instanceOwnerPartyId); string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}/data/{dataGuid}?delay={delay}"; string token = _userTokenProvider.GetUserToken(); @@ -344,6 +356,7 @@ public async Task InsertBinaryData( HttpRequest request ) { + using var activity = _telemetry?.StartInsertBinaryDataActivity(instanceGuid, instanceOwnerPartyId); string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data?dataType={dataType}"; @@ -378,6 +391,7 @@ public async Task InsertBinaryData( string? generatedFromTask = null ) { + using var activity = _telemetry?.StartInsertBinaryDataActivity(instanceId); string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceId}/data?dataType={dataType}"; if (!string.IsNullOrEmpty(generatedFromTask)) { @@ -423,6 +437,7 @@ public async Task UpdateBinaryData( HttpRequest request ) { + using var activity = _telemetry?.StartUpdateBinaryDataActivity(instanceGuid, instanceOwnerPartyId); string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}"; string token = _userTokenProvider.GetUserToken(); @@ -454,6 +469,7 @@ public async Task UpdateBinaryData( Stream stream ) { + using var activity = _telemetry?.StartUpdateBinaryDataActivity(instanceIdentifier.GetInstanceId()); string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}"; string token = _userTokenProvider.GetUserToken(); StreamContent content = new StreamContent(stream); @@ -479,6 +495,7 @@ Stream stream /// public async Task Update(Instance instance, DataElement dataElement) { + using var activity = _telemetry?.StartUpdateDataActivity(instance); string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instance.Id}/dataelements/{dataElement.Id}"; string token = _userTokenProvider.GetUserToken(); @@ -505,6 +522,7 @@ await response.Content.ReadAsStringAsync() /// public async Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) { + using var activity = _telemetry?.StartLockDataElementActivity(instanceIdentifier.GetInstanceId(), dataGuid); string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock"; string token = _userTokenProvider.GetUserToken(); @@ -534,6 +552,10 @@ await response.Content.ReadAsStringAsync() /// public async Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) { + using var activity = _telemetry?.StartUnlockDataElementActivity( + instanceIdentifier.GetInstanceId(), + dataGuid + ); string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock"; string token = _userTokenProvider.GetUserToken(); diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs index 81297460a..b74107ce1 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs @@ -4,6 +4,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; @@ -15,6 +16,7 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; +using static Altinn.App.Core.Features.Telemetry.Instances; namespace Altinn.App.Core.Infrastructure.Clients.Storage { @@ -26,6 +28,7 @@ public class InstanceClient : IInstanceClient private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; private readonly HttpClient _client; + private readonly Telemetry? _telemetry; private readonly AppSettings _settings; /// @@ -36,12 +39,14 @@ public class InstanceClient : IInstanceClient /// The http context accessor /// A HttpClient that can be used to perform HTTP requests against the platform. /// The application settings. + /// Telemetry for traces and metrics. public InstanceClient( IOptions platformSettings, ILogger logger, IHttpContextAccessor httpContextAccessor, HttpClient httpClient, - IOptionsMonitor settings + IOptionsMonitor settings, + Telemetry? telemetry = null ) { _logger = logger; @@ -55,11 +60,13 @@ IOptionsMonitor settings httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); _client = httpClient; + _telemetry = telemetry; } /// public async Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceGuid) { + using var activity = _telemetry?.StartGetInstanceByGuidActivity(instanceGuid); string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}"; @@ -85,10 +92,11 @@ public async Task GetInstance(string app, string org, int instanceOwne /// public async Task GetInstance(Instance instance) { + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + using var activity = _telemetry?.StartGetInstanceByInstanceActivity(instanceGuid); string app = instance.AppId.Split("/")[1]; string org = instance.Org; int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); return await GetInstance(app, org, instanceOwnerPartyId, instanceGuid); } @@ -96,6 +104,7 @@ public async Task GetInstance(Instance instance) /// public async Task> GetInstances(Dictionary queryParams) { + using var activity = _telemetry?.StartGetInstancesActivity(); var apiUrl = QueryHelpers.AddQueryString("instances", queryParams); string token = JwtTokenUtil.GetTokenFromContext( @@ -106,24 +115,21 @@ public async Task> GetInstances(Dictionary if (queryResponse.Count == 0) { - return new List(); + return []; } - - List instances = new(); - - instances.AddRange(queryResponse.Instances); + List instances = [.. queryResponse.Instances]; while (!string.IsNullOrEmpty(queryResponse.Next)) { queryResponse = await QueryInstances(token, queryResponse.Next); instances.AddRange(queryResponse.Instances); } - return instances; } private async Task> QueryInstances(string token, string url) { + using var activity = _telemetry?.StartQueryInstancesActivity(); HttpResponseMessage response = await _client.GetAsync(token, url); if (response.StatusCode == HttpStatusCode.OK) @@ -144,6 +150,7 @@ private async Task> QueryInstances(string token, string /// public async Task UpdateProcess(Instance instance) { + using var activity = _telemetry?.StartUpdateProcessActivity(instance); ProcessState processState = instance.Process; string apiUrl = $"instances/{instance.Id}/process"; @@ -155,13 +162,12 @@ public async Task UpdateProcess(Instance instance) string processStateString = JsonConvert.SerializeObject(processState); _logger.LogInformation($"update process state: {processStateString}"); - StringContent httpContent = new StringContent(processStateString, Encoding.UTF8, "application/json"); + StringContent httpContent = new(processStateString, Encoding.UTF8, "application/json"); HttpResponseMessage response = await _client.PutAsync(token, apiUrl, httpContent); if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); Instance updatedInstance = JsonConvert.DeserializeObject(instanceData)!; - return updatedInstance; } else @@ -174,17 +180,15 @@ public async Task UpdateProcess(Instance instance) /// public async Task CreateInstance(string org, string app, Instance instanceTemplate) { + using var activity = _telemetry?.StartCreateInstanceActivity(); string apiUrl = $"instances?appId={org}/{app}"; string token = JwtTokenUtil.GetTokenFromContext( _httpContextAccessor.HttpContext, _settings.RuntimeCookieName ); - StringContent content = new StringContent( - JsonConvert.SerializeObject(instanceTemplate), - Encoding.UTF8, - "application/json" - ); + StringContent content = + new(JsonConvert.SerializeObject(instanceTemplate), Encoding.UTF8, "application/json"); HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content); if (response.IsSuccessStatusCode) @@ -192,7 +196,7 @@ public async Task CreateInstance(string org, string app, Instance inst Instance createdInstance = JsonConvert.DeserializeObject( await response.Content.ReadAsStringAsync() )!; - + _telemetry?.InstanceCreated(createdInstance); return createdInstance; } @@ -205,6 +209,7 @@ await response.Content.ReadAsStringAsync() /// public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid) { + using var activity = _telemetry?.StartCompleteConfirmationActivity(instanceGuid, instanceOwnerPartyId); string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/complete"; string token = JwtTokenUtil.GetTokenFromContext( _httpContextAccessor.HttpContext, @@ -217,6 +222,7 @@ public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Gu { string instanceData = await response.Content.ReadAsStringAsync(); Instance instance = JsonConvert.DeserializeObject(instanceData)!; + _telemetry?.InstanceCompleted(instance); return instance; } @@ -226,6 +232,7 @@ public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Gu /// public async Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus) { + using var activity = _telemetry?.StartUpdateReadStatusActivity(instanceGuid, instanceOwnerPartyId); string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/readstatus?status={readStatus}"; string token = JwtTokenUtil.GetTokenFromContext( _httpContextAccessor.HttpContext, @@ -252,6 +259,7 @@ public async Task UpdateReadStatus(int instanceOwnerPartyId, Guid inst /// public async Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus) { + using var activity = _telemetry?.StartUpdateSubStatusActivity(instanceGuid, instanceOwnerPartyId); string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/substatus"; string token = JwtTokenUtil.GetTokenFromContext( _httpContextAccessor.HttpContext, @@ -281,6 +289,7 @@ public async Task UpdatePresentationTexts( PresentationTexts presentationTexts ) { + using var activity = _telemetry?.StartUpdatePresentationTextActivity(instanceGuid, instanceOwnerPartyId); string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/presentationtexts"; string token = JwtTokenUtil.GetTokenFromContext( _httpContextAccessor.HttpContext, @@ -306,6 +315,7 @@ PresentationTexts presentationTexts /// public async Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues) { + using var activity = _telemetry?.StartUpdateDataValuesActivity(instanceGuid, instanceOwnerPartyId); string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/datavalues"; string token = JwtTokenUtil.GetTokenFromContext( _httpContextAccessor.HttpContext, @@ -331,6 +341,7 @@ public async Task UpdateDataValues(int instanceOwnerPartyId, Guid inst /// public async Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard) { + using var activity = _telemetry?.StartDeleteInstanceActivity(instanceGuid, instanceOwnerPartyId); string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}?hard={hard}"; string token = JwtTokenUtil.GetTokenFromContext( _httpContextAccessor.HttpContext, @@ -343,6 +354,7 @@ public async Task DeleteInstance(int instanceOwnerPartyId, Guid instan { string instanceData = await response.Content.ReadAsStringAsync(); Instance instance = JsonConvert.DeserializeObject(instanceData)!; + _telemetry?.InstanceDeleted(instance); return instance; } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClientMetricsDecorator.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClientMetricsDecorator.cs deleted file mode 100644 index df1e935fc..000000000 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClientMetricsDecorator.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Internal.Instances; -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.Extensions.Primitives; -using Prometheus; - -namespace Altinn.App.Core.Infrastructure.Clients.Storage; - -/// -/// Decorator for the instance client that adds metrics for the number of instances created, completed and deleted. -/// -public class InstanceClientMetricsDecorator : IInstanceClient -{ - private readonly IInstanceClient _instanceClient; - private static readonly Counter InstancesCreatedCounter = Metrics.CreateCounter( - "altinn_app_instances_created", - "Number of instances created", - "result" - ); - private static readonly Counter InstancesCompletedCounter = Metrics.CreateCounter( - "altinn_app_instances_completed", - "Number of instances completed", - "result" - ); - private static readonly Counter InstancesDeletedCounter = Metrics.CreateCounter( - "altinn_app_instances_deleted", - "Number of instances completed", - "result", - "mode" - ); - - /// - /// Create a new instance of the class. - /// - /// The instance client to decorate. - public InstanceClientMetricsDecorator(IInstanceClient instanceClient) - { - _instanceClient = instanceClient; - } - - /// - public async Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceId) - { - return await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceId); - } - - /// - public async Task GetInstance(Instance instance) - { - return await _instanceClient.GetInstance(instance); - } - - /// - public async Task> GetInstances(Dictionary queryParams) - { - return await _instanceClient.GetInstances(queryParams); - } - - /// - public async Task UpdateProcess(Instance instance) - { - return await _instanceClient.UpdateProcess(instance); - } - - /// - public async Task CreateInstance(string org, string app, Instance instanceTemplate) - { - var success = false; - try - { - var instance = await _instanceClient.CreateInstance(org, app, instanceTemplate); - success = true; - return instance; - } - finally - { - InstancesCreatedCounter.WithLabels(success ? "success" : "failure").Inc(); - } - } - - /// - public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid) - { - var success = false; - try - { - var instance = await _instanceClient.AddCompleteConfirmation(instanceOwnerPartyId, instanceGuid); - success = true; - return instance; - } - finally - { - InstancesCompletedCounter.WithLabels(success ? "success" : "failure").Inc(); - } - } - - /// - public async Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus) - { - return await _instanceClient.UpdateReadStatus(instanceOwnerPartyId, instanceGuid, readStatus); - } - - /// - public async Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus) - { - return await _instanceClient.UpdateSubstatus(instanceOwnerPartyId, instanceGuid, substatus); - } - - /// - public async Task UpdatePresentationTexts( - int instanceOwnerPartyId, - Guid instanceGuid, - PresentationTexts presentationTexts - ) - { - return await _instanceClient.UpdatePresentationTexts(instanceOwnerPartyId, instanceGuid, presentationTexts); - } - - /// - public async Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues) - { - return await _instanceClient.UpdateDataValues(instanceOwnerPartyId, instanceGuid, dataValues); - } - - /// - public async Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard) - { - var success = false; - try - { - var deleteInstance = await _instanceClient.DeleteInstance(instanceOwnerPartyId, instanceGuid, hard); - success = true; - return deleteInstance; - } - finally - { - InstancesDeletedCounter.WithLabels(success ? "success" : "failure", hard ? "hard" : "soft").Inc(); - } - } -} diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs index 9cfcdd292..ac52c4860 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs @@ -2,6 +2,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Process; using Altinn.Platform.Storage.Interface.Models; @@ -21,6 +22,7 @@ public class ProcessClient : IProcessClient private readonly AppSettings _appSettings; private readonly ILogger _logger; private readonly HttpClient _client; + private readonly Telemetry? _telemetry; private readonly IHttpContextAccessor _httpContextAccessor; /// @@ -31,7 +33,8 @@ public ProcessClient( IOptions appSettings, ILogger logger, IHttpContextAccessor httpContextAccessor, - HttpClient httpClient + HttpClient httpClient, + Telemetry? telemetry = null ) { _appSettings = appSettings.Value; @@ -45,11 +48,13 @@ HttpClient httpClient httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); _client = httpClient; + _telemetry = telemetry; } /// public Stream GetProcessDefinition() { + using var activity = _telemetry?.StartGetProcessDefinitionActivity(); string bpmnFilePath = Path.Join( _appSettings.AppBasePath, _appSettings.ConfigurationFolder, @@ -75,6 +80,7 @@ public Stream GetProcessDefinition() /// public async Task GetProcessHistory(string instanceGuid, string instanceOwnerPartyId) { + using var activity = _telemetry?.StartGetProcessHistoryActivity(instanceGuid, instanceOwnerPartyId); string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/process/history"; string token = JwtTokenUtil.GetTokenFromContext( _httpContextAccessor.HttpContext, diff --git a/src/Altinn.App.Core/Internal/App/AppMetadata.cs b/src/Altinn.App.Core/Internal/App/AppMetadata.cs index e0a9d4157..9de52d66a 100644 --- a/src/Altinn.App.Core/Internal/App/AppMetadata.cs +++ b/src/Altinn.App.Core/Internal/App/AppMetadata.cs @@ -1,6 +1,7 @@ using System.Text; using System.Text.Json; using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; using Altinn.App.Core.Models; using Microsoft.Extensions.Options; @@ -21,6 +22,7 @@ public class AppMetadata : IAppMetadata private readonly AppSettings _settings; private readonly IFrontendFeatures _frontendFeatures; + private readonly Telemetry? _telemetry; private ApplicationMetadata? _application; /// @@ -28,10 +30,16 @@ public class AppMetadata : IAppMetadata /// /// The app repository settings. /// Application features service - public AppMetadata(IOptions settings, IFrontendFeatures frontendFeatures) + /// Telemetry for traces and metrics. + public AppMetadata( + IOptions settings, + IFrontendFeatures frontendFeatures, + Telemetry? telemetry = null + ) { _settings = settings.Value; _frontendFeatures = frontendFeatures; + _telemetry = telemetry; } /// @@ -39,6 +47,7 @@ public AppMetadata(IOptions settings, IFrontendFeatures frontendFea /// Thrown if applicationmetadata.json file not found public async Task GetApplicationMetadata() { + using var activity = _telemetry?.StartGetApplicationMetadataActivity(); // Cache application metadata if (_application != null) { @@ -86,6 +95,7 @@ public async Task GetApplicationMetadata() /// public async Task GetApplicationXACMLPolicy() { + using var activity = _telemetry?.StartGetApplicationXACMLPolicyActivity(); string filename = Path.Join( _settings.AppBasePath, _settings.ConfigurationFolder, @@ -103,6 +113,7 @@ public async Task GetApplicationXACMLPolicy() /// public async Task GetApplicationBPMNProcess() { + using var activity = _telemetry?.StartGetApplicationBPMNProcessActivity(); string filename = Path.Join( _settings.AppBasePath, _settings.ConfigurationFolder, diff --git a/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs index 1f943d39e..9d85af06b 100644 --- a/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs +++ b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; using Altinn.App.Core.Internal.Process.Authorization; using Altinn.App.Core.Internal.Process.Elements; @@ -16,30 +17,36 @@ public class AuthorizationService : IAuthorizationService { private readonly IAuthorizationClient _authorizationClient; private readonly IEnumerable _userActionAuthorizers; + private readonly Telemetry? _telemetry; /// /// Initializes a new instance of the class /// /// The authorization client /// The user action authorizers + /// Telemetry for traces and metrics. public AuthorizationService( IAuthorizationClient authorizationClient, - IEnumerable userActionAuthorizers + IEnumerable userActionAuthorizers, + Telemetry? telemetry = null ) { _authorizationClient = authorizationClient; _userActionAuthorizers = userActionAuthorizers; + _telemetry = telemetry; } /// public async Task?> GetPartyList(int userId) { + using var activity = _telemetry?.StartGetPartyListActivity(userId); return await _authorizationClient.GetPartyList(userId); } /// public async Task ValidateSelectedParty(int userId, int partyId) { + using var activity = _telemetry?.StartValidateSelectedPartyActivity(userId, partyId); return await _authorizationClient.ValidateSelectedParty(userId, partyId); } @@ -52,6 +59,7 @@ public async Task AuthorizeAction( string? taskId = null ) { + using var activity = _telemetry?.StartAuthorizeActionActivity(instanceIdentifier, action, taskId); if (!await _authorizationClient.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)) { return false; @@ -80,12 +88,13 @@ public async Task> AuthorizeActions( List actions ) { + using var activity = _telemetry?.StartAuthorizeActionsActivity(instance, actions); var authDecisions = await _authorizationClient.AuthorizeActions( instance, user, actions.Select(a => a.Value).ToList() ); - List authorizedActions = new(); + List authorizedActions = []; foreach (var action in actions) { authorizedActions.Add( diff --git a/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs b/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs index a2cc6128f..899eb8523 100644 --- a/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs +++ b/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; using Altinn.App.Core.Implementation; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,21 +17,29 @@ public class ApplicationLanguage : IApplicationLanguage private readonly AppSettings _settings; private readonly ILogger _logger; + private readonly Telemetry? _telemetry; /// /// Initializes a new instance of the class. /// /// The app repository settings. /// A logger from the built in logger factory. - public ApplicationLanguage(IOptions settings, ILogger logger) + /// Telemetry for traces and metrics. + public ApplicationLanguage( + IOptions settings, + ILogger logger, + Telemetry? telemetry = null + ) { _settings = settings.Value; _logger = logger; + _telemetry = telemetry; } /// public async Task> GetApplicationLanguages() { + using var activity = _telemetry?.StartGetApplicationLanguageActivity(); var pathTextsResourceFolder = Path.Join( _settings.AppBasePath, _settings.ConfigurationFolder, diff --git a/src/Altinn.App.Core/Internal/Patch/PatchService.cs b/src/Altinn.App.Core/Internal/Patch/PatchService.cs index a4484d2db..cf08d0133 100644 --- a/src/Altinn.App.Core/Internal/Patch/PatchService.cs +++ b/src/Altinn.App.Core/Internal/Patch/PatchService.cs @@ -11,6 +11,7 @@ using Altinn.App.Core.Models.Result; using Altinn.Platform.Storage.Interface.Models; using Json.Patch; +using static Altinn.App.Core.Features.Telemetry; namespace Altinn.App.Core.Internal.Patch; @@ -22,6 +23,7 @@ public class PatchService : IPatchService private readonly IAppMetadata _appMetadata; private readonly IDataClient _dataClient; private readonly IAppModel _appModel; + private readonly Telemetry? _telemetry; private readonly IValidationService _validationService; private readonly IEnumerable _dataProcessors; @@ -36,12 +38,14 @@ public class PatchService : IPatchService /// /// /// + /// public PatchService( IAppMetadata appMetadata, IDataClient dataClient, IValidationService validationService, IEnumerable dataProcessors, - IAppModel appModel + IAppModel appModel, + Telemetry? telemetry = null ) { _appMetadata = appMetadata; @@ -49,6 +53,7 @@ IAppModel appModel _validationService = validationService; _dataProcessors = dataProcessors; _appModel = appModel; + _telemetry = telemetry; } /// @@ -61,6 +66,8 @@ public async Task> ApplyPatch( List? ignoredValidators = null ) { + using var activity = _telemetry?.StartDataPatchActivity(instance); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instance); AppIdentifier appIdentifier = (await _appMetadata.GetApplicationMetadata()).AppIdentifier; var modelType = _appModel.GetModelType(dataType.AppLogic.ClassRef); @@ -74,6 +81,13 @@ public async Task> ApplyPatch( ); var oldModelNode = JsonSerializer.SerializeToNode(oldModel); var patchResult = jsonPatch.Apply(oldModelNode); + + var telemetryPatchResult = ( + patchResult.IsSuccess ? Telemetry.Data.PatchResult.Success : Telemetry.Data.PatchResult.Error + ); + activity?.SetTag(InternalLabels.Result, telemetryPatchResult.ToStringFast()); + _telemetry?.DataPatched(telemetryPatchResult); + if (!patchResult.IsSuccess) { bool testOperationFailed = patchResult.Error!.Contains("is not equal to the indicated value."); diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index 17968c818..d32e39221 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -2,6 +2,7 @@ using System.Xml.Serialization; using Altinn.App.Core.Configuration; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers.Extensions; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; @@ -28,7 +29,7 @@ public class PdfService : IPdfService private readonly IPdfGeneratorClient _pdfGeneratorClient; private readonly PdfGeneratorSettings _pdfGeneratorSettings; private readonly GeneralSettings _generalSettings; - + private readonly Telemetry? _telemetry; private const string PdfElementType = "ref-data-as-pdf"; private const string PdfContentType = "application/pdf"; @@ -42,6 +43,7 @@ public class PdfService : IPdfService /// PDF generator client for the experimental PDF generator service /// PDF generator related settings. /// The app general settings. + /// Telemetry for metrics and traces. public PdfService( IAppResources appResources, IDataClient dataClient, @@ -49,7 +51,8 @@ public PdfService( IProfileClient profileClient, IPdfGeneratorClient pdfGeneratorClient, IOptions pdfGeneratorSettings, - IOptions generalSettings + IOptions generalSettings, + Telemetry? telemetry = null ) { _resourceService = appResources; @@ -59,11 +62,13 @@ IOptions generalSettings _pdfGeneratorClient = pdfGeneratorClient; _pdfGeneratorSettings = pdfGeneratorSettings.Value; _generalSettings = generalSettings.Value; + _telemetry = telemetry; } /// public async Task GenerateAndStorePdf(Instance instance, string taskId, CancellationToken ct) { + using var activity = _telemetry?.StartGenerateAndStorePdfActivity(instance, taskId); string language = GetOverriddenLanguage(); // Avoid a costly call if the language is allready overriden by the user language = string.IsNullOrEmpty(language) ? await GetLanguage() : language; @@ -80,6 +85,7 @@ public async Task GenerateAndStorePdf(Instance instance, string taskId, Cancella /// public async Task GeneratePdf(Instance instance, string taskId, CancellationToken ct) { + using var activity = _telemetry?.StartGeneratePdfActivity(instance, taskId); var language = GetOverriddenLanguage(); // Avoid a costly call if the language is allready overriden by the user language = string.IsNullOrEmpty(language) ? await GetLanguage() : language; diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index d7c32e15a..9c8e2cbe0 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -26,6 +26,7 @@ public class ProcessEngine : IProcessEngine private readonly IProcessEventHandlerDelegator _processEventHandlerDelegator; private readonly IProcessEventDispatcher _processEventDispatcher; private readonly UserActionService _userActionService; + private readonly Telemetry? _telemetry; private readonly IProcessTaskCleaner _processTaskCleaner; /// @@ -38,6 +39,7 @@ public class ProcessEngine : IProcessEngine /// The process event dispatcher /// The process task cleaner /// The action handler factory + /// The telemetry service public ProcessEngine( IProcessReader processReader, IProfileClient profileClient, @@ -45,7 +47,8 @@ public ProcessEngine( IProcessEventHandlerDelegator processEventsDelegator, IProcessEventDispatcher processEventDispatcher, IProcessTaskCleaner processTaskCleaner, - UserActionService userActionService + UserActionService userActionService, + Telemetry? telemetry = null ) { _processReader = processReader; @@ -55,11 +58,14 @@ UserActionService userActionService _processEventDispatcher = processEventDispatcher; _processTaskCleaner = processTaskCleaner; _userActionService = userActionService; + _telemetry = telemetry; } /// public async Task GenerateProcessStartEvents(ProcessStartRequest processStartRequest) { + using var activity = _telemetry?.StartProcessStartActivity(processStartRequest.Instance); + if (processStartRequest.Instance.Process != null) { return new ProcessChangeResult() @@ -113,12 +119,16 @@ out ProcessError? startEventError Events = events }; + _telemetry?.ProcessStarted(); + return new ProcessChangeResult() { Success = true, ProcessStateChange = processStateChange }; } /// public async Task Next(ProcessNextRequest request) { + using var activity = _telemetry?.StartProcessNextActivity(request.Instance); + Instance instance = request.Instance; string? currentElementId = instance.Process?.CurrentTask?.ElementId; @@ -155,6 +165,11 @@ public async Task Next(ProcessNextRequest request) ProcessStateChange? nextResult = await HandleMoveToNext(instance, request.User, request.Action); + if (nextResult?.NewProcessState?.Ended is not null) + { + _telemetry?.ProcessEnded(nextResult); + } + return new ProcessChangeResult() { Success = true, ProcessStateChange = nextResult }; } @@ -267,6 +282,8 @@ private async Task> MoveProcessToNext( // ending process if next element is end event if (_processReader.IsEndEvent(nextElement?.Id)) { + using var activity = _telemetry?.StartProcessEndActivity(instance); + currentState.CurrentTask = null; currentState.Ended = now; currentState.EndEvent = nextElement!.Id; diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs deleted file mode 100644 index 3bc1c0836..000000000 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Altinn.App.Core.Models.Process; -using Altinn.Platform.Storage.Interface.Models; -using Prometheus; - -namespace Altinn.App.Core.Internal.Process; - -/// -/// Decorator for the process engine that adds metrics for the number of processes started, ended and moved to next. -/// -public class ProcessEngineMetricsDecorator : IProcessEngine -{ - private readonly IProcessEngine _processEngine; - private static readonly Counter ProcessTaskStartCounter = Metrics.CreateCounter( - "altinn_app_process_start_count", - "Number of tasks started", - labelNames: "result" - ); - private static readonly Counter ProcessTaskNextCounter = Metrics.CreateCounter( - "altinn_app_process_task_next_count", - "Number of tasks moved to next", - "result", - "action", - "task" - ); - private static readonly Counter ProcessTaskEndCounter = Metrics.CreateCounter( - "altinn_app_process_end_count", - "Number of tasks ended", - labelNames: "result" - ); - private static readonly Counter ProcessTimeCounter = Metrics.CreateCounter( - "altinn_app_process_end_time_total", - "Number of seconds used to complete instances", - labelNames: "result" - ); - - /// - /// Create a new instance of the class. - /// - /// The process engine to decorate. - public ProcessEngineMetricsDecorator(IProcessEngine processEngine) - { - _processEngine = processEngine; - } - - /// - public async Task GenerateProcessStartEvents(ProcessStartRequest processStartRequest) - { - var result = await _processEngine.GenerateProcessStartEvents(processStartRequest); - ProcessTaskStartCounter.WithLabels(result.Success ? "success" : "failure").Inc(); - return result; - } - - /// - public async Task Next(ProcessNextRequest request) - { - var result = await _processEngine.Next(request); - ProcessTaskNextCounter - .WithLabels( - result.Success ? "success" : "failure", - request.Action ?? "", - request.Instance.Process?.CurrentTask?.ElementId ?? "" - ) - .Inc(); - if (result.ProcessStateChange?.NewProcessState?.Ended != null) - { - ProcessTaskEndCounter.WithLabels(result.Success ? "success" : "failure").Inc(); - if (result.ProcessStateChange?.NewProcessState?.Started != null) - { - ProcessTimeCounter - .WithLabels(result.Success ? "success" : "failure") - .Inc( - result - .ProcessStateChange.NewProcessState.Ended.Value.Subtract( - result.ProcessStateChange.NewProcessState.Started.Value - ) - .TotalSeconds - ); - } - } - return result; - } - - /// - public async Task HandleEventsAndUpdateStorage( - Instance instance, - Dictionary? prefill, - List? events - ) - { - return await _processEngine.HandleEventsAndUpdateStorage(instance, prefill, events); - } -} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs index 6befd998e..ef68b625d 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs @@ -1,4 +1,5 @@ using System.Xml.Serialization; +using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Internal.Process.Elements.Base; @@ -11,13 +12,15 @@ namespace Altinn.App.Core.Internal.Process; public class ProcessReader : IProcessReader { private readonly Definitions _definitions; + private readonly Telemetry? _telemetry; /// /// Create instance of ProcessReader where process stream is fetched from /// /// Implementation of IProcessClient used to get stream of BPMN process + /// Telemetry for metrics and traces. /// If BPMN file could not be deserialized - public ProcessReader(IProcessClient processClient) + public ProcessReader(IProcessClient processClient, Telemetry? telemetry = null) { XmlSerializer serializer = new XmlSerializer(typeof(Definitions)); Definitions? definitions = (Definitions?)serializer.Deserialize(processClient.GetProcessDefinition()); @@ -25,89 +28,104 @@ public ProcessReader(IProcessClient processClient) _definitions = definitions ?? throw new InvalidOperationException("Failed to deserialize BPMN definitions. Definitions was null"); + _telemetry = telemetry; } /// public List GetStartEvents() { + using var activity = _telemetry?.StartGetStartEventsActivity(); return _definitions.Process.StartEvents; } /// public List GetStartEventIds() { + using var activity = _telemetry?.StartGetStartEventIdsActivity(); return GetStartEvents().Select(s => s.Id).ToList(); } /// public bool IsStartEvent(string? elementId) { + using var activity = _telemetry?.StartIsStartEventActivity(); return elementId != null && GetStartEventIds().Contains(elementId); } /// public List GetProcessTasks() { + using var activity = _telemetry?.StartGetProcessTasksActivity(); return _definitions.Process.Tasks; } /// public List GetProcessTaskIds() { + using var activity = _telemetry?.StartGetProcessTaskIdsActivity(); return GetProcessTasks().Select(t => t.Id).ToList(); } /// public bool IsProcessTask(string? elementId) { + using var activity = _telemetry?.StartIsProcessTaskActivity(); return elementId != null && GetProcessTaskIds().Contains(elementId); } /// public List GetExclusiveGateways() { + using var activity = _telemetry?.StartGetExclusiveGatewaysActivity(); return _definitions.Process.ExclusiveGateway; } /// public List GetExclusiveGatewayIds() { + using var activity = _telemetry?.StartGetExclusiveGatewayIdsActivity(); return GetExclusiveGateways().Select(g => g.Id).ToList(); } /// public List GetEndEvents() { + using var activity = _telemetry?.StartGetEndEventsActivity(); return _definitions.Process.EndEvents; } /// public List GetEndEventIds() { + using var activity = _telemetry?.StartGetEndEventIdsActivity(); return GetEndEvents().Select(e => e.Id).ToList(); } /// public bool IsEndEvent(string? elementId) { + using var activity = _telemetry?.StartIsEndEventActivity(); return elementId != null && GetEndEventIds().Contains(elementId); } /// public List GetSequenceFlows() { + using var activity = _telemetry?.StartGetSequenceFlowsActivity(); return _definitions.Process.SequenceFlow; } /// public List GetSequenceFlowIds() { + using var activity = _telemetry?.StartGetSequenceFlowIdsActivity(); return GetSequenceFlows().Select(s => s.Id).ToList(); } /// public ProcessElement? GetFlowElement(string? elementId) { + using var activity = _telemetry?.StartGetFlowElementActivity(); ArgumentNullException.ThrowIfNull(elementId); ProcessTask? task = _definitions.Process.Tasks.Find(t => t.Id == elementId); @@ -134,6 +152,7 @@ public List GetSequenceFlowIds() /// public List GetNextElements(string? currentElementId) { + using var activity = _telemetry?.StartGetNextElementsActivity(); ArgumentNullException.ThrowIfNull(currentElementId); List nextElements = new List(); List allElements = GetAllFlowElements(); @@ -153,6 +172,7 @@ public List GetNextElements(string? currentElementId) /// public List GetOutgoingSequenceFlows(ProcessElement? flowElement) { + using var activity = _telemetry?.StartGetOutgoingSequenceFlowsActivity(); if (flowElement == null) { return new List(); @@ -164,6 +184,7 @@ public List GetOutgoingSequenceFlows(ProcessElement? flowElement) /// public List GetAllFlowElements() { + using var activity = _telemetry?.StartGetAllFlowElementsActivity(); List flowElements = new List(); flowElements.AddRange(GetStartEvents()); flowElements.AddRange(GetProcessTasks()); @@ -175,6 +196,7 @@ public List GetAllFlowElements() /// public AltinnTaskExtension? GetAltinnTaskExtension(string elementId) { + using var activity = _telemetry?.StartGetAltinnTaskExtensionActivity(); ProcessElement? flowElement = GetFlowElement(elementId); if (flowElement is ProcessTask processTask) diff --git a/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs b/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs index 030b65136..a814df9ca 100644 --- a/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs +++ b/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs @@ -1,3 +1,4 @@ +using Altinn.App.Core.Features; using Altinn.App.Core.Features.FileAnalysis; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Models.Validation; @@ -11,13 +12,15 @@ namespace Altinn.App.Core.Internal.Validation public class FileValidationService : IFileValidationService { private readonly IFileValidatorFactory _fileValidatorFactory; + private readonly Telemetry? _telemetry; /// /// Initializes a new instance of the class. /// - public FileValidationService(IFileValidatorFactory fileValidatorFactory) + public FileValidationService(IFileValidatorFactory fileValidatorFactory, Telemetry? telemetry = null) { _fileValidatorFactory = fileValidatorFactory; + _telemetry = telemetry; } /// @@ -28,6 +31,7 @@ public FileValidationService(IFileValidatorFactory fileValidatorFactory) IEnumerable fileAnalysisResults ) { + using var activity = _telemetry?.StartFileValidateActivity(); List allErrors = new(); bool allSuccess = true; diff --git a/src/Altinn.App.Core/Internal/Validation/ValidationService.cs b/src/Altinn.App.Core/Internal/Validation/ValidationService.cs index f51cb759c..46e2fcd4c 100644 --- a/src/Altinn.App.Core/Internal/Validation/ValidationService.cs +++ b/src/Altinn.App.Core/Internal/Validation/ValidationService.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Features; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Internal.Data; @@ -18,6 +18,7 @@ public class ValidationService : IValidationService private readonly IAppModel _appModel; private readonly IAppMetadata _appMetadata; private readonly ILogger _logger; + private readonly Telemetry? _telemetry; /// /// Constructor with DI services @@ -27,7 +28,8 @@ public ValidationService( IDataClient dataClient, IAppModel appModel, IAppMetadata appMetadata, - ILogger logger + ILogger logger, + Telemetry? telemetry = null ) { _validatorFactory = validatorFactory; @@ -35,6 +37,7 @@ ILogger logger _appModel = appModel; _appMetadata = appMetadata; _logger = logger; + _telemetry = telemetry; } /// @@ -43,6 +46,8 @@ public async Task> ValidateInstanceAtTask(Instance instanc ArgumentNullException.ThrowIfNull(instance); ArgumentNullException.ThrowIfNull(taskId); + using var activity = _telemetry?.StartValidateInstanceAtTaskActivity(instance, taskId); + // Run task validations (but don't await yet) Task[]> taskIssuesTask = RunTaskValidators(instance, taskId, language); @@ -74,18 +79,19 @@ private Task[]> RunTaskValidators(Instance instance, strin var taskValidators = _validatorFactory.GetTaskValidators(taskId); return Task.WhenAll( - taskValidators.Select(async tv => + taskValidators.Select(async v => { + using var activity = _telemetry?.StartRunTaskValidatorActivity(v); try { _logger.LogDebug( "Start running validator {validatorName} on task {taskId} in instance {instanceId}", - tv.GetType().Name, + v.GetType().Name, taskId, instance.Id ); - var issues = await tv.ValidateTask(instance, taskId, language); - issues.ForEach(i => i.Source = tv.ValidationSource); // Ensure that the source is set to the validator source + var issues = await v.ValidateTask(instance, taskId, language); + issues.ForEach(i => i.Source = v.ValidationSource); // Ensure that the source is set to the validator source return issues; } catch (Exception e) @@ -93,10 +99,11 @@ private Task[]> RunTaskValidators(Instance instance, strin _logger.LogError( e, "Error while running validator {validatorName} on task {taskId} in instance {instanceId}", - tv.GetType().Name, + v.GetType().Name, taskId, instance.Id ); + activity?.Errored(e); throw; } }) @@ -115,6 +122,8 @@ public async Task> ValidateDataElement( ArgumentNullException.ThrowIfNull(dataElement); ArgumentNullException.ThrowIfNull(dataElement.DataType); + using var activity = _telemetry?.StartValidateDataElementActivity(instance, dataElement); + // Get both keyed and non-keyed validators for the data type Task[]> dataElementsIssuesTask = RunDataElementValidators( instance, @@ -170,6 +179,7 @@ private Task[]> RunDataElementValidators( var dataElementsIssuesTask = Task.WhenAll( validators.Select(async v => { + using var activity = _telemetry?.StartRunDataElementValidatorActivity(v); try { _logger.LogDebug( @@ -193,6 +203,7 @@ private Task[]> RunDataElementValidators( dataElement.Id, instance.Id ); + activity?.Errored(e); throw; } }) @@ -217,6 +228,8 @@ public async Task>> ValidateFormData( ArgumentNullException.ThrowIfNull(dataElement.DataType); ArgumentNullException.ThrowIfNull(data); + using var activity = _telemetry?.StartValidateFormDataActivity(instance, dataElement); + // Locate the relevant data validator services from normal and keyed services var dataValidators = _validatorFactory .GetFormDataValidators(dataType.Id) @@ -225,36 +238,36 @@ public async Task>> ValidateFormData( .ToArray(); var issuesLists = await Task.WhenAll( - dataValidators.Select( - async (v) => + dataValidators.Select(async v => + { + using var activity = _telemetry?.StartRunFormDataValidatorActivity(v); + try + { + _logger.LogDebug( + "Start running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", + v.GetType().Name, + dataElement.DataType, + dataElement.Id, + instance.Id + ); + var issues = await v.ValidateFormData(instance, dataElement, data, language); + issues.ForEach(i => i.Source = v.ValidationSource); // Ensure that the Source is set to the ValidatorSource + return issues; + } + catch (Exception e) { - try - { - _logger.LogDebug( - "Start running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", - v.GetType().Name, - dataElement.DataType, - dataElement.Id, - instance.Id - ); - var issues = await v.ValidateFormData(instance, dataElement, data, language); - issues.ForEach(i => i.Source = v.ValidationSource); // Ensure that the Source is set to the ValidatorSource - return issues; - } - catch (Exception e) - { - _logger.LogError( - e, - "Error while running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", - v.GetType().Name, - dataElement.DataType, - dataElement.Id, - instance.Id - ); - throw; - } + _logger.LogError( + e, + "Error while running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", + v.GetType().Name, + dataElement.DataType, + dataElement.Id, + instance.Id + ); + activity?.Errored(e); + throw; } - ) + }) ); return dataValidators.Zip(issuesLists).ToDictionary(kv => kv.First.ValidationSource, kv => kv.Second); diff --git a/test/Altinn.App.Api.Tests/DITests.cs b/test/Altinn.App.Api.Tests/Telemetry/TelemetryConfigurationTests.cs similarity index 51% rename from test/Altinn.App.Api.Tests/DITests.cs rename to test/Altinn.App.Api.Tests/Telemetry/TelemetryConfigurationTests.cs index b99f001c1..c65beddf1 100644 --- a/test/Altinn.App.Api.Tests/DITests.cs +++ b/test/Altinn.App.Api.Tests/Telemetry/TelemetryConfigurationTests.cs @@ -1,17 +1,23 @@ using System.Diagnostics.Tracing; +using System.Reflection; +using Altinn.App.Core.Features; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Options; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; using Xunit; namespace Altinn.App.Api.Tests; -public class DITests +public class TelemetryConfigurationTests { private sealed record FakeWebHostEnvironment : IWebHostEnvironment, IHostingEnvironment { @@ -214,4 +220,166 @@ public async Task KeyedServices_Produces_Error_Diagnostics() var events = listener.Events; Assert.NotEmpty(events.Where(e => errorLevels.Contains(e.Level))); } + + private WebApplication BuildApp( + IEnumerable> configData, + Action? registerCustomAppServices = null + ) + { + // Here we follow the order of operations currently present in the Program.cs generated by the template for apps, + // which is the following: + + // 0. WebApplication.CreateBuilder() + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.Environment.EnvironmentName = "Development"; + builder.Configuration.AddInMemoryCollection(configData); + // 1. AddAltinnAppControllersWithViews + Extensions.ServiceCollectionExtensions.AddAltinnAppControllersWithViews(builder.Services); + // 2. RegisterCustomAppServices + registerCustomAppServices?.Invoke(builder.Services); + // 3. AddAltinnAppServices + Extensions.ServiceCollectionExtensions.AddAltinnAppServices( + builder.Services, + builder.Configuration, + builder.Environment + ); + // 4. ConfigureAppWebHost + Extensions.WebHostBuilderExtensions.ConfigureAppWebHost(builder.WebHost, []); + // 5. UseAltinnAppCommonConfiguration + var app = builder.Build(); + Extensions.WebApplicationBuilderExtensions.UseAltinnAppCommonConfiguration(app); + return app; + } + + [Fact] + public async Task OpenTelemetry_Registers_Correctly_When_Enabled() + { + List> configData = + [ + new("ApplicationInsights:InstrumentationKey", "test"), + new("AppSettings:UseOpenTelemetry", "true"), + ]; + Telemetry? telemetry = null; + await using (var app = BuildApp(configData)) + { + var telemetryClient = app.Services.GetService(); + Assert.Null(telemetryClient); + + telemetry = app.Services.GetService(); + Assert.NotNull(telemetry); + Assert.True(telemetry.IsInitialized); + Assert.False(telemetry.IsDisposed); + } + Assert.True(telemetry.IsDisposed); + } + + [Fact] + public async Task OpenTelemetry_Does_Not_Register_By_Default() + { + List> configData = [new("ApplicationInsights:InstrumentationKey", "test"),]; + await using (var app = BuildApp(configData)) + { + var telemetryClient = app.Services.GetService(); + Assert.NotNull(telemetryClient); + + var telemetry = app.Services.GetService(); + Assert.Null(telemetry); + } + } + + [Fact] + public async Task OpenTelemetry_Development_Default_Sampler_Is_AlwaysOnSampler() + { + List> configData = + [ + new("ApplicationInsights:InstrumentationKey", "test"), + new("AppSettings:UseOpenTelemetry", "true"), + ]; + await using var app = BuildApp(configData); + + var traceProvider = app.Services.GetRequiredService(); + + var sampler = GetSampler(traceProvider); + Assert.IsType(sampler); + } + + [Fact] + public async Task OpenTelemetry_Development_Default_MetricReaderOptions() + { + List> configData = + [ + new("ApplicationInsights:InstrumentationKey", "test"), + new("AppSettings:UseOpenTelemetry", "true"), + ]; + await using var app = BuildApp(configData); + + var options = app.Services.GetRequiredService>().Value; + + Assert.Equal(10_000, options.ExportIntervalMilliseconds); + Assert.Equal(8_000, options.ExportTimeoutMilliseconds); + } + + [Fact] + public async Task OpenTelemetry_Sampler_Override_Is_Possible() + { + List> configData = + [ + new("ApplicationInsights:InstrumentationKey", "test"), + new("AppSettings:UseOpenTelemetry", "true"), + ]; + var samplerToUse = new ParentBasedSampler(new AlwaysOnSampler()); + await using var app = BuildApp( + configData, + registerCustomAppServices: services => + { + services.ConfigureOpenTelemetryTracerProvider(builder => + { + builder.SetSampler(samplerToUse); + }); + } + ); + + var traceProvider = app.Services.GetRequiredService(); + + var sampler = GetSampler(traceProvider); + Assert.Same(samplerToUse, sampler); + } + + [Fact] + public async Task OpenTelemetry_MetricReaderOptions_Override_Is_Possible_Through_Configure() + { + List> configData = + [ + new("ApplicationInsights:InstrumentationKey", "test"), + new("AppSettings:UseOpenTelemetry", "true"), + ]; + + var intervalToUse = 5_000; + var timeoutToUse = 4_000; + await using var app = BuildApp( + configData, + registerCustomAppServices: services => + { + services.Configure(options => + { + options.ExportIntervalMilliseconds = intervalToUse; + options.ExportTimeoutMilliseconds = timeoutToUse; + }); + } + ); + + var options = app.Services.GetRequiredService>().Value; + + Assert.Equal(intervalToUse, options.ExportIntervalMilliseconds); + Assert.Equal(timeoutToUse, options.ExportTimeoutMilliseconds); + } + + private Sampler GetSampler(TracerProvider provider) + { + var property = + provider.GetType().GetProperty("Sampler", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("Sampler internal property not found on TraceProvider"); + + return (property.GetValue(provider) as Sampler) ?? throw new InvalidOperationException("Sampler not found"); + } } diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index bf3475ae0..93956770d 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -42,6 +42,7 @@ + diff --git a/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.Order_VerifyHttpCall.verified.txt b/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.Order_VerifyHttpCall.verified.txt new file mode 100644 index 000000000..0aefea85f --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.Order_VerifyHttpCall.verified.txt @@ -0,0 +1,26 @@ +{ + Activities: [ + { + ActivityName: Notifications.Order, + Tags: [ + { + type: email + } + ], + IdFormat: W3C + } + ], + Metrics: [ + { + altinn_app_lib_notification_orders: [ + { + Value: 1, + Tags: { + result: success, + type: email + } + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs b/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs index 9cb574cf6..fa61a88f8 100644 --- a/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs @@ -10,9 +10,9 @@ namespace Altinn.App.Core.Tests.Features.Notifications.Email; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Notifications.Email; +using Altinn.App.Core.Tests.Mocks; using Altinn.Common.AccessTokenClient.Services; using FluentAssertions; -using Microsoft.ApplicationInsights; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -23,10 +23,8 @@ namespace Altinn.App.Core.Tests.Features.Notifications.Email; public class EmailNotificationClientTests { - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Order_VerifyHttpCall(bool includeTelemetryClient) + [Fact] + public async Task Order_VerifyHttpCall() { // Arrange var emailNotification = new EmailNotification @@ -67,16 +65,19 @@ public async Task Order_VerifyHttpCall(bool includeTelemetryClient) using var httpClient = new HttpClient(handlerMock.Object); - var emailNotificationClient = CreateEmailNotificationClient(httpClient, includeTelemetryClient); + using var fixture = CreateFixture(httpClient, true); + var (_, client, telemetrySink) = fixture; // Act - _ = await emailNotificationClient.Order(emailNotification, default); + _ = await client.Order(emailNotification, default); // Assert capturedContent.Should().Be(expectedContent); capturedRequest.Should().NotBeNull(); capturedRequest!.RequestUri.Should().NotBeNull(); capturedRequest!.RequestUri!.ToString().Should().Be(expectedUri); + + await Verify(telemetrySink?.GetSnapshot()); } [Fact] @@ -100,7 +101,9 @@ public async Task Order_ShouldReturnOrderId_OnSuccess() using var httpClient = new HttpClient(handlerMock.Object); - var emailNotificationClient = CreateEmailNotificationClient(httpClient); + using var fixture = CreateFixture(httpClient); + var (_, client, _) = fixture; + var recipients = new List() { new("test.testesen@testdirektoratet.no") }; var emailNotification = new EmailNotification @@ -112,7 +115,7 @@ public async Task Order_ShouldReturnOrderId_OnSuccess() }; // Act - var emailOrderResponse = await emailNotificationClient.Order(emailNotification, default); + var emailOrderResponse = await client.Order(emailNotification, default); // Assert emailOrderResponse.Should().NotBeNull(); @@ -140,7 +143,9 @@ public async Task Order_ShouldThrowEmailNotificationException_OnFailure() using var httpClient = new HttpClient(handlerMock.Object); - var emailNotificationClient = CreateEmailNotificationClient(httpClient); + using var fixture = CreateFixture(httpClient); + var (_, client, _) = fixture; + var recipients = new List() { new("test.testesen@testdirektoratet.no") }; var emailNotification = new EmailNotification @@ -153,7 +158,7 @@ public async Task Order_ShouldThrowEmailNotificationException_OnFailure() // Act // Define an asynchronous delegate action, allowing for the capture and testing of any exceptions thrown. - Func orderEmailNotification = async () => await emailNotificationClient.Order(emailNotification, default); + Func orderEmailNotification = async () => await client.Order(emailNotification, default); // Assert await FluentActions.Awaiting(orderEmailNotification).Should().ThrowAsync(); @@ -180,7 +185,9 @@ public async Task Order_ShouldThrowEmailNotificationException_OnInvalidJsonRespo using var httpClient = new HttpClient(handlerMock.Object); - var emailNotificationClient = CreateEmailNotificationClient(httpClient); + using var fixture = CreateFixture(httpClient); + var (_, client, _) = fixture; + var recipients = new List() { new("test.testesen@testdirektoratet.no") }; var emailNotification = new EmailNotification @@ -193,7 +200,7 @@ public async Task Order_ShouldThrowEmailNotificationException_OnInvalidJsonRespo // Act // Define an asynchronous delegate action, allowing for the capture and testing of any exceptions thrown. - Func orderEmailNotification = async () => await emailNotificationClient.Order(emailNotification, default); + Func orderEmailNotification = async () => await client.Order(emailNotification, default); // Assert await FluentActions.Awaiting(orderEmailNotification).Should().ThrowAsync(); @@ -226,9 +233,25 @@ public void Notification_RequestedSendTime_Always_Valid() [Fact] public void DIContainer_Accepts_Missing_TelemetryClient() + { + using var fixture = CreateFixture(withTelemetry: false); + var (_, client, _) = fixture; + Assert.NotNull(client); + } + + private static Fixture CreateFixture(HttpClient? httpClient = null, bool withTelemetry = true) { var services = new ServiceCollection(); - services.AddSingleton(_ => new HttpClient()); + + if (httpClient is not null) + { + services.AddSingleton(httpClient); + } + else + { + services.AddSingleton(_ => new HttpClient()); + } + services.AddSingleton(new Mock().Object); services.AddSingleton(new Mock().Object); services.AddSingleton>(Options.Create(new PlatformSettings())); @@ -237,35 +260,45 @@ public void DIContainer_Accepts_Missing_TelemetryClient() logging.ClearProviders(); logging.AddProvider(NullLoggerProvider.Instance); }); - services.AddTransient(); - using var serviceProvider = services.BuildServiceProvider( - new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true, } - ); - var smsNotificationClient = serviceProvider.GetRequiredService(); - smsNotificationClient.Should().NotBeNull(); - } - - private static EmailNotificationClient CreateEmailNotificationClient( - HttpClient httpClient, - bool withTelemetryClient = false - ) - { - using var loggerFactory = new NullLoggerFactory(); + var appId = Guid.NewGuid().ToString(); var appDataMock = new Mock(); - appDataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/app-lib-test")); + appDataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata($"ttd/{appId}")); + services.AddSingleton(appDataMock.Object); var accessTokenGenerator = new Mock(); accessTokenGenerator.Setup(a => a.GenerateAccessToken(It.IsAny(), It.IsAny())).Returns("token"); + services.AddSingleton(accessTokenGenerator.Object); + + if (withTelemetry) + { + services.AddTelemetrySink(); + } + + services.AddTransient(); - return new EmailNotificationClient( - loggerFactory.CreateLogger(), - httpClient, - Options.Create(new PlatformSettings()), - appDataMock.Object, - accessTokenGenerator.Object, - withTelemetryClient ? new TelemetryClient() : null + var sp = services.BuildServiceProvider( + new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true, } ); + + var client = (EmailNotificationClient)sp.GetRequiredService(); + var telemetrySink = sp.GetService(); + return new(sp, client, telemetrySink); + } + + private readonly record struct Fixture( + IServiceProvider ServiceProvider, + EmailNotificationClient Client, + TelemetrySink? TelemetrySink + ) : IDisposable + { + public void Dispose() + { + if (ServiceProvider is IDisposable sp) + { + sp.Dispose(); + } + } } } diff --git a/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.Order_VerifyHttpCall.verified.txt b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.Order_VerifyHttpCall.verified.txt new file mode 100644 index 000000000..7c7218af0 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.Order_VerifyHttpCall.verified.txt @@ -0,0 +1,26 @@ +{ + Activities: [ + { + ActivityName: Notifications.Order, + Tags: [ + { + type: sms + } + ], + IdFormat: W3C + } + ], + Metrics: [ + { + altinn_app_lib_notification_orders: [ + { + Value: 1, + Tags: { + result: success, + type: sms + } + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs index 92e2af03a..8e8de71b1 100644 --- a/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs @@ -10,9 +10,9 @@ namespace Altinn.App.Core.Tests.Features.Notifications.Sms; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Notifications.Sms; +using Altinn.App.Core.Tests.Mocks; using Altinn.Common.AccessTokenClient.Services; using FluentAssertions; -using Microsoft.ApplicationInsights; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -20,13 +20,12 @@ namespace Altinn.App.Core.Tests.Features.Notifications.Sms; using Moq; using Moq.Protected; using Xunit; +using static Altinn.App.Core.Features.Telemetry; public class SmsNotificationClientTests { - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Order_VerifyHttpCall(bool includeTelemetryClient) + [Fact] + public async Task Order_VerifyHttpCall() { // Arrange var smsNotification = new SmsNotification @@ -68,16 +67,19 @@ public async Task Order_VerifyHttpCall(bool includeTelemetryClient) using var httpClient = new HttpClient(handlerMock.Object); - var smsNotificationClient = CreateSmsNotificationClient(httpClient, includeTelemetryClient); + using var fixture = CreateFixture(httpClient, true); + var (_, client, telemetrySink) = fixture; // Act - _ = await smsNotificationClient.Order(smsNotification, default); + _ = await client.Order(smsNotification, default); // Assert capturedContent.Should().Be(expectedContent); capturedRequest.Should().NotBeNull(); capturedRequest!.RequestUri.Should().NotBeNull(); capturedRequest!.RequestUri!.ToString().Should().Be(expectedUri); + + await Verify(telemetrySink?.GetSnapshot()); } [Fact] @@ -101,7 +103,9 @@ public async Task Order_ShouldReturnOrderId_OnSuccess() using var httpClient = new HttpClient(handlerMock.Object); - var smsNotificationClient = CreateSmsNotificationClient(httpClient); + using var fixture = CreateFixture(httpClient); + var (_, client, _) = fixture; + var recipients = new List() { new("test.testesen@testdirektoratet.no") }; var smsNotification = new SmsNotification @@ -114,7 +118,7 @@ public async Task Order_ShouldReturnOrderId_OnSuccess() }; // Act - var smsOrderResponse = await smsNotificationClient.Order(smsNotification, default); + var smsOrderResponse = await client.Order(smsNotification, default); // Assert smsOrderResponse.Should().NotBeNull(); @@ -142,7 +146,9 @@ public async Task Order_ShouldThrowSmsNotificationException_OnFailure() using var httpClient = new HttpClient(handlerMock.Object); - var smsNotificationClient = CreateSmsNotificationClient(httpClient); + using var fixture = CreateFixture(httpClient); + var (_, client, _) = fixture; + var recipients = new List() { new("test.testesen@testdirektoratet.no") }; var smsNotification = new SmsNotification @@ -155,7 +161,7 @@ public async Task Order_ShouldThrowSmsNotificationException_OnFailure() }; // Act - Func orderSmsNotification = async () => await smsNotificationClient.Order(smsNotification, default); + Func orderSmsNotification = async () => await client.Order(smsNotification, default); // Assert await FluentActions.Awaiting(orderSmsNotification).Should().ThrowAsync(); @@ -182,7 +188,9 @@ public async Task Order_ShouldThrowSmsNotificationException_OnInvalidJsonRespons using var httpClient = new HttpClient(handlerMock.Object); - var smsNotificationClient = CreateSmsNotificationClient(httpClient); + using var fixture = CreateFixture(httpClient); + var (_, client, _) = fixture; + var recipients = new List() { new("test.testesen@testdirektoratet.no") }; var smsNotification = new SmsNotification @@ -195,7 +203,7 @@ public async Task Order_ShouldThrowSmsNotificationException_OnInvalidJsonRespons }; // Act - Func orderSmsNotification = async () => await smsNotificationClient.Order(smsNotification, default); + Func orderSmsNotification = async () => await client.Order(smsNotification, default); // Assert await FluentActions.Awaiting(orderSmsNotification).Should().ThrowAsync(); @@ -228,9 +236,25 @@ public void Notification_RequestedSendTime_Always_Valid() [Fact] public void DIContainer_Accepts_Missing_TelemetryClient() + { + using var fixture = CreateFixture(withTelemetry: false); + var (_, client, _) = fixture; + Assert.NotNull(client); + } + + private static Fixture CreateFixture(HttpClient? httpClient = null, bool withTelemetry = true) { var services = new ServiceCollection(); - services.AddSingleton(_ => new HttpClient()); + + if (httpClient is not null) + { + services.AddSingleton(httpClient); + } + else + { + services.AddSingleton(_ => new HttpClient()); + } + services.AddSingleton(new Mock().Object); services.AddSingleton(new Mock().Object); services.AddSingleton>(Options.Create(new PlatformSettings())); @@ -239,35 +263,45 @@ public void DIContainer_Accepts_Missing_TelemetryClient() logging.ClearProviders(); logging.AddProvider(NullLoggerProvider.Instance); }); - services.AddTransient(); - using var serviceProvider = services.BuildServiceProvider( - new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true, } - ); - var smsNotificationClient = serviceProvider.GetRequiredService(); - smsNotificationClient.Should().NotBeNull(); - } - - private static SmsNotificationClient CreateSmsNotificationClient( - HttpClient httpClient, - bool withTelemetryClient = false - ) - { - using var loggerFactory = new NullLoggerFactory(); + var appId = Guid.NewGuid().ToString(); var appDataMock = new Mock(); - appDataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/app-lib-test")); + appDataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata($"ttd/{appId}")); + services.AddSingleton(appDataMock.Object); var accessTokenGenerator = new Mock(); accessTokenGenerator.Setup(a => a.GenerateAccessToken(It.IsAny(), It.IsAny())).Returns("token"); + services.AddSingleton(accessTokenGenerator.Object); + + if (withTelemetry) + { + services.AddTelemetrySink(); + } + + services.AddTransient(); - return new SmsNotificationClient( - loggerFactory.CreateLogger(), - httpClient, - Options.Create(new PlatformSettings()), - appDataMock.Object, - accessTokenGenerator.Object, - withTelemetryClient ? new TelemetryClient() : null + var sp = services.BuildServiceProvider( + new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true, } ); + + var client = (SmsNotificationClient)sp.GetRequiredService(); + var telemetryFake = sp.GetService(); + return new(sp, client, telemetryFake); + } + + private readonly record struct Fixture( + IServiceProvider ServiceProvider, + SmsNotificationClient Client, + TelemetrySink? TelemetryFake + ) : IDisposable + { + public void Dispose() + { + if (ServiceProvider is IDisposable sp) + { + sp.Dispose(); + } + } } } diff --git a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs index 006fd3661..72f63d3e1 100644 --- a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs @@ -3,6 +3,7 @@ using Altinn.App.Core.Implementation; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; +using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; @@ -16,6 +17,7 @@ namespace Altinn.App.Core.Tests.Implementation; public class AppResourcesSITests { private readonly string appBasePath = Path.Combine("Implementation", "TestData") + Path.DirectorySeparatorChar; + private readonly TelemetrySink telemetry = new(); [Fact] public void GetApplication_desrializes_file_from_disk() @@ -23,7 +25,13 @@ public void GetApplication_desrializes_file_from_disk() AppSettings appSettings = GetAppSettings("AppMetadata", "default.applicationmetadata.json"); var settings = Microsoft.Extensions.Options.Options.Create(appSettings); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - IAppResources appResources = new AppResourcesSI(settings, appMetadata, null, new NullLogger()); + IAppResources appResources = new AppResourcesSI( + settings, + appMetadata, + null, + new NullLogger(), + telemetry.Object + ); Application expected = new Application() { Id = "tdd/bestilling", @@ -68,7 +76,13 @@ public void GetApplication_handles_onEntry_null() AppSettings appSettings = GetAppSettings("AppMetadata", "no-on-entry.applicationmetadata.json"); var settings = Microsoft.Extensions.Options.Options.Create(appSettings); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - IAppResources appResources = new AppResourcesSI(settings, appMetadata, null, new NullLogger()); + IAppResources appResources = new AppResourcesSI( + settings, + appMetadata, + null, + new NullLogger(), + telemetry.Object + ); Application expected = new Application() { Id = "tdd/bestilling", @@ -119,7 +133,13 @@ public void GetApplication_second_read_from_cache() Microsoft.Extensions.Options.Options.Create(appSettings), appFeaturesMock.Object ); - IAppResources appResources = new AppResourcesSI(settings, appMetadata, null, new NullLogger()); + IAppResources appResources = new AppResourcesSI( + settings, + appMetadata, + null, + new NullLogger(), + telemetry.Object + ); Application expected = new Application() { Id = "tdd/bestilling", @@ -168,7 +188,13 @@ public void GetApplicationMetadata_throws_ApplicationConfigException_if_file_not AppSettings appSettings = GetAppSettings("AppMetadata", "notfound.applicationmetadata.json"); var settings = Microsoft.Extensions.Options.Options.Create(appSettings); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - IAppResources appResources = new AppResourcesSI(settings, appMetadata, null, new NullLogger()); + IAppResources appResources = new AppResourcesSI( + settings, + appMetadata, + null, + new NullLogger(), + telemetry.Object + ); Assert.Throws(() => appResources.GetApplication()); } @@ -178,7 +204,13 @@ public void GetApplicationMetadata_throws_ApplicationConfigException_if_deserial AppSettings appSettings = GetAppSettings("AppMetadata", "invalid.applicationmetadata.json"); var settings = Microsoft.Extensions.Options.Options.Create(appSettings); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - IAppResources appResources = new AppResourcesSI(settings, appMetadata, null, new NullLogger()); + IAppResources appResources = new AppResourcesSI( + settings, + appMetadata, + null, + new NullLogger(), + telemetry.Object + ); Assert.Throws(() => appResources.GetApplication()); } @@ -188,7 +220,13 @@ public void GetApplicationXACMLPolicy_return_policyfile_as_string() AppSettings appSettings = GetAppSettings(subfolder: "AppPolicy", policyFilename: "policy.xml"); var settings = Microsoft.Extensions.Options.Options.Create(appSettings); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - IAppResources appResources = new AppResourcesSI(settings, appMetadata, null, new NullLogger()); + IAppResources appResources = new AppResourcesSI( + settings, + appMetadata, + null, + new NullLogger(), + telemetry.Object + ); string expected = "" + Environment.NewLine + "policy"; var actual = appResources.GetApplicationXACMLPolicy(); actual.Should().BeEquivalentTo(expected); @@ -200,7 +238,13 @@ public void GetApplicationXACMLPolicy_return_null_if_file_not_found() AppSettings appSettings = GetAppSettings(subfolder: "AppPolicy", policyFilename: "notfound.xml"); var settings = Microsoft.Extensions.Options.Options.Create(appSettings); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - IAppResources appResources = new AppResourcesSI(settings, appMetadata, null, new NullLogger()); + IAppResources appResources = new AppResourcesSI( + settings, + appMetadata, + null, + new NullLogger(), + telemetry.Object + ); var actual = appResources.GetApplicationXACMLPolicy(); actual.Should().BeNull(); } @@ -211,7 +255,13 @@ public void GetApplicationBPMNProcess_return_process_as_string() AppSettings appSettings = GetAppSettings(subfolder: "AppProcess", bpmnFilename: "process.bpmn"); var settings = Microsoft.Extensions.Options.Options.Create(appSettings); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - IAppResources appResources = new AppResourcesSI(settings, appMetadata, null, new NullLogger()); + IAppResources appResources = new AppResourcesSI( + settings, + appMetadata, + null, + new NullLogger(), + telemetry.Object + ); string expected = "" + Environment.NewLine + "process"; var actual = appResources.GetApplicationBPMNProcess(); actual.Should().BeEquivalentTo(expected); @@ -223,7 +273,13 @@ public void GetApplicationBPMNProcess_return_null_if_file_not_found() AppSettings appSettings = GetAppSettings(subfolder: "AppProcess", policyFilename: "notfound.xml"); var settings = Microsoft.Extensions.Options.Options.Create(appSettings); IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - IAppResources appResources = new AppResourcesSI(settings, appMetadata, null, new NullLogger()); + IAppResources appResources = new AppResourcesSI( + settings, + appMetadata, + null, + new NullLogger(), + telemetry.Object + ); var actual = appResources.GetApplicationBPMNProcess(); actual.Should().BeNull(); } diff --git a/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.AddEvent_RegisterEventWithInstanceOwnerOrganisation_CloudEventInRequestContainOrganisationNumber.verified.txt b/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.AddEvent_RegisterEventWithInstanceOwnerOrganisation_CloudEventInRequestContainOrganisationNumber.verified.txt new file mode 100644 index 000000000..0e437609e --- /dev/null +++ b/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.AddEvent_RegisterEventWithInstanceOwnerOrganisation_CloudEventInRequestContainOrganisationNumber.verified.txt @@ -0,0 +1,9 @@ +{ + Activities: [ + { + ActivityName: EventClient.GetAsyncWithId, + IdFormat: W3C + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs b/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs index 36c9fb4aa..bf2ef0863 100644 --- a/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs +++ b/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs @@ -2,17 +2,18 @@ using System.Net; using System.Text.Json; using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients.Events; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; +using Altinn.App.Core.Tests.Mocks; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Moq; using Moq.Protected; -using Xunit; namespace Altinn.App.Core.Tests.Implementation { @@ -42,35 +43,36 @@ public EventsClientTest() [Fact] public async Task AddEvent_RegisterEventWithInstanceOwnerOrganisation_CloudEventInRequestContainOrganisationNumber() { + TelemetrySink telemetrySink = new(); // Arrange - Instance instance = new Instance - { - AppId = "ttd/best-app", - Org = "ttd", - InstanceOwner = new InstanceOwner { OrganisationNumber = "org", PartyId = 123.ToString() } - }; + Instance instance = + new() + { + AppId = "ttd/best-app", + Org = "ttd", + InstanceOwner = new InstanceOwner { OrganisationNumber = "org", PartyId = 123.ToString() } + }; - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(Guid.NewGuid().ToString()) - }; + HttpResponseMessage httpResponseMessage = + new() { StatusCode = HttpStatusCode.OK, Content = new StringContent(Guid.NewGuid().ToString()) }; HttpRequestMessage actualRequest = null; void SetRequest(HttpRequestMessage request) => actualRequest = request; InitializeMocks(httpResponseMessage, SetRequest); - HttpClient httpClient = new HttpClient(handlerMock.Object); + HttpClient httpClient = new(handlerMock.Object); - EventsClient target = new EventsClient( - platformSettingsOptions, - contextAccessor.Object, - httpClient, - accessTokenGeneratorMock.Object, - _appMetadataMock.Object, - appSettingsOptions.Object, - generalSettingsOptions - ); + EventsClient target = + new( + platformSettingsOptions, + contextAccessor.Object, + httpClient, + accessTokenGeneratorMock.Object, + _appMetadataMock.Object, + appSettingsOptions.Object, + generalSettingsOptions, + telemetrySink.Object + ); // Act await target.AddEvent("created", instance); @@ -89,6 +91,8 @@ public async Task AddEvent_RegisterEventWithInstanceOwnerOrganisation_CloudEvent Assert.Contains("ttd.apps.at22.altinn.cloud/ttd/best-app/instances", actualEvent.Source.OriginalString); handlerMock.VerifyAll(); + + await Verify(telemetrySink.GetSnapshot()); } [Fact] diff --git a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.AddCompleteConfirmation_SuccessfulCallToStorage.verified.txt b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.AddCompleteConfirmation_SuccessfulCallToStorage.verified.txt new file mode 100644 index 000000000..7fe13848a --- /dev/null +++ b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.AddCompleteConfirmation_SuccessfulCallToStorage.verified.txt @@ -0,0 +1,25 @@ +{ + Activities: [ + { + ActivityName: Instance.CompleteConfirmation, + Tags: [ + { + instance.guid: Guid_1 + }, + { + instance.owner_party_id: 1337 + } + ], + IdFormat: W3C + } + ], + Metrics: [ + { + altinn_app_lib_instances_completed: [ + { + Value: 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs index 6dee3de07..8e1c471fb 100644 --- a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs @@ -5,6 +5,7 @@ using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients.Storage; using Altinn.App.Core.Models; +using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -13,17 +14,17 @@ using Moq; using Moq.Protected; using Newtonsoft.Json; -using Xunit; namespace Altinn.App.PlatformServices.Tests.Implementation { - public class InstanceClientTests + public class InstanceClientTests : IDisposable { private readonly Mock> platformSettingsOptions; private readonly Mock> appSettingsOptions; private readonly Mock handlerMock; private readonly Mock contextAccessor; private readonly Mock> logger; + private readonly TelemetrySink telemetry; public InstanceClientTests() { @@ -32,6 +33,7 @@ public InstanceClientTests() handlerMock = new Mock(MockBehavior.Strict); contextAccessor = new Mock(); logger = new Mock>(); + telemetry = new TelemetrySink(); } [Fact] @@ -61,7 +63,8 @@ public async Task AddCompleteConfirmation_SuccessfulCallToStorage() logger.Object, contextAccessor.Object, httpClient, - appSettingsOptions.Object + appSettingsOptions.Object, + telemetry.Object ); // Act @@ -69,6 +72,8 @@ public async Task AddCompleteConfirmation_SuccessfulCallToStorage() // Assert handlerMock.VerifyAll(); + + await Verify(telemetry.GetSnapshot()); } [Fact] @@ -90,7 +95,8 @@ public async Task AddCompleteConfirmation_StorageReturnsNonSuccess_ThrowsPlatfor logger.Object, contextAccessor.Object, httpClient, - appSettingsOptions.Object + appSettingsOptions.Object, + telemetry.Object ); PlatformHttpException actualException = null; @@ -130,7 +136,8 @@ public async Task UpdateReadStatus_StorageReturnsNonSuccess_LogsErrorAppContinue logger.Object, contextAccessor.Object, httpClient, - appSettingsOptions.Object + appSettingsOptions.Object, + telemetry.Object ); PlatformHttpException actualException = null; @@ -172,7 +179,8 @@ public async Task UpdateReadStatus_StorageReturnsSuccess() logger.Object, contextAccessor.Object, httpClient, - appSettingsOptions.Object + appSettingsOptions.Object, + telemetry.Object ); // Act @@ -210,7 +218,8 @@ public async Task UpdateSubtatus_StorageReturnsSuccess() logger.Object, contextAccessor.Object, httpClient, - appSettingsOptions.Object + appSettingsOptions.Object, + telemetry.Object ); // Act @@ -245,7 +254,8 @@ public async Task UpdateSubtatus_StorageReturnsNonSuccess_ThrowsPlatformHttpExce logger.Object, contextAccessor.Object, httpClient, - appSettingsOptions.Object + appSettingsOptions.Object, + telemetry.Object ); PlatformHttpException actualException = null; @@ -294,7 +304,8 @@ public async Task DeleteInstance_StorageReturnsSuccess() logger.Object, contextAccessor.Object, httpClient, - appSettingsOptions.Object + appSettingsOptions.Object, + telemetry.Object ); // Act @@ -326,7 +337,8 @@ public async Task DeleteInstance_StorageReturnsNonSuccess_ThrowsPlatformHttpExce logger.Object, contextAccessor.Object, httpClient, - appSettingsOptions.Object + appSettingsOptions.Object, + telemetry.Object ); PlatformHttpException actualException = null; @@ -368,7 +380,8 @@ public async Task UpdatePresentationTexts_StorageReturnsNonSuccess_ThrowsPlatfor logger.Object, contextAccessor.Object, httpClient, - appSettingsOptions.Object + appSettingsOptions.Object, + telemetry.Object ); PlatformHttpException actualException = null; @@ -417,7 +430,8 @@ public async Task UpdatePresentationTexts_SuccessfulCallToStorage() logger.Object, contextAccessor.Object, httpClient, - appSettingsOptions.Object + appSettingsOptions.Object, + telemetry.Object ); // Act @@ -481,7 +495,8 @@ public async Task QueryInstances_QueryResponseContainsNext() logger.Object, contextAccessor.Object, httpClient, - appSettingsOptions.Object + appSettingsOptions.Object, + telemetry.Object ); Dictionary queryParams = @@ -542,5 +557,10 @@ private void InitializeMocks(HttpResponseMessage[] httpResponseMessages, string[ .Verifiable(); } } + + public void Dispose() + { + telemetry.Dispose(); + } } } diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.AuthorizeActions_returns_dictionary_with_one_action_denied.verified.txt b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.AuthorizeActions_returns_dictionary_with_one_action_denied.verified.txt new file mode 100644 index 000000000..c109255df --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.AuthorizeActions_returns_dictionary_with_one_action_denied.verified.txt @@ -0,0 +1,14 @@ +{ + Activities: [ + { + ActivityName: Authorization.Client.AuthorizeActions, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs index 050af81c9..00e762c56 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Altinn.App.Core.Configuration; using Altinn.App.Core.Infrastructure.Clients.Authorization; +using Altinn.App.Core.Tests.Mocks; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Interfaces; using Altinn.Platform.Storage.Interface.Models; @@ -11,7 +12,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Infrastructure.Clients.Authorization; @@ -20,20 +20,23 @@ public class AuthorizationClientTests [Fact] public async Task AuthorizeActions_returns_dictionary_with_one_action_denied() { + TelemetrySink telemetrySink = new(); Mock pdpMock = new(); Mock httpContextAccessorMock = new(); Mock httpClientMock = new(); Mock> appSettingsMock = new(); var pdpResponse = GetXacmlJsonRespons("one-action-denied"); pdpMock.Setup(s => s.GetDecisionForRequest(It.IsAny())).ReturnsAsync(pdpResponse); - AuthorizationClient client = new AuthorizationClient( - Options.Create(new PlatformSettings()), - httpContextAccessorMock.Object, - httpClientMock.Object, - appSettingsMock.Object, - pdpMock.Object, - NullLogger.Instance - ); + AuthorizationClient client = + new( + Options.Create(new PlatformSettings()), + httpContextAccessorMock.Object, + httpClientMock.Object, + appSettingsMock.Object, + pdpMock.Object, + NullLogger.Instance, + telemetrySink.Object + ); var claimsPrincipal = GetClaims("1337"); @@ -60,11 +63,14 @@ public async Task AuthorizeActions_returns_dictionary_with_one_action_denied() var actions = new List() { "read", "write", "complete", "lookup" }; var actual = await client.AuthorizeActions(instance, claimsPrincipal, actions); actual.Should().BeEquivalentTo(expected); + + await Verify(telemetrySink.GetSnapshot()); } [Fact] public async Task AuthorizeActions_returns_empty_dictionary_if_no_response_from_pdp() { + TelemetrySink telemetry = new(); Mock pdpMock = new(); Mock httpContextAccessorMock = new(); Mock httpClientMock = new(); @@ -78,7 +84,8 @@ public async Task AuthorizeActions_returns_empty_dictionary_if_no_response_from_ httpClientMock.Object, appSettingsMock.Object, pdpMock.Object, - NullLogger.Instance + NullLogger.Instance, + telemetry.Object ); var claimsPrincipal = GetClaims("1337"); diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.InsertBinaryData_MethodProduceValidPlatformRequest.verified.txt b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.InsertBinaryData_MethodProduceValidPlatformRequest.verified.txt new file mode 100644 index 000000000..f46589450 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.InsertBinaryData_MethodProduceValidPlatformRequest.verified.txt @@ -0,0 +1,14 @@ +{ + Activities: [ + { + ActivityName: DataClient.InsertBinaryData, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs index 110197252..c622665d1 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs @@ -8,6 +8,7 @@ using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; using Altinn.App.Core.Tests.Infrastructure.Clients.Storage.TestData; +using Altinn.App.Core.Tests.Mocks; using Altinn.App.PlatformServices.Tests.Data; using Altinn.App.PlatformServices.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; @@ -16,7 +17,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage { @@ -43,6 +43,7 @@ public async Task InsertBinaryData_MethodProduceValidPlatformRequest() { // Arrange HttpRequestMessage? platformRequest = null; + TelemetrySink telemetrySink = new(); var target = GetDataClient( async (HttpRequestMessage request, CancellationToken token) => @@ -52,7 +53,8 @@ public async Task InsertBinaryData_MethodProduceValidPlatformRequest() DataElement dataElement = new DataElement { Id = "DataElement.Id", InstanceGuid = "InstanceGuid" }; await Task.CompletedTask; return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; - } + }, + telemetrySink ); var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); @@ -76,6 +78,8 @@ public async Task InsertBinaryData_MethodProduceValidPlatformRequest() Assert.NotNull(platformRequest); AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Post, "\"a cats story.pdf\"", "application/pdf"); + + await Verify(telemetrySink.GetSnapshot()); } [Fact] @@ -835,7 +839,8 @@ public async Task UnlockDataElement_throws_platformhttpexception_if_platform_req } private DataClient GetDataClient( - Func> handlerFunc + Func> handlerFunc, + TelemetrySink? telemetrySink = null ) { DelegatingHandlerStub delegatingHandlerStub = new(handlerFunc); @@ -843,7 +848,8 @@ Func> handlerFu platformSettingsOptions.Object, logger, new HttpClient(delegatingHandlerStub), - userTokenProvide.Object + userTokenProvide.Object, + telemetrySink?.Object ); } diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs deleted file mode 100644 index 594144baa..000000000 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs +++ /dev/null @@ -1,335 +0,0 @@ -#nullable disable -using System.Net; -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Infrastructure.Clients.Storage; -using Altinn.App.Core.Internal.Instances; -using Altinn.App.Core.Tests.TestHelpers; -using Altinn.Platform.Storage.Interface.Models; -using FluentAssertions; -using Microsoft.Extensions.Primitives; -using Moq; -using Prometheus; -using Xunit; - -namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage; - -public class InstanceClientMetricsDecoratorTests -{ - public InstanceClientMetricsDecoratorTests() - { - Metrics.SuppressDefaultMetrics(); - } - - [Fact] - public async Task CreateInstance_calls_decorated_service_and_update_on_success() - { - // Arrange - Mock instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - var instanceTemplate = new Instance(); - - // Act - await instanceClientMetricsDecorator.CreateInstance("org", "app", instanceTemplate); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCountGreaterOrEqualTo(1); - diff.Should().Contain("altinn_app_instances_created{result=\"success\"} 1"); - instanceClient.Verify(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task CreateInstance_calls_decorated_service_and_update_on_failure() - { - // Arrange - var instanceClient = new Mock(); - var platformHttpException = new PlatformHttpException( - new HttpResponseMessage(HttpStatusCode.BadRequest), - "test" - ); - instanceClient - .Setup(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(platformHttpException); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - var instanceTemplate = new Instance(); - - // Act - var ex = await Assert.ThrowsAsync( - async () => await instanceClientMetricsDecorator.CreateInstance("org", "app", instanceTemplate) - ); - ex.Should().BeSameAs(platformHttpException); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCountGreaterOrEqualTo(1); - diff.Should().Contain("altinn_app_instances_created{result=\"failure\"} 1"); - instanceClient.Verify(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task AddCompleteConfirmation_calls_decorated_service_and_update_on_success() - { - // Arrange - Mock instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Act - await instanceClientMetricsDecorator.AddCompleteConfirmation(1337, Guid.NewGuid()); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCountGreaterOrEqualTo(1); - diff.Should().Contain("altinn_app_instances_completed{result=\"success\"} 1"); - instanceClient.Verify(i => i.AddCompleteConfirmation(It.IsAny(), It.IsAny())); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task AddCompleteConfirmation_calls_decorated_service_and_update_on_failure() - { - // Arrange - var instanceClient = new Mock(); - var platformHttpException = new PlatformHttpException( - new HttpResponseMessage(HttpStatusCode.BadRequest), - "test" - ); - instanceClient - .Setup(i => i.AddCompleteConfirmation(It.IsAny(), It.IsAny())) - .ThrowsAsync(platformHttpException); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Act - var ex = await Assert.ThrowsAsync( - async () => await instanceClientMetricsDecorator.AddCompleteConfirmation(1337, Guid.NewGuid()) - ); - ex.Should().BeSameAs(platformHttpException); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCountGreaterOrEqualTo(1); - diff.Should().Contain("altinn_app_instances_completed{result=\"failure\"} 1"); - instanceClient.Verify(i => i.AddCompleteConfirmation(It.IsAny(), It.IsAny())); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task DeleteInstance_calls_decorated_service_and_update_on_success_soft_delete() - { - // Arrange - Mock instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Act - await instanceClientMetricsDecorator.DeleteInstance(1337, Guid.NewGuid(), false); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCountGreaterOrEqualTo(1); - diff.Should().Contain("altinn_app_instances_deleted{result=\"success\",mode=\"soft\"} 1"); - instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task DeleteInstance_calls_decorated_service_and_update_on_success_soft_hard() - { - // Arrange - Mock instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Act - await instanceClientMetricsDecorator.DeleteInstance(1337, Guid.NewGuid(), true); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCountGreaterOrEqualTo(1); - diff.Should().Contain("altinn_app_instances_deleted{result=\"success\",mode=\"hard\"} 1"); - instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task DeleteInstance_calls_decorated_service_and_update_on_failure() - { - // Arrange - var instanceClient = new Mock(); - var platformHttpException = new PlatformHttpException( - new HttpResponseMessage(HttpStatusCode.BadRequest), - "test" - ); - instanceClient - .Setup(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(platformHttpException); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Act - var ex = await Assert.ThrowsAsync( - async () => await instanceClientMetricsDecorator.DeleteInstance(1337, Guid.NewGuid(), false) - ); - ex.Should().BeSameAs(platformHttpException); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); - - // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCountGreaterOrEqualTo(1); - diff.Should().Contain("altinn_app_instances_deleted{result=\"failure\",mode=\"soft\"} 1"); - instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task GetInstance_calls_decorated_service() - { - // Arrange - var instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - - // Act - var instanceId = Guid.NewGuid(); - await instanceClientMetricsDecorator.GetInstance("test-app", "ttd", 1337, instanceId); - - // Assert - instanceClient.Verify(i => i.GetInstance("test-app", "ttd", 1337, instanceId)); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task GetInstance_instance_calls_decorated_service() - { - // Arrange - var instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var instance = new Instance(); - - // Act - await instanceClientMetricsDecorator.GetInstance(instance); - - // Assert - instanceClient.Verify(i => i.GetInstance(instance)); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task GetInstances_calls_decorated_service() - { - // Arrange - var instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - - // Act - await instanceClientMetricsDecorator.GetInstances(new Dictionary()); - - // Assert - instanceClient.Verify(i => i.GetInstances(new Dictionary())); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task UpdateProcess_of_instance_owner_calls_decorated_service() - { - // Arrange - var instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var instance = new Instance(); - - // Act - await instanceClientMetricsDecorator.UpdateProcess(instance); - - // Assert - instanceClient.Verify(i => i.UpdateProcess(instance)); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task UpdateReadStatus_of_instance_owner_calls_decorated_service() - { - // Arrange - var instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var instanceGuid = Guid.NewGuid(); - - // Act - await instanceClientMetricsDecorator.UpdateReadStatus(1337, instanceGuid, "read"); - - // Assert - instanceClient.Verify(i => i.UpdateReadStatus(1337, instanceGuid, "read")); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task UpdateSubstatus_of_instance_owner_calls_decorated_service() - { - // Arrange - var instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var instanceGuid = Guid.NewGuid(); - var substatus = new Substatus(); - - // Act - await instanceClientMetricsDecorator.UpdateSubstatus(1337, instanceGuid, substatus); - - // Assert - instanceClient.Verify(i => i.UpdateSubstatus(1337, instanceGuid, substatus)); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task UpdatePresentationTexts_of_instance_owner_calls_decorated_service() - { - // Arrange - var instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var instanceGuid = Guid.NewGuid(); - var presentationTexts = new PresentationTexts(); - - // Act - await instanceClientMetricsDecorator.UpdatePresentationTexts(1337, instanceGuid, presentationTexts); - - // Assert - instanceClient.Verify(i => i.UpdatePresentationTexts(1337, instanceGuid, presentationTexts)); - instanceClient.VerifyNoOtherCalls(); - } - - [Fact] - public async Task UpdateDataValues_of_instance_owner_calls_decorated_service() - { - // Arrange - var instanceClient = new Mock(); - var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var instanceGuid = Guid.NewGuid(); - var dataValues = new DataValues(); - - // Act - await instanceClientMetricsDecorator.UpdateDataValues(1337, instanceGuid, dataValues); - - // Assert - instanceClient.Verify(i => i.UpdateDataValues(1337, instanceGuid, dataValues)); - instanceClient.VerifyNoOtherCalls(); - } - - private static List GetDiff(string s1, string s2) - { - List diff; - IEnumerable set1 = s1.Split('\n').Distinct().Where(s => !s.StartsWith("#")); - IEnumerable set2 = s2.Split('\n').Distinct().Where(s => !s.StartsWith("#")); - - diff = set2.Count() > set1.Count() ? set2.Except(set1).ToList() : set1.Except(set2).ToList(); - - return diff; - } -} diff --git a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.GetApplicationMetadata_desrializes_file_from_disk.verified.txt b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.GetApplicationMetadata_desrializes_file_from_disk.verified.txt new file mode 100644 index 000000000..5256a44cd --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.GetApplicationMetadata_desrializes_file_from_disk.verified.txt @@ -0,0 +1,9 @@ +{ + Activities: [ + { + ActivityName: ApplicationMetadata.Client.Get, + IdFormat: W3C + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs index 50de1f40f..5af60298f 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs @@ -2,8 +2,10 @@ using System.Text.Encodings.Web; using System.Text.Json; using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; +using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.Extensions.Options; @@ -25,11 +27,12 @@ public class AppMedataTest public async Task GetApplicationMetadata_desrializes_file_from_disk() { var featureManagerMock = new Mock(); - IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + FrontendFeatures frontendFeatures = new(featureManagerMock.Object); Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + TelemetrySink telemetrySink = new(); AppSettings appSettings = GetAppSettings("AppMetadata", "default.applicationmetadata.json"); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings), null, telemetrySink); ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") { Id = "tdd/bestilling", @@ -67,6 +70,8 @@ public async Task GetApplicationMetadata_desrializes_file_from_disk() var actual = await appMetadata.GetApplicationMetadata(); actual.Should().NotBeNull(); actual.Should().BeEquivalentTo(expected); + + await Verify(telemetrySink.GetSnapshot()); } [Fact] @@ -555,17 +560,22 @@ private AppSettings GetAppSettings( private static IAppMetadata SetupAppMedata( IOptions appsettings, - IFrontendFeatures frontendFeatures = null + IFrontendFeatures frontendFeatures = null, + TelemetrySink telemetrySink = null ) { var featureManagerMock = new Mock(); - + telemetrySink ??= new TelemetrySink(); if (frontendFeatures == null) { - return new AppMetadata(appsettings, new FrontendFeatures(featureManagerMock.Object)); + return new AppMetadata( + appsettings, + new FrontendFeatures(featureManagerMock.Object), + telemetrySink.Object + ); } - return new AppMetadata(appsettings, frontendFeatures); + return new AppMetadata(appsettings, frontendFeatures, telemetrySink.Object); } } } diff --git a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.GetPartyList_returns_party_list_from_AuthorizationClient.verified.txt b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.GetPartyList_returns_party_list_from_AuthorizationClient.verified.txt new file mode 100644 index 000000000..8171592c2 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.GetPartyList_returns_party_list_from_AuthorizationClient.verified.txt @@ -0,0 +1,14 @@ +{ + Activities: [ + { + ActivityName: Authorization.Service.GetPartyList, + Tags: [ + { + user.id: 1337 + } + ], + IdFormat: W3C + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs index 57351b68a..598c10895 100644 --- a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs @@ -6,6 +6,7 @@ using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; +using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; @@ -21,6 +22,7 @@ public async Task GetPartyList_returns_party_list_from_AuthorizationClient() { // Input int userId = 1337; + TelemetrySink telemetrySink = new(); // Arrange Mock authorizationClientMock = new Mock(); @@ -28,7 +30,8 @@ public async Task GetPartyList_returns_party_list_from_AuthorizationClient() authorizationClientMock.Setup(a => a.GetPartyList(userId)).ReturnsAsync(partyList); AuthorizationService authorizationService = new AuthorizationService( authorizationClientMock.Object, - new List() + new List(), + telemetrySink.Object ); // Act @@ -37,6 +40,8 @@ public async Task GetPartyList_returns_party_list_from_AuthorizationClient() // Assert result.Should().BeSameAs(partyList); authorizationClientMock.Verify(a => a.GetPartyList(userId), Times.Once); + + await Verify(telemetrySink.GetSnapshot()); } [Fact] diff --git a/test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.Test_Ok.verified.txt b/test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.Test_Ok.verified.txt new file mode 100644 index 000000000..5a0488bdb --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.Test_Ok.verified.txt @@ -0,0 +1,28 @@ +{ + Activities: [ + { + ActivityName: Data.Patch, + Tags: [ + { + instance.guid: Guid_1 + }, + { + result: success + } + ], + IdFormat: W3C + } + ], + Metrics: [ + { + altinn_app_lib_data_patched: [ + { + Value: 1, + Tags: { + result: success + } + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.cs index 26bc046e2..274a8260e 100644 --- a/test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.cs @@ -7,18 +7,18 @@ using Altinn.App.Core.Internal.Validation; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; +using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Json.Patch; using Json.Pointer; using Microsoft.Extensions.Logging; using Moq; -using Xunit; using DataType = Altinn.Platform.Storage.Interface.Models.DataType; namespace Altinn.App.Core.Tests.Internal.Patch; -public class PatchServiceTests +public class PatchServiceTests : IDisposable { // Test data private static readonly Guid DataGuid = new("12345678-1234-1234-1234-123456789123"); @@ -31,6 +31,7 @@ public class PatchServiceTests private readonly Mock _dataProcessorMock = new(MockBehavior.Strict); private readonly Mock _appModelMock = new(MockBehavior.Strict); private readonly Mock _appMetadataMock = new(MockBehavior.Strict); + private readonly TelemetrySink _telemetrySink = new(); // ValidatorMocks private readonly Mock _formDataValidator = new(MockBehavior.Strict); @@ -83,7 +84,8 @@ public PatchServiceTests() _dataClientMock.Object, validationService, new List { _dataProcessorMock.Object }, - _appModelMock.Object + _appModelMock.Object, + _telemetrySink.Object ); } @@ -171,6 +173,8 @@ public async Task Test_Ok() _dataProcessorMock.Verify(d => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null) ); + + await Verify(_telemetrySink.GetSnapshot()); } [Fact] @@ -311,4 +315,9 @@ public async Task Test_JsonPatch_does_not_deserialize() ); err.ErrorType.Should().Be(DataPatchErrorType.DeserializationFailed); } + + public void Dispose() + { + _telemetrySink.Dispose(); + } } diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.GenerateAndStorePdf.verified.txt b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.GenerateAndStorePdf.verified.txt new file mode 100644 index 000000000..ae3a9a974 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.GenerateAndStorePdf.verified.txt @@ -0,0 +1,17 @@ +{ + Activities: [ + { + ActivityName: PdfService.GenerateAndStorePdf, + Tags: [ + { + instance.guid: Guid_1 + }, + { + task.id: Task_1 + } + ], + IdFormat: W3C + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index 0b0c2cad1..9207fb561 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -1,11 +1,13 @@ using System.Net; using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; using Altinn.App.Core.Infrastructure.Clients.Pdf; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Pdf; using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Tests.Mocks; using Altinn.App.PlatformServices.Tests.Helpers; using Altinn.App.PlatformServices.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; @@ -129,6 +131,7 @@ await pdfGeneratorClient.GeneratePdf( public async Task GenerateAndStorePdf() { // Arrange + TelemetrySink telemetrySink = new(); _pdfGeneratorClient.Setup(s => s.GeneratePdf(It.IsAny(), It.IsAny())); _generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}"; @@ -139,7 +142,8 @@ public async Task GenerateAndStorePdf() _profile.Object, _pdfGeneratorClient.Object, _pdfGeneratorSettingsOptions, - _generalSettingsOptions + _generalSettingsOptions, + telemetrySink.Object ); Instance instance = @@ -180,6 +184,8 @@ public async Task GenerateAndStorePdf() ), Times.Once ); + + await Verify(telemetrySink.GetSnapshot()); } [Fact] diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs deleted file mode 100644 index cfec0500a..000000000 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs +++ /dev/null @@ -1,319 +0,0 @@ -#nullable disable -using Altinn.App.Core.Internal.Process; -using Altinn.App.Core.Models.Process; -using Altinn.App.Core.Tests.TestHelpers; -using Altinn.Platform.Storage.Interface.Models; -using FluentAssertions; -using Moq; -using Prometheus; -using Xunit; - -namespace Altinn.App.Core.Tests.Internal.Process; - -public class ProcessEngineMetricsDecoratorTests -{ - public ProcessEngineMetricsDecoratorTests() - { - Metrics.SuppressDefaultMetrics(); - } - - [Fact] - public async Task StartProcess_calls_decorated_service_and_increments_success_counter_when_successful() - { - // Arrange - var processEngine = new Mock(); - processEngine - .Setup(p => p.GenerateProcessStartEvents(It.IsAny())) - .ReturnsAsync(new ProcessChangeResult { Success = true }); - var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); - (await ReadPrometheusMetricsToString()) - .Should() - .NotContain("altinn_app_process_start_count{result=\"success\"}"); - - var result = await decorator.GenerateProcessStartEvents(new ProcessStartRequest()); - - (await ReadPrometheusMetricsToString()) - .Should() - .Contain("altinn_app_process_start_count{result=\"success\"} 1"); - result.Success.Should().BeTrue(); - result = await decorator.GenerateProcessStartEvents(new ProcessStartRequest()); - (await ReadPrometheusMetricsToString()) - .Should() - .Contain("altinn_app_process_start_count{result=\"success\"} 2"); - result.Success.Should().BeTrue(); - processEngine.Verify(p => p.GenerateProcessStartEvents(It.IsAny()), Times.Exactly(2)); - processEngine.VerifyNoOtherCalls(); - } - - [Fact] - public async Task StartProcess_calls_decorated_service_and_increments_failure_counter_when_unsuccessful() - { - // Arrange - var processEngine = new Mock(); - processEngine - .Setup(p => p.GenerateProcessStartEvents(It.IsAny())) - .ReturnsAsync(new ProcessChangeResult { Success = false }); - var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); - (await ReadPrometheusMetricsToString()) - .Should() - .NotContain("altinn_app_process_start_count{result=\"failure\"}"); - - var result = await decorator.GenerateProcessStartEvents(new ProcessStartRequest()); - - (await ReadPrometheusMetricsToString()) - .Should() - .Contain("altinn_app_process_start_count{result=\"failure\"} 1"); - result.Success.Should().BeFalse(); - result = await decorator.GenerateProcessStartEvents(new ProcessStartRequest()); - (await ReadPrometheusMetricsToString()) - .Should() - .Contain("altinn_app_process_start_count{result=\"failure\"} 2"); - result.Success.Should().BeFalse(); - processEngine.Verify(p => p.GenerateProcessStartEvents(It.IsAny()), Times.Exactly(2)); - processEngine.VerifyNoOtherCalls(); - } - - [Fact] - public async Task Next_calls_decorated_service_and_increments_success_counter_when_successful() - { - // Arrange - var processEngine = new Mock(); - processEngine - .Setup(p => p.Next(It.IsAny())) - .ReturnsAsync(new ProcessChangeResult { Success = true }); - var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); - (await ReadPrometheusMetricsToString()) - .Should() - .NotContain("altinn_app_process_task_next_count{result=\"success\",action=\"write\",task=\"Task_1\"}"); - - var result = await decorator.Next( - new ProcessNextRequest() - { - Instance = new() { Process = new() { CurrentTask = new() { ElementId = "Task_1" } } }, - Action = "write" - } - ); - - (await ReadPrometheusMetricsToString()) - .Should() - .Contain("altinn_app_process_task_next_count{result=\"success\",action=\"write\",task=\"Task_1\"} 1"); - result.Success.Should().BeTrue(); - result = await decorator.Next( - new ProcessNextRequest() - { - Instance = new() { Process = new() { CurrentTask = new() { ElementId = "Task_1" } } }, - Action = "write" - } - ); - var prometheusMetricsToString = await ReadPrometheusMetricsToString(); - prometheusMetricsToString - .Should() - .Contain("altinn_app_process_task_next_count{result=\"success\",action=\"write\",task=\"Task_1\"} 2"); - result.Success.Should().BeTrue(); - processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); - processEngine.VerifyNoOtherCalls(); - } - - [Fact] - public async Task Next_calls_decorated_service_and_increments_failure_counter_when_unsuccessful() - { - // Arrange - var processEngine = new Mock(); - processEngine - .Setup(p => p.Next(It.IsAny())) - .ReturnsAsync(new ProcessChangeResult { Success = false }); - var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); - (await ReadPrometheusMetricsToString()) - .Should() - .NotContain("altinn_app_process_task_next_count{result=\"failure\",action=\"write\",task=\"Task_1\"}"); - - var result = await decorator.Next( - new ProcessNextRequest() - { - Instance = new() { Process = new() { CurrentTask = new() { ElementId = "Task_1" } } }, - Action = "write" - } - ); - - (await ReadPrometheusMetricsToString()) - .Should() - .Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"write\",task=\"Task_1\"} 1"); - result.Success.Should().BeFalse(); - result = await decorator.Next( - new ProcessNextRequest() - { - Instance = new() { Process = new() { CurrentTask = new() { ElementId = "Task_1" } } }, - Action = "write" - } - ); - var prometheusMetricsToString = await ReadPrometheusMetricsToString(); - prometheusMetricsToString - .Should() - .Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"write\",task=\"Task_1\"} 2"); - result.Success.Should().BeFalse(); - processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); - processEngine.VerifyNoOtherCalls(); - } - - [Fact] - public async Task Next_calls_decorated_service_and_increments_success_and_end_counters_when_successful_and_process_ended() - { - // Arrange - var processEngine = new Mock(); - var ended = DateTime.Now; - var started = ended.AddSeconds(-20); - processEngine - .Setup(p => p.Next(It.IsAny())) - .ReturnsAsync( - new ProcessChangeResult - { - Success = true, - ProcessStateChange = new() - { - NewProcessState = new() { Ended = ended, Started = started } - } - } - ); - var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); - (await ReadPrometheusMetricsToString()) - .Should() - .NotContain("altinn_app_process_task_next_count{result=\"success\",action=\"confirm\",task=\"Task_2\"}"); - (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_end_count{result=\"success\"}"); - (await ReadPrometheusMetricsToString()) - .Should() - .NotContain("altinn_app_process_end_time_total{result=\"success\"}"); - - var result = await decorator.Next( - new ProcessNextRequest() - { - Instance = new() { Process = new() { CurrentTask = new() { ElementId = "Task_2" } } }, - Action = "confirm" - } - ); - - (await ReadPrometheusMetricsToString()) - .Should() - .Contain("altinn_app_process_task_next_count{result=\"success\",action=\"confirm\",task=\"Task_2\"} 1"); - (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_end_count{result=\"success\"} 1"); - (await ReadPrometheusMetricsToString()) - .Should() - .Contain("altinn_app_process_end_time_total{result=\"success\"} 20"); - result.Success.Should().BeTrue(); - result = await decorator.Next( - new ProcessNextRequest() - { - Instance = new() { Process = new() { CurrentTask = new() { ElementId = "Task_2" } } }, - Action = "confirm" - } - ); - var prometheusMetricsToString = await ReadPrometheusMetricsToString(); - prometheusMetricsToString - .Should() - .Contain("altinn_app_process_task_next_count{result=\"success\",action=\"confirm\",task=\"Task_2\"} 2"); - (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_end_count{result=\"success\"} 2"); - (await ReadPrometheusMetricsToString()) - .Should() - .Contain("altinn_app_process_end_time_total{result=\"success\"} 40"); - result.Success.Should().BeTrue(); - processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); - processEngine.VerifyNoOtherCalls(); - } - - [Fact] - public async Task Next_calls_decorated_service_and_increments_failure_and_end_counters_when_unsuccessful_and_process_ended_no_time_added_if_started_null() - { - // Arrange - var processEngine = new Mock(); - var ended = DateTime.Now; - processEngine - .Setup(p => p.Next(It.IsAny())) - .ReturnsAsync( - new ProcessChangeResult - { - Success = false, - ProcessStateChange = new() { NewProcessState = new() { Ended = ended } } - } - ); - var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); - var prometheusMetricsToString = await ReadPrometheusMetricsToString(); - prometheusMetricsToString - .Should() - .NotContain("altinn_app_process_task_next_count{result=\"failure\",action=\"confirm\",task=\"Task_3\"}"); - prometheusMetricsToString.Should().NotContain("altinn_app_process_end_count{result=\"failure\"}"); - prometheusMetricsToString.Should().NotContain("altinn_app_process_end_time_total{result=\"failure\"}"); - - var result = await decorator.Next( - new ProcessNextRequest() - { - Instance = new() { Process = new() { CurrentTask = new() { ElementId = "Task_3" } } }, - Action = "confirm" - } - ); - - prometheusMetricsToString = await ReadPrometheusMetricsToString(); - prometheusMetricsToString - .Should() - .Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"confirm\",task=\"Task_3\"} 1"); - prometheusMetricsToString.Should().Contain("altinn_app_process_end_count{result=\"failure\"} 1"); - prometheusMetricsToString.Should().NotContain("altinn_app_process_end_time_total{result=\"failure\"}"); - result.Success.Should().BeFalse(); - result = await decorator.Next( - new ProcessNextRequest() - { - Instance = new() { Process = new() { CurrentTask = new() { ElementId = "Task_3" } } }, - Action = "confirm" - } - ); - prometheusMetricsToString = await ReadPrometheusMetricsToString(); - prometheusMetricsToString - .Should() - .Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"confirm\",task=\"Task_3\"} 2"); - prometheusMetricsToString.Should().Contain("altinn_app_process_end_count{result=\"failure\"} 2"); - prometheusMetricsToString.Should().NotContain("altinn_app_process_end_time_total{result=\"failure\"}"); - result.Success.Should().BeFalse(); - processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); - processEngine.VerifyNoOtherCalls(); - } - - [Fact] - public async Task UpdateInstanceAndRerunEvents_calls_decorated_service() - { - // Arrange - var processEngine = new Mock(); - processEngine - .Setup(p => - p.HandleEventsAndUpdateStorage( - It.IsAny(), - It.IsAny>(), - It.IsAny>() - ) - ) - .ReturnsAsync(new Instance { }); - var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); - (await ReadPrometheusMetricsToString()) - .Should() - .NotContain("altinn_app_process_start_count{result=\"success\"}"); - - await decorator.HandleEventsAndUpdateStorage( - It.IsAny(), - It.IsAny>(), - new List() - ); - - processEngine.Verify( - p => - p.HandleEventsAndUpdateStorage( - It.IsAny(), - It.IsAny>(), - It.IsAny>() - ), - Times.Once - ); - processEngine.VerifyNoOtherCalls(); - } - - private static async Task ReadPrometheusMetricsToString() - { - return await PrometheusTestHelper.ReadPrometheusMetricsToString(); - } -} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task.verified.txt new file mode 100644 index 000000000..41a598c5b --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task.verified.txt @@ -0,0 +1,17 @@ +{ + Activities: [ + { + ActivityName: Process.Start, + IdFormat: W3C + } + ], + Metrics: [ + { + altinn_app_lib_processes_started: [ + { + Value: 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 5f3d14efc..14c91ba35 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -8,6 +8,7 @@ using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.UserAction; +using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Enums; @@ -102,7 +103,8 @@ public async Task StartProcess_starts_process_and_moves_to_first_task_without_ev [Fact] public async Task StartProcess_starts_process_and_moves_to_first_task() { - ProcessEngine processEngine = GetProcessEngine(); + TelemetrySink telemetrySink = new(); + ProcessEngine processEngine = GetProcessEngine(telemetrySink: telemetrySink); Instance instance = new Instance() { InstanceOwner = new InstanceOwner() { PartyId = "1337" } }; ClaimsPrincipal user = new( @@ -203,6 +205,8 @@ public async Task StartProcess_starts_process_and_moves_to_first_task() ); result.Success.Should().BeTrue(); + + await Verify(telemetrySink.GetSnapshot()); } [Fact] @@ -901,7 +905,8 @@ public async Task UpdateInstanceAndRerunEvents_sends_instance_and_events_to_even private ProcessEngine GetProcessEngine( Mock? processReaderMock = null, Instance? updatedInstance = null, - List? userActions = null + List? userActions = null, + TelemetrySink? telemetrySink = null ) { if (processReaderMock == null) @@ -982,7 +987,8 @@ private ProcessEngine GetProcessEngine( _processEventHandlingDelegatorMock.Object, _processEventDispatcherMock.Object, _processTaskCleanerMock.Object, - new UserActionService(userActions ?? []) + new UserActionService(userActions ?? []), + telemetrySink?.Object ); } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.IsStartEvent_returns_true_when_element_is_StartEvent.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.IsStartEvent_returns_true_when_element_is_StartEvent.verified.txt new file mode 100644 index 000000000..80e1c7ae7 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.IsStartEvent_returns_true_when_element_is_StartEvent.verified.txt @@ -0,0 +1,17 @@ +{ + Activities: [ + { + ActivityName: ProcessReader.GetStartEvents, + IdFormat: W3C + }, + { + ActivityName: ProcessReader.GetStartEventIds, + IdFormat: W3C + }, + { + ActivityName: ProcessReader.IsStartEvent, + IdFormat: W3C + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs index 73e3e8758..7e7f5b630 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs @@ -1,8 +1,10 @@ +using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Internal.Process.Elements.Base; using Altinn.App.Core.Tests.Internal.Process.TestUtils; +using Altinn.App.Core.Tests.Mocks; using FluentAssertions; using Xunit; @@ -22,10 +24,13 @@ public void TestBpmnRead() } [Fact] - public void IsStartEvent_returns_true_when_element_is_StartEvent() + public async Task IsStartEvent_returns_true_when_element_is_StartEvent() { - IProcessReader pr = ProcessTestUtils.SetupProcessReader("simple-gateway.bpmn"); + TelemetrySink telemetrySink = new(); + IProcessReader pr = ProcessTestUtils.SetupProcessReader("simple-gateway.bpmn", null, telemetrySink); pr.IsStartEvent("StartEvent").Should().BeTrue(); + + await Verify(telemetrySink.GetSnapshot()); } [Fact] diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs b/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs index fbb2a1f42..2dd3b69df 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs @@ -1,4 +1,6 @@ +using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Tests.Mocks; using Moq; namespace Altinn.App.Core.Tests.Internal.Process.TestUtils; @@ -7,7 +9,11 @@ internal static class ProcessTestUtils { private static readonly string TestDataPath = Path.Combine("Internal", "Process", "TestData"); - internal static ProcessReader SetupProcessReader(string bpmnfile, string? testDataPath = null) + internal static ProcessReader SetupProcessReader( + string bpmnfile, + string? testDataPath = null, + TelemetrySink? telemetrySink = null + ) { if (testDataPath == null) { @@ -17,6 +23,6 @@ internal static ProcessReader SetupProcessReader(string bpmnfile, string? testDa Mock processServiceMock = new Mock(); var s = new FileStream(Path.Combine(testDataPath, bpmnfile), FileMode.Open, FileAccess.Read); processServiceMock.Setup(p => p.GetProcessDefinition()).Returns(s); - return new ProcessReader(processServiceMock.Object); + return new ProcessReader(processServiceMock.Object, telemetrySink?.Object); } } diff --git a/test/Altinn.App.Core.Tests/Mocks/TelemetrySink.cs b/test/Altinn.App.Core.Tests/Mocks/TelemetrySink.cs new file mode 100644 index 000000000..94b87b905 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Mocks/TelemetrySink.cs @@ -0,0 +1,188 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; +using Altinn.App.Core.Models; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using static Altinn.App.Core.Tests.Mocks.TelemetrySink; + +namespace Altinn.App.Core.Tests.Mocks; + +internal sealed record TelemetrySink : IDisposable +{ + internal bool IsDisposed { get; private set; } + + internal static ConcurrentDictionary Scopes { get; } = []; + + internal Telemetry Object { get; } + + internal ActivityListener ActivityListener { get; } + + internal MeterListener MeterListener { get; } + + private readonly ConcurrentBag _activities = []; + private readonly ConcurrentDictionary> _metricValues = []; + + internal readonly record struct MetricMeasurement(long Value, IReadOnlyDictionary Tags); + + internal IEnumerable CapturedActivities => _activities; + + internal IReadOnlyDictionary> CapturedMetrics => _metricValues; + + internal TelemetrySnapshot GetSnapshot() => new(CapturedActivities, CapturedMetrics); + + internal TelemetrySink(string org = "ttd", string name = "test", string version = "v1") + { + var appId = new AppIdentifier(org, name); + var options = new AppSettings { AppVersion = version, }; + + Object = new Telemetry(appId, Options.Create(options)); + + ActivityListener = new ActivityListener() + { + ShouldListenTo = (activitySource) => + { + var sameSource = ReferenceEquals(activitySource, Object.ActivitySource); + return sameSource; + }, + Sample = (ref ActivityCreationOptions options) => + ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = activity => + { + _activities.Add(activity); + }, + }; + ActivitySource.AddActivityListener(ActivityListener); + + MeterListener = new MeterListener() + { + InstrumentPublished = (instrument, listener) => + { + var sameSource = ReferenceEquals(instrument.Meter, Object.Meter); + if (!sameSource) + { + return; + } + + _metricValues.TryAdd(instrument.Name, new List()); + listener.EnableMeasurementEvents(instrument, this); + }, + }; + MeterListener.SetMeasurementEventCallback( + static (instrument, measurement, tagSpan, state) => + { + Debug.Assert(state is not null); + var self = (TelemetrySink)state!; + Debug.Assert(self._metricValues[instrument.Name] is List); + var measurements = (List)self._metricValues[instrument.Name]; + var tags = new Dictionary(tagSpan.Length); + for (int i = 0; i < tagSpan.Length; i++) + { + tags.Add(tagSpan[i].Key, tagSpan[i].Value); + } + + foreach (var t in instrument.Tags ?? []) + { + tags.Add(t.Key, t.Value); + } + + measurements.Add(new(measurement, tags)); + } + ); + MeterListener.Start(); + } + + public void Dispose() + { + ActivityListener.Dispose(); + MeterListener.Dispose(); + Object.Dispose(); + IsDisposed = true; + + foreach (var (scope, _) in Scopes) + { + scope.IsDisposed = true; + } + } + + internal sealed class Scope : IDisposable + { + public bool IsDisposed { get; internal set; } + + public void Dispose() => Scopes.TryRemove(this, out _).Should().BeTrue(); + } + + internal static Scope CreateScope() + { + var scope = new Scope(); + Scopes.TryAdd(scope, default).Should().BeTrue(); + return scope; + } +} + +internal class TelemetrySnapshot( + IEnumerable? activities, + IReadOnlyDictionary>? metrics +) +{ + // Properties must be public to be accessible for Verify.Xunit + public readonly IEnumerable? Activities = activities?.Select(a => new + { + ActivityName = a.DisplayName, + Tags = a.TagObjects.Select(tag => new KeyValuePair(tag.Key, tag.Value?.ToString())), + a.IdFormat, + a.Status, + a.Events, + a.Kind + }); + public readonly IEnumerable>>? Metrics = metrics + ?.Select(m => new KeyValuePair>(m.Key, m.Value)) + .Where(x => x.Value.Count != 0); +} + +internal static class TelemetryDI +{ + internal static IServiceCollection AddTelemetrySink(this IServiceCollection services) + { + services.AddSingleton(_ => new TelemetrySink()); + services.AddSingleton(sp => sp.GetRequiredService().Object); + return services; + } +} + +public class TelemetryDITests +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TelemetryFake_Is_Disposed(bool materialize) + { + using var scope = TelemetrySink.CreateScope(); + scope.IsDisposed.Should().BeFalse(); + + var services = new ServiceCollection(); + services.AddTelemetrySink(); + var sp = services.BuildServiceProvider( + new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true, } + ); + + if (materialize) + { + var fake = sp.GetRequiredService(); + fake.IsDisposed.Should().BeFalse(); + scope.IsDisposed.Should().BeFalse(); + sp.Dispose(); + fake.IsDisposed.Should().BeTrue(); + scope.IsDisposed.Should().BeTrue(); + } + else + { + scope.IsDisposed.Should().BeFalse(); + sp.Dispose(); + scope.IsDisposed.Should().BeFalse(); + } + } +} diff --git a/test/Altinn.App.Core.Tests/TestHelpers/PrometheusTestHelper.cs b/test/Altinn.App.Core.Tests/TestHelpers/PrometheusTestHelper.cs deleted file mode 100644 index 3356ce6ec..000000000 --- a/test/Altinn.App.Core.Tests/TestHelpers/PrometheusTestHelper.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable disable -using Prometheus; - -namespace Altinn.App.Core.Tests.TestHelpers; - -public class PrometheusTestHelper -{ - public static async Task ReadPrometheusMetricsToString() - { - MemoryStream memoryStream = new MemoryStream(); - await Metrics.DefaultRegistry.CollectAndExportAsTextAsync(memoryStream); - using StreamReader reader = new StreamReader(memoryStream); - memoryStream.Position = 0; - return reader.ReadToEnd(); - } -} From 71c56ca716ae119c2baac6c80a0a4607efecb3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Fri, 24 May 2024 16:41:47 +0200 Subject: [PATCH 15/22] Payment using Nets Easy. (#624) Adds a new type of process task for adding payment to apps. For now, using Nets Easy as payment processor. --- .../Controllers/ActionsController.cs | 9 +- .../Controllers/PaymentController.cs | 115 ++++ .../Models/UserActionResponse.cs | 6 + .../Extensions/ServiceCollectionExtensions.cs | 20 + .../Features/Action/PaymentUserAction.cs | 90 +++ .../Payment/Exceptions/PaymentException.cs | 15 + .../Payment/IOrderDetailsCalculator.cs | 21 + .../Features/Payment/Models/Address.cs | 37 ++ .../Features/Payment/Models/CardDetails.cs | 17 + .../Features/Payment/Models/InvoiceDetails.cs | 12 + .../Features/Payment/Models/OrderDetails.cs | 52 ++ .../Features/Payment/Models/Payer.cs | 27 + .../Features/Payment/Models/PayerCompany.cs | 22 + .../Payment/Models/PayerPrivatePerson.cs | 27 + .../Features/Payment/Models/PayerType.cs | 20 + .../Features/Payment/Models/PaymentDetails.cs | 52 ++ .../Payment/Models/PaymentInformation.cs | 28 + .../Payment/Models/PaymentOrderLine.cs | 45 ++ .../Payment/Models/PaymentReceiver.cs | 37 ++ .../Features/Payment/Models/PaymentStatus.cs | 41 ++ .../Features/Payment/Models/PhoneNumber.cs | 17 + .../Payment/Processors/IPaymentProcessor.cs | 35 ++ .../Payment/Processors/Nets/INetsClient.cs | 31 + .../Processors/Nets/Models/HttpApiResult.cs | 64 ++ .../Processors/Nets/Models/NetsCheckout.cs | 196 ++++++ .../Nets/Models/NetsCreatePayment.cs | 57 ++ .../Nets/Models/NetsCreatePaymentSuccess.cs | 17 + .../Nets/Models/NetsNotifications.cs | 36 ++ .../Processors/Nets/Models/NetsOrder.cs | 32 + .../Processors/Nets/Models/NetsOrderItem.cs | 60 ++ .../Processors/Nets/Models/NetsPaymentFull.cs | 141 +++++ .../Nets/Models/NetsPaymentMethod.cs | 16 + .../Models/NetsPaymentMethodConfiguration.cs | 22 + .../Payment/Processors/Nets/NetsClient.cs | 63 ++ .../Payment/Processors/Nets/NetsMapper.cs | 128 ++++ .../Processors/Nets/NetsPaymentProcessor.cs | 186 ++++++ .../Processors/Nets/NetsPaymentSettings.cs | 32 + .../Payment/Services/IPaymentService.cs | 40 ++ .../Payment/Services/PaymentService.cs | 288 +++++++++ .../Expressions/ExpressionEvaluator.cs | 8 +- .../AltinnPaymentConfiguration.cs | 16 + .../AltinnTaskExtension.cs | 6 + .../Process/ExpressionsExclusiveGateway.cs | 7 +- .../Internal/Process/ProcessEngine.cs | 4 +- .../ProcessTasks/PaymentProcessTask.cs | 100 ++++ .../Models/UserAction/UserActionContext.cs | 10 +- .../Models/UserAction/UserActionResult.cs | 6 +- .../Mocks/DataClientMock.cs | 1 + .../Altinn.App.Api.Tests/OpenApi/swagger.json | 541 +++++++++++++++++ .../Altinn.App.Api.Tests/OpenApi/swagger.yaml | 365 +++++++++++ .../Features/Action/PaymentUserActionTests.cs | 172 ++++++ .../Action/TestData/payment-task-process.bpmn | 49 ++ .../Features/Payment/PaymentServiceTests.cs | 566 ++++++++++++++++++ .../Providers/Nets/HttpApiResultTests.cs | 73 +++ .../Payment/Providers/Nets/NetsMapperTests.cs | 206 +++++++ .../Nets/NetsPaymentProcessorTests.cs | 270 +++++++++ .../ProcessTask/StartTaskEventHandlerTests.cs | 2 +- .../Internal/Process/ProcessEngineTest.cs | 4 +- .../ProcessTasks/PaymentProcessTaskTests.cs | 251 ++++++++ 59 files changed, 4792 insertions(+), 19 deletions(-) create mode 100644 src/Altinn.App.Api/Controllers/PaymentController.cs create mode 100644 src/Altinn.App.Core/Features/Action/PaymentUserAction.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Exceptions/PaymentException.cs create mode 100644 src/Altinn.App.Core/Features/Payment/IOrderDetailsCalculator.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/Address.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/CardDetails.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/InvoiceDetails.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/OrderDetails.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/Payer.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/PayerCompany.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/PayerPrivatePerson.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/PayerType.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/PaymentDetails.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/PaymentInformation.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/PaymentOrderLine.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/PaymentReceiver.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/PaymentStatus.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Models/PhoneNumber.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/IPaymentProcessor.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/INetsClient.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/HttpApiResult.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCheckout.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePayment.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePaymentSuccess.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsNotifications.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrder.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrderItem.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentFull.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethod.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethodConfiguration.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsClient.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsMapper.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentSettings.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs create mode 100644 src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Action/TestData/payment-task-process.bpmn create mode 100644 test/Altinn.App.Core.Tests/Features/Payment/PaymentServiceTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/HttpApiResultTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsPaymentProcessorTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs diff --git a/src/Altinn.App.Api/Controllers/ActionsController.cs b/src/Altinn.App.Api/Controllers/ActionsController.cs index d4aad444c..c0a94906f 100644 --- a/src/Altinn.App.Api/Controllers/ActionsController.cs +++ b/src/Altinn.App.Api/Controllers/ActionsController.cs @@ -131,7 +131,7 @@ public async Task> Perform( } UserActionContext userActionContext = - new(instance, userId.Value, actionRequest.ButtonId, actionRequest.Metadata); + new(instance, userId.Value, actionRequest.ButtonId, actionRequest.Metadata, language); IUserAction? actionHandler = _userActionService.GetActionHandler(action); if (actionHandler == null) { @@ -148,12 +148,8 @@ public async Task> Perform( } UserActionResult result = await actionHandler.HandleAction(userActionContext); - if (result.ResultType == ResultType.Redirect) - { - return new RedirectResult(result.RedirectUrl ?? throw new ProcessException("Redirect URL missing")); - } - if (result.ResultType != ResultType.Success) + if (result.ResultType == ResultType.Failure) { return StatusCode( statusCode: result.ErrorType switch @@ -183,6 +179,7 @@ public async Task> Perform( actionRequest.IgnoredValidators, language ), + RedirectUrl = result.RedirectUrl, } ); } diff --git a/src/Altinn.App.Api/Controllers/PaymentController.cs b/src/Altinn.App.Api/Controllers/PaymentController.cs new file mode 100644 index 000000000..4167847d2 --- /dev/null +++ b/src/Altinn.App.Api/Controllers/PaymentController.cs @@ -0,0 +1,115 @@ +using Altinn.App.Api.Infrastructure.Filters; +using Altinn.App.Core.Features.Payment; +using Altinn.App.Core.Features.Payment.Exceptions; +using Altinn.App.Core.Features.Payment.Models; +using Altinn.App.Core.Features.Payment.Services; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Altinn.App.Api.Controllers; + +/// +/// Controller for handling payment operations. +/// +[AutoValidateAntiforgeryTokenIfAuthCookie] +[ApiController] +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/payment")] +public class PaymentController : ControllerBase +{ + private readonly IInstanceClient _instanceClient; + private readonly IProcessReader _processReader; + private readonly IPaymentService _paymentService; + private readonly IOrderDetailsCalculator? _orderDetailsCalculator; + + /// + /// Initializes a new instance of the class. + /// + public PaymentController( + IInstanceClient instanceClient, + IProcessReader processReader, + IPaymentService paymentService, + IOrderDetailsCalculator? orderDetailsCalculator = null + ) + { + _instanceClient = instanceClient; + _processReader = processReader; + _paymentService = paymentService; + _orderDetailsCalculator = orderDetailsCalculator; + } + + /// + /// Get updated payment information for the instance. Will contact the payment processor to check the status of the payment. Current task must be a payment task. See payment related documentation. + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that this the owner of the instance + /// unique id to identify the instance + /// The currently used language by the user (or null if not available) + /// An object containing updated payment information + [HttpGet] + [ProducesResponseType(typeof(PaymentInformation), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetPaymentInformation( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromQuery] string? language = null + ) + { + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + AltinnPaymentConfiguration? paymentConfiguration = _processReader + .GetAltinnTaskExtension(instance.Process.CurrentTask.ElementId) + ?.PaymentConfiguration; + + if (paymentConfiguration == null) + { + throw new PaymentException("Payment configuration not found in AltinnTaskExtension"); + } + + PaymentInformation paymentInformation = await _paymentService.CheckAndStorePaymentStatus( + instance, + paymentConfiguration, + language + ); + + return Ok(paymentInformation); + } + + /// + /// Run order details calculations and return the result. Does not require the current task to be a payment task. + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that this the owner of the instance + /// unique id to identify the instance + /// The currently used language by the user (or null if not available) + /// An object containing updated payment information + [HttpGet("order-details")] + [ProducesResponseType(typeof(OrderDetails), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetOrderDetails( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromQuery] string? language = null + ) + { + if (_orderDetailsCalculator == null) + { + throw new PaymentException( + "You must add an implementation of the IOrderDetailsCalculator interface to the DI container. See payment related documentation." + ); + } + + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + OrderDetails orderDetails = await _orderDetailsCalculator.CalculateOrderDetails(instance, language); + + return Ok(orderDetails); + } +} diff --git a/src/Altinn.App.Api/Models/UserActionResponse.cs b/src/Altinn.App.Api/Models/UserActionResponse.cs index deef1074a..2eb23150c 100644 --- a/src/Altinn.App.Api/Models/UserActionResponse.cs +++ b/src/Altinn.App.Api/Models/UserActionResponse.cs @@ -33,4 +33,10 @@ public class UserActionResponse /// [JsonPropertyName("error")] public ActionError? Error { get; set; } + + /// + /// If the action requires the client to redirect to another url, this property should be set + /// + [JsonPropertyName("redirectUrl")] + public Uri? RedirectUrl { get; set; } } diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 9fd678b58..d2bca1616 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -8,6 +8,9 @@ using Altinn.App.Core.Features.Notifications.Sms; using Altinn.App.Core.Features.Options; using Altinn.App.Core.Features.PageOrder; +using Altinn.App.Core.Features.Payment.Processors; +using Altinn.App.Core.Features.Payment.Processors.Nets; +using Altinn.App.Core.Features.Payment.Services; using Altinn.App.Core.Features.Pdf; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Features.Validation.Default; @@ -177,6 +180,7 @@ IWebHostEnvironment env AddAppOptions(services); AddActionServices(services); AddPdfServices(services); + AddNetsPaymentServices(services, configuration); AddSignatureServices(services); AddEventServices(services); AddNotificationServices(services); @@ -261,6 +265,22 @@ private static void AddPdfServices(IServiceCollection services) #pragma warning restore CS0618 // Type or member is obsolete } + private static void AddNetsPaymentServices(this IServiceCollection services, IConfiguration configuration) + { + IConfigurationSection configurationSection = configuration.GetSection("NetsPaymentSettings"); + + if (configurationSection.Exists()) + { + services.Configure(configurationSection); + services.AddHttpClient(); + services.AddTransient(); + } + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + private static void AddSignatureServices(IServiceCollection services) { services.AddHttpClient(); diff --git a/src/Altinn.App.Core/Features/Action/PaymentUserAction.cs b/src/Altinn.App.Core/Features/Action/PaymentUserAction.cs new file mode 100644 index 000000000..3e5100567 --- /dev/null +++ b/src/Altinn.App.Core/Features/Action/PaymentUserAction.cs @@ -0,0 +1,90 @@ +using Altinn.App.Core.Features.Payment.Models; +using Altinn.App.Core.Features.Payment.Services; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Models.Process; +using Altinn.App.Core.Models.UserAction; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Features.Action +{ + /// + /// User action for payment + /// + internal class PaymentUserAction : IUserAction + { + private readonly IProcessReader _processReader; + private readonly ILogger _logger; + private readonly IPaymentService _paymentService; + + /// + /// Initializes a new instance of the class + /// + public PaymentUserAction( + IProcessReader processReader, + IPaymentService paymentService, + ILogger logger + ) + { + _processReader = processReader; + _paymentService = paymentService; + _logger = logger; + } + + /// + public string Id => "pay"; + + /// + public async Task HandleAction(UserActionContext context) + { + if ( + _processReader.GetFlowElement(context.Instance.Process.CurrentTask.ElementId) + is not ProcessTask currentTask + ) + { + return UserActionResult.FailureResult( + new ActionError() { Code = "NoProcessTask", Message = "Current task is not a process task." } + ); + } + + _logger.LogInformation( + "Payment action handler invoked for instance {Id}. In task: {CurrentTaskId}", + context.Instance.Id, + currentTask.Id + ); + + AltinnPaymentConfiguration? paymentConfiguration = currentTask + .ExtensionElements + ?.TaskExtension + ?.PaymentConfiguration; + if (paymentConfiguration == null) + { + throw new ApplicationConfigException( + "PaymentConfig is missing in the payment process task configuration." + ); + } + + (PaymentInformation paymentInformation, bool alreadyPaid) = await _paymentService.StartPayment( + context.Instance, + paymentConfiguration, + context.Language + ); + + if (alreadyPaid) + { + return UserActionResult.FailureResult( + error: new ActionError { Code = "PaymentAlreadyCompleted", Message = "Payment already completed." }, + errorType: ProcessErrorType.Conflict + ); + } + + string? paymentDetailsRedirectUrl = paymentInformation.PaymentDetails?.RedirectUrl; + + return paymentDetailsRedirectUrl == null + ? UserActionResult.SuccessResult() + : UserActionResult.RedirectResult(new Uri(paymentDetailsRedirectUrl)); + } + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Exceptions/PaymentException.cs b/src/Altinn.App.Core/Features/Payment/Exceptions/PaymentException.cs new file mode 100644 index 000000000..2f383f11a --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Exceptions/PaymentException.cs @@ -0,0 +1,15 @@ +namespace Altinn.App.Core.Features.Payment.Exceptions +{ + /// + /// Represents an exception that is thrown when an error occurs during payment processing. + /// + public class PaymentException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// + public PaymentException(string message) + : base(message) { } + } +} diff --git a/src/Altinn.App.Core/Features/Payment/IOrderDetailsCalculator.cs b/src/Altinn.App.Core/Features/Payment/IOrderDetailsCalculator.cs new file mode 100644 index 000000000..c2343e4ee --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/IOrderDetailsCalculator.cs @@ -0,0 +1,21 @@ +namespace Altinn.App.Core.Features.Payment; + +using Altinn.Platform.Storage.Interface.Models; +using Models; + +/// +/// Interface that app developers need to implement in order to use the payment feature +/// +public interface IOrderDetailsCalculator +{ + /// + /// Method that calculates an order based on an instance. + /// + /// + /// The instance (and its data) needs to be fetched based on the if the calculation + /// depends on instance or data properties. + /// This method can be called multiple times for the same instance, in order to preview the price before payment starts. + /// + /// The Payment order that contains information about the requested payment + Task CalculateOrderDetails(Instance instance, string? language); +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/Address.cs b/src/Altinn.App.Core/Features/Payment/Models/Address.cs new file mode 100644 index 000000000..efc672a2c --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/Address.cs @@ -0,0 +1,37 @@ +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// Represents an address. +/// +public class Address +{ + /// + /// The name associated with the address. + /// + public string? Name { get; set; } + + /// + /// The first line of the address. + /// + public string? AddressLine1 { get; set; } + + /// + /// The second line of the address. + /// + public string? AddressLine2 { get; set; } + + /// + /// The postal code of the address. + /// + public string? PostalCode { get; set; } + + /// + /// The city of the address. + /// + public string? City { get; set; } + + /// + /// The country of the address. + /// + public string? Country { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/CardDetails.cs b/src/Altinn.App.Core/Features/Payment/Models/CardDetails.cs new file mode 100644 index 000000000..1e8cb197c --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/CardDetails.cs @@ -0,0 +1,17 @@ +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// The details of a payment card. +/// +public class CardDetails +{ + /// + /// The masked PAN of the card. + /// + public string? MaskedPan { get; set; } + + /// + /// The expiry date of the card. + /// + public string? ExpiryDate { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/InvoiceDetails.cs b/src/Altinn.App.Core/Features/Payment/Models/InvoiceDetails.cs new file mode 100644 index 000000000..131717355 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/InvoiceDetails.cs @@ -0,0 +1,12 @@ +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// The details of an invoice. +/// +public class InvoiceDetails +{ + /// + /// The invoice number, if available. + /// + public string? InvoiceNumber { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/OrderDetails.cs b/src/Altinn.App.Core/Features/Payment/Models/OrderDetails.cs new file mode 100644 index 000000000..147b6fae7 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/OrderDetails.cs @@ -0,0 +1,52 @@ +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// Wrapping class to represent a payment order +/// +public class OrderDetails +{ + /// + /// The ID of the payment provider that should handle the payment + /// + public required string PaymentProcessorId { get; set; } + + /// + /// The party that will receive the payment. Used in the receipt. + /// + public required PaymentReceiver Receiver { get; set; } + + /// + /// Monetary unit of the prices in the order. + /// + public required string Currency { get; set; } + + /// + /// The lines that make up the order + /// + public required List OrderLines { get; set; } + + /// + /// Used to tell payment processor if the payer should be a person, company or any of the two. How this is used/respected can vary between payment processors. + /// + public PayerType[]? AllowedPayerTypes { get; set; } + + /// + /// Optional reference to the order. Could be used by other systems to identify the order. + /// + public string? OrderReference { get; set; } + + /// + /// Sum of all order line prices excluding VAT + /// + public decimal TotalPriceExVat => OrderLines.Sum(x => x.PriceExVat * x.Quantity); + + /// + /// Sum of all order line VAT + /// + public decimal TotalVat => OrderLines.Sum(x => x.PriceExVat * x.Quantity * x.VatPercent / 100M); + + /// + /// Total order price including VAT + /// + public decimal TotalPriceIncVat => OrderLines.Sum(l => l.PriceExVat * l.Quantity * (1 + l.VatPercent / 100M)); +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/Payer.cs b/src/Altinn.App.Core/Features/Payment/Models/Payer.cs new file mode 100644 index 000000000..299ca5084 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/Payer.cs @@ -0,0 +1,27 @@ +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// Represents the person or company making the payment. +/// +public class Payer +{ + /// + /// If the payer is a private person, this property should be set. + /// + public PayerPrivatePerson? PrivatePerson { get; set; } + + /// + /// If the payer is a company, this property should be set. + /// + public PayerCompany? Company { get; set; } + + /// + /// The shipping address of the payer. + /// + public Address? ShippingAddress { get; set; } + + /// + /// The billing address of the payer. + /// + public Address? BillingAddress { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/PayerCompany.cs b/src/Altinn.App.Core/Features/Payment/Models/PayerCompany.cs new file mode 100644 index 000000000..7f7762100 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/PayerCompany.cs @@ -0,0 +1,22 @@ +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// Company making the payment. +/// +public class PayerCompany +{ + /// + /// The organisation number of the company. + /// + public string? OrganisationNumber { get; set; } + + /// + /// The name of the company. + /// + public string? Name { get; set; } + + /// + /// The contact person of the company. + /// + public PayerPrivatePerson? ContactPerson { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/PayerPrivatePerson.cs b/src/Altinn.App.Core/Features/Payment/Models/PayerPrivatePerson.cs new file mode 100644 index 000000000..8d2f6a8bc --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/PayerPrivatePerson.cs @@ -0,0 +1,27 @@ +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// Private person making the payment. +/// +public class PayerPrivatePerson +{ + /// + /// The first name of the person. + /// + public string? FirstName { get; set; } + + /// + /// The last name of the person. + /// + public string? LastName { get; set; } + + /// + /// The email address of the person. + /// + public string? Email { get; set; } + + /// + /// The phone number of the person. + /// + public PhoneNumber? PhoneNumber { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/PayerType.cs b/src/Altinn.App.Core/Features/Payment/Models/PayerType.cs new file mode 100644 index 000000000..f7212f3ce --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/PayerType.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// Represents the type of payer. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum PayerType +{ + /// + /// The payer is a person. + /// + Person, + + /// + /// The payer is a company. + /// + Company +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/PaymentDetails.cs b/src/Altinn.App.Core/Features/Payment/Models/PaymentDetails.cs new file mode 100644 index 000000000..41dff85dd --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/PaymentDetails.cs @@ -0,0 +1,52 @@ +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// Represents the details of a payment +/// +public class PaymentDetails +{ + /// + /// The payment reference for the transaction. + /// + public required string PaymentId { get; set; } + + /// + /// The redirect URL for the payment. Used to redirect the user to payment processors GUI. + /// + public string? RedirectUrl { get; set; } + + /// + /// Person/Company making the payment + /// + public Payer? Payer { get; set; } + + /// + /// Type of payment. Typically 'CARD' or 'INVOICE'. Up to payment processor to define. + /// + public string? PaymentType { get; set; } + + /// + /// The payment method, for example Visa or Mastercard. Up to payment processor to define. + /// + public string? PaymentMethod { get; set; } + + /// + /// The time and date the payment was created. + /// + public string? CreatedDate { get; set; } + + /// + /// The time and date the payment was charged. + /// + public string? ChargedDate { get; set; } + + /// + /// If invoice was used, this will contain the invoice number. + /// + public InvoiceDetails? InvoiceDetails { get; set; } + + /// + /// If card was used, this will contain the card details. + /// + public CardDetails? CardDetails { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/PaymentInformation.cs b/src/Altinn.App.Core/Features/Payment/Models/PaymentInformation.cs new file mode 100644 index 000000000..35024d503 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/PaymentInformation.cs @@ -0,0 +1,28 @@ +namespace Altinn.App.Core.Features.Payment.Models +{ + /// + /// The payment information for a transaction. + /// + public class PaymentInformation + { + /// + /// The taskId of the payment task this payment information is associated with. + /// + public required string TaskId { get; set; } + + /// + /// The status of the payment. + /// + public required PaymentStatus Status { get; set; } + + /// + /// The order details for the transaction. + /// + public required OrderDetails OrderDetails { get; set; } + + /// + /// Contains details about the payment, set by the payment processor implementation. + /// + public PaymentDetails? PaymentDetails { get; set; } + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/PaymentOrderLine.cs b/src/Altinn.App.Core/Features/Payment/Models/PaymentOrderLine.cs new file mode 100644 index 000000000..2e6d9d5a8 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/PaymentOrderLine.cs @@ -0,0 +1,45 @@ +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// Description of a order line in a payment. +/// +public class PaymentOrderLine +{ + /// + /// Id of the item in the order. Could be used by other systems to identify the item + /// + public required string Id { get; set; } + + /// + /// Item name to use when we don't know the language of the user + /// + public required string Name { get; set; } + + /// + /// Optional TextResourceKey to use instead of name when translating the item name + /// + public string? TextResourceKey { get; set; } + + /// + /// Price excluding MVA + /// + public required decimal PriceExVat { get; set; } + + /// + /// Quantity of this item (defaults to 1) + /// + public int Quantity { get; set; } = 1; + + /// + /// Value added tax percent. Defaults to 0 (no VAT). Gets automatically added to the price. + /// + /// + /// The value added tax percent is a decimal number. 25% VAT is represented as 25.00M. + /// + public required decimal VatPercent { get; set; } + + /// + /// The unit of the unit price, for example pcs, liters, or kg. + /// + public string Unit { get; set; } = "pcs"; +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/PaymentReceiver.cs b/src/Altinn.App.Core/Features/Payment/Models/PaymentReceiver.cs new file mode 100644 index 000000000..e271f7ff8 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/PaymentReceiver.cs @@ -0,0 +1,37 @@ +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// The receiver of the payment. Used in receipt. +/// +public class PaymentReceiver +{ + /// + /// The organisation number of the receiver. + /// + public string? OrganisationNumber { get; set; } + + /// + /// The name of the receiver. + /// + public string? Name { get; set; } + + /// + /// The postal address of the receiver. + /// + public Address? PostalAddress { get; set; } + + /// + /// The bank account number of the receiver. + /// + public string? BankAccountNumber { get; set; } + + /// + /// The email address of the receiver. + /// + public string? Email { get; set; } + + /// + /// The phone number of the receiver. + /// + public PhoneNumber? PhoneNumber { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/PaymentStatus.cs b/src/Altinn.App.Core/Features/Payment/Models/PaymentStatus.cs new file mode 100644 index 000000000..d537c42cd --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/PaymentStatus.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Features.Payment.Models +{ + /// + /// The status of a payment. + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum PaymentStatus + { + /// + /// The payment is not initialized. We have not contacted the payment processor yet. + /// + Uninitialized, + + /// + /// The payment request has been created and sent to payment processor. + /// + Created, + + /// + /// The payment has been paid. + /// + Paid, + + /// + /// Something went wrong and the payment is considered failed. + /// + Failed, + + /// + /// The payment has been cancelled. + /// + Cancelled, + + /// + /// The payment was skipped, likely because the sum of the order was zero. + /// + Skipped, + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Models/PhoneNumber.cs b/src/Altinn.App.Core/Features/Payment/Models/PhoneNumber.cs new file mode 100644 index 000000000..8d0775d71 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Models/PhoneNumber.cs @@ -0,0 +1,17 @@ +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// Represents a phone number. +/// +public class PhoneNumber +{ + /// + /// Gets or sets the country code for the phone number. + /// + public string? Prefix { get; set; } + + /// + /// Gets or sets the phone number (without the country code prefix). + /// + public string? Number { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/IPaymentProcessor.cs b/src/Altinn.App.Core/Features/Payment/Processors/IPaymentProcessor.cs new file mode 100644 index 000000000..64bb0df93 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/IPaymentProcessor.cs @@ -0,0 +1,35 @@ +using Altinn.App.Core.Features.Payment.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Payment.Processors; + +/// +/// Represents a payment processor that handles payment-related operations. +/// +public interface IPaymentProcessor +{ + /// + /// Internal ID for the payment processor. + /// + public string PaymentProcessorId { get; } + + /// + /// Starts a payment process for the specified instance and order details. + /// + public Task StartPayment(Instance instance, OrderDetails orderDetails, string? language); + + /// + /// Terminate a payment for the specified instance and payment reference. + /// + public Task TerminatePayment(Instance instance, PaymentInformation paymentInformation); + + /// + /// Gets the payment status for the specified instance and payment reference. + /// + public Task<(PaymentStatus status, PaymentDetails paymentDetails)> GetPaymentStatus( + Instance instance, + string paymentId, + decimal expectedTotalIncVat, + string? language + ); +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/INetsClient.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/INetsClient.cs new file mode 100644 index 000000000..ec92fbd12 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/INetsClient.cs @@ -0,0 +1,31 @@ +using Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +namespace Altinn.App.Core.Features.Payment.Processors.Nets +{ + /// + /// Represents a client for interacting with the Nets payment provider. + /// + internal interface INetsClient + { + /// + /// Creates a payment using the Nets payment provider. + /// + /// The payment details. + /// + Task> CreatePayment(NetsCreatePayment payment); + + /// + /// Retrieve existing payment. + /// + /// + /// + Task> RetrievePayment(string paymentId); + + /// + /// Terminate a payment that has not been captured. + /// + /// + /// + Task TerminatePayment(string paymentId); + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/HttpApiResult.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/HttpApiResult.cs new file mode 100644 index 000000000..69e5b9c56 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/HttpApiResult.cs @@ -0,0 +1,64 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http.Json; +using System.Reflection; +using System.Text.Json; + +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +internal class HttpApiResult +{ + // ReSharper disable once StaticMemberInGenericType + private static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; + + public HttpApiResult() { } + + public HttpApiResult(T? result, HttpStatusCode status, string? rawError) + { + Result = result; + Status = status; + RawError = rawError; + } + + [MemberNotNullWhen(true, nameof(Result))] + public bool IsSuccess => Result is not null; + public T? Result { get; init; } + public HttpStatusCode Status { get; set; } + public string? RawError { get; init; } + + public static async Task> FromHttpResponse(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + if (response.StatusCode == HttpStatusCode.NoContent) + { + return new HttpApiResult { Status = response.StatusCode, Result = default, }; + } + + try + { + return new HttpApiResult + { + Status = response.StatusCode, + Result = + await response.Content.ReadFromJsonAsync(JSON_OPTIONS) + ?? throw new JsonException("Could not deserialize response"), + }; + } + catch (JsonException e) + { + return new HttpApiResult() { Status = response.StatusCode, RawError = e.Message, }; + } + } + + return new HttpApiResult + { + Status = response.StatusCode, + RawError = await response.Content.ReadAsStringAsync(), + }; + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCheckout.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCheckout.cs new file mode 100644 index 000000000..35c3b2a87 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCheckout.cs @@ -0,0 +1,196 @@ +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +/// +/// Defines the behavior and style of the checkout page. +/// +internal class NetsCheckout +{ + /// + /// Specifies where the checkout will be loaded if using an embedded checkout page. See also the integrationType property. + /// Length: 0-256 + /// Allowed: & + /// The following special characters are not supported: <,>,\,’,”,&,\\ + /// Additional notes: HTTPS is not required + /// + public string? Url { get; set; } + + /// + /// Determines whether the checkout should be embedded in your webshop or if the checkout + /// should be hosted by Nexi Group on a separate page. + /// Valid values are: + /// 'EmbeddedCheckout' (default) or 'HostedPaymentPage'. + /// Please note that the string values are case sensitive. If set to 'HostedPaymentPage', + /// your website should redirect the customer to the hostedPaymentPageUrl provided in the + /// response body. Using a hosted checkout page requires that you specify the returnUrl + /// property. If set to 'EmbeddedCheckout', the checkout page will be embedded within an + /// iframe on your website using the Checkout JS SDK. Using an embedded checkout page requires + /// that you also specify the url property. + /// + /// Listed as optional in API doc, but seems strange to depend on default value + public required string IntegrationType { get; set; } + + /// + /// Specifies where your customer will return after a completed payment when using a hosted checkout page. See also the property. + /// + public string? ReturnUrl { get; set; } + + /// + /// Specifies where your customer will return after a canceled payment when using a hosted checkout page. See also the property. + /// + public string? CancelUrl { get; set; } + + /// + /// The URL to the terms and conditions of your webshop. + /// Whitelist: “[&]” => “” + /// The following special characters are not supported: <,>,\,’,”,&,\\ + /// Additional notes: HTTPS is not required + /// + public required string TermsUrl { get; set; } + + /// + /// The URL to the privacy and cookie settings of your webshop. + /// Whitelist: “[&]” => “” + /// The following special characters are not supported: <,>,\,’,”,&,\\ + /// Additional notes: HTTPS is not required + /// + public string? MerchantTermsUrl { get; set; } + + /// + /// An array of countries that limits the set of countries available for shipping. If left unspecified, all countries supported by Easy Checkout will be available for shipping on the checkout page. + /// + public List? ShippingCountries { get; set; } + + /// + /// Properties related to shipping of packets. + /// + public NetsShipping? Shipping { get; set; } + + /// + /// Configures which consumer types should be accepted. Defaults to 'B2C'.These options are ignore if the property merchantHandlesConsumerData is set to true. + /// + public NetsConsumerType? ConsumerType { get; set; } + + /// + /// If set to true, the transaction will be charged automatically after the reservation has been accepted. Default value is false if not specified. + /// + public bool? Charge { get; set; } + + /// + /// If set to true, the checkout will not load any user data, and also the checkout will not remember the current consumer on this device. Default value is false if not specified. + /// + public bool? PublicDevice { get; set; } + + /// + /// Allows you to initiate the checkout with customer data so that your customer only need to provide payment details. It is possible to exclude all consumer and company information from the payment (only for certain payment methods) when it is set to true. If you still want to add consumer information to the payment you need to use the consumer object (either a privatePerson or a company, not both). + /// + public bool? MerchantHandlesConsumerData { get; set; } + + /// + /// Defines the appearance of the checkout page. + /// + public NetsApparence? Appearance { get; set; } + + /// + /// Merchant's three-letter checkout country code (ISO 3166-1), for example GBR. See also the list of supported languages. + /// Important: For Klarna payments, the countryCode field is mandatory. If not provided, Klarna will not be available as a payment method. + /// Length: 3 + /// Pattern: [A-Z]{3} + /// The following special characters are not supported: <,>,\,’,”,&,\\ + /// + public string? CountryCode { get; set; } +} + +/// +/// Defines the appearance of the checkout page. +/// +internal class NetsApparence +{ + /// + /// Controls what is displayed on the checkout page. + /// + public NetsDisplayOptions? DisplayOptions { get; set; } + + /// + /// Controls what text is displayed on the checkout page. + /// + public NetsTextOptions? TextOptions { get; set; } + + /// + /// Controls what is displayed on the checkout page. + /// + public class NetsDisplayOptions + { + /// + /// If set to true, displays the merchant name above the checkout. Default value is true when using a HostedPaymentPage. + /// + public bool? ShowMerchantName { get; set; } + + /// + /// If set to true, displays the order summary above the checkout. Default value is true when using a HostedPaymentPage. + /// + public bool? ShowOrderSummary { get; set; } + } + + /// + /// Controls what text is displayed on the checkout page. + /// + internal class NetsTextOptions + { + /// + /// Overrides payment button text. The following predefined values are allowed: pay, purchase, order, book, reserve, signup, storecard, subscribe, accept. The payment button text is localized. + /// + public string? CompletePaymentButtonText { get; set; } + } +} + +/// +/// Configures which consumer types should be accepted. Defaults to 'B2C'.These options are ignore if the property merchantHandlesConsumerData is set to true. +/// +internal class NetsConsumerType +{ + /// + /// The checkout form defaults to this consumer type when first loaded. + /// + public string? Default { get; set; } + + /// + /// The array of consumer types that should be supported on the checkout page. Allowed values are: 'B2B' and 'B2C'. + /// + public List? SupportedTypes { get; set; } +} + +/// +/// Properties related to shipping of packets. +/// +internal class NetsShipping +{ + /// + /// Not documented in API doc why this is duplicated in the shipping object + /// + public List? Countries { get; set; } + + /// + /// If set to true, the payment order is required to be updated (using the Update order method) with shipping.costSpecified set to true before the customer can complete a purchase. Defaults to false if not specified. + /// + public bool MerchantHandlesShippingCost { get; set; } = false; + + /// + /// If set to true, the customer is provided an option to specify separate addresses for billing and shipping on the checkout page. If set to false, the billing address is used as the shipping address. + /// + public bool? EnableBillingAddress { get; set; } +} + +/// +/// An array of countries that limits the set of countries available for shipping. If left unspecified, all countries supported by Easy Checkout will be available for shipping on the checkout page. +/// +internal class NetsShippingCountry +{ + /// + /// A three-letter country code (ISO 3166-1), for example GBR. See also the list of supported countries. + /// Important: For Klarna payments, the countryCode field is mandatory. If not provided, Klarna will not be available as a payment method. + /// Length: 3 + /// Pattern: [A-Z]{3} + /// The following special characters are not supported: <,>,\,’,”,&,\\ + /// + public string? CountryCode { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePayment.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePayment.cs new file mode 100644 index 000000000..8185d231d --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePayment.cs @@ -0,0 +1,57 @@ +using Altinn.App.Core.Features.Payment.Models; + +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +/// +/// Initializes a new payment object that becomes the object used throughout the checkout flow for a particular customer and order. Creating a payment object is the first step when you intend to accept a payment from your customer. Entering the amount 100 corresponds to 1 unit of the currency entered, such as e.g. 1 NOK. Typically you provide the following information: +/// The order details including order items, total amount, and currency. +/// Checkout page settings, which specify what type of integration you want: a checkout page embedded on your site or a pre-built checkout page hosted by Nexi Group. You can also specify data about your customer so that your customer only needs to provide payment details on the checkout page. +/// +/// Optionally, you can also provide information regarding: +/// Notifications if you want to be notified through webhooks when the status of the payment changes. +/// Fees added when using payment methods such as invoice. +/// Charge set to true so you can enable autocapture for subscriptions. +/// +/// On success, this method returns a paymentId that can be used in subsequent requests to refer to the newly created payment object. Optionally, the response object will also contain a hostedPaymentPageUrl, which is the URL you should redirect to if using a hosted pre-built checkout page. +/// +internal class NetsCreatePayment +{ + /// + /// Specifies an order associated with a payment. An order must contain at least one order item. The amount of the order must match the sum of the specified order items. + /// + public required NetsOrder Order { get; set; } + + /// + /// Defines the behavior and style of the checkout page. + /// + public required NetsCheckout Checkout { get; set; } + + /// + /// The merchant number. Use this header only if you are a Nexi Group partner and initiating the checkout with your partner keys. If you are using the integration keys for your webshop, there is no need to specify this header. + /// Length: 0-128 + /// The following special characters are not supported: <,>,\,’,”,&,\\ + /// + public string? MerchantNumber { get; set; } + + /// + /// Notifications allow you to subscribe to status updates for a payment. + /// + public NetsNotifications? Notifications { get; set; } + + /// + /// Specifies payment methods configuration to be used for this payment, ignored if empty or null. + /// + public List? PaymentMethodsConfiguration { get; set; } + + /// + /// Optional (seems useless with current documentation) + /// + public List? PaymentMethods { get; set; } + + /// + /// Merchant payment reference + /// Length: 0-36 + /// The following special characters are not supported: <,>,\,’,”,&,\\ + /// + public required string MyReference { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePaymentSuccess.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePaymentSuccess.cs new file mode 100644 index 000000000..3b82fb08d --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePaymentSuccess.cs @@ -0,0 +1,17 @@ +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +/// +/// Successful response from /payments endpoint +/// +internal class NetsCreatePaymentSuccess +{ + /// + /// The identifier (UUID) of the newly created payment object. Use this identifier in subsequent request when referring to the new payment. + /// + public required string PaymentId { get; set; } + + /// + /// The URL your website should redirect to if using a hosted pre-built checkout page. + /// + public string? HostedPaymentPageUrl { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsNotifications.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsNotifications.cs new file mode 100644 index 000000000..4469d65fe --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsNotifications.cs @@ -0,0 +1,36 @@ +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +/// +/// Notifications allow you to subscribe to status updates for a payment. +/// +internal class NetsNotifications +{ + /// + /// The list of webhooks. The maximum number of webhooks is 32. + /// + public List? WebHooks { get; set; } +} + +internal class NetsWebHook +{ + /// + /// The name of the event you want to subscribe to. See webhooks for the complete list of events. + /// The following special characters are not supported: <,>,\,',”,&,\\ + /// + public string? EventName { get; set; } + + /// + /// The callback is sent to this URL. Must be HTTPS to ensure a secure communication. Maximum allowed length of the URL is 256 characters. + /// Whitelist: “[&]” => “” + /// The following special characters are not supported: <,>,\,',”,&,\\ + /// Additional notes: HTTPS is not required + /// + public string? Url { get; set; } + + /// + /// The credentials that will be sent in the HTTP Authorization request header of the callback. Must be between 8 and 32 characters long and contain alphanumeric characters. + /// Length: 8-64 + /// Pattern: @^[a-zA-Z0-9\-= ]*$ + /// + public string? Authorization { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrder.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrder.cs new file mode 100644 index 000000000..80a64bdd3 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrder.cs @@ -0,0 +1,32 @@ +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +/// +/// Specifies an order associated with a payment. An order must contain at least one order item. The amount of the order must match the sum of the specified order items. +/// +internal class NetsOrder +{ + /// + /// A list of order items. At least one item must be specified. + /// + public required List Items { get; set; } + + /// + /// The total amount of the order including VAT, if any. (Sum of all grossTotalAmounts in the order.) + /// Allowed: >0 + /// + public required int Amount { get; set; } + + /// + /// The currency of the payment, for example 'SEK'. + /// Length: 3 + /// The following special characters are not supported: <,>,\,’,”,&,\\ + /// + public required string Currency { get; set; } + + /// + /// A reference to recognize this order. Usually a number sequence (order number). + /// Length: 0-128 + /// The following special characters are not supported: <,>,\,’,”,&,\\ + /// + public string? Reference { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrderItem.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrderItem.cs new file mode 100644 index 000000000..36f9e692f --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrderItem.cs @@ -0,0 +1,60 @@ +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models +{ + internal class NetsOrderItem + { + /// + /// A reference to recognize the product, usually the SKU (stock keeping unit) of the product. For convenience in the case of refunds or modifications of placed orders, the reference should be unique for each variation of a product item (size, color, etc). + /// Length: 0-128 + /// The following special characters are not supported: <,>,\\ + /// + public required string Reference { get; set; } + + /// + /// The name of the product. + /// Length: 0-128 + /// The following special characters are not supported: <,>,\\ + /// + public required string Name { get; set; } + + /// + /// The quantity of the product. + /// Allowed: >=0 + /// + public required double Quantity { get; set; } + + /// + /// The defined unit of measurement for the product, for example pcs, liters, or kg. + /// Length: 0-128 + /// The following special characters are not supported: <,>,\,’,”,&,\\ + /// + public required string Unit { get; set; } + + /// + /// The price per unit excluding VAT. + /// Note: The amount can be negative. + /// + public required int UnitPrice { get; set; } + + /// + /// The tax/VAT rate (in percentage times 100). For examlpe, the value 2500 corresponds to 25%. Defaults to 0 if not provided. + /// + public int? TaxRate { get; set; } = 0; + + /// + /// The tax/VAT amount (unitPrice * quantity * taxRate / 10000). Defaults to 0 if not provided. taxAmount should include the total tax amount for the entire order item. + /// + public int? TaxAmount { get; set; } = 0; + + /// + /// The total amount including VAT (netTotalAmount + taxAmount). + /// Note: The amount can be negative. + /// + public required int GrossTotalAmount { get; set; } + + /// + /// The total amount excluding VAT (unitPrice * quantity). + /// Note: The amount can be negative. + /// + public required int NetTotalAmount { get; set; } + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentFull.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentFull.cs new file mode 100644 index 000000000..00771110f --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentFull.cs @@ -0,0 +1,141 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +internal class NetsPaymentFull +{ + public NetsPayment? Payment { get; set; } +} + +internal class NetsPayment +{ + public string? PaymentId { get; set; } + public NetsSummary? Summary { get; set; } + public NetsConsumer? Consumer { get; set; } + public NetsPaymentDetails? PaymentDetails { get; set; } + public NetsOrderDetails? OrderDetails { get; set; } + public NetsCheckoutUrls? Checkout { get; set; } + public string? Created { get; set; } + public NetsRefunds[]? Refunds { get; set; } + public NetsCharges[]? Charges { get; set; } + public string? Terminated { get; set; } + public NetsSubscription? Subscription { get; set; } + public NetsUnscheduledSubscription? UnscheduledSubscription { get; set; } + public string? MyReference { get; set; } +} + +internal class NetsSummary +{ + public decimal? ReservedAmount { get; set; } + public decimal? ChargedAmount { get; set; } + public decimal? RefundedAmount { get; set; } + public decimal? CancelledAmount { get; set; } +} + +internal class NetsConsumer +{ + public NetsAddress? ShippingAddress { get; set; } + public NetsCompany? Company { get; set; } + public NetsPrivatePerson? PrivatePerson { get; set; } + public NetsAddress? BillingAddress { get; set; } +} + +internal class NetsAddress +{ + public string? AddressLine1 { get; set; } + public string? AddressLine2 { get; set; } + public string? ReceiverLine { get; set; } + public string? PostalCode { get; set; } + public string? City { get; set; } + public string? Country { get; set; } +} + +internal class NetsCompany +{ + public string? MerchantReference { get; set; } + public string? Name { get; set; } + public string? RegistrationNumber { get; set; } + public NetsContactDetails? ContactDetails { get; set; } +} + +internal class NetsContactDetails +{ + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? Email { get; set; } + public NetsPhoneNumber? PhoneNumber { get; set; } +} + +internal class NetsPhoneNumber +{ + public string? Prefix { get; set; } + public string? Number { get; set; } +} + +internal class NetsPrivatePerson +{ + public string? MerchantReference { get; set; } + public string? DateOfBirth { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? Email { get; set; } + public NetsPhoneNumber? PhoneNumber { get; set; } +} + +internal class NetsPaymentDetails +{ + public string? PaymentType { get; set; } + public string? PaymentMethod { get; set; } + public NetsInvoiceDetails? InvoiceDetails { get; set; } + public NetsCardDetails? CardDetails { get; set; } +} + +internal class NetsInvoiceDetails +{ + public string? InvoiceNumber { get; set; } +} + +internal class NetsCardDetails +{ + public string? MaskedPan { get; set; } + public string? ExpiryDate { get; set; } +} + +internal class NetsOrderDetails +{ + public decimal Amount { get; set; } + public string? Currency { get; set; } + public string? Reference { get; set; } +} + +internal class NetsCheckoutUrls +{ + public string? Url { get; set; } + public string? CancelUrl { get; set; } +} + +internal class NetsRefunds +{ + public string? RefundId { get; set; } + public decimal Amount { get; set; } + public string? State { get; set; } + public string? LastUpdated { get; set; } + public NetsOrderItem[]? OrderItems { get; set; } +} + +internal class NetsCharges +{ + public string? ChargeId { get; set; } + public decimal? Amount { get; set; } + public string? Created { get; set; } + public NetsOrderItem[]? OrderItems { get; set; } +} + +internal class NetsSubscription +{ + public string? Id { get; set; } +} + +internal class NetsUnscheduledSubscription +{ + public string? UnscheduledSubscriptionId { get; set; } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethod.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethod.cs new file mode 100644 index 000000000..55e4c6a38 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethod.cs @@ -0,0 +1,16 @@ +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models +{ + internal class NetsPaymentMethod + { + /// + /// The name of the payment method. + /// Possible value currently is: 'easy-invoice'. + /// + public string? Name { get; set; } + + /// + /// Represents a line of a customer order. An order item refers to a product that the customer has bought. A product can be anything from a physical product to an online subscription or shipping. + /// + public NetsOrderItem? Fee { get; set; } + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethodConfiguration.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethodConfiguration.cs new file mode 100644 index 000000000..57e28492e --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethodConfiguration.cs @@ -0,0 +1,22 @@ +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models +{ + /// + /// Specifies payment methods configuration to be used for this payment, ignored if empty or null. + /// + internal class NetsPaymentMethodConfiguration + { + /// + /// The name of the payment method or payment type to be configured, if the specified payment method is not configured correctly in the merchant configurations then this won't take effect. + /// Payment type cannot be specified alongside payment methods that belong to it, if it happens the request will fail with an error. + /// Possible payment methods values: "Visa", "MasterCard", "Dankort", "AmericanExpress", "PayPal", "Vipps", "MobilePay", "Swish", "Arvato", "EasyInvoice", "EasyInstallment", "EasyCampaign", "RatePayInvoice", "RatePayInstallment", "RatePaySepa", "Sofort", "Trustly". + /// Possible payment types values: "Card", "Invoice", "Installment", "A2A", "Wallet". + /// + public required string Name { get; set; } + + /// + /// Indicates that the specified payment method/type is allowed to be used for this payment, defaults to true. + /// If one or more payment method/type is configured in the parent array then this value will be considered false for any other payment method that the parent array doesn't cover. + /// + public bool Enabled { get; set; } = true; + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsClient.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsClient.cs new file mode 100644 index 000000000..a4908b466 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsClient.cs @@ -0,0 +1,63 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using Altinn.App.Core.Features.Payment.Processors.Nets.Models; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Features.Payment.Processors.Nets; + +/// +/// Http client for Nets API +/// +internal class NetsClient : INetsClient +{ + private readonly HttpClient _httpClient; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public NetsClient(HttpClient httpClient, IOptions settings) + { + NetsPaymentSettings netsPaymentSettings = settings.Value; + + _httpClient = httpClient; + _httpClient.BaseAddress = new Uri(netsPaymentSettings.BaseUrl); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + netsPaymentSettings.SecretApiKey + ); + _httpClient.DefaultRequestHeaders.Add("CommercePlatformTag", "Altinn 3"); + } + + /// + /// Initializes a new payment object that becomes the object used throughout the checkout flow for a particular customer and order. Creating a payment object is the first step when you intend to accept a payment from your customer. Entering the amount 100 corresponds to 1 unit of the currency entered, such as e.g. 1 NOK. Typically you provide the following information: + /// The order details including order items, total amount, and currency. + /// Checkout page settings, which specify what type of integration you want: a checkout page embedded on your site or a pre-built checkout page hosted by Nexi Group. You can also specify data about your customer so that your customer only needs to provide payment details on the checkout page. + /// + /// Optionally, you can also provide information regarding: + /// Notifications if you want to be notified through webhooks when the status of the payment changes. + /// Fees added when using payment methods such as invoice. + /// Charge set to true so you can enable autocapture for subscriptions. + /// + /// On success, this method returns a paymentId that can be used in subsequent requests to refer to the newly created payment object. Optionally, the response object will also contain a hostedPaymentPageUrl, which is the URL you should redirect to if using a hosted pre-built checkout page. + /// + public async Task> CreatePayment(NetsCreatePayment payment) + { + HttpResponseMessage? response = await _httpClient.PostAsJsonAsync("/v1/payments", payment); + return await HttpApiResult.FromHttpResponse(response); + } + + /// + public async Task> RetrievePayment(string paymentId) + { + HttpResponseMessage response = await _httpClient.GetAsync($"/v1/payments/{paymentId}"); + return await HttpApiResult.FromHttpResponse(response); + } + + /// + public async Task TerminatePayment(string paymentId) + { + HttpResponseMessage response = await _httpClient.PutAsync($"v1/payments/{paymentId}/terminate", null); + return response.IsSuccessStatusCode; + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsMapper.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsMapper.cs new file mode 100644 index 000000000..310b8b37f --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsMapper.cs @@ -0,0 +1,128 @@ +using Altinn.App.Core.Features.Payment.Models; +using Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +namespace Altinn.App.Core.Features.Payment.Processors.Nets; + +/// +/// Various mapping methods between Nets types and our internal types. +/// +internal static class NetsMapper +{ + /// + /// Map from NetsConsumer to our Payer type. + /// + public static Payer? MapPayerDetails(NetsConsumer? consumer) + { + if (consumer == null) + { + return null; + } + + PayerCompany? payerCompany = + consumer.Company != null + ? new PayerCompany + { + Name = consumer.Company.Name, + OrganisationNumber = consumer.Company.RegistrationNumber, + ContactPerson = new PayerPrivatePerson + { + FirstName = consumer.Company.ContactDetails?.FirstName, + LastName = consumer.Company.ContactDetails?.LastName, + Email = consumer.Company.ContactDetails?.Email, + PhoneNumber = new PhoneNumber + { + Prefix = consumer.Company.ContactDetails?.PhoneNumber?.Prefix, + Number = consumer.Company.ContactDetails?.PhoneNumber?.Number, + } + } + } + : null; + + PayerPrivatePerson? payerPrivatePerson = + consumer.PrivatePerson != null + ? new PayerPrivatePerson + { + FirstName = consumer.PrivatePerson.FirstName, + LastName = consumer.PrivatePerson.LastName, + Email = consumer.PrivatePerson.Email, + PhoneNumber = new PhoneNumber + { + Prefix = consumer.PrivatePerson.PhoneNumber?.Prefix, + Number = consumer.PrivatePerson.PhoneNumber?.Number, + } + } + : null; + + return new Payer + { + Company = payerCompany, + PrivatePerson = payerPrivatePerson, + ShippingAddress = MapAddress(consumer.ShippingAddress), + BillingAddress = MapAddress(consumer.BillingAddress), + }; + } + + /// + /// Map from our PayerType enum to Nets consumer types. + /// + public static List MapConsumerTypes(PayerType[]? payerTypes) + { + List consumerTypes = []; + + if (payerTypes == null) + return consumerTypes; + + if (payerTypes.Contains(PayerType.Company)) + { + consumerTypes.Add("B2B"); + } + + if (payerTypes.Contains(PayerType.Person)) + { + consumerTypes.Add("B2C"); + } + + return consumerTypes; + } + + /// + /// Map from NetsInvoiceDetails to our InvoiceDetails type. + /// + public static InvoiceDetails? MapInvoiceDetails(NetsInvoiceDetails? netsInvoiceDetails) + { + if (netsInvoiceDetails == null) + return null; + + return new InvoiceDetails { InvoiceNumber = netsInvoiceDetails.InvoiceNumber, }; + } + + /// + /// Map from NetsCardDetails to our CardDetails type. + /// + public static CardDetails? MapCardDetails(NetsCardDetails? netsCardDetails) + { + if (netsCardDetails == null) + return null; + + return new CardDetails { MaskedPan = netsCardDetails.MaskedPan, ExpiryDate = netsCardDetails.ExpiryDate }; + } + + /// + /// Map from NetsAddress to our Address type. + /// + public static Address? MapAddress(NetsAddress? address) + { + if (address == null) + return null; + + return new Address + { + Name = address.ReceiverLine, + AddressLine1 = address.AddressLine1, + AddressLine2 = address.AddressLine2, + PostalCode = address.PostalCode, + City = address.City, + Country = address.Country, + }; + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs new file mode 100644 index 000000000..e51c18384 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs @@ -0,0 +1,186 @@ +using System.Web; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Payment.Exceptions; +using Altinn.App.Core.Features.Payment.Models; +using Altinn.App.Core.Features.Payment.Processors.Nets.Models; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Options; +using OrderDetails = Altinn.App.Core.Features.Payment.Models.OrderDetails; + +namespace Altinn.App.Core.Features.Payment.Processors.Nets; + +/// +/// Implementation of IPaymentProcessor for Nets. https://developer.nexigroup.com/nexi-checkout/en-EU/api/ +/// +internal class NetsPaymentProcessor : IPaymentProcessor +{ + private readonly NetsPaymentSettings _settings; + private readonly GeneralSettings _generalSettings; + private readonly INetsClient _netsClient; + + /// + /// Amounts are specified in the lowest monetary unit for the given currency, without punctuation marks. For example: 100,00 NOK is specified as 10000 and 9.99 USD is specified as 999. + /// Entering the amount 100 corresponds to 1 unit of the currency entered, such as e.g. 1 NOK. + /// + private const int LowestMonetaryUnitMultiplier = 100; + + /// + /// Implementation of IPaymentProcessor for Nets. + /// + public NetsPaymentProcessor( + INetsClient netsClient, + IOptions settings, + IOptions generalSettings + ) + { + _netsClient = netsClient; + _settings = settings.Value; + _generalSettings = generalSettings.Value; + } + + /// + public string PaymentProcessorId => "Nets Easy"; + + /// + public async Task StartPayment(Instance instance, OrderDetails orderDetails, string? language) + { + var instanceIdentifier = new InstanceIdentifier(instance); + string baseUrl = _generalSettings.FormattedExternalAppBaseUrl(new AppIdentifier(instance)); + var altinnAppUrl = $"{baseUrl}#/instance/{instanceIdentifier}"; + + var payment = new NetsCreatePayment() + { + Order = new NetsOrder + { + Amount = (int)(orderDetails.TotalPriceIncVat * LowestMonetaryUnitMultiplier), + Currency = orderDetails.Currency, + Reference = orderDetails.OrderReference, + Items = orderDetails + .OrderLines.Select(l => new NetsOrderItem() + { + Reference = l.Id, + Name = l.Name, + Quantity = l.Quantity, + Unit = l.Unit, + UnitPrice = (int)(l.PriceExVat * LowestMonetaryUnitMultiplier), + GrossTotalAmount = (int)( + l.PriceExVat * LowestMonetaryUnitMultiplier * l.Quantity * (1 + l.VatPercent / 100) + ), + NetTotalAmount = (int)(l.PriceExVat * LowestMonetaryUnitMultiplier * l.Quantity), + TaxAmount = (int)( + l.PriceExVat * LowestMonetaryUnitMultiplier * l.Quantity * (l.VatPercent / 100) + ), + TaxRate = (int)(l.VatPercent * LowestMonetaryUnitMultiplier), + }) + .ToList(), + }, + MyReference = instance.Id.Split('/')[1], + Checkout = new NetsCheckout + { + IntegrationType = "HostedPaymentPage", + TermsUrl = _settings.TermsUrl, + ReturnUrl = altinnAppUrl, + CancelUrl = altinnAppUrl, + ConsumerType = new NetsConsumerType + { + SupportedTypes = NetsMapper.MapConsumerTypes(orderDetails.AllowedPayerTypes), + }, + Appearance = new NetsApparence + { + DisplayOptions = new NetsApparence.NetsDisplayOptions + { + ShowOrderSummary = _settings.ShowOrderSummary, + ShowMerchantName = _settings.ShowMerchantName, + }, + }, + Charge = true + }, + }; + + HttpApiResult httpApiResult = await _netsClient.CreatePayment(payment); + if (!httpApiResult.IsSuccess || httpApiResult.Result?.HostedPaymentPageUrl is null) + { + throw new PaymentException( + "Failed to create payment\n" + httpApiResult.Status + " - " + httpApiResult.RawError + ); + } + + string hostedPaymentPageUrl = httpApiResult.Result.HostedPaymentPageUrl; + string paymentId = httpApiResult.Result.PaymentId; + + return new PaymentDetails + { + PaymentId = paymentId, + RedirectUrl = AddLanguageQueryParam(hostedPaymentPageUrl, language) + }; + } + + /// + public async Task TerminatePayment(Instance instance, PaymentInformation paymentInformation) + { + if (paymentInformation.PaymentDetails?.PaymentId is null) + { + throw new PaymentException("PaymentId is missing in paymentInformation. Can't terminate."); + } + + bool result = await _netsClient.TerminatePayment(paymentInformation.PaymentDetails.PaymentId); + return result; + } + + /// + public async Task<(PaymentStatus status, PaymentDetails paymentDetails)> GetPaymentStatus( + Instance instance, + string paymentId, + decimal expectedTotalIncVat, + string? language + ) + { + HttpApiResult httpApiResult = await _netsClient.RetrievePayment(paymentId); + + if (!httpApiResult.IsSuccess || httpApiResult.Result is null) + { + throw new PaymentException( + "Failed to retrieve payment\n" + httpApiResult.Status + " - " + httpApiResult.RawError + ); + } + + NetsPayment payment = httpApiResult.Result.Payment!; + decimal? chargedAmount = payment.Summary?.ChargedAmount; + + PaymentStatus status = chargedAmount > 0 ? PaymentStatus.Paid : PaymentStatus.Created; + NetsPaymentDetails? paymentPaymentDetails = payment.PaymentDetails; + + PaymentDetails paymentDetails = + new() + { + PaymentId = paymentId, + RedirectUrl = AddLanguageQueryParam(payment.Checkout!.Url!, language), + Payer = NetsMapper.MapPayerDetails(payment.Consumer), + PaymentType = paymentPaymentDetails?.PaymentType, + PaymentMethod = paymentPaymentDetails?.PaymentMethod, + CreatedDate = payment.Created, + ChargedDate = payment.Charges?.FirstOrDefault()?.Created, + InvoiceDetails = NetsMapper.MapInvoiceDetails(paymentPaymentDetails?.InvoiceDetails), + CardDetails = NetsMapper.MapCardDetails(paymentPaymentDetails?.CardDetails), + }; + + return (status, paymentDetails); + } + + private static string AddLanguageQueryParam(string url, string? language) + { + string? languageCode = language switch + { + "nb" => "nb-NO", + "nn" => "nb-NO", //No support for Nynorsk. Using Bokmål. + _ => null + }; + + if (string.IsNullOrEmpty(languageCode)) + return url; + + return QueryHelpers.AddQueryString(url, "language", languageCode); + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentSettings.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentSettings.cs new file mode 100644 index 000000000..6b1d768e7 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentSettings.cs @@ -0,0 +1,32 @@ +namespace Altinn.App.Core.Features.Payment.Processors.Nets; + +/// +/// Represents settings for the Nets payment processor. +/// +public class NetsPaymentSettings +{ + /// + /// Main authentication key for the Nets payment processor. Should be set in the secret manager being used. Never check in to source control. + /// + public required string SecretApiKey { get; set; } + + /// + /// Base API url for Nets Easy. + /// + public required string BaseUrl { get; set; } + + /// + /// Terms for the payment being made. This link will be shown to the user before they confirm the payment. + /// + public required string TermsUrl { get; set; } + + /// + /// If true, a summary of the order will be shown to the user before they confirm the payment. + /// + public bool ShowOrderSummary { get; set; } = true; + + /// + /// If true, the name of the merchant will be shown to the user before they confirm the payment. + /// + public bool ShowMerchantName { get; set; } = true; +} diff --git a/src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs b/src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs new file mode 100644 index 000000000..7d87dcb15 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs @@ -0,0 +1,40 @@ +using Altinn.App.Core.Features.Payment.Models; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Payment.Services +{ + /// + /// Service for handling payment. + /// + public interface IPaymentService + { + /// + /// Start payment for an instance. Will clean up any existing non-completed payment before starting a new payment. + /// + Task<(PaymentInformation paymentInformation, bool alreadyPaid)> StartPayment( + Instance instance, + AltinnPaymentConfiguration paymentConfiguration, + string? language + ); + + /// + /// Check updated payment information from payment provider and store the updated data. + /// + Task CheckAndStorePaymentStatus( + Instance instance, + AltinnPaymentConfiguration paymentConfiguration, + string? language + ); + + /// + /// Check our internal state to see if payment is complete. + /// + Task IsPaymentCompleted(Instance instance, AltinnPaymentConfiguration paymentConfiguration); + + /// + /// Cancel payment with payment processor and delete internal payment information. + /// + Task CancelAndDeleteAnyExistingPayment(Instance instance, AltinnPaymentConfiguration paymentConfiguration); + } +} diff --git a/src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs b/src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs new file mode 100644 index 000000000..3c428daa2 --- /dev/null +++ b/src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs @@ -0,0 +1,288 @@ +using Altinn.App.Core.Features.Payment.Exceptions; +using Altinn.App.Core.Features.Payment.Models; +using Altinn.App.Core.Features.Payment.Processors; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Features.Payment.Services; + +/// +/// Service that wraps most payment related features +/// +internal class PaymentService : IPaymentService +{ + private readonly IEnumerable _paymentProcessors; + private readonly IDataService _dataService; + private readonly ILogger _logger; + private readonly IOrderDetailsCalculator? _orderDetailsCalculator; + + /// + /// Initializes a new instance of the class. + /// + public PaymentService( + IEnumerable paymentProcessors, + IDataService dataService, + ILogger logger, + IOrderDetailsCalculator? orderDetailsCalculator = null + ) + { + _paymentProcessors = paymentProcessors; + _dataService = dataService; + _logger = logger; + _orderDetailsCalculator = orderDetailsCalculator; + } + + /// + public async Task<(PaymentInformation paymentInformation, bool alreadyPaid)> StartPayment( + Instance instance, + AltinnPaymentConfiguration paymentConfiguration, + string? language + ) + { + _logger.LogInformation("Starting payment for instance {InstanceId}.", instance.Id); + + if (_orderDetailsCalculator == null) + { + throw new PaymentException( + "You must add an implementation of the IOrderDetailsCalculator interface to the DI container. See payment related documentation." + ); + } + + ValidateConfig(paymentConfiguration); + string dataTypeId = paymentConfiguration.PaymentDataType!; + + (Guid dataElementId, PaymentInformation? existingPaymentInformation) = + await _dataService.GetByType(instance, dataTypeId); + + if (existingPaymentInformation?.PaymentDetails != null) + { + if (existingPaymentInformation.Status == PaymentStatus.Paid) + { + _logger.LogWarning( + "Payment with payment id {PaymentId} already paid for instance {InstanceId}. Cannot start new payment.", + existingPaymentInformation.PaymentDetails.PaymentId, + instance.Id + ); + + return (existingPaymentInformation, true); + } + + _logger.LogWarning( + "Payment with payment id {PaymentId} already started for instance {InstanceId}. Trying to cancel before creating new payment.", + existingPaymentInformation.PaymentDetails.PaymentId, + instance.Id + ); + + await CancelAndDelete(instance, dataElementId, existingPaymentInformation); + } + + OrderDetails orderDetails = await _orderDetailsCalculator.CalculateOrderDetails(instance, language); + IPaymentProcessor paymentProcessor = + _paymentProcessors.FirstOrDefault(p => p.PaymentProcessorId == orderDetails.PaymentProcessorId) + ?? throw new PaymentException( + $"Payment processor with ID '{orderDetails.PaymentProcessorId}' not found for instance {instance.Id}." + ); + + //If the sum of the order is 0, we can skip invoking the payment processor. + PaymentDetails? startedPayment = + orderDetails.TotalPriceIncVat > 0 + ? await paymentProcessor.StartPayment(instance, orderDetails, language) + : null; + + _logger.LogInformation( + startedPayment != null + ? "Payment started successfully using {PaymentProcessorId} for instance {InstanceId}." + : "Skipping starting payment using {PaymentProcessorId} since order sum is zero for instance {InstanceId}.", + paymentProcessor.PaymentProcessorId, + instance.Id + ); + + PaymentInformation paymentInformation = + new() + { + TaskId = instance.Process.CurrentTask.ElementId, + Status = startedPayment != null ? PaymentStatus.Created : PaymentStatus.Skipped, + OrderDetails = orderDetails, + PaymentDetails = startedPayment + }; + + await _dataService.InsertJsonObject(new InstanceIdentifier(instance), dataTypeId, paymentInformation); + return (paymentInformation, false); + } + + /// + public async Task CheckAndStorePaymentStatus( + Instance instance, + AltinnPaymentConfiguration paymentConfiguration, + string? language + ) + { + _logger.LogInformation("Checking payment status for instance {InstanceId}.", instance.Id); + + if (_orderDetailsCalculator == null) + { + throw new PaymentException( + "You must add an implementation of the IOrderDetailsCalculator interface to the DI container. See payment related documentation." + ); + } + + ValidateConfig(paymentConfiguration); + + string dataTypeId = paymentConfiguration.PaymentDataType!; + (Guid dataElementId, PaymentInformation? paymentInformation) = await _dataService.GetByType( + instance, + dataTypeId + ); + + if (paymentInformation == null) + { + _logger.LogInformation( + "No payment information stored yet for instance {InstanceId}. Returning uninitialized result.", + instance.Id + ); + + return new PaymentInformation + { + TaskId = instance.Process.CurrentTask.ElementId, + Status = PaymentStatus.Uninitialized, + OrderDetails = await _orderDetailsCalculator.CalculateOrderDetails(instance, language), + }; + } + + PaymentDetails paymentDetails = paymentInformation.PaymentDetails!; + decimal totalPriceIncVat = paymentInformation.OrderDetails.TotalPriceIncVat; + string paymentProcessorId = paymentInformation.OrderDetails.PaymentProcessorId; + + if (paymentInformation.Status == PaymentStatus.Skipped) + { + _logger.LogInformation( + "Payment status is '{Skipped}' for instance {InstanceId}. Won't ask payment processor for status.", + PaymentStatus.Skipped.ToString(), + instance.Id + ); + + return paymentInformation; + } + + IPaymentProcessor paymentProcessor = + _paymentProcessors.FirstOrDefault(p => p.PaymentProcessorId == paymentProcessorId) + ?? throw new PaymentException($"Payment processor with ID '{paymentProcessorId}' not found."); + + (PaymentStatus paymentStatus, PaymentDetails updatedPaymentDetails) = await paymentProcessor.GetPaymentStatus( + instance, + paymentDetails.PaymentId, + totalPriceIncVat, + language + ); + + paymentInformation.Status = paymentStatus; + paymentInformation.PaymentDetails = updatedPaymentDetails; + + _logger.LogInformation( + "Updated payment status is {Status} for instance {InstanceId}.", + paymentInformation.Status.ToString(), + instance.Id + ); + + await _dataService.UpdateJsonObject( + new InstanceIdentifier(instance), + dataTypeId, + dataElementId, + paymentInformation + ); + + return paymentInformation; + } + + /// + public async Task IsPaymentCompleted(Instance instance, AltinnPaymentConfiguration paymentConfiguration) + { + ValidateConfig(paymentConfiguration); + + string dataTypeId = paymentConfiguration.PaymentDataType!; + (Guid _, PaymentInformation? paymentInformation) = await _dataService.GetByType( + instance, + dataTypeId + ); + + if (paymentInformation == null) + { + throw new PaymentException("Payment information not found."); + } + + return paymentInformation.Status is PaymentStatus.Paid or PaymentStatus.Skipped; + } + + /// + public async Task CancelAndDeleteAnyExistingPayment( + Instance instance, + AltinnPaymentConfiguration paymentConfiguration + ) + { + ValidateConfig(paymentConfiguration); + + string dataTypeId = paymentConfiguration.PaymentDataType!; + (Guid dataElementId, PaymentInformation? paymentInformation) = await _dataService.GetByType( + instance, + dataTypeId + ); + + if (paymentInformation == null) + return; + + await CancelAndDelete(instance, dataElementId, paymentInformation); + } + + private async Task CancelAndDelete(Instance instance, Guid dataElementId, PaymentInformation paymentInformation) + { + if (paymentInformation.Status == PaymentStatus.Paid) + { + _logger.LogDebug("Payment already paid for instance {InstanceId}. Aborting cancellation.", instance.Id); + return; + } + + if (paymentInformation.Status != PaymentStatus.Skipped) + { + string paymentProcessorId = paymentInformation.OrderDetails.PaymentProcessorId; + IPaymentProcessor paymentProcessor = + _paymentProcessors.FirstOrDefault(pp => pp.PaymentProcessorId == paymentProcessorId) + ?? throw new PaymentException($"Payment processor with ID '{paymentProcessorId}' not found."); + + bool success = await paymentProcessor.TerminatePayment(instance, paymentInformation); + string paymentId = paymentInformation.PaymentDetails?.PaymentId ?? "missing"; + + if (!success) + { + throw new PaymentException( + $"Unable to cancel existing {paymentProcessorId} payment with ID: {paymentId}." + ); + } + + _logger.LogDebug("Payment {PaymentId} cancelled for instance {InstanceId}.", paymentId, instance.Id); + } + + await _dataService.DeleteById(new InstanceIdentifier(instance), dataElementId); + _logger.LogDebug("Payment information for deleted for instance {InstanceId}.", instance.Id); + } + + private static void ValidateConfig(AltinnPaymentConfiguration paymentConfiguration) + { + List errorMessages = []; + + if (string.IsNullOrWhiteSpace(paymentConfiguration.PaymentDataType)) + { + errorMessages.Add("PaymentDataType is missing."); + } + + if (errorMessages.Count != 0) + { + throw new ApplicationConfigException( + "Payment process task configuration is not valid: " + string.Join(",\n", errorMessages) + ); + } + } +} diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs index 162c613e1..564c40a66 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs @@ -58,7 +58,7 @@ bool defaultReturn public static object? EvaluateExpression( LayoutEvaluatorState state, Expression expr, - ComponentContext context, + ComponentContext? context, object[]? positionalArguments = null ) { @@ -105,7 +105,7 @@ bool defaultReturn return ret; } - private static object? DataModel(string? key, ComponentContext context, LayoutEvaluatorState state) + private static object? DataModel(string? key, ComponentContext? context, LayoutEvaluatorState state) { var data = state.GetModelData(key, context); @@ -118,7 +118,7 @@ bool defaultReturn }; } - private static object? Component(object?[] args, ComponentContext context, LayoutEvaluatorState state) + private static object? Component(object?[] args, ComponentContext? context, LayoutEvaluatorState state) { var componentId = args.First()?.ToString(); if (componentId is null) @@ -126,7 +126,7 @@ bool defaultReturn throw new ArgumentException("Cannot lookup component null"); } - if (context.Component is null) + if (context?.Component is null) { throw new ArgumentException("The component expression requires a component context"); } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs new file mode 100644 index 000000000..3ec5f1bc4 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs @@ -0,0 +1,16 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +{ + /// + /// Configuration properties for payments in a process task + /// + public class AltinnPaymentConfiguration + { + /// + /// Set what dataTypeId that should be used for storing the payment + /// + [XmlElement("paymentDataType", Namespace = "http://altinn.no/process")] + public string? PaymentDataType { get; set; } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs index b50c30dd7..d52c05587 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs @@ -26,5 +26,11 @@ public class AltinnTaskExtension /// [XmlElement("signatureConfig", Namespace = "http://altinn.no/process")] public AltinnSignatureConfiguration? SignatureConfiguration { get; set; } = new AltinnSignatureConfiguration(); + + /// + /// Gets or sets the configuration for signature + /// + [XmlElement("paymentConfig", Namespace = "http://altinn.no/process")] + public AltinnPaymentConfiguration? PaymentConfiguration { get; set; } = new AltinnPaymentConfiguration(); } } diff --git a/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs index d6e8e177c..fe2604934 100644 --- a/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs +++ b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs @@ -25,6 +25,7 @@ public class ExpressionsExclusiveGateway : IProcessExclusiveGateway ReadCommentHandling = JsonCommentHandling.Skip, PropertyNameCaseInsensitive = true, }; + private static readonly JsonSerializerOptions _jsonSerializerOptionsCamelCase = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; @@ -112,7 +113,11 @@ private static bool EvaluateSequenceFlow(LayoutEvaluatorState state, SequenceFlo if (sequenceFlow.ConditionExpression != null) { var expression = GetExpressionFromCondition(sequenceFlow.ConditionExpression); - foreach (var componentContext in state.GetComponentContexts()) + // If there is no component context in the state, evaluate the expression once without a component context + var stateComponentContexts = state.GetComponentContexts().Any() + ? state.GetComponentContexts().ToList() + : [null]; + foreach (ComponentContext? componentContext in stateComponentContexts) { var result = ExpressionEvaluator.EvaluateExpression(state, expression, componentContext); if (result is bool boolResult && boolResult) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 9c8e2cbe0..a7c7a2166 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -305,7 +305,9 @@ await GenerateProcessChangeEvent(InstanceEventType.process_EndEvent.ToString(), Name = nextElement!.Name, Started = now, AltinnTaskType = task?.ExtensionElements?.TaskExtension?.TaskType, - FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), + FlowType = action is "reject" + ? ProcessSequenceFlowType.AbandonCurrentMoveToNext.ToString() + : ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), Validated = null, }; diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs new file mode 100644 index 000000000..d2330f85c --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs @@ -0,0 +1,100 @@ +using Altinn.App.Core.Features.Payment.Exceptions; +using Altinn.App.Core.Features.Payment.Services; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks +{ + /// + /// Represents the process task responsible for collecting user payment. + /// + internal sealed class PaymentProcessTask : IProcessTask + { + private readonly IPdfService _pdfService; + private readonly IDataClient _dataClient; + private readonly IProcessReader _processReader; + private readonly IPaymentService _paymentService; + + private const string PdfContentType = "application/pdf"; + private const string ReceiptFileName = "Betalingskvittering.pdf"; + + /// + /// Initializes a new instance of the class. + /// + public PaymentProcessTask( + IPdfService pdfService, + IDataClient dataClient, + IProcessReader processReader, + IPaymentService paymentService + ) + { + _pdfService = pdfService; + _dataClient = dataClient; + _processReader = processReader; + _paymentService = paymentService; + } + + /// + public string Type => "payment"; + + /// + public async Task Start(string taskId, Instance instance) + { + AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); + await _paymentService.CancelAndDeleteAnyExistingPayment(instance, paymentConfiguration); + } + + /// + public async Task End(string taskId, Instance instance) + { + AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); + + if (!await _paymentService.IsPaymentCompleted(instance, paymentConfiguration)) + throw new PaymentException("The payment is not completed."); + + Stream pdfStream = await _pdfService.GeneratePdf(instance, taskId, CancellationToken.None); + + await _dataClient.InsertBinaryData( + instance.Id, + paymentConfiguration.PaymentDataType!, + PdfContentType, + ReceiptFileName, + pdfStream, + taskId + ); + } + + /// + public async Task Abandon(string taskId, Instance instance) + { + AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); + await _paymentService.CancelAndDeleteAnyExistingPayment(instance, paymentConfiguration); + } + + private AltinnPaymentConfiguration GetAltinnPaymentConfiguration(string taskId) + { + AltinnPaymentConfiguration? paymentConfiguration = _processReader + .GetAltinnTaskExtension(taskId) + ?.PaymentConfiguration; + + if (paymentConfiguration == null) + { + throw new ApplicationConfigException( + "PaymentConfig is missing in the payment process task configuration." + ); + } + + if (string.IsNullOrWhiteSpace(paymentConfiguration.PaymentDataType)) + { + throw new ApplicationConfigException( + "PaymentDataType is missing in the payment process task configuration." + ); + } + + return paymentConfiguration; + } + } +} diff --git a/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs index 482a6babe..f48370b29 100644 --- a/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs +++ b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs @@ -14,17 +14,20 @@ public class UserActionContext /// The user performing the action /// The id of the button that triggered the action (optional) /// + /// The currently used language by the user (or null if not available) public UserActionContext( Instance instance, int? userId, string? buttonId = null, - Dictionary? actionMetadata = null + Dictionary? actionMetadata = null, + string? language = null ) { Instance = instance; UserId = userId; ButtonId = buttonId; ActionMetadata = actionMetadata ?? new Dictionary(); + Language = language; } /// @@ -46,4 +49,9 @@ public UserActionContext( /// Additional metadata for the action /// public Dictionary ActionMetadata { get; } + + /// + /// The language that will be used for the action + /// + public string? Language { get; } } diff --git a/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs b/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs index d2e9206e7..e143141f4 100644 --- a/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs +++ b/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs @@ -31,7 +31,7 @@ public class UserActionResult /// /// Gets or sets a value indicating whether the user action was a success /// - public bool Success { get; init; } + public bool Success { get; set; } /// /// Indicates the type of result @@ -61,7 +61,7 @@ public class UserActionResult /// /// If this is set, the client should redirect to this url /// - public string? RedirectUrl { get; set; } + public Uri? RedirectUrl { get; set; } /// /// Creates a success result @@ -102,7 +102,7 @@ public static UserActionResult FailureResult( /// /// /// - public static UserActionResult RedirectResult(string redirectUrl) + public static UserActionResult RedirectResult(Uri redirectUrl) { return new UserActionResult { diff --git a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs index 8b75f1e35..6d18d0bb1 100644 --- a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs @@ -17,6 +17,7 @@ public class DataClientMock : IDataClient { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAppMetadata _appMetadata; + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; diff --git a/test/Altinn.App.Api.Tests/OpenApi/swagger.json b/test/Altinn.App.Api.Tests/OpenApi/swagger.json index c57941e5a..35e0ac51a 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/swagger.json +++ b/test/Altinn.App.Api.Tests/OpenApi/swagger.json @@ -2394,6 +2394,230 @@ } } }, + "/{org}/{app}/instances/{instanceOwnerPartyId}/{instanceGuid}/payment": { + "get": { + "tags": [ + "Payment" + ], + "parameters": [ + { + "name": "org", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "app", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "instanceOwnerPartyId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "instanceGuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "language", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/PaymentInformation" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentInformation" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PaymentInformation" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/PaymentInformation" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/PaymentInformation" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/{org}/{app}/instances/{instanceOwnerPartyId}/{instanceGuid}/payment/order-details": { + "get": { + "tags": [ + "Payment" + ], + "parameters": [ + { + "name": "org", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "app", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "instanceOwnerPartyId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "instanceGuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "language", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/OrderDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/OrderDetails" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/OrderDetails" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/OrderDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, "/{org}/{app}/instances/{instanceOwnerPartyId}/{instanceGuid}/data/{dataGuid}/pdf/format": { "get": { "tags": [ @@ -4249,6 +4473,36 @@ "type": "integer", "format": "int32" }, + "Address": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "addressLine1": { + "type": "string", + "nullable": true + }, + "addressLine2": { + "type": "string", + "nullable": true + }, + "postalCode": { + "type": "string", + "nullable": true + }, + "city": { + "type": "string", + "nullable": true + }, + "country": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "AppProcessElementInfo": { "type": "object", "properties": { @@ -4607,6 +4861,20 @@ }, "additionalProperties": false }, + "CardDetails": { + "type": "object", + "properties": { + "maskedPan": { + "type": "string", + "nullable": true + }, + "expiryDate": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "ClientAction": { "type": "object", "properties": { @@ -5287,6 +5555,16 @@ }, "additionalProperties": false }, + "InvoiceDetails": { + "type": "object", + "properties": { + "invoiceNumber": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "JsonNode": { "type": "object", "properties": { @@ -5385,6 +5663,56 @@ ], "type": "string" }, + "OrderDetails": { + "type": "object", + "properties": { + "paymentProcessorId": { + "type": "string", + "nullable": true + }, + "receiver": { + "$ref": "#/components/schemas/PaymentReceiver" + }, + "currency": { + "type": "string", + "nullable": true + }, + "orderLines": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentOrderLine" + }, + "nullable": true + }, + "allowedPayerTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PayerType" + }, + "nullable": true + }, + "orderReference": { + "type": "string", + "nullable": true + }, + "totalPriceExVat": { + "type": "number", + "format": "double", + "readOnly": true + }, + "totalVat": { + "type": "number", + "format": "double", + "readOnly": true + }, + "totalPriceIncVat": { + "type": "number", + "format": "double", + "readOnly": true + } + }, + "additionalProperties": false + }, "PartyTypesAllowed": { "type": "object", "properties": { @@ -5431,6 +5759,214 @@ }, "additionalProperties": false }, + "Payer": { + "type": "object", + "properties": { + "privatePerson": { + "$ref": "#/components/schemas/PayerPrivatePerson" + }, + "company": { + "$ref": "#/components/schemas/PayerCompany" + }, + "shippingAddress": { + "$ref": "#/components/schemas/Address" + }, + "billingAddress": { + "$ref": "#/components/schemas/Address" + } + }, + "additionalProperties": false + }, + "PayerCompany": { + "type": "object", + "properties": { + "organisationNumber": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "contactPerson": { + "$ref": "#/components/schemas/PayerPrivatePerson" + } + }, + "additionalProperties": false + }, + "PayerPrivatePerson": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + }, + "phoneNumber": { + "$ref": "#/components/schemas/PhoneNumber" + } + }, + "additionalProperties": false + }, + "PayerType": { + "enum": [ + "Person", + "Company" + ], + "type": "string" + }, + "PaymentDetails": { + "type": "object", + "properties": { + "paymentId": { + "type": "string", + "nullable": true + }, + "redirectUrl": { + "type": "string", + "nullable": true + }, + "payer": { + "$ref": "#/components/schemas/Payer" + }, + "paymentType": { + "type": "string", + "nullable": true + }, + "paymentMethod": { + "type": "string", + "nullable": true + }, + "createdDate": { + "type": "string", + "nullable": true + }, + "chargedDate": { + "type": "string", + "nullable": true + }, + "invoiceDetails": { + "$ref": "#/components/schemas/InvoiceDetails" + }, + "cardDetails": { + "$ref": "#/components/schemas/CardDetails" + } + }, + "additionalProperties": false + }, + "PaymentInformation": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/PaymentStatus" + }, + "orderDetails": { + "$ref": "#/components/schemas/OrderDetails" + }, + "paymentDetails": { + "$ref": "#/components/schemas/PaymentDetails" + } + }, + "additionalProperties": false + }, + "PaymentOrderLine": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "textResourceKey": { + "type": "string", + "nullable": true + }, + "priceExVat": { + "type": "number", + "format": "double" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "vatPercent": { + "type": "number", + "format": "double" + }, + "unit": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "PaymentReceiver": { + "type": "object", + "properties": { + "organisationNumber": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "postalAddress": { + "$ref": "#/components/schemas/Address" + }, + "bankAccountNumber": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + }, + "phoneNumber": { + "$ref": "#/components/schemas/PhoneNumber" + } + }, + "additionalProperties": false + }, + "PaymentStatus": { + "enum": [ + "Uninitialized", + "Created", + "Paid", + "Failed", + "Cancelled", + "Skipped" + ], + "type": "string" + }, + "PhoneNumber": { + "type": "object", + "properties": { + "prefix": { + "type": "string", + "nullable": true + }, + "number": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "ProblemDetails": { "type": "object", "properties": { @@ -5798,6 +6334,11 @@ }, "error": { "$ref": "#/components/schemas/ActionError" + }, + "redirectUrl": { + "type": "string", + "format": "uri", + "nullable": true } }, "additionalProperties": false diff --git a/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml b/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml index 394eaa1c9..f85c071d6 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml +++ b/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml @@ -1461,6 +1461,142 @@ paths: responses: '200': description: Success + '/{org}/{app}/instances/{instanceOwnerPartyId}/{instanceGuid}/payment': + get: + tags: + - Payment + parameters: + - name: org + in: path + required: true + schema: + type: string + - name: app + in: path + required: true + schema: + type: string + - name: instanceOwnerPartyId + in: path + required: true + schema: + type: integer + format: int32 + - name: instanceGuid + in: path + required: true + schema: + type: string + format: uuid + - name: language + in: query + schema: + type: string + responses: + '200': + description: Success + content: + text/plain: + schema: + $ref: '#/components/schemas/PaymentInformation' + application/json: + schema: + $ref: '#/components/schemas/PaymentInformation' + text/json: + schema: + $ref: '#/components/schemas/PaymentInformation' + application/xml: + schema: + $ref: '#/components/schemas/PaymentInformation' + text/xml: + schema: + $ref: '#/components/schemas/PaymentInformation' + '404': + description: Not Found + content: + text/plain: + schema: + $ref: '#/components/schemas/ProblemDetails' + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + text/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + application/xml: + schema: + $ref: '#/components/schemas/ProblemDetails' + text/xml: + schema: + $ref: '#/components/schemas/ProblemDetails' + '/{org}/{app}/instances/{instanceOwnerPartyId}/{instanceGuid}/payment/order-details': + get: + tags: + - Payment + parameters: + - name: org + in: path + required: true + schema: + type: string + - name: app + in: path + required: true + schema: + type: string + - name: instanceOwnerPartyId + in: path + required: true + schema: + type: integer + format: int32 + - name: instanceGuid + in: path + required: true + schema: + type: string + format: uuid + - name: language + in: query + schema: + type: string + responses: + '200': + description: Success + content: + text/plain: + schema: + $ref: '#/components/schemas/OrderDetails' + application/json: + schema: + $ref: '#/components/schemas/OrderDetails' + text/json: + schema: + $ref: '#/components/schemas/OrderDetails' + application/xml: + schema: + $ref: '#/components/schemas/OrderDetails' + text/xml: + schema: + $ref: '#/components/schemas/OrderDetails' + '404': + description: Not Found + content: + text/plain: + schema: + $ref: '#/components/schemas/ProblemDetails' + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + text/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + application/xml: + schema: + $ref: '#/components/schemas/ProblemDetails' + text/xml: + schema: + $ref: '#/components/schemas/ProblemDetails' '/{org}/{app}/instances/{instanceOwnerPartyId}/{instanceGuid}/data/{dataGuid}/pdf/format': get: tags: @@ -2589,6 +2725,28 @@ components: - 1 type: integer format: int32 + Address: + type: object + properties: + name: + type: string + nullable: true + addressLine1: + type: string + nullable: true + addressLine2: + type: string + nullable: true + postalCode: + type: string + nullable: true + city: + type: string + nullable: true + country: + type: string + nullable: true + additionalProperties: false AppProcessElementInfo: type: object properties: @@ -2850,6 +3008,16 @@ components: nullable: true nullable: true additionalProperties: false + CardDetails: + type: object + properties: + maskedPan: + type: string + nullable: true + expiryDate: + type: string + nullable: true + additionalProperties: false ClientAction: type: object properties: @@ -3345,6 +3513,13 @@ components: type: string nullable: true additionalProperties: false + InvoiceDetails: + type: object + properties: + invoiceNumber: + type: string + nullable: true + additionalProperties: false JsonNode: type: object properties: @@ -3414,6 +3589,43 @@ components: - copy - test type: string + OrderDetails: + type: object + properties: + paymentProcessorId: + type: string + nullable: true + receiver: + $ref: '#/components/schemas/PaymentReceiver' + currency: + type: string + nullable: true + orderLines: + type: array + items: + $ref: '#/components/schemas/PaymentOrderLine' + nullable: true + allowedPayerTypes: + type: array + items: + $ref: '#/components/schemas/PayerType' + nullable: true + orderReference: + type: string + nullable: true + totalPriceExVat: + type: number + format: double + readOnly: true + totalVat: + type: number + format: double + readOnly: true + totalPriceIncVat: + type: number + format: double + readOnly: true + additionalProperties: false PartyTypesAllowed: type: object properties: @@ -3446,6 +3658,155 @@ components: value: $ref: '#/components/schemas/JsonNode' additionalProperties: false + Payer: + type: object + properties: + privatePerson: + $ref: '#/components/schemas/PayerPrivatePerson' + company: + $ref: '#/components/schemas/PayerCompany' + shippingAddress: + $ref: '#/components/schemas/Address' + billingAddress: + $ref: '#/components/schemas/Address' + additionalProperties: false + PayerCompany: + type: object + properties: + organisationNumber: + type: string + nullable: true + name: + type: string + nullable: true + contactPerson: + $ref: '#/components/schemas/PayerPrivatePerson' + additionalProperties: false + PayerPrivatePerson: + type: object + properties: + firstName: + type: string + nullable: true + lastName: + type: string + nullable: true + email: + type: string + nullable: true + phoneNumber: + $ref: '#/components/schemas/PhoneNumber' + additionalProperties: false + PayerType: + enum: + - Person + - Company + type: string + PaymentDetails: + type: object + properties: + paymentId: + type: string + nullable: true + redirectUrl: + type: string + nullable: true + payer: + $ref: '#/components/schemas/Payer' + paymentType: + type: string + nullable: true + paymentMethod: + type: string + nullable: true + createdDate: + type: string + nullable: true + chargedDate: + type: string + nullable: true + invoiceDetails: + $ref: '#/components/schemas/InvoiceDetails' + cardDetails: + $ref: '#/components/schemas/CardDetails' + additionalProperties: false + PaymentInformation: + type: object + properties: + taskId: + type: string + nullable: true + status: + $ref: '#/components/schemas/PaymentStatus' + orderDetails: + $ref: '#/components/schemas/OrderDetails' + paymentDetails: + $ref: '#/components/schemas/PaymentDetails' + additionalProperties: false + PaymentOrderLine: + type: object + properties: + id: + type: string + nullable: true + name: + type: string + nullable: true + textResourceKey: + type: string + nullable: true + priceExVat: + type: number + format: double + quantity: + type: integer + format: int32 + vatPercent: + type: number + format: double + unit: + type: string + nullable: true + additionalProperties: false + PaymentReceiver: + type: object + properties: + organisationNumber: + type: string + nullable: true + name: + type: string + nullable: true + postalAddress: + $ref: '#/components/schemas/Address' + bankAccountNumber: + type: string + nullable: true + email: + type: string + nullable: true + phoneNumber: + $ref: '#/components/schemas/PhoneNumber' + additionalProperties: false + PaymentStatus: + enum: + - Uninitialized + - Created + - Paid + - Failed + - Cancelled + - Skipped + type: string + PhoneNumber: + type: object + properties: + prefix: + type: string + nullable: true + number: + type: string + nullable: true + additionalProperties: false ProblemDetails: type: object properties: @@ -3713,6 +4074,10 @@ components: nullable: true error: $ref: '#/components/schemas/ActionError' + redirectUrl: + type: string + format: uri + nullable: true additionalProperties: false ValidationIssue: type: object diff --git a/test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs new file mode 100644 index 000000000..88cec9965 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs @@ -0,0 +1,172 @@ +#nullable disable +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Features.Payment.Models; +using Altinn.App.Core.Features.Payment.Services; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Sign; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Process; +using Altinn.App.Core.Models.UserAction; +using Altinn.App.Core.Tests.Internal.Process.TestUtils; +using Altinn.Platform.Profile.Models; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Json.Patch; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; +using Signee = Altinn.App.Core.Internal.Sign.Signee; + +namespace Altinn.App.Core.Tests.Features.Action; + +public class PaymentUserActionTests +{ + private readonly Mock _dataServiceMock = new(); + private readonly Mock _paymentServiceMock = new(); + + [Fact] + public async Task HandleAction_returns_redirect_result_correctly() + { + // Arrange + Instance instance = CreateInstance(); + + PaymentInformation paymentInformation = + new() + { + TaskId = instance.Process.CurrentTask.ElementId, + Status = PaymentStatus.Created, + OrderDetails = new OrderDetails + { + PaymentProcessorId = "paymentProcessorId", + Currency = "NOK", + OrderLines = [], + Receiver = new PaymentReceiver() + }, + PaymentDetails = new PaymentDetails { PaymentId = "1", RedirectUrl = "https://example.com", } + }; + + var userActionContext = new UserActionContext(instance, 1337); + + _paymentServiceMock + .Setup(x => + x.StartPayment(It.IsAny(), It.IsAny(), It.IsAny()) + ) + .ReturnsAsync((paymentInformation, false)); + + // Act + PaymentUserAction userAction = CreatePaymentUserAction(); + UserActionResult result = await userAction.HandleAction(userActionContext); + + // Assert + result + .Should() + .BeEquivalentTo(UserActionResult.RedirectResult(new Uri(paymentInformation.PaymentDetails.RedirectUrl))); + } + + [Fact] + public async Task HandleAction_returns_success_result_when_no_redirect_url() + { + // Arrange + Instance instance = CreateInstance(); + + PaymentInformation paymentInformation = + new() + { + TaskId = instance.Process.CurrentTask.ElementId, + Status = PaymentStatus.Skipped, + OrderDetails = new OrderDetails + { + PaymentProcessorId = "paymentProcessorId", + Currency = "NOK", + OrderLines = [], + Receiver = new PaymentReceiver() + }, + PaymentDetails = new PaymentDetails { PaymentId = null, RedirectUrl = null, } + }; + + var userActionContext = new UserActionContext(instance, 1337); + + _paymentServiceMock + .Setup(x => + x.StartPayment(It.IsAny(), It.IsAny(), It.IsAny()) + ) + .ReturnsAsync((paymentInformation, false)); + + // Act + PaymentUserAction userAction = CreatePaymentUserAction(); + UserActionResult result = await userAction.HandleAction(userActionContext); + + // Assert + result.Should().BeEquivalentTo(UserActionResult.SuccessResult()); + } + + [Fact] + public async Task HandleAction_returns_failure_result_when_already_paid() + { + // Arrange + Instance instance = CreateInstance(); + + PaymentInformation paymentInformation = + new() + { + TaskId = instance.Process.CurrentTask.ElementId, + Status = PaymentStatus.Skipped, + OrderDetails = new OrderDetails + { + PaymentProcessorId = "paymentProcessorId", + Currency = "NOK", + OrderLines = [], + Receiver = new PaymentReceiver() + }, + PaymentDetails = new PaymentDetails { PaymentId = "1", RedirectUrl = "https://example.com", } + }; + + var userActionContext = new UserActionContext(instance, 1337); + + _paymentServiceMock + .Setup(x => + x.StartPayment(It.IsAny(), It.IsAny(), It.IsAny()) + ) + .ReturnsAsync((paymentInformation, true)); + + // Act + PaymentUserAction userAction = CreatePaymentUserAction(); + UserActionResult result = await userAction.HandleAction(userActionContext); + + // Assert + result + .Should() + .BeEquivalentTo( + UserActionResult.FailureResult( + error: new ActionError { Code = "PaymentAlreadyCompleted", Message = "Payment already completed." }, + errorType: ProcessErrorType.Conflict + ) + ); + } + + private PaymentUserAction CreatePaymentUserAction(string testBpmnFilename = "payment-task-process.bpmn") + { + IProcessReader processReader = ProcessTestUtils.SetupProcessReader( + testBpmnFilename, + Path.Combine("Features", "Action", "TestData") + ); + return new PaymentUserAction(processReader, _paymentServiceMock.Object, NullLogger.Instance); + } + + private static Instance CreateInstance() + { + return new Instance() + { + Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", + InstanceOwner = new InstanceOwner { PartyId = "5000", }, + Process = new ProcessState { CurrentTask = new ProcessElementInfo { ElementId = "Task2" } }, + Data = [new DataElement { Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", DataType = "Model" }] + }; + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/payment-task-process.bpmn b/test/Altinn.App.Core.Tests/Features/Action/TestData/payment-task-process.bpmn new file mode 100644 index 000000000..5581308a6 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/payment-task-process.bpmn @@ -0,0 +1,49 @@ + + + + + Flow1 + + + + Flow1 + Flow2 + + + + + + data + + + + + + Flow2 + Flow3 + + + payment + + pay + confirm + reject + + + paymentInformation + + + + + + + Flow3 + + + diff --git a/test/Altinn.App.Core.Tests/Features/Payment/PaymentServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Payment/PaymentServiceTests.cs new file mode 100644 index 000000000..e7ce4c8ea --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Payment/PaymentServiceTests.cs @@ -0,0 +1,566 @@ +using Altinn.App.Core.Features.Payment; +using Altinn.App.Core.Features.Payment.Exceptions; +using Altinn.App.Core.Features.Payment.Models; +using Altinn.App.Core.Features.Payment.Processors; +using Altinn.App.Core.Features.Payment.Services; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Payment; + +public class PaymentServiceTests +{ + private readonly PaymentService _paymentService; + private readonly Mock _paymentProcessor = new(MockBehavior.Strict); + private readonly Mock _orderDetailsCalculator = new(MockBehavior.Strict); + private readonly Mock _dataService = new(MockBehavior.Strict); + private readonly Mock> _logger = new(); + + public PaymentServiceTests() + { + _paymentService = new PaymentService( + [_paymentProcessor.Object], + _dataService.Object, + _logger.Object, + _orderDetailsCalculator.Object + ); + } + + [Fact] + public async Task StartPayment_ReturnsRedirectUrl_WhenPaymentStartedSuccessfully() + { + Instance instance = CreateInstance(); + OrderDetails orderDetails = CreateOrderDetails(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + PaymentInformation paymentInformation = CreatePaymentInformation(); + PaymentDetails paymentDetails = + paymentInformation.PaymentDetails ?? throw new NullReferenceException("PaymentDetails should not be null"); + const string language = "nb"; + + _orderDetailsCalculator.Setup(p => p.CalculateOrderDetails(instance, language)).ReturnsAsync(orderDetails); + _dataService + .Setup(ds => ds.GetByType(It.IsAny(), It.IsAny())) + .ReturnsAsync((Guid.Empty, null)); + _dataService + .Setup(ds => ds.InsertJsonObject(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new DataElement()); + _paymentProcessor.Setup(pp => pp.PaymentProcessorId).Returns(orderDetails.PaymentProcessorId); + _paymentProcessor.Setup(p => p.StartPayment(instance, orderDetails, language)).ReturnsAsync(paymentDetails); + + // Act + (PaymentInformation paymentInformationResult, bool alreadyPaid) = await _paymentService.StartPayment( + instance, + paymentConfiguration, + language + ); + + // Assert + paymentInformationResult.PaymentDetails.Should().NotBeNull(); + paymentInformationResult + .PaymentDetails!.RedirectUrl.Should() + .Be(paymentInformation.PaymentDetails!.RedirectUrl); + paymentInformationResult.OrderDetails.Should().BeEquivalentTo(orderDetails); + paymentInformationResult + .OrderDetails.PaymentProcessorId.Should() + .Be(paymentInformation.OrderDetails.PaymentProcessorId); + alreadyPaid.Should().BeFalse(); + + // Verify calls + _orderDetailsCalculator.Verify(odc => odc.CalculateOrderDetails(instance, language), Times.Once); + _paymentProcessor.Verify(pp => pp.StartPayment(instance, orderDetails, language), Times.Once); + _dataService.Verify(ds => + ds.InsertJsonObject( + It.IsAny(), + paymentConfiguration.PaymentDataType!, + It.IsAny() + ) + ); + } + + [Fact] + public async Task StartPayment_ReturnsAlreadyPaidTrue_WhenPaymentIsAlreadyPaid() + { + Instance instance = CreateInstance(); + OrderDetails orderDetails = CreateOrderDetails(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + const string language = "nb"; + + _paymentProcessor.Setup(pp => pp.PaymentProcessorId).Returns(orderDetails.PaymentProcessorId); + + _dataService + .Setup(ds => ds.GetByType(It.IsAny(), It.IsAny())) + .ReturnsAsync( + ( + Guid.NewGuid(), + new PaymentInformation + { + TaskId = "Task_1", + Status = PaymentStatus.Paid, + OrderDetails = orderDetails, + PaymentDetails = new PaymentDetails { PaymentId = "id", RedirectUrl = "url" }, + } + ) + ); + + // Act + (PaymentInformation paymentInformationResult, bool alreadyPaid) = await _paymentService.StartPayment( + instance, + paymentConfiguration, + language + ); + + // Assert + paymentInformationResult.PaymentDetails.Should().NotBeNull(); + alreadyPaid.Should().BeTrue(); + } + + [Fact] + public async Task StartPayment_ThrowsException_WhenOrderDetailsCannotBeRetrieved() + { + Instance instance = CreateInstance(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + const string language = "nb"; + + _dataService + .Setup(ds => ds.GetByType(It.IsAny(), It.IsAny())) + .ReturnsAsync((Guid.Empty, null)); + + _orderDetailsCalculator + .Setup(odc => odc.CalculateOrderDetails(instance, language)) + .ThrowsAsync(new Exception()); + + _paymentProcessor.Setup(x => x.PaymentProcessorId).Returns("paymentProcessorId"); + + // Act & Assert + await Assert.ThrowsAsync( + () => _paymentService.StartPayment(instance, paymentConfiguration, language) + ); + } + + [Fact] + public async Task StartPayment_ThrowsException_WhenPaymentCannotBeStarted() + { + Instance instance = CreateInstance(); + OrderDetails orderDetails = CreateOrderDetails(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + const string language = "nb"; + + _dataService + .Setup(ds => ds.GetByType(It.IsAny(), It.IsAny())) + .ReturnsAsync((Guid.Empty, null)); + _orderDetailsCalculator.Setup(pp => pp.CalculateOrderDetails(instance, language)).ReturnsAsync(orderDetails); + + _paymentProcessor.Setup(x => x.PaymentProcessorId).Returns(orderDetails.PaymentProcessorId); + _paymentProcessor.Setup(pp => pp.StartPayment(instance, orderDetails, language)).ThrowsAsync(new Exception()); + + // Act & Assert + await Assert.ThrowsAsync( + () => _paymentService.StartPayment(instance, paymentConfiguration, language) + ); + } + + [Fact] + public async Task StartPayment_ThrowsException_WhenPaymentInformationCannotBeStored() + { + Instance instance = CreateInstance(); + OrderDetails orderDetails = CreateOrderDetails(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + PaymentInformation paymentInformation = CreatePaymentInformation(); + PaymentDetails paymentDetails = + paymentInformation.PaymentDetails ?? throw new NullReferenceException("PaymentDetails should not be null"); + const string language = "nb"; + + _orderDetailsCalculator.Setup(p => p.CalculateOrderDetails(instance, language)).ReturnsAsync(orderDetails); + _dataService + .Setup(ds => ds.GetByType(It.IsAny(), It.IsAny())) + .ReturnsAsync((Guid.Empty, null)); + _dataService + .Setup(ds => ds.InsertJsonObject(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception()); + _paymentProcessor.Setup(pp => pp.PaymentProcessorId).Returns(orderDetails.PaymentProcessorId); + _paymentProcessor.Setup(pp => pp.StartPayment(instance, orderDetails, language)).ReturnsAsync(paymentDetails); + + // Act & Assert + await Assert.ThrowsAsync( + () => _paymentService.StartPayment(instance, paymentConfiguration, language) + ); + } + + [Fact] + public async Task CheckAndStorePaymentInformation_ReturnsNull_WhenNoPaymentInformationFound() + { + Instance instance = CreateInstance(); + OrderDetails orderDetails = CreateOrderDetails(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + const string language = "nb"; + + _orderDetailsCalculator.Setup(p => p.CalculateOrderDetails(instance, language)).ReturnsAsync(orderDetails); + + _dataService + .Setup(ds => ds.GetByType(It.IsAny(), It.IsAny())) + .ReturnsAsync((Guid.Empty, null)); + + // Act + PaymentInformation? result = await _paymentService.CheckAndStorePaymentStatus( + instance, + paymentConfiguration, + language + ); + + // Assert + result.Should().NotBeNull(); + instance.Process.CurrentTask.ElementId.Should().Be(result!.TaskId); + orderDetails.Should().BeEquivalentTo(result.OrderDetails); + result.PaymentDetails.Should().BeNull(); + } + + [Fact] + public async Task CheckAndStorePaymentInformation_ThrowsException_WhenUnableToCheckPaymentStatus() + { + Instance instance = CreateInstance(); + OrderDetails orderDetails = CreateOrderDetails(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + PaymentInformation paymentInformation = CreatePaymentInformation(); + const string language = "nb"; + + _orderDetailsCalculator.Setup(odc => odc.CalculateOrderDetails(instance, language)).ReturnsAsync(orderDetails); + + _dataService + .Setup(ds => ds.GetByType(It.IsAny(), It.IsAny())) + .ReturnsAsync((Guid.NewGuid(), paymentInformation)); + + _paymentProcessor.Setup(x => x.PaymentProcessorId).Returns(orderDetails.PaymentProcessorId); + + _paymentProcessor + .Setup(x => x.GetPaymentStatus(It.IsAny(), It.IsAny(), It.IsAny(), language)) + .ThrowsAsync(new PaymentException("Some exception")); + + var paymentService = new PaymentService( + [_paymentProcessor.Object], + _dataService.Object, + _logger.Object, + _orderDetailsCalculator.Object + ); + + // Act & Assert + await Assert.ThrowsAsync( + () => paymentService.CheckAndStorePaymentStatus(instance, paymentConfiguration, language) + ); + } + + [Fact] + public async Task CheckAndStorePaymentInformation_ReturnsPaymentInformation_WhenPaymentStatusCheckedSuccessfully() + { + Instance instance = CreateInstance(); + OrderDetails orderDetails = CreateOrderDetails(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + PaymentInformation paymentInformation = CreatePaymentInformation(); + const string language = "nb"; + + _orderDetailsCalculator.Setup(p => p.CalculateOrderDetails(instance, language)).ReturnsAsync(orderDetails); + + _dataService + .Setup(ds => ds.GetByType(It.IsAny(), It.IsAny())) + .ReturnsAsync((Guid.NewGuid(), paymentInformation)); + + _dataService + .Setup(ds => + ds.UpdateJsonObject( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new DataElement()); + + _paymentProcessor.Setup(x => x.PaymentProcessorId).Returns(orderDetails.PaymentProcessorId); + + _paymentProcessor + .Setup(pp => pp.GetPaymentStatus(It.IsAny(), It.IsAny(), It.IsAny(), language)) + .ReturnsAsync( + (PaymentStatus.Paid, new PaymentDetails { PaymentId = "paymentId", RedirectUrl = "redirect url" }) + ); + + var paymentService = new PaymentService( + [_paymentProcessor.Object], + _dataService.Object, + _logger.Object, + _orderDetailsCalculator.Object + ); + + // Act + PaymentInformation? result = await paymentService.CheckAndStorePaymentStatus( + instance, + paymentConfiguration, + language + ); + + // Assert + result.Should().NotBeNull(); + result!.PaymentDetails.Should().NotBeNull(); + result.Status.Should().Be(PaymentStatus.Paid); + } + + [Fact] + public async Task CancelPayment_ShouldCallCancelAndDelete_WhenPaymentIsNotPaid() + { + // Arrange + Instance instance = CreateInstance(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + OrderDetails orderDetails = CreateOrderDetails(); + PaymentInformation paymentInformation = CreatePaymentInformation(); + PaymentDetails paymentDetails = + paymentInformation.PaymentDetails ?? throw new NullReferenceException("PaymentDetails should not be null"); + const string language = "nb"; + + paymentInformation.Status = PaymentStatus.Cancelled; + + _dataService + .Setup(ds => ds.GetByType(It.IsAny(), It.IsAny())) + .ReturnsAsync((Guid.NewGuid(), paymentInformation)); + + _dataService.Setup(ds => ds.DeleteById(It.IsAny(), It.IsAny())).ReturnsAsync(true); + + _dataService + .Setup(x => + x.InsertJsonObject( + It.IsAny(), + paymentConfiguration.PaymentDataType!, + It.IsAny() + ) + ) + .ReturnsAsync(new DataElement()); + + _orderDetailsCalculator.Setup(p => p.CalculateOrderDetails(instance, language)).ReturnsAsync(orderDetails); + + _paymentProcessor.Setup(x => x.PaymentProcessorId).Returns(orderDetails.PaymentProcessorId); + + _paymentProcessor + .Setup(pp => pp.TerminatePayment(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + _paymentProcessor.Setup(x => x.StartPayment(instance, orderDetails, language)).ReturnsAsync(paymentDetails); + + // Act + await _paymentService.StartPayment(instance, paymentConfiguration, language); + + // Assert + _paymentProcessor.Verify( + pp => pp.TerminatePayment(It.IsAny(), It.IsAny()), + Times.Once + ); + _dataService.Verify(ds => ds.DeleteById(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task CancelPayment_ShouldNotDelete_WhenPaymentCancellationFails() + { + // Arrange + Instance instance = CreateInstance(); + OrderDetails orderDetails = CreateOrderDetails(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + PaymentInformation paymentInformation = CreatePaymentInformation(); + PaymentDetails paymentDetails = + paymentInformation.PaymentDetails ?? throw new NullReferenceException("PaymentDetails should not be null"); + const string language = "nb"; + + _orderDetailsCalculator.Setup(p => p.CalculateOrderDetails(instance, language)).ReturnsAsync(orderDetails); + _dataService + .Setup(ds => ds.InsertJsonObject(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new DataElement()); + _paymentProcessor.Setup(pp => pp.PaymentProcessorId).Returns(orderDetails.PaymentProcessorId); + _paymentProcessor.Setup(p => p.StartPayment(instance, orderDetails, language)).ReturnsAsync(paymentDetails); + + _dataService + .Setup(ds => ds.GetByType(It.IsAny(), It.IsAny())) + .ReturnsAsync((Guid.NewGuid(), paymentInformation)); + + _paymentProcessor + .Setup(pp => pp.TerminatePayment(It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + await Assert.ThrowsAsync( + async () => await _paymentService.StartPayment(instance, paymentConfiguration, language) + ); + + // Act & Assert + _paymentProcessor.Verify( + pp => pp.TerminatePayment(It.IsAny(), It.IsAny()), + Times.Once + ); + _dataService.Verify(ds => ds.DeleteById(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task StartPayment_ShouldThrowPaymentException_WhenOrderDetailsCalculatorIsNull() + { + // Arrange + Instance instance = CreateInstance(); + AltinnPaymentConfiguration paymentConfiguration = new() { PaymentDataType = "paymentDataType" }; + + IPaymentProcessor[] paymentProcessors = []; //No payment processor added. + var paymentService = new PaymentService(paymentProcessors, _dataService.Object, _logger.Object); + + // Act + Func act = async () => await paymentService.StartPayment(instance, paymentConfiguration, "en"); + + // Assert + await act.Should() + .ThrowAsync() + .WithMessage( + "You must add an implementation of the IOrderDetailsCalculator interface to the DI container. See payment related documentation." + ); + } + + [Fact] + public async Task CheckAndStorePaymentStatus_ShouldThrowPaymentException_WhenOrderDetailsCalculatorIsNull() + { + // Arrange + IPaymentProcessor[] paymentProcessors = []; + var paymentService = new PaymentService(paymentProcessors, _dataService.Object, _logger.Object); + Instance instance = CreateInstance(); + var paymentConfiguration = new AltinnPaymentConfiguration { PaymentDataType = "paymentDataType" }; + + // Act + Func act = async () => + await paymentService.CheckAndStorePaymentStatus(instance, paymentConfiguration, "en"); + + // Assert + await act.Should() + .ThrowAsync() + .WithMessage( + "You must add an implementation of the IOrderDetailsCalculator interface to the DI container. See payment related documentation." + ); + } + + [Fact] + public async Task IsPaymentCompleted_ShouldThrowPaymentException_WhenPaymentInformationNotFound() + { + // Arrange + Instance instance = CreateInstance(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + + string paymentDataType = + paymentConfiguration.PaymentDataType + ?? throw new Exception("PaymentDataType should not be null. Fix test."); + + _dataService + .Setup(ds => ds.GetByType(instance, paymentDataType)) + .ReturnsAsync((Guid.NewGuid(), null)); + + // Act + Func act = async () => await _paymentService.IsPaymentCompleted(instance, paymentConfiguration); + + // Assert + await act.Should().ThrowAsync().WithMessage("Payment information not found."); + } + + [Fact] + public async Task IsPaymentCompleted_ShouldReturnTrue_WhenPaymentStatusIsPaidOrSkipped() + { + // Arrange + Instance instance = CreateInstance(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + PaymentInformation paymentInformation = CreatePaymentInformation(); + paymentInformation.Status = PaymentStatus.Paid; + + string paymentDataType = + paymentConfiguration.PaymentDataType + ?? throw new Exception("PaymentDataType should not be null. Fix test."); + + _dataService + .Setup(ds => ds.GetByType(instance, paymentDataType)) + .ReturnsAsync((Guid.NewGuid(), paymentInformation)); + + // Act + bool result = await _paymentService.IsPaymentCompleted(instance, paymentConfiguration); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public async Task IsPaymentCompleted_ShouldReturnFalse_WhenPaymentStatusIsNotPaidOrSkipped() + { + // Arrange + Instance instance = CreateInstance(); + AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + PaymentInformation paymentInformation = CreatePaymentInformation(); + + string paymentDataType = + paymentConfiguration.PaymentDataType + ?? throw new Exception("PaymentDataType should not be null. Fix test."); + + _dataService + .Setup(ds => ds.GetByType(instance, paymentDataType)) + .ReturnsAsync((Guid.NewGuid(), paymentInformation)); + + // Act + var result = await _paymentService.IsPaymentCompleted(instance, paymentConfiguration); + + // Assert + result.Should().BeFalse(); + } + + private static PaymentInformation CreatePaymentInformation() + { + return new PaymentInformation + { + TaskId = "taskId", + Status = PaymentStatus.Created, + OrderDetails = new OrderDetails + { + PaymentProcessorId = "paymentProcessorId", + Currency = "NOK", + OrderLines = [], + Receiver = new PaymentReceiver() + }, + PaymentDetails = new PaymentDetails { RedirectUrl = "Redirect URL", PaymentId = "paymentId", } + }; + } + + private static Instance CreateInstance() + { + return new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo { AltinnTaskType = "payment", ElementId = "Task_1", }, + }, + }; + } + + private static OrderDetails CreateOrderDetails() + { + return new OrderDetails() + { + PaymentProcessorId = "paymentProcessorId", + Currency = "NOK", + OrderLines = + [ + new PaymentOrderLine + { + Id = "001", + Name = "Fee", + PriceExVat = 1000, + VatPercent = 25, + } + ], + Receiver = new PaymentReceiver() + }; + } + + private static AltinnPaymentConfiguration CreatePaymentConfiguration() + { + return new AltinnPaymentConfiguration { PaymentDataType = "paymentInformation" }; + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/HttpApiResultTests.cs b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/HttpApiResultTests.cs new file mode 100644 index 000000000..679a26095 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/HttpApiResultTests.cs @@ -0,0 +1,73 @@ +using System.Net; +using Altinn.App.Core.Features.Payment.Processors.Nets.Models; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Payment.Providers.Nets; + +public class HttpApiResultTests +{ + [Fact] + public async Task FromHttpResponse_ReturnsSuccessResult_WhenStatusCodeIsOk() + { + // Arrange + using var mockResponse = new HttpResponseMessage(HttpStatusCode.OK); + mockResponse.Content = new StringContent("{\"property\": \"value\"}"); + + // Act + HttpApiResult result = await HttpApiResult.FromHttpResponse(mockResponse); + + // Assert + Assert.True(result.IsSuccess); + Assert.NotNull(result.Result); + Assert.Equal(HttpStatusCode.OK, result.Status); + } + + [Fact] + public async Task FromHttpResponse_ReturnsNoContentResult_WhenStatusCodeIsNoContent() + { + // Arrange + using var mockResponse = new HttpResponseMessage(HttpStatusCode.NoContent); + + // Act + HttpApiResult result = await HttpApiResult.FromHttpResponse(mockResponse); + + // Assert + Assert.False(result.IsSuccess); + Assert.Null(result.Result); + Assert.Equal(HttpStatusCode.NoContent, result.Status); + } + + [Fact] + public async Task FromHttpResponse_ReturnsErrorResult_WhenStatusCodeIsError() + { + // Arrange + using var mockResponse = new HttpResponseMessage(HttpStatusCode.InternalServerError); + mockResponse.Content = new StringContent("Internal Server Error"); + + // Act + HttpApiResult result = await HttpApiResult.FromHttpResponse(mockResponse); + + // Assert + Assert.False(result.IsSuccess); + Assert.Null(result.Result); + Assert.Equal(HttpStatusCode.InternalServerError, result.Status); + Assert.Equal("Internal Server Error", result.RawError); + } + + [Fact] + public async Task FromHttpResponse_ReturnsErrorResult_WhenJsonDeserializationFails() + { + // Arrange + using var mockResponse = new HttpResponseMessage(HttpStatusCode.OK); + mockResponse.Content = new StringContent("Invalid Json"); + + // Act + HttpApiResult result = await HttpApiResult.FromHttpResponse(mockResponse); + + // Assert + Assert.False(result.IsSuccess); + Assert.Null(result.Result); + Assert.Equal(HttpStatusCode.OK, result.Status); + Assert.NotNull(result.RawError); + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs new file mode 100644 index 000000000..6e5f752a8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs @@ -0,0 +1,206 @@ +using Altinn.App.Core.Features.Payment.Models; +using Altinn.App.Core.Features.Payment.Processors.Nets; +using Altinn.App.Core.Features.Payment.Processors.Nets.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Payment.Providers.Nets; + +public class NetsMapperTests +{ + [Fact] + public void MapPayerDetails_ValidConsumer_ReturnsPayer() + { + // Arrange + var consumer = new NetsConsumer + { + Company = new NetsCompany + { + Name = "Firmanavn", + RegistrationNumber = "123456789", + ContactDetails = new NetsContactDetails + { + FirstName = "Ola", + LastName = "Normann", + Email = "ola.normann@example.com", + PhoneNumber = new NetsPhoneNumber { Prefix = "+47", Number = "12345678" } + } + }, + PrivatePerson = new NetsPrivatePerson + { + FirstName = "Kari", + LastName = "Normann", + Email = "ola.normann@example.com", + PhoneNumber = new NetsPhoneNumber { Prefix = "+47", Number = "87654321" } + }, + ShippingAddress = new NetsAddress + { + ReceiverLine = "Ola Normann", + AddressLine1 = "Gate 1", + AddressLine2 = "Adresselinje 1", + PostalCode = "1234", + City = "By", + Country = "Land" + }, + BillingAddress = new NetsAddress + { + ReceiverLine = "Kari Normann", + AddressLine1 = "Gate 2", + AddressLine2 = "Adresselinje 2", + PostalCode = "5678", + City = "By", + Country = "Land" + } + }; + + // Act + Payer? result = NetsMapper.MapPayerDetails(consumer); + + // Assert + result.Should().NotBeNull(); + result!.Company.Should().NotBeNull(); + result.Company!.Name.Should().Be("Firmanavn"); + result.Company.OrganisationNumber.Should().Be("123456789"); + result.Company.ContactPerson.Should().NotBeNull(); + result.Company.ContactPerson!.FirstName.Should().Be("Ola"); + result.Company.ContactPerson.LastName.Should().Be("Normann"); + result.Company.ContactPerson.Email.Should().Be("ola.normann@example.com"); + result.Company.ContactPerson.PhoneNumber.Should().NotBeNull(); + result.Company.ContactPerson.PhoneNumber!.Prefix.Should().Be("+47"); + result.Company.ContactPerson.PhoneNumber.Number.Should().Be("12345678"); + + result.PrivatePerson.Should().NotBeNull(); + result.PrivatePerson!.FirstName.Should().Be("Kari"); + result.PrivatePerson.LastName.Should().Be("Normann"); + result.PrivatePerson.Email.Should().Be("ola.normann@example.com"); + result.PrivatePerson.PhoneNumber.Should().NotBeNull(); + result.PrivatePerson.PhoneNumber!.Prefix.Should().Be("+47"); + result.PrivatePerson.PhoneNumber.Number.Should().Be("87654321"); + + result.ShippingAddress.Should().NotBeNull(); + result.ShippingAddress!.Name.Should().Be("Ola Normann"); + result.ShippingAddress.AddressLine1.Should().Be("Gate 1"); + result.ShippingAddress.AddressLine2.Should().Be("Adresselinje 1"); + result.ShippingAddress.PostalCode.Should().Be("1234"); + result.ShippingAddress.City.Should().Be("By"); + result.ShippingAddress.Country.Should().Be("Land"); + + result.BillingAddress.Should().NotBeNull(); + result.BillingAddress!.Name.Should().Be("Kari Normann"); + result.BillingAddress.AddressLine1.Should().Be("Gate 2"); + result.BillingAddress.AddressLine2.Should().Be("Adresselinje 2"); + result.BillingAddress.PostalCode.Should().Be("5678"); + result.BillingAddress.City.Should().Be("By"); + result.BillingAddress.Country.Should().Be("Land"); + } + + [Fact] + public void MapConsumerTypes_ValidPayerTypes_ReturnsCorrectConsumerTypes() + { + // Arrange + var payerTypes = new PayerType[] { PayerType.Company, PayerType.Person }; + + // Act + List result = NetsMapper.MapConsumerTypes(payerTypes); + + // Assert + result.Should().Contain("B2B"); + result.Should().Contain("B2C"); + } + + [Fact] + public void MapInvoiceDetails_ValidNetsInvoiceDetails_ReturnsInvoiceDetails() + { + // Arrange + var netsInvoiceDetails = new NetsInvoiceDetails { InvoiceNumber = "INV123456" }; + + // Act + InvoiceDetails? result = NetsMapper.MapInvoiceDetails(netsInvoiceDetails); + + // Assert + result.Should().NotBeNull(); + result!.InvoiceNumber.Should().Be("INV123456"); + } + + [Fact] + public void MapCardDetails_ValidNetsCardDetails_ReturnsCardDetails() + { + // Arrange + var netsCardDetails = new NetsCardDetails { MaskedPan = "1234********5678", ExpiryDate = "12/25" }; + + // Act + CardDetails? result = NetsMapper.MapCardDetails(netsCardDetails); + + // Assert + result.Should().NotBeNull(); + result!.MaskedPan.Should().Be("1234********5678"); + result.ExpiryDate.Should().Be("12/25"); + } + + [Fact] + public void MapAddress_NullAddress_ReturnsNull() + { + NetsAddress? address = null; + Address? result = NetsMapper.MapAddress(address); + result.Should().BeNull(); + } + + [Fact] + public void MapAddress_ValidAddress_ReturnsMappedAddress() + { + // Arrange + var address = new NetsAddress + { + ReceiverLine = "Mottaker", + AddressLine1 = "Adresselinje 1", + AddressLine2 = "Adresselinje 2", + PostalCode = "1234", + City = "By", + Country = "Land" + }; + + // Act + Address? result = NetsMapper.MapAddress(address); + + // Assert + result.Should().NotBeNull(); + result!.Name.Should().Be("Mottaker"); + result.AddressLine1.Should().Be("Adresselinje 1"); + result.AddressLine2.Should().Be("Adresselinje 2"); + result.PostalCode.Should().Be("1234"); + result.City.Should().Be("By"); + result.Country.Should().Be("Land"); + } + + [Fact] + public void MapPayerDetails_NullConsumer_ReturnsNull() + { + NetsConsumer? consumer = null; + Payer? result = NetsMapper.MapPayerDetails(consumer); + result.Should().BeNull(); + } + + [Fact] + public void MapConsumerTypes_NullPayerTypes_ReturnsEmptyList() + { + PayerType[]? payerTypes = null; + List result = NetsMapper.MapConsumerTypes(payerTypes); + result.Should().BeEmpty(); + } + + [Fact] + public void MapCardDetails_NullNetsCardDetails_ReturnsNull() + { + NetsCardDetails? netsCardDetails = null; + CardDetails? result = NetsMapper.MapCardDetails(netsCardDetails); + result.Should().BeNull(); + } + + [Fact] + public void MapInvoiceDetails_NullNetsInvoiceDetails_ReturnsNull() + { + NetsInvoiceDetails? netsInvoiceDetails = null; + InvoiceDetails? result = NetsMapper.MapInvoiceDetails(netsInvoiceDetails); + result.Should().BeNull(); + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsPaymentProcessorTests.cs b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsPaymentProcessorTests.cs new file mode 100644 index 000000000..a9a18ea90 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsPaymentProcessorTests.cs @@ -0,0 +1,270 @@ +using System.Diagnostics; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Payment; +using Altinn.App.Core.Features.Payment.Exceptions; +using Altinn.App.Core.Features.Payment.Models; +using Altinn.App.Core.Features.Payment.Processors.Nets; +using Altinn.App.Core.Features.Payment.Processors.Nets.Models; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Payment.Providers.Nets; + +public class NetsPaymentProcessorTests +{ + private readonly Mock _netsClientMock; + private readonly IOptions _settings; + private readonly IOptions _generalSettings; + private readonly NetsPaymentProcessor _processor; + + public NetsPaymentProcessorTests() + { + _netsClientMock = new Mock(); + _settings = Microsoft.Extensions.Options.Options.Create( + new NetsPaymentSettings + { + SecretApiKey = "secret", + BaseUrl = "baseUrl", + TermsUrl = "termsUrl", + } + ); + _generalSettings = Microsoft.Extensions.Options.Options.Create(new GeneralSettings()); + _processor = new NetsPaymentProcessor(_netsClientMock.Object, _settings, _generalSettings); + } + + [Fact] + public async Task StartPayment_WithValidOrderDetails_ReturnsPaymentInformation() + { + // Arrange + Instance instance = CreateInstance(); + var orderDetails = new OrderDetails + { + PaymentProcessorId = "paymentProcessorId", + Currency = "NOK", + OrderLines = [], + Receiver = new PaymentReceiver() + }; + + _netsClientMock + .Setup(x => x.CreatePayment(It.IsAny())) + .ReturnsAsync( + new HttpApiResult + { + Result = new NetsCreatePaymentSuccess + { + HostedPaymentPageUrl = "https://payment-url.com", + PaymentId = "12345" + } + } + ); + + // Act + PaymentDetails result = await _processor.StartPayment(instance, orderDetails, "nb"); + + // Assert + Assert.NotNull(result); + Assert.Equal("12345", result.PaymentId); + Assert.Equal("https://payment-url.com?language=nb-NO", result.RedirectUrl); + } + + [Fact] + public async Task StartPayment_WithValidInstanceAndOrderDetails_ReturnsPaymentInformation() + { + // Arrange + Instance instance = CreateInstance(); + var orderDetails = new OrderDetails + { + PaymentProcessorId = "paymentProcessorId", + Currency = "NOK", + OrderLines = + [ + new PaymentOrderLine() + { + Id = "1", + Name = "Item 1", + Quantity = 1, + PriceExVat = 100, + VatPercent = 25M + }, + new PaymentOrderLine() + { + Id = "2", + Name = "Item 2", + Quantity = 2, + PriceExVat = 200, + VatPercent = 25M + } + ], + Receiver = new PaymentReceiver() + }; + + int expectedSum = orderDetails.OrderLines.Sum(x => + (int)(x.PriceExVat * 100 * x.Quantity * (1 + (x.VatPercent / 100))) + ); + + _netsClientMock + .Setup(x => x.CreatePayment(It.IsAny())) + .ReturnsAsync( + new HttpApiResult + { + Result = new NetsCreatePaymentSuccess + { + HostedPaymentPageUrl = "https://payment-url.com", + PaymentId = "12345" + } + } + ); + + // Act + PaymentDetails result = await _processor.StartPayment(instance, orderDetails, "nb"); + + // Assert + result.Should().NotBeNull(); + result.PaymentId.Should().Be("12345"); + result.RedirectUrl.Should().Be("https://payment-url.com?language=nb-NO"); + + _netsClientMock.Verify( + x => + x.CreatePayment( + It.Is(netsCreatePayment => netsCreatePayment.Order.Amount == expectedSum) + ), + Times.Once + ); + } + + [Fact] + public async Task StartPayment_WithInvalidOrderDetails_ThrowsPaymentException() + { + // Arrange + Instance instance = CreateInstance(); + var orderDetails = new OrderDetails + { + PaymentProcessorId = "paymentProcessorId", + Currency = "NOK", + OrderLines = [], + Receiver = new PaymentReceiver() + }; + + _netsClientMock + .Setup(x => x.CreatePayment(It.IsAny())) + .ReturnsAsync(new HttpApiResult()); + + // Act & Assert + await Assert.ThrowsAsync(() => _processor.StartPayment(instance, orderDetails, null)); + } + + [Fact] + public async Task CancelPayment_WithValidPaymentReference_CallsNetsClientCancelPayment() + { + // Arrange + Instance instance = CreateInstance(); + PaymentInformation paymentInformation = + new() + { + TaskId = "taskId", + Status = PaymentStatus.Created, + OrderDetails = new OrderDetails + { + PaymentProcessorId = "paymentProcessorId", + Currency = "NOK", + OrderReference = "orderReference", + OrderLines = + [ + new PaymentOrderLine + { + Id = "1", + Name = "Item 1", + PriceExVat = 500, + VatPercent = 25, + } + ], + Receiver = new PaymentReceiver() + }, + PaymentDetails = new PaymentDetails { PaymentId = "paymentReference", RedirectUrl = "redirectUrl", }, + }; + + _netsClientMock.Setup(x => x.TerminatePayment(paymentInformation.PaymentDetails.PaymentId)).ReturnsAsync(true); + + // Act + await _processor.TerminatePayment(instance, paymentInformation); + + // Assert + _netsClientMock.Verify(x => x.TerminatePayment(paymentInformation.PaymentDetails.PaymentId), Times.Once); + } + + [Fact] + public async Task GetPaymentStatus_WithValidPaymentReferenceAndExpectedTotal_ReturnsPaymentStatus() + { + // Arrange + Instance instance = CreateInstance(); + const string paymentReference = "12345"; + const decimal expectedTotalIncVat = 100; + const string language = "nb"; + + _netsClientMock + .Setup(x => x.RetrievePayment(paymentReference)) + .ReturnsAsync( + new HttpApiResult + { + Result = new NetsPaymentFull + { + Payment = new NetsPayment + { + Summary = new NetsSummary + { + // All amounts sent to and received from Nets are in the lowest monetary unit for the given currency, without punctuation marks. + ChargedAmount = expectedTotalIncVat * 100 + }, + Checkout = new() { Url = "https://redirect-url.com", CancelUrl = "https://cancel-url.com" } + } + } + } + ); + + // Act + (PaymentStatus result, PaymentDetails paymentDetails) = await _processor.GetPaymentStatus( + instance, + paymentReference, + expectedTotalIncVat, + language + ); + + // Assert + Assert.Equal(PaymentStatus.Paid, result); + paymentDetails.RedirectUrl.Should().Contain("language=nb-NO"); + } + + [Fact] + public async Task GetPaymentStatus_WithInvalidPaymentReference_ThrowsPaymentException() + { + // Arrange + Instance instance = CreateInstance(); + const string paymentReference = "12345"; + const decimal expectedTotalIncVat = 100; + + _netsClientMock + .Setup(x => x.RetrievePayment(paymentReference)) + .ReturnsAsync(new HttpApiResult()); + + // Act & Assert + await Assert.ThrowsAsync( + () => _processor.GetPaymentStatus(instance, paymentReference, expectedTotalIncVat, null) + ); + } + + private static Instance CreateInstance() + { + return new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo { AltinnTaskType = "payment", ElementId = "Task_1", }, + }, + }; + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandlerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandlerTests.cs index f4aee529b..4a5c4a602 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandlerTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandlerTests.cs @@ -21,7 +21,7 @@ public StartTaskEventHandlerTests() } [Fact] - public async Task Execute_handles_no_IProcessTaskStart_injected() + public async Task Execute_handles_happy_path() { StartTaskEventHandler steh = new StartTaskEventHandler( _processTaskDataLocker.Object, diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 14c91ba35..c59d7e38b 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -550,7 +550,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_abandon_instance ElementId = "Task_2", Flow = 3, AltinnTaskType = "confirmation", - FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), + FlowType = ProcessSequenceFlowType.AbandonCurrentMoveToNext.ToString(), Name = "Bekreft" }, StartEvent = "StartEvent_1" @@ -638,7 +638,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_abandon_instance ElementId = "Task_2", Name = "Bekreft", AltinnTaskType = "confirmation", - FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), + FlowType = ProcessSequenceFlowType.AbandonCurrentMoveToNext.ToString(), Flow = 3 } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs new file mode 100644 index 000000000..091793c92 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs @@ -0,0 +1,251 @@ +using Altinn.App.Core.Features.Payment.Exceptions; +using Altinn.App.Core.Features.Payment.Services; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.ProcessTasks; + +public class PaymentProcessTaskTests +{ + public class ProcessTaskDataLockerTests + { + private readonly Mock _pdfServiceMock; + private readonly Mock _dataClientMock; + private readonly Mock _processReaderMock; + private readonly Mock _paymentServiceMock; + private readonly PaymentProcessTask _paymentProcessTask; + + public ProcessTaskDataLockerTests() + { + _pdfServiceMock = new Mock(); + _dataClientMock = new Mock(); + _processReaderMock = new Mock(); + _paymentServiceMock = new Mock(); + + _paymentProcessTask = new PaymentProcessTask( + _pdfServiceMock.Object, + _dataClientMock.Object, + _processReaderMock.Object, + _paymentServiceMock.Object + ); + } + + [Fact] + public async Task Start_ShouldCancelAndDelete() + { + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; + + var altinnTaskExtension = new AltinnTaskExtension + { + PaymentConfiguration = new AltinnPaymentConfiguration { PaymentDataType = "paymentDataType" } + }; + + _processReaderMock.Setup(x => x.GetAltinnTaskExtension(It.IsAny())).Returns(altinnTaskExtension); + + // Act + await _paymentProcessTask.Start(taskId, instance); + + // Assert + _paymentServiceMock.Verify(x => + x.CancelAndDeleteAnyExistingPayment(instance, altinnTaskExtension.PaymentConfiguration) + ); + } + + [Fact] + public async Task End_PaymentCompleted_ShouldGeneratePdfReceipt() + { + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; + + var altinnTaskExtension = new AltinnTaskExtension + { + PaymentConfiguration = new AltinnPaymentConfiguration { PaymentDataType = "paymentDataType" } + }; + + _processReaderMock.Setup(x => x.GetAltinnTaskExtension(It.IsAny())).Returns(altinnTaskExtension); + + _paymentServiceMock + .Setup(x => x.IsPaymentCompleted(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + // Act + await _paymentProcessTask.End(taskId, instance); + + // Assert + _pdfServiceMock.Verify(x => x.GeneratePdf(instance, taskId, CancellationToken.None)); + _dataClientMock.Verify(x => + x.InsertBinaryData( + instance.Id, + altinnTaskExtension.PaymentConfiguration.PaymentDataType, + "application/pdf", + "Betalingskvittering.pdf", + It.IsAny(), + taskId + ) + ); + } + + [Fact] + public async Task End_PaymentNotCompleted_ShouldThrowException() + { + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; + + var altinnTaskExtension = new AltinnTaskExtension + { + PaymentConfiguration = new AltinnPaymentConfiguration { PaymentDataType = "paymentDataType" } + }; + + _processReaderMock.Setup(x => x.GetAltinnTaskExtension(It.IsAny())).Returns(altinnTaskExtension); + + _paymentServiceMock + .Setup(x => x.IsPaymentCompleted(It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + // Act and assert + _pdfServiceMock.Verify(x => x.GeneratePdf(instance, taskId, CancellationToken.None), Times.Never); + _dataClientMock.Verify( + x => + x.InsertBinaryData( + instance.Id, + altinnTaskExtension.PaymentConfiguration.PaymentDataType, + "application/pdf", + "Betalingskvittering.pdf", + It.IsAny(), + taskId + ), + Times.Never + ); + + await Assert.ThrowsAsync(async () => await _paymentProcessTask.End(taskId, instance)); + } + + [Fact] + public async Task Abandon_ShouldCancelAndDelete() + { + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; + + var altinnTaskExtension = new AltinnTaskExtension + { + PaymentConfiguration = new AltinnPaymentConfiguration { PaymentDataType = "paymentDataType" } + }; + + _processReaderMock.Setup(x => x.GetAltinnTaskExtension(It.IsAny())).Returns(altinnTaskExtension); + + // Act + await _paymentProcessTask.Abandon(taskId, instance); + + // Assert + _paymentServiceMock.Verify(x => + x.CancelAndDeleteAnyExistingPayment(instance, altinnTaskExtension.PaymentConfiguration) + ); + } + + [Fact] + public async Task End_PaymentConfigurationIsNull_ShouldThrowApplicationConfigException() + { + _processReaderMock + .Setup(pr => pr.GetAltinnTaskExtension(It.IsAny())) + .Returns((AltinnTaskExtension?)null); + + Func act = async () => await _paymentProcessTask.End("taskId", new Instance()); + + await act.Should().ThrowAsync().WithMessage("*PaymentConfig is missing*"); + } + + [Fact] + public async Task End_PaymentDataTypeIsNullOrWhitespace_ShouldThrowApplicationConfigException() + { + _processReaderMock + .Setup(pr => pr.GetAltinnTaskExtension(It.IsAny())) + .Returns( + new AltinnTaskExtension + { + PaymentConfiguration = new AltinnPaymentConfiguration { PaymentDataType = "" } + } + ); + + Func act = async () => await _paymentProcessTask.End("taskId", new Instance()); + + await act.Should().ThrowAsync().WithMessage("*PaymentDataType is missing*"); + } + + [Fact] + public async Task End_ValidConfiguration_ShouldNotThrow() + { + _processReaderMock + .Setup(pr => pr.GetAltinnTaskExtension(It.IsAny())) + .Returns( + new AltinnTaskExtension + { + PaymentConfiguration = new AltinnPaymentConfiguration { PaymentDataType = "paymentDataType" } + } + ); + + _paymentServiceMock + .Setup(ps => ps.IsPaymentCompleted(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + using var memoryStream = new MemoryStream(); + _pdfServiceMock + .Setup(ps => ps.GeneratePdf(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(memoryStream); + + Func act = async () => await _paymentProcessTask.End("taskId", new Instance()); + + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task Abandon_PaymentConfigurationIsNull_ShouldThrowApplicationConfigException() + { + _processReaderMock + .Setup(pr => pr.GetAltinnTaskExtension(It.IsAny())) + .Returns((AltinnTaskExtension?)null); + + Func act = async () => await _paymentProcessTask.Abandon("taskId", new Instance()); + + await act.Should().ThrowAsync().WithMessage("*PaymentConfig is missing*"); + } + + [Fact] + public async Task Abandon_ValidConfiguration_ShouldNotThrow() + { + _processReaderMock + .Setup(pr => pr.GetAltinnTaskExtension(It.IsAny())) + .Returns( + new AltinnTaskExtension + { + PaymentConfiguration = new AltinnPaymentConfiguration { PaymentDataType = "paymentDataType" } + } + ); + + Func act = async () => await _paymentProcessTask.Abandon("taskId", new Instance()); + + await act.Should().NotThrowAsync(); + } + + private static Instance CreateInstance() + { + return new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo { AltinnTaskType = "payment", ElementId = "Task_1", }, + }, + }; + } + } +} From d70d21aaba93769675a57569a897e307449755cb Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Sat, 25 May 2024 21:33:39 +0200 Subject: [PATCH 16/22] Remove/fix or suppress null-forgiving operator usage in `src/`, enable warning/build errors (#636) --- .editorconfig | 3 ++ Directory.Build.props | 5 --- .../Controllers/InstancesController.cs | 22 +++++++-- .../Controllers/PdfController.cs | 6 ++- .../RequestHandling/MultipartRequestReader.cs | 3 +- .../Helpers/RequestHandling/RequestPart.cs | 10 ++++- src/Altinn.App.Api/Models/SimpleInstance.cs | 9 +++- .../DefaultEFormidlingService.cs | 4 +- .../EformidlingStatusCheckEventHandler.cs | 4 +- .../EformidlingStatusCheckEventHandler2.cs | 4 +- .../Extensions/HttpContextExtensions.cs | 3 +- .../Extensions/XmlToLinqExtensions.cs | 5 ++- .../Features/Action/SigningUserAction.cs | 18 ++++---- .../Action/UniqueSignatureAuthorizer.cs | 6 +-- .../Altinn2CodeListProvider.cs | 1 + .../Features/Options/AppOptionsFileHandler.cs | 4 +- .../Processors/Nets/NetsPaymentProcessor.cs | 11 ++++- .../Payment/Services/PaymentService.cs | 9 +++- .../Default/DataAnnotationValidator.cs | 10 ++++- ...gacyIInstanceValidatorFormDataValidator.cs | 11 ++++- .../LegacyIInstanceValidatorTaskValidator.cs | 11 ++++- .../Validation/Helpers/ModelStateHelpers.cs | 2 +- src/Altinn.App.Core/Helpers/ProcessHelper.cs | 3 ++ .../Serialization/ModelDeserializer.cs | 2 +- .../Implementation/AppResourcesSI.cs | 9 ++-- .../Implementation/PrefillSI.cs | 30 ++++++++++--- .../Clients/Events/EventsClient.cs | 2 +- .../Clients/KeyVault/SecretsLocalClient.cs | 1 + .../Clients/Storage/DataClient.cs | 13 +++++- .../Clients/Storage/InstanceClient.cs | 15 +++++-- .../Clients/Storage/InstanceEventClient.cs | 4 +- .../Clients/Storage/ProcessClient.cs | 1 + .../Internal/AppModel/DefaultAppModel.cs | 7 ++- .../Expressions/ExpressionEvaluator.cs | 1 + .../Internal/Language/ApplicationLanguage.cs | 2 + .../Internal/Linq/Extensions.cs | 18 ++++++++ .../Internal/Patch/PatchService.cs | 6 +-- .../Internal/Process/ProcessEngine.cs | 28 +++++++++--- .../Process/ProcessEventHandlingDelegator.cs | 3 ++ .../Internal/Process/ProcessReader.cs | 7 +-- .../Common/ProcessTaskFinalizer.cs | 6 ++- .../ProcessTasks/PaymentProcessTask.cs | 5 ++- .../Models/ApplicationMetadata.cs | 3 +- .../Models/Layout/Components/GridComponent.cs | 10 ++--- .../Models/Layout/PageComponentConverter.cs | 45 ++++++++++++++----- .../Models/Validation/ValidationIssue.cs | 5 ++- src/Directory.Build.props | 4 ++ .../Validators/ValidationServiceTests.cs | 22 +++++++++ .../LayoutModelConverterFromObject.cs | 15 +++++-- 49 files changed, 333 insertions(+), 95 deletions(-) create mode 100644 src/Altinn.App.Core/Internal/Linq/Extensions.cs diff --git a/.editorconfig b/.editorconfig index 5fb6a86b8..5243a2812 100644 --- a/.editorconfig +++ b/.editorconfig @@ -102,6 +102,9 @@ dotnet_diagnostic.CA2254.severity = none # CA1822: Mark members as static dotnet_diagnostic.CA1822.severity = suggestion +# IDE0080: Remove unnecessary suppression operator +dotnet_diagnostic.IDE0080.severity = error + [Program.cs] dotnet_diagnostic.CA1050.severity = none dotnet_diagnostic.S1118.severity = none diff --git a/Directory.Build.props b/Directory.Build.props index 7f5799428..ad238e3a6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,10 +16,5 @@ all runtime; build; native; contentfiles; analyzers - - diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index a32dfa55e..2e895eb7d 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -231,10 +231,15 @@ [FromQuery] int? instanceOwnerPartyId } else { + if (instanceOwnerPartyId is null) + { + return StatusCode(500, "Can't create minimal instance when Instance owner Party ID is null"); + } + // create minimum instance template instanceTemplate = new Instance { - InstanceOwner = new InstanceOwner { PartyId = instanceOwnerPartyId!.Value.ToString() } + InstanceOwner = new InstanceOwner { PartyId = instanceOwnerPartyId.Value.ToString() } }; } @@ -1072,6 +1077,15 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI foreach (RequestPart part in parts) { + // NOTE: part.Name is nullable on the type here, but `RequestPartValidator.ValidatePart` which is called + // further up the stack will error out if it actually null, so we just sanity-check here + // and throw if it is null. + // TODO: improve the modelling of this type. + if (part.Name is null) + { + throw new InvalidOperationException("Unexpected state - part name is null"); + } + DataType? dataType = appInfo.DataTypes.Find(d => d.Id == part.Name); DataElement dataElement; @@ -1101,7 +1115,7 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI throw new InvalidOperationException(deserializer.Error); } - await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, part.Name!, data); + await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, part.Name, data); await _instantiationProcessor.DataCreation(instance, data, null); @@ -1114,14 +1128,14 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI org, app, instanceOwnerIdAsInt, - part.Name! + part.Name ); } else { dataElement = await _dataClient.InsertBinaryData( instance.Id, - part.Name!, + part.Name, part.ContentType, part.FileName, part.Stream diff --git a/src/Altinn.App.Api/Controllers/PdfController.cs b/src/Altinn.App.Api/Controllers/PdfController.cs index 97d49b650..81ea4944a 100644 --- a/src/Altinn.App.Api/Controllers/PdfController.cs +++ b/src/Altinn.App.Api/Controllers/PdfController.cs @@ -128,7 +128,9 @@ [FromRoute] Guid dataGuid LayoutSet? layoutSet = null; if (!string.IsNullOrEmpty(layoutSetsString)) { - layoutSets = JsonSerializer.Deserialize(layoutSetsString, _jsonSerializerOptions)!; + layoutSets = + JsonSerializer.Deserialize(layoutSetsString, _jsonSerializerOptions) + ?? throw new JsonException("Could not deserialize LayoutSets"); layoutSet = layoutSets.Sets?.FirstOrDefault(t => t.DataType.Equals(dataElement.DataType) && t.Tasks.Contains(taskId) ); @@ -145,7 +147,7 @@ [FromRoute] Guid dataGuid layoutSettings = JsonSerializer.Deserialize( layoutSettingsFileContent, _jsonSerializerOptions - )!; + ); } // Ensure layoutsettings are initialized in FormatPdf diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs b/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs index e9d53242a..462a35ada 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs @@ -132,7 +132,8 @@ out ContentDispositionHeaderValue? contentDisposition private string GetBoundary() { MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(request.ContentType); - return mediaType.Boundary.Value!.Trim('"'); + return mediaType.Boundary.Value?.Trim('"') + ?? throw new Exception("Could not retrieve boundary value from Content-Type header"); } } } diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs index e9fe66efb..b0b37213c 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs @@ -8,7 +8,10 @@ public class RequestPart /// /// The stream to access this part. /// - public Stream Stream { get; set; } = default!; +#nullable disable + public Stream Stream { get; set; } + +#nullable restore /// /// The file name as given in content description. @@ -23,7 +26,10 @@ public class RequestPart /// /// The content type of the part. /// - public string ContentType { get; set; } = default!; +#nullable disable + public string ContentType { get; set; } + +#nullable restore /// /// The file size of the part, 0 if not given. diff --git a/src/Altinn.App.Api/Models/SimpleInstance.cs b/src/Altinn.App.Api/Models/SimpleInstance.cs index 839bfebe7..626a59eb9 100644 --- a/src/Altinn.App.Api/Models/SimpleInstance.cs +++ b/src/Altinn.App.Api/Models/SimpleInstance.cs @@ -8,7 +8,10 @@ public class SimpleInstance /// /// The instance identifier formated as {instanceOwner.partyId}/{instanceGuid}. /// - public string Id { get; set; } = default!; +#nullable disable + public string Id { get; set; } + +#nullable restore /// /// Presentation texts from the instance @@ -28,5 +31,7 @@ public class SimpleInstance /// /// Full name of user to last change the instance. /// - public string LastChangedBy { get; set; } = default!; +#nullable disable + public string LastChangedBy { get; set; } +#nullable restore } diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs index 48f476f79..dbe3e2d61 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.EFormidling.Interface; @@ -216,7 +217,8 @@ private async Task SendInstanceData(Instance instance, Dictionary AddCompleteConfirmation(InstanceIdentifier instanceIdentifier) + private async Task AddCompleteConfirmation(InstanceIdentifier instanceIdentifier) { string url = $"instances/{instanceIdentifier.InstanceOwnerPartyId}/{instanceIdentifier.InstanceGuid}/complete"; @@ -169,7 +169,7 @@ private async Task AddCompleteConfirmation(InstanceIdentifier instance if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); - Instance instance = JsonConvert.DeserializeObject(instanceData)!; + Instance? instance = JsonConvert.DeserializeObject(instanceData); return instance; } diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs index b5b23f5c6..dae04b275 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs @@ -133,7 +133,7 @@ await response.Content.ReadAsStringAsync() /// endpoint in InstanceController. This method should be removed once we have a better /// alernative for authenticating the app/org without having a http request context with /// a logged on user/org. - private async Task AddCompleteConfirmation(InstanceIdentifier instanceIdentifier) + private async Task AddCompleteConfirmation(InstanceIdentifier instanceIdentifier) { string url = $"instances/{instanceIdentifier.InstanceOwnerPartyId}/{instanceIdentifier.InstanceGuid}/complete"; @@ -154,7 +154,7 @@ private async Task AddCompleteConfirmation(InstanceIdentifier instance if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); - Instance instance = JsonConvert.DeserializeObject(instanceData)!; + Instance? instance = JsonConvert.DeserializeObject(instanceData); return instance; } diff --git a/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs index f12393448..ce049a48e 100644 --- a/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs +++ b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs @@ -15,7 +15,8 @@ public static class HttpContextExtensions public static StreamContent CreateContentStream(this HttpRequest request) { StreamContent content = new StreamContent(request.Body); - content.Headers.ContentType = MediaTypeHeaderValue.Parse(request.ContentType!); + ArgumentNullException.ThrowIfNull(request.ContentType); + content.Headers.ContentType = MediaTypeHeaderValue.Parse(request.ContentType); if (request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) { diff --git a/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs b/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs index 00d7a7c5a..e534e4353 100644 --- a/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs +++ b/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs @@ -27,7 +27,10 @@ public static class XmlToLinqExtensions /// public static XElement AddAttribute(this XElement element, string attributeName, object value) { - element.Add(new XAttribute(attributeName, value.ToString()!)); + var valueStr = + value.ToString() + ?? throw new ArgumentException("String representation of parameter 'value' is null", nameof(value)); + element.Add(new XAttribute(attributeName, valueStr)); return element; } diff --git a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs index 86444a228..f1c95b63c 100644 --- a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; @@ -75,14 +76,16 @@ public async Task HandleAction(UserActionContext context) ?.DataTypes?.Where(d => dataTypeIds.Contains(d.Id, StringComparer.OrdinalIgnoreCase)) .ToList(); - if (ShouldSign(currentTask, context.Instance.Data, dataTypesToSign)) + if ( + GetDataTypeForSignature(currentTask, context.Instance.Data, dataTypesToSign) is string signatureDataType + ) { var dataElementSignatures = GetDataElementSignatures(context.Instance.Data, dataTypesToSign); SignatureContext signatureContext = new( new InstanceIdentifier(context.Instance), currentTask.Id, - currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType!, + signatureDataType, await GetSignee(context.UserId.Value), dataElementSignatures ); @@ -100,24 +103,23 @@ await GetSignee(context.UserId.Value), ); } - private static bool ShouldSign( + private static string? GetDataTypeForSignature( ProcessTask currentTask, List dataElements, List? dataTypesToSign ) { - var signatureIsConfigured = - currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType is not null; - if (dataTypesToSign is null or [] || !signatureIsConfigured) + var signatureDataType = currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType; + if (dataTypesToSign is null or [] || signatureDataType is null) { - return false; + return null; } var dataElementMatchExists = dataElements.Any(de => dataTypesToSign.Any(dt => string.Equals(dt.Id, de.DataType, StringComparison.OrdinalIgnoreCase)) ); var allDataTypesAreOptional = dataTypesToSign.All(d => d.MinCount == 0); - return dataElementMatchExists || allDataTypesAreOptional; + return dataElementMatchExists || allDataTypesAreOptional ? signatureDataType : null; } private static List GetDataElementSignatures( diff --git a/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs index f4066185a..6deaf60f7 100644 --- a/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs +++ b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs @@ -64,9 +64,9 @@ public async Task AuthorizeAction(UserActionAuthorizerContext context) context.InstanceIdentifier.InstanceGuid ); var dataTypes = flowElement - .ExtensionElements! - .TaskExtension! - .SignatureConfiguration! + .ExtensionElements + .TaskExtension + .SignatureConfiguration .UniqueFromSignaturesInDataTypes; var signatureDataElements = instance.Data.Where(d => dataTypes.Contains(d.DataType)).ToList(); foreach (var signatureDataElement in signatureDataElements) diff --git a/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2CodeListProvider.cs b/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2CodeListProvider.cs index 40326e05e..5a253fce1 100644 --- a/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2CodeListProvider.cs +++ b/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2CodeListProvider.cs @@ -76,6 +76,7 @@ public async Task GetRawAltinn2CodelistAsync(string? l _ => "1044", // default to norwegian bokmål }; + // ! TODO: address this is next major release, should never return null return ( await _cache.GetOrCreateAsync( $"{_metadataApiId}{langCode}{_codeListVersion}", diff --git a/src/Altinn.App.Core/Features/Options/AppOptionsFileHandler.cs b/src/Altinn.App.Core/Features/Options/AppOptionsFileHandler.cs index ca12a2036..66026c9bb 100644 --- a/src/Altinn.App.Core/Features/Options/AppOptionsFileHandler.cs +++ b/src/Altinn.App.Core/Features/Options/AppOptionsFileHandler.cs @@ -37,10 +37,10 @@ public AppOptionsFileHandler(IOptions settings) if (File.Exists(filename)) { string fileData = await File.ReadAllTextAsync(filename, Encoding.UTF8); - List options = JsonSerializer.Deserialize>( + List? options = JsonSerializer.Deserialize>( fileData, _jsonSerializerOptions - )!; + ); return options; } diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs index e51c18384..3503e9812 100644 --- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs @@ -146,17 +146,24 @@ public async Task TerminatePayment(Instance instance, PaymentInformation p ); } - NetsPayment payment = httpApiResult.Result.Payment!; + NetsPayment payment = + httpApiResult.Result.Payment + ?? throw new PaymentException("Payment information is null in the response from Nets"); decimal? chargedAmount = payment.Summary?.ChargedAmount; PaymentStatus status = chargedAmount > 0 ? PaymentStatus.Paid : PaymentStatus.Created; NetsPaymentDetails? paymentPaymentDetails = payment.PaymentDetails; + var checkout = + payment.Checkout ?? throw new PaymentException("Checkout information is missing in the response from Nets"); + var checkoutUrl = + checkout.Url ?? throw new PaymentException("Checkout URL is missing in the response from Nets"); + PaymentDetails paymentDetails = new() { PaymentId = paymentId, - RedirectUrl = AddLanguageQueryParam(payment.Checkout!.Url!, language), + RedirectUrl = AddLanguageQueryParam(checkoutUrl, language), Payer = NetsMapper.MapPayerDetails(payment.Consumer), PaymentType = paymentPaymentDetails?.PaymentType, PaymentMethod = paymentPaymentDetails?.PaymentMethod, diff --git a/src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs b/src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs index 3c428daa2..5735a4194 100644 --- a/src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs +++ b/src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs @@ -53,6 +53,7 @@ public PaymentService( } ValidateConfig(paymentConfiguration); + // ! TODO: restructure code to avoid assertion (it is validated above) string dataTypeId = paymentConfiguration.PaymentDataType!; (Guid dataElementId, PaymentInformation? existingPaymentInformation) = @@ -132,6 +133,7 @@ public async Task CheckAndStorePaymentStatus( ValidateConfig(paymentConfiguration); + // ! TODO: restructure code to avoid assertion (it is validated above) string dataTypeId = paymentConfiguration.PaymentDataType!; (Guid dataElementId, PaymentInformation? paymentInformation) = await _dataService.GetByType( instance, @@ -153,7 +155,6 @@ public async Task CheckAndStorePaymentStatus( }; } - PaymentDetails paymentDetails = paymentInformation.PaymentDetails!; decimal totalPriceIncVat = paymentInformation.OrderDetails.TotalPriceIncVat; string paymentProcessorId = paymentInformation.OrderDetails.PaymentProcessorId; @@ -172,6 +173,10 @@ public async Task CheckAndStorePaymentStatus( _paymentProcessors.FirstOrDefault(p => p.PaymentProcessorId == paymentProcessorId) ?? throw new PaymentException($"Payment processor with ID '{paymentProcessorId}' not found."); + PaymentDetails paymentDetails = + paymentInformation.PaymentDetails + ?? throw new PaymentException("Payment details unexpectedly missing from payment information."); + (PaymentStatus paymentStatus, PaymentDetails updatedPaymentDetails) = await paymentProcessor.GetPaymentStatus( instance, paymentDetails.PaymentId, @@ -203,6 +208,7 @@ public async Task IsPaymentCompleted(Instance instance, AltinnPaymentConfi { ValidateConfig(paymentConfiguration); + // ! TODO: restructure code to avoid assertion (it is validated above) string dataTypeId = paymentConfiguration.PaymentDataType!; (Guid _, PaymentInformation? paymentInformation) = await _dataService.GetByType( instance, @@ -225,6 +231,7 @@ AltinnPaymentConfiguration paymentConfiguration { ValidateConfig(paymentConfiguration); + // ! TODO: restructure code to avoid assertion (it is validated above) string dataTypeId = paymentConfiguration.PaymentDataType!; (Guid dataElementId, PaymentInformation? paymentInformation) = await _dataService.GetByType( instance, diff --git a/src/Altinn.App.Core/Features/Validation/Default/DataAnnotationValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/DataAnnotationValidator.cs index e082d4a64..210afb25b 100644 --- a/src/Altinn.App.Core/Features/Validation/Default/DataAnnotationValidator.cs +++ b/src/Altinn.App.Core/Features/Validation/Default/DataAnnotationValidator.cs @@ -60,13 +60,21 @@ public Task> ValidateFormData( try { var modelState = new ModelStateDictionary(); + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext is null) + { + throw new Exception("Could not get HttpContext - must be in a request context to validate form data"); + } var actionContext = new ActionContext( - _httpContextAccessor.HttpContext!, + httpContext, new Microsoft.AspNetCore.Routing.RouteData(), new ActionDescriptor(), modelState ); ValidationStateDictionary validationState = new ValidationStateDictionary(); + // ! TODO: 'prefix' on the interfacee is non-nullable, but on the actual implementation + // ! TODO: it seems to be nullable, so this should be safe.. + // ! TODO: https://github.com/dotnet/aspnetcore/blob/5ff2399a2b9ea6346dcdcf2cc8ba65fba67d035a/src/Mvc/Mvc.Core/src/ModelBinding/ObjectModelValidator.cs#L41 _objectModelValidator.Validate(actionContext, validationState, null!, data); return Task.FromResult( diff --git a/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorFormDataValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorFormDataValidator.cs index 402202385..5533a391f 100644 --- a/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorFormDataValidator.cs +++ b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorFormDataValidator.cs @@ -1,4 +1,5 @@ #pragma warning disable CS0618 // Type or member is obsolete +using System.Diagnostics; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features.Validation.Helpers; using Altinn.App.Core.Models.Validation; @@ -34,7 +35,15 @@ public LegacyIInstanceValidatorFormDataValidator( public string DataType => _instanceValidator is null ? "" : "*"; /// > - public string ValidationSource => _instanceValidator?.GetType().FullName ?? GetType().FullName!; + public string ValidationSource + { + get + { + var type = _instanceValidator?.GetType() ?? GetType(); + Debug.Assert(type.FullName is not null, "FullName does not return null on class/struct types"); + return type.FullName; + } + } /// /// Always run for incremental validation (if it exists) diff --git a/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs index b22b90b12..246fb7ef7 100644 --- a/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs +++ b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs @@ -1,4 +1,5 @@ #pragma warning disable CS0618 // Type or member is obsolete +using System.Diagnostics; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features.Validation.Helpers; using Altinn.App.Core.Models.Validation; @@ -35,7 +36,15 @@ public LegacyIInstanceValidatorTaskValidator( public string TaskId => "*"; /// - public string ValidationSource => _instanceValidator?.GetType().FullName ?? GetType().FullName!; + public string ValidationSource + { + get + { + var type = _instanceValidator?.GetType() ?? GetType(); + Debug.Assert(type.FullName is not null, "FullName does not return null on class/struct types"); + return type.FullName; + } + } /// public async Task> ValidateTask(Instance instance, string taskId, string? language) diff --git a/src/Altinn.App.Core/Features/Validation/Helpers/ModelStateHelpers.cs b/src/Altinn.App.Core/Features/Validation/Helpers/ModelStateHelpers.cs index c491c539c..a5ab2fc19 100644 --- a/src/Altinn.App.Core/Features/Validation/Helpers/ModelStateHelpers.cs +++ b/src/Altinn.App.Core/Features/Validation/Helpers/ModelStateHelpers.cs @@ -49,7 +49,7 @@ string source DataElementId = dataElement.Id, Source = source, Code = severityAndMessage.Message, - Field = ModelKeyToField(modelKey, objectType)!, + Field = ModelKeyToField(modelKey, objectType), Severity = severityAndMessage.Severity, Description = severityAndMessage.Message } diff --git a/src/Altinn.App.Core/Helpers/ProcessHelper.cs b/src/Altinn.App.Core/Helpers/ProcessHelper.cs index c0e9ef1cd..786f07dde 100644 --- a/src/Altinn.App.Core/Helpers/ProcessHelper.cs +++ b/src/Altinn.App.Core/Helpers/ProcessHelper.cs @@ -17,6 +17,9 @@ public static class ProcessHelper /// List of possible start events /// Any error preventing the process from starting. /// The name of the start event or null if start event wasn't found. + // TODO: improve implementation of this so that we help out nullability flow analysis in the compiler + // i.e. startEventError is non-null when the function returns null + // this should probably also be internal... public static string? GetValidStartEventOrError( string? proposedStartEvent, List possibleStartEvents, diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs index 9bc2867cc..981c8362b 100644 --- a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs +++ b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs @@ -70,7 +70,7 @@ public ModelDeserializer(ILogger logger, Type modelType) { using StreamReader reader = new StreamReader(stream, Encoding.UTF8); string content = await reader.ReadToEndAsync(); - return JsonSerializer.Deserialize(content, _modelType, _jsonSerializerOptions)!; + return JsonSerializer.Deserialize(content, _modelType, _jsonSerializerOptions); } catch (JsonException jsonReaderException) { diff --git a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs index 6c0a3d6b4..b9328c297 100644 --- a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs +++ b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs @@ -83,12 +83,11 @@ public byte[] GetText(string org, string app, string textResource) using (FileStream fileStream = new(fullFileName, FileMode.Open, FileAccess.Read)) { - TextResource textResource = ( + TextResource textResource = await System.Text.Json.JsonSerializer.DeserializeAsync( fileStream, _jsonSerializerOptions - ) - )!; + ) ?? throw new System.Text.Json.JsonException("Failed to deserialize text resource"); textResource.Id = $"{org}-{app}-{language}"; textResource.Org = org; textResource.Language = language; @@ -208,6 +207,7 @@ public LayoutSettings GetLayoutSettings() if (File.Exists(filename)) { var filedata = File.ReadAllText(filename, Encoding.UTF8); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release LayoutSettings layoutSettings = JsonConvert.DeserializeObject(filedata)!; return layoutSettings; } @@ -243,6 +243,7 @@ public string GetLayouts() if (File.Exists(fileName)) { string fileData = File.ReadAllText(fileName, Encoding.UTF8); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release layouts.Add("FormLayout", JsonConvert.DeserializeObject(fileData)!); return JsonConvert.SerializeObject(layouts); } @@ -254,6 +255,7 @@ public string GetLayouts() { string data = File.ReadAllText(file, Encoding.UTF8); string name = file.Replace(layoutsPath, string.Empty).Replace(".json", string.Empty); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release layouts.Add(name, JsonConvert.DeserializeObject(data)!); } } @@ -313,6 +315,7 @@ public string GetLayoutsForSet(string layoutSetId) { string data = File.ReadAllText(file, Encoding.UTF8); string name = file.Replace(layoutsPath, string.Empty).Replace(".json", string.Empty); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release layouts.Add(name, JsonConvert.DeserializeObject(data)!); } } diff --git a/src/Altinn.App.Core/Implementation/PrefillSI.cs b/src/Altinn.App.Core/Implementation/PrefillSI.cs index 27269b4cd..1eeb85c5c 100644 --- a/src/Altinn.App.Core/Implementation/PrefillSI.cs +++ b/src/Altinn.App.Core/Implementation/PrefillSI.cs @@ -106,11 +106,18 @@ public async Task PrefillDataModel( if (profilePrefill != null) { - var userProfileDict = profilePrefill.ToObject>()!; + var userProfileDict = + profilePrefill.ToObject>() + ?? throw new Exception("Unexpectedly failed to convert profilePrefill JToken to dictionary"); if (userProfileDict.Count > 0) { - int userId = AuthenticationHelper.GetUserId(_httpContextAccessor.HttpContext!); + var httpContext = + _httpContextAccessor.HttpContext + ?? throw new Exception( + "Could not get HttpContext - must be in a request context to get current user" + ); + int userId = AuthenticationHelper.GetUserId(httpContext); UserProfile? userProfile = userId != 0 ? await _profileClient.GetUserProfile(userId) : null; if (userProfile != null) { @@ -135,7 +142,9 @@ public async Task PrefillDataModel( JToken? enhetsregisteret = prefillConfiguration.SelectToken(ER_KEY); if (enhetsregisteret != null) { - var enhetsregisterPrefill = enhetsregisteret.ToObject>()!; + var enhetsregisterPrefill = + enhetsregisteret.ToObject>() + ?? throw new Exception("Unexpectedly failed to convert enhetsregisteret JToken to dictionary"); if (enhetsregisterPrefill.Count > 0) { @@ -162,7 +171,9 @@ public async Task PrefillDataModel( JToken? folkeregisteret = prefillConfiguration.SelectToken(DSF_KEY); if (folkeregisteret != null) { - var folkeregisterPrefill = folkeregisteret.ToObject>()!; + var folkeregisterPrefill = + folkeregisteret.ToObject>() + ?? throw new Exception("Unexpectedly failed to convert folkeregisteret JToken to dictionary"); if (folkeregisterPrefill.Count > 0) { @@ -240,11 +251,18 @@ private void AssignValueToDataModel( if (propertyValue == null) { // the object does not exsist, create a new one with the property type - propertyValue = Activator.CreateInstance(property.PropertyType)!; + propertyValue = + Activator.CreateInstance(property.PropertyType) + ?? throw new Exception( + $"Could not create instance of type {property.PropertyType.Name} while prefilling" + ); property.SetValue(currentObject, propertyValue, null); } - // recurivly assign values + // recursively assign values + // TODO: handle Nullable (nullable value types), propertyValue may be null here + // due to Activator.CreateInstance above. Right now there is an exception + // but we could handle this better AssignValueToDataModel(keys, value, propertyValue, index + 1, continueOnError); } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs index f7054284e..56b48b576 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs @@ -88,7 +88,7 @@ public async Task AddEvent(string eventType, Instance instance) { Subject = $"/party/{instance.InstanceOwner.PartyId}", Type = eventType, - AlternativeSubject = alternativeSubject!, + AlternativeSubject = alternativeSubject, Time = DateTime.UtcNow, SpecVersion = "1.0", Source = new Uri($"{baseUrl}instances/{instance.Id}") diff --git a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs index d3eba2b58..9eed9b54e 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs @@ -34,6 +34,7 @@ public Task GetCertificateAsync(string certificateName) public Task GetKeyAsync(string keyName) { string token = GetTokenFromSecrets(keyName); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release JsonWebKey key = JsonSerializer.Deserialize(token)!; return Task.FromResult(key); } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index 16682e316..73821ac39 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -100,6 +100,7 @@ Type type if (response.IsSuccessStatusCode) { string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release dataElement = JsonConvert.DeserializeObject(instanceData)!; return dataElement; @@ -143,6 +144,7 @@ Guid dataId if (response.IsSuccessStatusCode) { string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release DataElement dataElement = JsonConvert.DeserializeObject(instanceData)!; return dataElement; } @@ -256,7 +258,9 @@ Guid instanceGuid if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); - dataList = JsonConvert.DeserializeObject(instanceData)!; + dataList = + JsonConvert.DeserializeObject(instanceData) + ?? throw new JsonException("Could not deserialize DataElementList"); ExtractAttachments(dataList.DataElements, attachmentList); @@ -370,6 +374,7 @@ HttpRequest request if (response.IsSuccessStatusCode) { string instancedata = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release dataElement = JsonConvert.DeserializeObject(instancedata)!; return dataElement; @@ -416,6 +421,7 @@ public async Task InsertBinaryData( if (response.IsSuccessStatusCode) { string instancedata = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release dataElement = JsonConvert.DeserializeObject(instancedata)!; return dataElement; @@ -449,6 +455,7 @@ HttpRequest request if (response.IsSuccessStatusCode) { string instancedata = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release DataElement dataElement = JsonConvert.DeserializeObject(instancedata)!; return dataElement; @@ -485,6 +492,7 @@ Stream stream if (response.IsSuccessStatusCode) { string instancedata = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release DataElement dataElement = JsonConvert.DeserializeObject(instancedata)!; return dataElement; @@ -509,6 +517,7 @@ public async Task Update(Instance instance, DataElement dataElement if (response.IsSuccessStatusCode) { + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release DataElement result = JsonConvert.DeserializeObject( await response.Content.ReadAsStringAsync() )!; @@ -535,6 +544,7 @@ public async Task LockDataElement(InstanceIdentifier instanceIdenti HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content: null); if (response.IsSuccessStatusCode) { + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release DataElement result = JsonConvert.DeserializeObject( await response.Content.ReadAsStringAsync() )!; @@ -568,6 +578,7 @@ public async Task UnlockDataElement(InstanceIdentifier instanceIden HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl); if (response.IsSuccessStatusCode) { + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release DataElement result = JsonConvert.DeserializeObject( await response.Content.ReadAsStringAsync() )!; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs index b74107ce1..b6b8b6f88 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs @@ -79,6 +79,7 @@ public async Task GetInstance(string app, string org, int instanceOwne if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release Instance instance = JsonConvert.DeserializeObject(instanceData)!; return instance; } @@ -135,9 +136,9 @@ private async Task> QueryInstances(string token, string if (response.StatusCode == HttpStatusCode.OK) { string responseString = await response.Content.ReadAsStringAsync(); - QueryResponse queryResponse = JsonConvert.DeserializeObject>( - responseString - )!; + QueryResponse queryResponse = + JsonConvert.DeserializeObject>(responseString) + ?? throw new JsonException("Could not deserialize Instance query response"); return queryResponse; } else @@ -167,6 +168,7 @@ public async Task UpdateProcess(Instance instance) if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release Instance updatedInstance = JsonConvert.DeserializeObject(instanceData)!; return updatedInstance; } @@ -193,6 +195,7 @@ public async Task CreateInstance(string org, string app, Instance inst if (response.IsSuccessStatusCode) { + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release Instance createdInstance = JsonConvert.DeserializeObject( await response.Content.ReadAsStringAsync() )!; @@ -221,6 +224,7 @@ public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Gu if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release Instance instance = JsonConvert.DeserializeObject(instanceData)!; _telemetry?.InstanceCompleted(instance); return instance; @@ -244,6 +248,7 @@ public async Task UpdateReadStatus(int instanceOwnerPartyId, Guid inst if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release Instance instance = JsonConvert.DeserializeObject(instanceData)!; return instance; } @@ -275,6 +280,7 @@ public async Task UpdateSubstatus(int instanceOwnerPartyId, Guid insta if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release Instance instance = JsonConvert.DeserializeObject(instanceData)!; return instance; } @@ -305,6 +311,7 @@ PresentationTexts presentationTexts if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release Instance instance = JsonConvert.DeserializeObject(instanceData)!; return instance; } @@ -331,6 +338,7 @@ public async Task UpdateDataValues(int instanceOwnerPartyId, Guid inst if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release Instance instance = JsonConvert.DeserializeObject(instanceData)!; return instance; } @@ -353,6 +361,7 @@ public async Task DeleteInstance(int instanceOwnerPartyId, Guid instan if (response.StatusCode == HttpStatusCode.OK) { string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release Instance instance = JsonConvert.DeserializeObject(instanceData)!; _telemetry?.InstanceDeleted(instance); return instance; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs index 28e3d40c6..545bc0c26 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs @@ -89,7 +89,9 @@ string to if (response.IsSuccessStatusCode) { string eventData = await response.Content.ReadAsStringAsync(); - InstanceEventList instanceEvents = JsonConvert.DeserializeObject(eventData)!; + InstanceEventList instanceEvents = + JsonConvert.DeserializeObject(eventData) + ?? throw new JsonException("Could not deserialize InstanceEventList"); return instanceEvents.InstanceEvents; } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs index ac52c4860..554209a9d 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs @@ -92,6 +92,7 @@ public async Task GetProcessHistory(string instanceGuid, str if (response.IsSuccessStatusCode) { string eventData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release ProcessHistoryList processHistoryList = JsonConvert.DeserializeObject(eventData)!; return processHistoryList; diff --git a/src/Altinn.App.Core/Internal/AppModel/DefaultAppModel.cs b/src/Altinn.App.Core/Internal/AppModel/DefaultAppModel.cs index e2e780e49..42f867bfd 100644 --- a/src/Altinn.App.Core/Internal/AppModel/DefaultAppModel.cs +++ b/src/Altinn.App.Core/Internal/AppModel/DefaultAppModel.cs @@ -20,6 +20,7 @@ public DefaultAppModel(ILogger logger) public object Create(string classRef) { _logger.LogInformation($"CreateNewAppModel {classRef}"); + // ! TODO: Activator.CreateInstance only returns null for Nullable (nullable value types) return Activator.CreateInstance(GetModelType(classRef))!; } @@ -27,6 +28,10 @@ public object Create(string classRef) public Type GetModelType(string classRef) { _logger.LogInformation($"GetAppModelType {classRef}"); - return Assembly.GetEntryAssembly()!.GetType(classRef, true)!; + var assembly = + Assembly.GetEntryAssembly() + ?? throw new Exception("Could not get entry assembly while resolving model type"); + // ! TODO: need some way to handle this for the next major version + return assembly.GetType(classRef, true)!; } } diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs index 564c40a66..518348230 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs @@ -72,6 +72,7 @@ bool defaultReturn } var args = expr.Args.Select(a => EvaluateExpression(state, a, context, positionalArguments)).ToArray(); + // ! TODO: should find better ways to deal with nulls here for the next major version var ret = expr.Function switch { ExpressionFunction.dataModel => DataModel(args.First()?.ToString(), context, state), diff --git a/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs b/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs index 899eb8523..c2680393a 100644 --- a/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs +++ b/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs @@ -53,6 +53,8 @@ public ApplicationLanguage( { await using (FileStream fileStream = new(fileInfo.FullName, FileMode.Open, FileAccess.Read)) { + // ! TODO: find a better way to deal with deserialization errors here, rather than adding nulls to the list + // ! JSON deserialization returns null if the input is literally "null" var applicationLanguage = ( await JsonSerializer.DeserializeAsync( fileStream, diff --git a/src/Altinn.App.Core/Internal/Linq/Extensions.cs b/src/Altinn.App.Core/Internal/Linq/Extensions.cs new file mode 100644 index 000000000..76575e820 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Linq/Extensions.cs @@ -0,0 +1,18 @@ +namespace System.Linq; + +internal static class Extensions +{ + internal static IEnumerable WhereNotNull(this IEnumerable source) + where T : class + { + ArgumentNullException.ThrowIfNull(source); + + foreach (var item in source) + { + if (item is not null) + { + yield return item; + } + } + } +} diff --git a/src/Altinn.App.Core/Internal/Patch/PatchService.cs b/src/Altinn.App.Core/Internal/Patch/PatchService.cs index cf08d0133..ec15c56dd 100644 --- a/src/Altinn.App.Core/Internal/Patch/PatchService.cs +++ b/src/Altinn.App.Core/Internal/Patch/PatchService.cs @@ -90,7 +90,7 @@ public async Task> ApplyPatch( if (!patchResult.IsSuccess) { - bool testOperationFailed = patchResult.Error!.Contains("is not equal to the indicated value."); + bool testOperationFailed = patchResult.Error.Contains("is not equal to the indicated value."); return new DataPatchError() { Title = testOperationFailed ? "Precondition in patch failed" : "Patch Operation Failed", @@ -106,7 +106,7 @@ public async Task> ApplyPatch( }; } - var result = DeserializeModel(oldModel.GetType(), patchResult.Result!); + var result = DeserializeModel(oldModel.GetType(), patchResult.Result); if (!result.Success) { return new DataPatchError() @@ -149,7 +149,7 @@ await _dataClient.UpdateData( return new DataPatchResult { NewDataModel = result.Ok, ValidationIssues = validationIssues }; } - private static ServiceResult DeserializeModel(Type type, JsonNode patchResult) + private static ServiceResult DeserializeModel(Type type, JsonNode? patchResult) { try { diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index a7c7a2166..9e92c8996 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Security.Claims; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; @@ -81,7 +82,7 @@ public async Task GenerateProcessStartEvents(ProcessStartRe _processReader.GetStartEventIds(), out ProcessError? startEventError ); - if (startEventError != null) + if (startEventError is not null) { return new ProcessChangeResult() { @@ -91,10 +92,16 @@ out ProcessError? startEventError }; } + // TODO: assert can be removed when we improve nullability annotation in GetValidStartEventOrError + Debug.Assert( + validStartElement is not null, + "validStartElement should always be nonnull when startEventError is null" + ); + // start process ProcessStateChange? startChange = await ProcessStart( processStartRequest.Instance, - validStartElement!, + validStartElement, processStartRequest.User ); InstanceEvent? startEvent = startChange?.Events?[0].CopyValues(); @@ -210,6 +217,7 @@ public async Task HandleEventsAndUpdateStorage( await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString(), instance, now, user) ]; + // ! TODO: should probably improve nullability handling in the next major version return new ProcessStateChange { OldProcessState = null!, @@ -280,13 +288,14 @@ private async Task> MoveProcessToNext( } // ending process if next element is end event - if (_processReader.IsEndEvent(nextElement?.Id)) + var nextElementId = nextElement?.Id; + if (_processReader.IsEndEvent(nextElementId)) { using var activity = _telemetry?.StartProcessEndActivity(instance); currentState.CurrentTask = null; currentState.Ended = now; - currentState.EndEvent = nextElement!.Id; + currentState.EndEvent = nextElementId; events.Add( await GenerateProcessChangeEvent(InstanceEventType.process_EndEvent.ToString(), instance, now, user) @@ -295,14 +304,19 @@ await GenerateProcessChangeEvent(InstanceEventType.process_EndEvent.ToString(), // add submit event (to support Altinn2 SBL) events.Add(await GenerateProcessChangeEvent(InstanceEventType.Submited.ToString(), instance, now, user)); } - else if (_processReader.IsProcessTask(nextElement?.Id)) + else if (_processReader.IsProcessTask(nextElementId)) { + if (nextElement is null) + { + throw new Exception("Next process element was unexpectedly null"); + } + var task = nextElement as ProcessTask; currentState.CurrentTask = new ProcessElementInfo { Flow = currentState.CurrentTask?.Flow + 1, - ElementId = nextElement!.Id, - Name = nextElement!.Name, + ElementId = nextElementId, + Name = nextElement.Name, Started = now, AltinnTaskType = task?.ExtensionElements?.TaskExtension?.TaskType, FlowType = action is "reject" diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs b/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs index 497041a0b..ac860b447 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs @@ -72,6 +72,7 @@ public async Task HandleEvents( case InstanceEventType.process_StartEvent: break; case InstanceEventType.process_StartTask: + // ! TODO: figure out why taskId can be null here await _startTaskEventHandler.Execute( GetProcessTaskInstance(altinnTaskType), taskId!, @@ -80,6 +81,7 @@ await _startTaskEventHandler.Execute( ); break; case InstanceEventType.process_EndTask: + // ! TODO: figure out why taskId can be null here await _endTaskEventHandler.Execute( GetProcessTaskInstance(altinnTaskType), taskId!, @@ -88,6 +90,7 @@ await _endTaskEventHandler.Execute( break; case InstanceEventType.process_AbandonTask: // InstanceEventType is set to Abandon when action performed is `Reject`. This is to keep backwards compatability with existing code that only should be run when a task is abandoned/rejected. + // ! TODO: figure out why taskId can be null here await _abandonTaskEventHandler.Execute( GetProcessTaskInstance(altinnTaskType), taskId!, diff --git a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs index ef68b625d..f5f2aba41 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Xml.Serialization; using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.Elements; @@ -46,7 +47,7 @@ public List GetStartEventIds() } /// - public bool IsStartEvent(string? elementId) + public bool IsStartEvent([NotNullWhen(true)] string? elementId) { using var activity = _telemetry?.StartIsStartEventActivity(); return elementId != null && GetStartEventIds().Contains(elementId); @@ -67,7 +68,7 @@ public List GetProcessTaskIds() } /// - public bool IsProcessTask(string? elementId) + public bool IsProcessTask([NotNullWhen(true)] string? elementId) { using var activity = _telemetry?.StartIsProcessTaskActivity(); return elementId != null && GetProcessTaskIds().Contains(elementId); @@ -102,7 +103,7 @@ public List GetEndEventIds() } /// - public bool IsEndEvent(string? elementId) + public bool IsEndEvent([NotNullWhen(true)] string? elementId) { using var activity = _telemetry?.StartIsEndEventActivity(); return elementId != null && GetEndEventIds().Contains(elementId); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs index 7d1eb7d28..996baeba0 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs @@ -139,7 +139,11 @@ await _dataClient.InsertFormData( else { // Remove the shadow fields from the data - data = JsonSerializer.Deserialize(serializedData, modelType)!; + data = + JsonSerializer.Deserialize(serializedData, modelType) + ?? throw new JsonException( + "Could not deserialize back datamodel after removing shadow fields. Data was \"null\"" + ); } } // remove AltinnRowIds diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs index d2330f85c..56b9be233 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs @@ -57,9 +57,12 @@ public async Task End(string taskId, Instance instance) Stream pdfStream = await _pdfService.GeneratePdf(instance, taskId, CancellationToken.None); + // ! TODO: restructure code to avoid assertion. Codepaths above have already validated this field + var paymentDataType = paymentConfiguration.PaymentDataType!; + await _dataClient.InsertBinaryData( instance.Id, - paymentConfiguration.PaymentDataType!, + paymentDataType, PdfContentType, ReceiptFileName, pdfStream, diff --git a/src/Altinn.App.Core/Models/ApplicationMetadata.cs b/src/Altinn.App.Core/Models/ApplicationMetadata.cs index 0341f9cb3..2dae6f346 100644 --- a/src/Altinn.App.Core/Models/ApplicationMetadata.cs +++ b/src/Altinn.App.Core/Models/ApplicationMetadata.cs @@ -63,7 +63,8 @@ public ApplicationMetadata(string id) /// [JsonProperty(PropertyName = "altinnNugetVersion")] public string AltinnNugetVersion { get; set; } = - typeof(ApplicationMetadata).Assembly!.GetName().Version!.ToString(); + typeof(ApplicationMetadata).Assembly.GetName().Version?.ToString() + ?? throw new Exception("Assembly version is null"); /// /// Holds properties that are not mapped to other properties diff --git a/src/Altinn.App.Core/Models/Layout/Components/GridComponent.cs b/src/Altinn.App.Core/Models/Layout/Components/GridComponent.cs index d71f70544..38c921ca0 100644 --- a/src/Altinn.App.Core/Models/Layout/Components/GridComponent.cs +++ b/src/Altinn.App.Core/Models/Layout/Components/GridComponent.cs @@ -78,12 +78,12 @@ public class GridCell /// Returns the child component IDs /// /// - public List Children() + public List Children() { return this.Rows?.Where(r => r.Cells is not null) - .SelectMany(r => r.Cells!) - .Where(c => c.ComponentId is not null) - .Select(c => c.ComponentId!) - .ToList() ?? new List(); + .SelectMany(r => r.Cells ?? []) + .Select(c => c.ComponentId) + .WhereNotNull() + .ToList() ?? []; } } diff --git a/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs b/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs index c47aeff17..26b790377 100644 --- a/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs +++ b/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs @@ -52,7 +52,9 @@ public PageComponent ReadNotNull(ref Utf8JsonReader reader, string pageName, Jso { if (reader.TokenType != JsonTokenType.StartObject) { - throw new JsonException(); + throw new JsonException( + $"Unexpected JSON token type '{reader.TokenType}', expected '{nameof(JsonTokenType.StartObject)}'" + ); } PageComponent? page = null; @@ -60,10 +62,13 @@ public PageComponent ReadNotNull(ref Utf8JsonReader reader, string pageName, Jso { if (reader.TokenType != JsonTokenType.PropertyName) { - throw new JsonException(); //Think this is impossible. After a JsonTokenType.StartObject, everything should be JsonTokenType.PropertyName + // Think this is impossible. After a JsonTokenType.StartObject, everything should be JsonTokenType.PropertyName + throw new JsonException( + $"Unexpected JSON token type after StartObject: '{reader.TokenType}', expected '{nameof(JsonTokenType.PropertyName)}'" + ); } - var propertyName = reader.GetString()!; + var propertyName = reader.GetString(); reader.Read(); if (propertyName == "data") { @@ -86,7 +91,9 @@ private PageComponent ReadData(ref Utf8JsonReader reader, string pageName, JsonS { if (reader.TokenType != JsonTokenType.StartObject) { - throw new JsonException(); + throw new JsonException( + $"Unexpected JSON token type '{reader.TokenType}', expected '{nameof(JsonTokenType.StartObject)}'" + ); } List? componentListFlat = null; @@ -105,10 +112,18 @@ private PageComponent ReadData(ref Utf8JsonReader reader, string pageName, JsonS { if (reader.TokenType != JsonTokenType.PropertyName) { - throw new JsonException(); //Think this is impossible. After a JsonTokenType.StartObject, everything should be JsonTokenType.PropertyName + // Think this is impossible. After a JsonTokenType.StartObject, everything should be JsonTokenType.PropertyName + throw new JsonException( + $"Unexpected JSON token type after StartObject: '{reader.TokenType}', expected '{nameof(JsonTokenType.PropertyName)}'" + ); } - var propertyName = reader.GetString()!; + var propertyName = + reader.GetString() + ?? throw new JsonException( + $"Could not read property name from JSON token with type '{nameof(JsonTokenType.PropertyName)}'" + ); + reader.Read(); switch (propertyName.ToLowerInvariant()) { @@ -157,7 +172,7 @@ JsonSerializerOptions options while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) { - var component = ReadComponent(ref reader, options)!; + var component = ReadComponent(ref reader, options); // Add component to the collections componentListFlat.Add(component); @@ -237,7 +252,9 @@ private BaseComponent ReadComponent(ref Utf8JsonReader reader, JsonSerializerOpt { if (reader.TokenType != JsonTokenType.StartObject) { - throw new JsonException(); + throw new JsonException( + $"Unexpected JSON token type '{reader.TokenType}', expected '{nameof(JsonTokenType.StartObject)}'" + ); } string? id = null; string? type = null; @@ -264,10 +281,18 @@ private BaseComponent ReadComponent(ref Utf8JsonReader reader, JsonSerializerOpt { if (reader.TokenType != JsonTokenType.PropertyName) { - throw new JsonException(); // Not possiblie? + // Think this is impossible. After a JsonTokenType.StartObject, everything should be JsonTokenType.PropertyName + throw new JsonException( + $"Unexpected JSON token type after StartObject: '{reader.TokenType}', expected '{nameof(JsonTokenType.PropertyName)}'" + ); } - var propertyName = reader.GetString()!; + var propertyName = + reader.GetString() + ?? throw new JsonException( + $"Could not read property name from JSON token with type '{nameof(JsonTokenType.PropertyName)}'" + ); + reader.Read(); switch (propertyName.ToLowerInvariant()) { diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs index ab2af0f81..b94a6a941 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs @@ -71,7 +71,10 @@ public class ValidationIssue /// [JsonProperty(PropertyName = "source")] [JsonPropertyName("source")] - public string Source { get; set; } = default!; +#nullable disable + public string Source { get; set; } + +#nullable restore /// /// The custom text key to use for the localized text in the frontend. diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 4140f5be8..d827d60b7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,6 +4,10 @@ + + all + runtime; build; native; contentfiles; analyzers + $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs index 37da9418e..8d9899e4d 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs @@ -201,6 +201,23 @@ private void SetupFormDataValidatorReturn( .Verifiable(hasRelevantChanges is null ? Times.Never : Times.AtLeastOnce); } + private void SourcePropertyIsSet(Dictionary> result) + { + Assert.All(result.Values, SourcePropertyIsSet); + } + + private void SourcePropertyIsSet(List result) + { + Assert.All( + result, + issue => + { + Assert.NotNull(issue.Source); + Assert.NotEqual("[]", issue.Source); + } + ); + } + private void SetupDataClient(MyModel data) { _dataClientMock @@ -299,6 +316,7 @@ public async Task ValidateFormData_WithSpecificValidator() result.Should().ContainKey("specificValidator").WhoseValue.Should().HaveCount(0); result.Should().HaveCount(1); + SourcePropertyIsSet(result); } [Fact] @@ -350,6 +368,7 @@ public async Task ValidateFormData_WithMyNameValidator_ReturnsErrorsWhenNameIsKa .Which.CustomTextKey.Should() .Be("AlwaysNameNotOla"); resultData.Should().HaveCount(2); + SourcePropertyIsSet(resultData); } [Fact] @@ -435,6 +454,7 @@ List CreateIssues(string code) .Which.Severity.Should() .Be(ValidationIssueSeverity.Error); taskResult.Should().HaveCount(6); + SourcePropertyIsSet(taskResult); var elementResult = await validationService.ValidateDataElement( DefaultInstance, @@ -463,6 +483,7 @@ List CreateIssues(string code) .Which.Severity.Should() .Be(ValidationIssueSeverity.Error); elementResult.Should().HaveCount(4); + SourcePropertyIsSet(elementResult); var dataResult = await validationService.ValidateFormData( DefaultInstance, @@ -488,6 +509,7 @@ List CreateIssues(string code) .Which.Severity.Should() .Be(ValidationIssueSeverity.Error); dataResult.Should().HaveCount(2); + SourcePropertyIsSet(dataResult); } [Fact] diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/LayoutModelConverterFromObject.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/LayoutModelConverterFromObject.cs index 706802047..9ec0925e8 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/LayoutModelConverterFromObject.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/LayoutModelConverterFromObject.cs @@ -19,7 +19,9 @@ public class LayoutModelConverterFromObject : JsonConverter { if (reader.TokenType != JsonTokenType.StartObject) { - throw new JsonException(); + throw new JsonException( + $"Unexpected JSON token type '{reader.TokenType}', expected '{nameof(JsonTokenType.StartObject)}'" + ); } var componentModel = new LayoutModel(); @@ -29,10 +31,17 @@ public class LayoutModelConverterFromObject : JsonConverter { if (reader.TokenType != JsonTokenType.PropertyName) { - throw new JsonException(); // Think this is impossible. After a JsonTokenType.StartObject, everything should be JsonTokenType.PropertyName + // Think this is impossible. After a JsonTokenType.StartObject, everything should be JsonTokenType.PropertyName + throw new JsonException( + $"Unexpected JSON token type after StartObject: '{reader.TokenType}', expected '{nameof(JsonTokenType.PropertyName)}'" + ); } - var pageName = reader.GetString()!; + var pageName = + reader.GetString() + ?? throw new JsonException( + $"Could not read property name from JSON token with type '{nameof(JsonTokenType.PropertyName)}'" + ); reader.Read(); PageComponentConverter.SetAsyncLocalPageName(pageName); From e3a4d687bd53da65a822ae7f9c9617c4be963d7b Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Mon, 27 May 2024 10:53:07 +0200 Subject: [PATCH 17/22] Refactor to remove null-forgiving operator related to `AltinnPaymentConfiguration.PaymentDataType` (#657) --- .../Controllers/PaymentController.cs | 8 ++-- .../Features/Action/PaymentUserAction.cs | 2 +- .../Payment/Services/IPaymentService.cs | 10 ++--- .../Payment/Services/PaymentService.cs | 45 ++++--------------- .../AltinnPaymentConfiguration.cs | 42 +++++++++++++++++ .../ProcessTasks/PaymentProcessTask.cs | 18 +++----- .../Features/Action/PaymentUserActionTests.cs | 6 +-- .../AltinnPaymentConfigurationTests.cs | 32 +++++++++++++ .../Features/Payment/PaymentServiceTests.cs | 35 +++++++-------- .../ProcessTasks/PaymentProcessTaskTests.cs | 10 ++--- 10 files changed, 124 insertions(+), 84 deletions(-) create mode 100644 test/Altinn.App.Core.Tests/Features/Payment/AltinnPaymentConfigurationTests.cs diff --git a/src/Altinn.App.Api/Controllers/PaymentController.cs b/src/Altinn.App.Api/Controllers/PaymentController.cs index 4167847d2..935ce810f 100644 --- a/src/Altinn.App.Api/Controllers/PaymentController.cs +++ b/src/Altinn.App.Api/Controllers/PaymentController.cs @@ -29,15 +29,15 @@ public class PaymentController : ControllerBase /// Initializes a new instance of the class. /// public PaymentController( + IServiceProvider serviceProvider, IInstanceClient instanceClient, IProcessReader processReader, - IPaymentService paymentService, IOrderDetailsCalculator? orderDetailsCalculator = null ) { _instanceClient = instanceClient; _processReader = processReader; - _paymentService = paymentService; + _paymentService = serviceProvider.GetRequiredService(); _orderDetailsCalculator = orderDetailsCalculator; } @@ -71,9 +71,11 @@ public async Task GetPaymentInformation( throw new PaymentException("Payment configuration not found in AltinnTaskExtension"); } + var validPaymentConfiguration = paymentConfiguration.Validate(); + PaymentInformation paymentInformation = await _paymentService.CheckAndStorePaymentStatus( instance, - paymentConfiguration, + validPaymentConfiguration, language ); diff --git a/src/Altinn.App.Core/Features/Action/PaymentUserAction.cs b/src/Altinn.App.Core/Features/Action/PaymentUserAction.cs index 3e5100567..58d46240a 100644 --- a/src/Altinn.App.Core/Features/Action/PaymentUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/PaymentUserAction.cs @@ -68,7 +68,7 @@ is not ProcessTask currentTask (PaymentInformation paymentInformation, bool alreadyPaid) = await _paymentService.StartPayment( context.Instance, - paymentConfiguration, + paymentConfiguration.Validate(), context.Language ); diff --git a/src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs b/src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs index 7d87dcb15..da5219fe2 100644 --- a/src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs +++ b/src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs @@ -7,14 +7,14 @@ namespace Altinn.App.Core.Features.Payment.Services /// /// Service for handling payment. /// - public interface IPaymentService + internal interface IPaymentService { /// /// Start payment for an instance. Will clean up any existing non-completed payment before starting a new payment. /// Task<(PaymentInformation paymentInformation, bool alreadyPaid)> StartPayment( Instance instance, - AltinnPaymentConfiguration paymentConfiguration, + ValidAltinnPaymentConfiguration paymentConfiguration, string? language ); @@ -23,18 +23,18 @@ public interface IPaymentService /// Task CheckAndStorePaymentStatus( Instance instance, - AltinnPaymentConfiguration paymentConfiguration, + ValidAltinnPaymentConfiguration paymentConfiguration, string? language ); /// /// Check our internal state to see if payment is complete. /// - Task IsPaymentCompleted(Instance instance, AltinnPaymentConfiguration paymentConfiguration); + Task IsPaymentCompleted(Instance instance, ValidAltinnPaymentConfiguration paymentConfiguration); /// /// Cancel payment with payment processor and delete internal payment information. /// - Task CancelAndDeleteAnyExistingPayment(Instance instance, AltinnPaymentConfiguration paymentConfiguration); + Task CancelAndDeleteAnyExistingPayment(Instance instance, ValidAltinnPaymentConfiguration paymentConfiguration); } } diff --git a/src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs b/src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs index 5735a4194..7486f966f 100644 --- a/src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs +++ b/src/Altinn.App.Core/Features/Payment/Services/PaymentService.cs @@ -1,7 +1,6 @@ using Altinn.App.Core.Features.Payment.Exceptions; using Altinn.App.Core.Features.Payment.Models; using Altinn.App.Core.Features.Payment.Processors; -using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; @@ -39,7 +38,7 @@ public PaymentService( /// public async Task<(PaymentInformation paymentInformation, bool alreadyPaid)> StartPayment( Instance instance, - AltinnPaymentConfiguration paymentConfiguration, + ValidAltinnPaymentConfiguration paymentConfiguration, string? language ) { @@ -52,9 +51,7 @@ public PaymentService( ); } - ValidateConfig(paymentConfiguration); - // ! TODO: restructure code to avoid assertion (it is validated above) - string dataTypeId = paymentConfiguration.PaymentDataType!; + string dataTypeId = paymentConfiguration.PaymentDataType; (Guid dataElementId, PaymentInformation? existingPaymentInformation) = await _dataService.GetByType(instance, dataTypeId); @@ -118,7 +115,7 @@ public PaymentService( /// public async Task CheckAndStorePaymentStatus( Instance instance, - AltinnPaymentConfiguration paymentConfiguration, + ValidAltinnPaymentConfiguration paymentConfiguration, string? language ) { @@ -131,10 +128,7 @@ public async Task CheckAndStorePaymentStatus( ); } - ValidateConfig(paymentConfiguration); - - // ! TODO: restructure code to avoid assertion (it is validated above) - string dataTypeId = paymentConfiguration.PaymentDataType!; + string dataTypeId = paymentConfiguration.PaymentDataType; (Guid dataElementId, PaymentInformation? paymentInformation) = await _dataService.GetByType( instance, dataTypeId @@ -204,12 +198,9 @@ await _dataService.UpdateJsonObject( } /// - public async Task IsPaymentCompleted(Instance instance, AltinnPaymentConfiguration paymentConfiguration) + public async Task IsPaymentCompleted(Instance instance, ValidAltinnPaymentConfiguration paymentConfiguration) { - ValidateConfig(paymentConfiguration); - - // ! TODO: restructure code to avoid assertion (it is validated above) - string dataTypeId = paymentConfiguration.PaymentDataType!; + string dataTypeId = paymentConfiguration.PaymentDataType; (Guid _, PaymentInformation? paymentInformation) = await _dataService.GetByType( instance, dataTypeId @@ -226,13 +217,10 @@ public async Task IsPaymentCompleted(Instance instance, AltinnPaymentConfi /// public async Task CancelAndDeleteAnyExistingPayment( Instance instance, - AltinnPaymentConfiguration paymentConfiguration + ValidAltinnPaymentConfiguration paymentConfiguration ) { - ValidateConfig(paymentConfiguration); - - // ! TODO: restructure code to avoid assertion (it is validated above) - string dataTypeId = paymentConfiguration.PaymentDataType!; + string dataTypeId = paymentConfiguration.PaymentDataType; (Guid dataElementId, PaymentInformation? paymentInformation) = await _dataService.GetByType( instance, dataTypeId @@ -275,21 +263,4 @@ private async Task CancelAndDelete(Instance instance, Guid dataElementId, Paymen await _dataService.DeleteById(new InstanceIdentifier(instance), dataElementId); _logger.LogDebug("Payment information for deleted for instance {InstanceId}.", instance.Id); } - - private static void ValidateConfig(AltinnPaymentConfiguration paymentConfiguration) - { - List errorMessages = []; - - if (string.IsNullOrWhiteSpace(paymentConfiguration.PaymentDataType)) - { - errorMessages.Add("PaymentDataType is missing."); - } - - if (errorMessages.Count != 0) - { - throw new ApplicationConfigException( - "Payment process task configuration is not valid: " + string.Join(",\n", errorMessages) - ); - } - } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs index 3ec5f1bc4..ed1746997 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs @@ -1,4 +1,6 @@ +using System.Diagnostics.CodeAnalysis; using System.Xml.Serialization; +using Altinn.App.Core.Internal.App; namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties { @@ -12,5 +14,45 @@ public class AltinnPaymentConfiguration /// [XmlElement("paymentDataType", Namespace = "http://altinn.no/process")] public string? PaymentDataType { get; set; } + + internal ValidAltinnPaymentConfiguration Validate() + { + List? errorMessages = null; + + var paymentDataType = PaymentDataType; + + if (paymentDataType.IsNullOrWhitespace(ref errorMessages, "PaymentDataType is missing.")) + ThrowApplicationConfigException(errorMessages); + + return new ValidAltinnPaymentConfiguration(paymentDataType); + } + + [DoesNotReturn] + private static void ThrowApplicationConfigException(List errorMessages) + { + throw new ApplicationConfigException( + "Payment process task configuration is not valid: " + string.Join(",\n", errorMessages) + ); + } + } + + internal readonly record struct ValidAltinnPaymentConfiguration(string PaymentDataType); + + file static class ValidationExtensions + { + internal static bool IsNullOrWhitespace( + [NotNullWhen(false)] this string? value, + [NotNullWhen(true)] ref List? errors, + string error + ) + { + var result = string.IsNullOrWhiteSpace(value); + if (result) + { + errors ??= new List(1); + errors.Add(error); + } + return result; + } } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs index 56b9be233..bd4ab9591 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs @@ -44,7 +44,7 @@ IPaymentService paymentService public async Task Start(string taskId, Instance instance) { AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); - await _paymentService.CancelAndDeleteAnyExistingPayment(instance, paymentConfiguration); + await _paymentService.CancelAndDeleteAnyExistingPayment(instance, paymentConfiguration.Validate()); } /// @@ -52,17 +52,16 @@ public async Task End(string taskId, Instance instance) { AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); - if (!await _paymentService.IsPaymentCompleted(instance, paymentConfiguration)) + if (!await _paymentService.IsPaymentCompleted(instance, paymentConfiguration.Validate())) throw new PaymentException("The payment is not completed."); Stream pdfStream = await _pdfService.GeneratePdf(instance, taskId, CancellationToken.None); - // ! TODO: restructure code to avoid assertion. Codepaths above have already validated this field - var paymentDataType = paymentConfiguration.PaymentDataType!; + var validatedPaymentConfiguration = paymentConfiguration.Validate(); await _dataClient.InsertBinaryData( instance.Id, - paymentDataType, + validatedPaymentConfiguration.PaymentDataType, PdfContentType, ReceiptFileName, pdfStream, @@ -74,7 +73,7 @@ await _dataClient.InsertBinaryData( public async Task Abandon(string taskId, Instance instance) { AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); - await _paymentService.CancelAndDeleteAnyExistingPayment(instance, paymentConfiguration); + await _paymentService.CancelAndDeleteAnyExistingPayment(instance, paymentConfiguration.Validate()); } private AltinnPaymentConfiguration GetAltinnPaymentConfiguration(string taskId) @@ -90,12 +89,7 @@ private AltinnPaymentConfiguration GetAltinnPaymentConfiguration(string taskId) ); } - if (string.IsNullOrWhiteSpace(paymentConfiguration.PaymentDataType)) - { - throw new ApplicationConfigException( - "PaymentDataType is missing in the payment process task configuration." - ); - } + _ = paymentConfiguration.Validate(); return paymentConfiguration; } diff --git a/test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs index 88cec9965..8090d14f0 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs @@ -55,7 +55,7 @@ public async Task HandleAction_returns_redirect_result_correctly() _paymentServiceMock .Setup(x => - x.StartPayment(It.IsAny(), It.IsAny(), It.IsAny()) + x.StartPayment(It.IsAny(), It.IsAny(), It.IsAny()) ) .ReturnsAsync((paymentInformation, false)); @@ -94,7 +94,7 @@ public async Task HandleAction_returns_success_result_when_no_redirect_url() _paymentServiceMock .Setup(x => - x.StartPayment(It.IsAny(), It.IsAny(), It.IsAny()) + x.StartPayment(It.IsAny(), It.IsAny(), It.IsAny()) ) .ReturnsAsync((paymentInformation, false)); @@ -131,7 +131,7 @@ public async Task HandleAction_returns_failure_result_when_already_paid() _paymentServiceMock .Setup(x => - x.StartPayment(It.IsAny(), It.IsAny(), It.IsAny()) + x.StartPayment(It.IsAny(), It.IsAny(), It.IsAny()) ) .ReturnsAsync((paymentInformation, true)); diff --git a/test/Altinn.App.Core.Tests/Features/Payment/AltinnPaymentConfigurationTests.cs b/test/Altinn.App.Core.Tests/Features/Payment/AltinnPaymentConfigurationTests.cs new file mode 100644 index 000000000..94a03af2c --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Payment/AltinnPaymentConfigurationTests.cs @@ -0,0 +1,32 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using FluentAssertions; + +namespace Altinn.App.Core.Tests.Features.Payment; + +public class AltinnPaymentConfigurationTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Validation_ThrowsException_When_PaymentDataType_Is_Invalid(string? paymentDataType) + { + AltinnPaymentConfiguration paymentConfiguration = new() { PaymentDataType = paymentDataType }; + + var action = () => paymentConfiguration.Validate(); + + action.Should().Throw(); + } + + [Fact] + public void Validation_Succeeds() + { + var paymentDataType = "paymentDataType"; + AltinnPaymentConfiguration paymentConfiguration = new() { PaymentDataType = paymentDataType }; + paymentConfiguration.PaymentDataType.Should().Be(paymentDataType); + + var validPaymentConfiguration = paymentConfiguration.Validate(); + validPaymentConfiguration.PaymentDataType.Should().Be(paymentDataType); + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Payment/PaymentServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Payment/PaymentServiceTests.cs index e7ce4c8ea..6a872ffed 100644 --- a/test/Altinn.App.Core.Tests/Features/Payment/PaymentServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Payment/PaymentServiceTests.cs @@ -10,7 +10,6 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Features.Payment; @@ -37,7 +36,7 @@ public async Task StartPayment_ReturnsRedirectUrl_WhenPaymentStartedSuccessfully { Instance instance = CreateInstance(); OrderDetails orderDetails = CreateOrderDetails(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); PaymentInformation paymentInformation = CreatePaymentInformation(); PaymentDetails paymentDetails = paymentInformation.PaymentDetails ?? throw new NullReferenceException("PaymentDetails should not be null"); @@ -88,7 +87,7 @@ public async Task StartPayment_ReturnsAlreadyPaidTrue_WhenPaymentIsAlreadyPaid() { Instance instance = CreateInstance(); OrderDetails orderDetails = CreateOrderDetails(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); const string language = "nb"; _paymentProcessor.Setup(pp => pp.PaymentProcessorId).Returns(orderDetails.PaymentProcessorId); @@ -124,7 +123,7 @@ public async Task StartPayment_ReturnsAlreadyPaidTrue_WhenPaymentIsAlreadyPaid() public async Task StartPayment_ThrowsException_WhenOrderDetailsCannotBeRetrieved() { Instance instance = CreateInstance(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); const string language = "nb"; _dataService @@ -148,7 +147,7 @@ public async Task StartPayment_ThrowsException_WhenPaymentCannotBeStarted() { Instance instance = CreateInstance(); OrderDetails orderDetails = CreateOrderDetails(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); const string language = "nb"; _dataService @@ -170,7 +169,7 @@ public async Task StartPayment_ThrowsException_WhenPaymentInformationCannotBeSto { Instance instance = CreateInstance(); OrderDetails orderDetails = CreateOrderDetails(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); PaymentInformation paymentInformation = CreatePaymentInformation(); PaymentDetails paymentDetails = paymentInformation.PaymentDetails ?? throw new NullReferenceException("PaymentDetails should not be null"); @@ -197,7 +196,7 @@ public async Task CheckAndStorePaymentInformation_ReturnsNull_WhenNoPaymentInfor { Instance instance = CreateInstance(); OrderDetails orderDetails = CreateOrderDetails(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); const string language = "nb"; _orderDetailsCalculator.Setup(p => p.CalculateOrderDetails(instance, language)).ReturnsAsync(orderDetails); @@ -225,7 +224,7 @@ public async Task CheckAndStorePaymentInformation_ThrowsException_WhenUnableToCh { Instance instance = CreateInstance(); OrderDetails orderDetails = CreateOrderDetails(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); PaymentInformation paymentInformation = CreatePaymentInformation(); const string language = "nb"; @@ -259,7 +258,7 @@ public async Task CheckAndStorePaymentInformation_ReturnsPaymentInformation_When { Instance instance = CreateInstance(); OrderDetails orderDetails = CreateOrderDetails(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); PaymentInformation paymentInformation = CreatePaymentInformation(); const string language = "nb"; @@ -313,7 +312,7 @@ public async Task CancelPayment_ShouldCallCancelAndDelete_WhenPaymentIsNotPaid() { // Arrange Instance instance = CreateInstance(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); OrderDetails orderDetails = CreateOrderDetails(); PaymentInformation paymentInformation = CreatePaymentInformation(); PaymentDetails paymentDetails = @@ -365,7 +364,7 @@ public async Task CancelPayment_ShouldNotDelete_WhenPaymentCancellationFails() // Arrange Instance instance = CreateInstance(); OrderDetails orderDetails = CreateOrderDetails(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); PaymentInformation paymentInformation = CreatePaymentInformation(); PaymentDetails paymentDetails = paymentInformation.PaymentDetails ?? throw new NullReferenceException("PaymentDetails should not be null"); @@ -403,7 +402,7 @@ public async Task StartPayment_ShouldThrowPaymentException_WhenOrderDetailsCalcu { // Arrange Instance instance = CreateInstance(); - AltinnPaymentConfiguration paymentConfiguration = new() { PaymentDataType = "paymentDataType" }; + ValidAltinnPaymentConfiguration paymentConfiguration = new() { PaymentDataType = "paymentDataType" }; IPaymentProcessor[] paymentProcessors = []; //No payment processor added. var paymentService = new PaymentService(paymentProcessors, _dataService.Object, _logger.Object); @@ -430,7 +429,7 @@ public async Task CheckAndStorePaymentStatus_ShouldThrowPaymentException_WhenOrd // Act Func act = async () => - await paymentService.CheckAndStorePaymentStatus(instance, paymentConfiguration, "en"); + await paymentService.CheckAndStorePaymentStatus(instance, paymentConfiguration.Validate(), "en"); // Assert await act.Should() @@ -445,7 +444,7 @@ public async Task IsPaymentCompleted_ShouldThrowPaymentException_WhenPaymentInfo { // Arrange Instance instance = CreateInstance(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); string paymentDataType = paymentConfiguration.PaymentDataType @@ -467,7 +466,7 @@ public async Task IsPaymentCompleted_ShouldReturnTrue_WhenPaymentStatusIsPaidOrS { // Arrange Instance instance = CreateInstance(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); PaymentInformation paymentInformation = CreatePaymentInformation(); paymentInformation.Status = PaymentStatus.Paid; @@ -491,7 +490,7 @@ public async Task IsPaymentCompleted_ShouldReturnFalse_WhenPaymentStatusIsNotPai { // Arrange Instance instance = CreateInstance(); - AltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); + ValidAltinnPaymentConfiguration paymentConfiguration = CreatePaymentConfiguration(); PaymentInformation paymentInformation = CreatePaymentInformation(); string paymentDataType = @@ -559,8 +558,8 @@ private static OrderDetails CreateOrderDetails() }; } - private static AltinnPaymentConfiguration CreatePaymentConfiguration() + private static ValidAltinnPaymentConfiguration CreatePaymentConfiguration() { - return new AltinnPaymentConfiguration { PaymentDataType = "paymentInformation" }; + return new AltinnPaymentConfiguration { PaymentDataType = "paymentInformation" }.Validate(); } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs index 091793c92..0e0f7b291 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs @@ -56,7 +56,7 @@ public async Task Start_ShouldCancelAndDelete() // Assert _paymentServiceMock.Verify(x => - x.CancelAndDeleteAnyExistingPayment(instance, altinnTaskExtension.PaymentConfiguration) + x.CancelAndDeleteAnyExistingPayment(instance, altinnTaskExtension.PaymentConfiguration.Validate()) ); } @@ -74,7 +74,7 @@ public async Task End_PaymentCompleted_ShouldGeneratePdfReceipt() _processReaderMock.Setup(x => x.GetAltinnTaskExtension(It.IsAny())).Returns(altinnTaskExtension); _paymentServiceMock - .Setup(x => x.IsPaymentCompleted(It.IsAny(), It.IsAny())) + .Setup(x => x.IsPaymentCompleted(It.IsAny(), It.IsAny())) .ReturnsAsync(true); // Act @@ -108,7 +108,7 @@ public async Task End_PaymentNotCompleted_ShouldThrowException() _processReaderMock.Setup(x => x.GetAltinnTaskExtension(It.IsAny())).Returns(altinnTaskExtension); _paymentServiceMock - .Setup(x => x.IsPaymentCompleted(It.IsAny(), It.IsAny())) + .Setup(x => x.IsPaymentCompleted(It.IsAny(), It.IsAny())) .ReturnsAsync(false); // Act and assert @@ -147,7 +147,7 @@ public async Task Abandon_ShouldCancelAndDelete() // Assert _paymentServiceMock.Verify(x => - x.CancelAndDeleteAnyExistingPayment(instance, altinnTaskExtension.PaymentConfiguration) + x.CancelAndDeleteAnyExistingPayment(instance, altinnTaskExtension.PaymentConfiguration.Validate()) ); } @@ -193,7 +193,7 @@ public async Task End_ValidConfiguration_ShouldNotThrow() ); _paymentServiceMock - .Setup(ps => ps.IsPaymentCompleted(It.IsAny(), It.IsAny())) + .Setup(ps => ps.IsPaymentCompleted(It.IsAny(), It.IsAny())) .ReturnsAsync(true); using var memoryStream = new MemoryStream(); From cd55bb2f119a19ddfb28eb515f3e145d9d6e8394 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Mon, 27 May 2024 14:02:29 +0200 Subject: [PATCH 18/22] Ignore warning during pack due to Azure.Monitor prerelease dep (#658) --- src/Altinn.App.Api/Altinn.App.Api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index 333548902..51bc24cec 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -29,7 +29,7 @@ - + From be0b6164058b96750062cdd12265f6b167cc7c41 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Tue, 28 May 2024 12:21:33 +0200 Subject: [PATCH 19/22] Tests for telemetry enriching middleware, make ASP.NET Core spans root, misc improvements (#659) --- AppLibDotnet.sln | 7 + .../Extensions/ServiceCollectionExtensions.cs | 98 ++++++++++++- .../TelemetryEnrichingMiddleware.cs | 19 +-- .../Configuration/AppSettings.cs | 6 + src/Altinn.App.Core/Features/Telemetry.cs | 18 ++- .../Features/TelemetryActivityExtensions.cs | 33 ++++- .../Altinn.App.Api.Tests.csproj | 1 + .../CustomWebApplicationFactory.cs | 71 +++++++--- ...Should_Always_Be_A_Root_Trace.verified.txt | 48 +++++++ ...ave_Root_AspNetCore_Trace_Org.verified.txt | 45 ++++++ ...ve_Root_AspNetCore_Trace_User.verified.txt | 48 +++++++ .../TelemetryEnrichingMiddlewareTests.cs | 112 +++++++++++++++ .../Mocks/PepWithPDPAuthorizationMockSI.cs | 2 +- test/Altinn.App.Api.Tests/Program.cs | 4 + .../Utils/PrincipalUtil.cs | 6 +- .../Altinn.App.Common.Tests.csproj | 43 ++++++ .../TelemetryDITests.cs | 38 +++++ .../TelemetrySink.cs | 131 +++++++++--------- .../Altinn.App.Core.Tests.csproj | 1 + .../Email/EmailNotificationClientTests.cs | 2 +- .../Sms/SmsNotificationClientTests.cs | 2 +- .../Implementation/AppResourcesSITests.cs | 2 +- .../Implementation/EventsClientTest.cs | 3 +- ...ation_SuccessfulCallToStorage.verified.txt | 2 +- .../Implementation/InstanceClientTests.cs | 2 +- .../Authorization/AuthorizationClientTests.cs | 2 +- .../Clients/Storage/DataClientTests.cs | 2 +- .../Internal/App/AppMedataTest.cs | 2 +- .../Auth/AuthorizationServiceTests.cs | 2 +- .../Internal/Patch/PatchServiceTests.cs | 2 +- .../Internal/Pdf/PdfServiceTests.cs | 4 +- .../Internal/Process/ProcessEngineTest.cs | 2 +- .../Internal/Process/ProcessReaderTests.cs | 4 +- .../Process/TestUtils/ProcessTestUtils.cs | 3 +- 34 files changed, 645 insertions(+), 122 deletions(-) create mode 100644 test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace.verified.txt create mode 100644 test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_Org.verified.txt create mode 100644 test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_User.verified.txt create mode 100644 test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs create mode 100644 test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj create mode 100644 test/Altinn.App.Common.Tests/TelemetryDITests.cs rename test/{Altinn.App.Core.Tests/Mocks => Altinn.App.Common.Tests}/TelemetrySink.cs (58%) diff --git a/AppLibDotnet.sln b/AppLibDotnet.sln index 826edaf71..9330287d6 100644 --- a/AppLibDotnet.sln +++ b/AppLibDotnet.sln @@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7AD5FADE-607 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.App.Core", "src\Altinn.App.Core\Altinn.App.Core.csproj", "{1745B251-BD5C-43B7-BA7D-9C4BFAB37535}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.App.Common.Tests", "test\Altinn.App.Common.Tests\Altinn.App.Common.Tests.csproj", "{D5838692-2703-4E42-8802-6E1FA7F1B50B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -40,6 +42,10 @@ Global {1745B251-BD5C-43B7-BA7D-9C4BFAB37535}.Debug|Any CPU.Build.0 = Debug|Any CPU {1745B251-BD5C-43B7-BA7D-9C4BFAB37535}.Release|Any CPU.ActiveCfg = Release|Any CPU {1745B251-BD5C-43B7-BA7D-9C4BFAB37535}.Release|Any CPU.Build.0 = Release|Any CPU + {D5838692-2703-4E42-8802-6E1FA7F1B50B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5838692-2703-4E42-8802-6E1FA7F1B50B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5838692-2703-4E42-8802-6E1FA7F1B50B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5838692-2703-4E42-8802-6E1FA7F1B50B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -49,6 +55,7 @@ Global {17D7DCE9-7797-4BC1-B448-D0529FD6FB3D} = {6C8EB054-1747-4BAC-A637-754F304BCAFA} {2FD56505-1DB2-4AE1-8911-E076E535EAC6} = {6C8EB054-1747-4BAC-A637-754F304BCAFA} {1745B251-BD5C-43B7-BA7D-9C4BFAB37535} = {7AD5FADE-607F-4D5F-8511-6647D0C1AA1C} + {D5838692-2703-4E42-8802-6E1FA7F1B50B} = {6C8EB054-1747-4BAC-A637-754F304BCAFA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4584C6E1-D5B4-40B1-A8C4-CF4620EB0896} diff --git a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs index dde2c8e0f..ba6f55b53 100644 --- a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Altinn.App.Api.Controllers; using Altinn.App.Api.Helpers; using Altinn.App.Api.Infrastructure.Filters; @@ -15,6 +16,8 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.FeatureManagement; using Microsoft.IdentityModel.Tokens; +using OpenTelemetry; +using OpenTelemetry.Context.Propagation; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -148,6 +151,14 @@ IWebHostEnvironment env services.AddHostedService(); services.AddSingleton(); + // This bit of code makes ASP.NET Core spans always root. + // Depending on infrastructure used and how the application is exposed/called, + // it might be a good idea to be in control of the root span (and therefore the size, baggage etch) + // Taken from: https://github.com/open-telemetry/opentelemetry-dotnet-contrib/issues/1773 + _ = Sdk.SuppressInstrumentation; // Just to trigger static constructor. The static constructor in Sdk initializes Propagators.DefaultTextMapPropagator which we depend on below + Sdk.SetDefaultTextMapPropagator(new OtelPropagator(Propagators.DefaultTextMapPropagator)); + DistributedContextPropagator.Current = new AspNetCorePropagator(); + var appInsightsConnectionString = GetAppInsightsConnectionStringForOtel(config, env); services @@ -232,7 +243,7 @@ private sealed class TelemetryInitialization( MeterProvider meterProvider ) : IHostedService { - public Task StartAsync(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken cancellationToken) { // This codepath for initialization is here only because it makes it a lot easier to // query the metrics from Prometheus using 'increase' without the appearance of a "missed" sample. @@ -244,21 +255,100 @@ public Task StartAsync(CancellationToken cancellationToken) telemetry.Init(); try { - if (!meterProvider.ForceFlush(10_000)) + var task = Task.Factory.StartNew( + () => + { + if (!meterProvider.ForceFlush(10_000)) + { + logger.LogInformation("Failed to flush metrics after 10 seconds"); + } + }, + cancellationToken, + // Long running to avoid doing this blocking on a "normal" thread pool thread + TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, + TaskScheduler.Default + ); + if (await Task.WhenAny(task, Task.Delay(500, cancellationToken)) != task) { - logger.LogWarning("Failed to flush metrics after 10 seconds"); + logger.LogInformation( + "Tried to flush metrics within 0.5 seconds but it was taking too long, proceeding with startup" + ); } } catch (Exception ex) { + if (ex is OperationCanceledException) + return; logger.LogWarning(ex, "Failed to flush metrics"); } - return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } + internal sealed class OtelPropagator : TextMapPropagator + { + private readonly TextMapPropagator _inner; + + public OtelPropagator(TextMapPropagator inner) => _inner = inner; + + public override ISet Fields => _inner.Fields; + + public override PropagationContext Extract( + PropagationContext context, + T carrier, + Func> getter + ) + { + if (carrier is HttpRequest) + return default; + return _inner.Extract(context, carrier, getter); + } + + public override void Inject(PropagationContext context, T carrier, Action setter) => + _inner.Inject(context, carrier, setter); + } + + internal sealed class AspNetCorePropagator : DistributedContextPropagator + { + private readonly DistributedContextPropagator _inner; + + public AspNetCorePropagator() => _inner = CreateDefaultPropagator(); + + public override IReadOnlyCollection Fields => _inner.Fields; + + public override IEnumerable>? ExtractBaggage( + object? carrier, + PropagatorGetterCallback? getter + ) + { + if (carrier is IHeaderDictionary) + return null; + + return _inner.ExtractBaggage(carrier, getter); + } + + public override void ExtractTraceIdAndState( + object? carrier, + PropagatorGetterCallback? getter, + out string? traceId, + out string? traceState + ) + { + if (carrier is IHeaderDictionary) + { + traceId = null; + traceState = null; + return; + } + + _inner.ExtractTraceIdAndState(carrier, getter, out traceId, out traceState); + } + + public override void Inject(Activity? activity, object? carrier, PropagatorSetterCallback? setter) => + _inner.Inject(activity, carrier, setter); + } + private static void AddAuthorizationPolicies(IServiceCollection services) { services.AddAuthorization(options => diff --git a/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs b/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs index f5641ff95..01edcfb01 100644 --- a/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs +++ b/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs @@ -7,10 +7,7 @@ namespace Altinn.App.Api.Infrastructure.Middleware; -/// -/// Middleware for adding telemetry to the request. -/// -public class TelemetryEnrichingMiddleware +internal sealed class TelemetryEnrichingMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; @@ -41,6 +38,10 @@ static TelemetryEnrichingMiddleware() } } }, + { + AltinnCoreClaimTypes.AuthenticateMethod, + static (claim, activity) => activity.SetAuthenticationMethod(claim.Value) + }, { AltinnCoreClaimTypes.AuthenticationLevel, static (claim, activity) => @@ -50,7 +51,9 @@ static TelemetryEnrichingMiddleware() activity.SetAuthenticationLevel(result); } } - } + }, + { AltinnCoreClaimTypes.Org, static (claim, activity) => activity.SetOrganisationName(claim.Value) }, + { AltinnCoreClaimTypes.OrgNumber, static (claim, activity) => activity.SetOrganisationNumber(claim.Value) }, }; ClaimActions = actions.ToFrozenDictionary(); @@ -73,8 +76,8 @@ public TelemetryEnrichingMiddleware(RequestDelegate next, ILoggerThe HTTP context. public async Task InvokeAsync(HttpContext context) { - var trace = context.Features.Get(); - if (trace is null) + var activity = context.Features.Get()?.Activity; + if (activity is null) { await _next(context); return; @@ -82,8 +85,6 @@ public async Task InvokeAsync(HttpContext context) try { - var activity = trace.Activity; - foreach (var claim in context.User.Claims) { if (ClaimActions.TryGetValue(claim.Type, out var action)) diff --git a/src/Altinn.App.Core/Configuration/AppSettings.cs b/src/Altinn.App.Core/Configuration/AppSettings.cs index 4ada89a59..5f98b5783 100644 --- a/src/Altinn.App.Core/Configuration/AppSettings.cs +++ b/src/Altinn.App.Core/Configuration/AppSettings.cs @@ -215,5 +215,11 @@ public class AppSettings /// Enable the functionality to run expression validation in backend /// public bool ExpressionValidation { get; set; } = false; + + /// + /// Enables OpenTelemetry as a substitute for Application Insights SDK + /// Improves instrumentation throughout the Altinn app libraries. + /// + public bool UseOpenTelemetry { get; set; } } } diff --git a/src/Altinn.App.Core/Features/Telemetry.cs b/src/Altinn.App.Core/Features/Telemetry.cs index 110a01cb6..1bbe6c2d0 100644 --- a/src/Altinn.App.Core/Features/Telemetry.cs +++ b/src/Altinn.App.Core/Features/Telemetry.cs @@ -117,7 +117,7 @@ public static class Labels /// /// Label for the party ID of the instance owner. /// - public static readonly string InstanceOwnerPartyId = "instance.owner_party_id"; + public static readonly string InstanceOwnerPartyId = "instance.owner.party.id"; /// /// Label for the guid that identifies the instance. @@ -147,12 +147,22 @@ public static class Labels /// /// Label for the ID of the party. /// - public const string UserPartyId = "user.party_id"; + public const string UserPartyId = "user.party.id"; + + /// + /// Label for the authentication method of the user. + /// + public const string UserAuthenticationMethod = "user.authentication.method"; /// /// Label for the authentication level of the user. /// - public const string UserAuthenticationLevel = "user.authentication_level"; + public const string UserAuthenticationLevel = "user.authentication.level"; + + /// + /// Label for the organisation name. + /// + public const string OrganisationName = "organisation.name"; /// /// Label for the organisation number. @@ -166,7 +176,7 @@ internal static class InternalLabels internal const string Type = "type"; internal const string AuthorizationAction = "authorization.action"; internal const string AuthorizerAction = "authorization.authorizer.action"; - internal const string AuthorizerTaskId = "authorization.authorizer.task_id"; + internal const string AuthorizerTaskId = "authorization.authorizer.task.id"; internal const string ValidatorType = "validator.type"; internal const string ValidatorSource = "validator.source"; } diff --git a/src/Altinn.App.Core/Features/TelemetryActivityExtensions.cs b/src/Altinn.App.Core/Features/TelemetryActivityExtensions.cs index ad8079ea9..50220fe91 100644 --- a/src/Altinn.App.Core/Features/TelemetryActivityExtensions.cs +++ b/src/Altinn.App.Core/Features/TelemetryActivityExtensions.cs @@ -55,6 +55,21 @@ public static Activity SetUsername(this Activity activity, string? username) return activity; } + /// + /// Sets the user authentication method as a tag/attribute on the activity/span + /// + /// Activity + /// Authentication method + /// Activity + public static Activity SetAuthenticationMethod(this Activity activity, string? authenticationMethod) + { + if (!string.IsNullOrWhiteSpace(authenticationMethod)) + { + activity.SetTag(Labels.UserAuthenticationMethod, authenticationMethod); + } + return activity; + } + /// /// Sets the user authentication level as a tag/attribute on the activity/span /// @@ -217,7 +232,23 @@ public static Activity SetTaskId(this Activity activity, string? taskId) } /// - /// Sets the Process Task ID as a tag/attribute on the activity/span + /// Sets the Organisation name as a tag/attribute on the activity/span + /// + /// Activity + /// Organisation name + /// Activity + public static Activity SetOrganisationName(this Activity activity, string? organisationName) + { + if (!string.IsNullOrWhiteSpace(organisationName)) + { + activity.SetTag(Labels.OrganisationName, organisationName); + } + + return activity; + } + + /// + /// Sets the Organisation number as a tag/attribute on the activity/span /// /// Activity /// Organisation number diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index d472686d2..f66ebf7c9 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -28,6 +28,7 @@ + diff --git a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs index 5c8c64def..b1435f869 100644 --- a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs +++ b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Net; using System.Net.Http.Headers; using System.Text.Json; @@ -12,6 +13,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; +using OpenTelemetry; +using OpenTelemetry.Context.Propagation; using Xunit.Abstractions; namespace Altinn.App.Api.Tests; @@ -27,9 +30,12 @@ public class ApiTestBase protected readonly ITestOutputHelper _outputHelper; private readonly WebApplicationFactory _factory; + protected IServiceProvider Services { get; private set; } + public ApiTestBase(WebApplicationFactory factory, ITestOutputHelper outputHelper) { _factory = factory; + Services = _factory.Services; _outputHelper = outputHelper; } @@ -45,38 +51,71 @@ public HttpClient GetRootedClient(string org, string app, int userId, int? party /// Gets a client that adds appsettings from the specified org/app /// test application under TestData/Apps to the service collection. /// - public HttpClient GetRootedClient(string org, string app) + public HttpClient GetRootedClient(string org, string app, bool includeTraceContext = false) { string appRootPath = TestData.GetApplicationDirectory(org, app); string appSettingsPath = Path.Join(appRootPath, "appsettings.json"); - var client = _factory - .WithWebHostBuilder(builder => - { - var configuration = new ConfigurationBuilder().AddJsonFile(appSettingsPath).Build(); + var factory = _factory.WithWebHostBuilder(builder => + { + var configuration = new ConfigurationBuilder().AddJsonFile(appSettingsPath).Build(); - configuration.GetSection("AppSettings:AppBasePath").Value = appRootPath; - IConfigurationSection appSettingSection = configuration.GetSection("AppSettings"); + configuration.GetSection("AppSettings:AppBasePath").Value = appRootPath; + IConfigurationSection appSettingSection = configuration.GetSection("AppSettings"); - builder.ConfigureLogging(ConfigureFakeLogging); + builder.ConfigureLogging(logging => ConfigureFakeLogging(logging, _outputHelper)); - builder.ConfigureServices(services => services.Configure(appSettingSection)); - builder.ConfigureTestServices(services => OverrideServicesForAllTests(services)); - builder.ConfigureTestServices(OverrideServicesForThisTest); - builder.ConfigureTestServices(ConfigureFakeHttpClientHandler); - }) - .CreateClient(new WebApplicationFactoryClientOptions() { AllowAutoRedirect = false }); + builder.ConfigureServices(services => services.Configure(appSettingSection)); + builder.ConfigureTestServices(services => OverrideServicesForAllTests(services)); + builder.ConfigureTestServices(OverrideServicesForThisTest); + builder.ConfigureTestServices(ConfigureFakeHttpClientHandler); + }); + Services = factory.Services; + + var client = includeTraceContext + ? factory.CreateDefaultClient(new DiagnosticHandler()) + : factory.CreateClient(new WebApplicationFactoryClientOptions() { AllowAutoRedirect = false }); return client; } - private void ConfigureFakeLogging(ILoggingBuilder builder) + private sealed class DiagnosticHandler : DelegatingHandler + { + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken + ) + { + var activity = Activity.Current; + var propagator = Propagators.DefaultTextMapPropagator; + if (activity is not null) + { + propagator.Inject( + new PropagationContext(activity.Context, Baggage.Current), + request.Headers, + (c, k, v) => c.TryAddWithoutValidation(k, v) + ); + } + return base.SendAsync(request, cancellationToken); + } + } + + public static void ConfigureFakeLogging(ILoggingBuilder builder, ITestOutputHelper? outputHelper = null) { builder .ClearProviders() .AddFakeLogging(options => { - options.OutputSink = (message) => _outputHelper.WriteLine(message); + options.OutputSink = message => outputHelper?.WriteLine(message); + if (outputHelper is null) + { + options.FilteredLevels = new HashSet + { + LogLevel.Warning, + LogLevel.Error, + LogLevel.Critical + }; + } options.OutputFormatter = log => $""" [{ShortLogLevel(log.Level)}] {log.Category}: diff --git a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace.verified.txt b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace.verified.txt new file mode 100644 index 000000000..6adfaa848 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace.verified.txt @@ -0,0 +1,48 @@ +{ + Activities: [ + { + ActivityName: GET {org}/{app}/api/v1/applicationmetadata, + Tags: [ + { + server.address: localhost + }, + { + http.request.method: GET + }, + { + url.scheme: http + }, + { + url.path: /tdd/contributer-restriction/api/v1/applicationmetadata + }, + { + network.protocol.version: 1.1 + }, + { + user.id: 10 + }, + { + user.party.id: Scrubbed + }, + { + user.name: User10 + }, + { + user.authentication.method: Mock + }, + { + user.authentication.level: 4 + }, + { + http.route: {org}/{app}/api/v1/applicationmetadata + }, + { + http.response.status_code: 200 + } + ], + IdFormat: W3C, + Kind: Server + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_Org.verified.txt b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_Org.verified.txt new file mode 100644 index 000000000..0b45cc20e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_Org.verified.txt @@ -0,0 +1,45 @@ +{ + Activities: [ + { + ActivityName: GET {org}/{app}/api/v1/applicationmetadata, + Tags: [ + { + server.address: localhost + }, + { + http.request.method: GET + }, + { + url.scheme: http + }, + { + url.path: /tdd/contributer-restriction/api/v1/applicationmetadata + }, + { + network.protocol.version: 1.1 + }, + { + organisation.name: Guid_1 + }, + { + organisation.number: 160694123 + }, + { + user.authentication.method: Mock + }, + { + user.authentication.level: 4 + }, + { + http.route: {org}/{app}/api/v1/applicationmetadata + }, + { + http.response.status_code: 200 + } + ], + IdFormat: W3C, + Kind: Server + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_User.verified.txt b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_User.verified.txt new file mode 100644 index 000000000..6adfaa848 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_User.verified.txt @@ -0,0 +1,48 @@ +{ + Activities: [ + { + ActivityName: GET {org}/{app}/api/v1/applicationmetadata, + Tags: [ + { + server.address: localhost + }, + { + http.request.method: GET + }, + { + url.scheme: http + }, + { + url.path: /tdd/contributer-restriction/api/v1/applicationmetadata + }, + { + network.protocol.version: 1.1 + }, + { + user.id: 10 + }, + { + user.party.id: Scrubbed + }, + { + user.name: User10 + }, + { + user.authentication.method: Mock + }, + { + user.authentication.level: 4 + }, + { + http.route: {org}/{app}/api/v1/applicationmetadata + }, + { + http.response.status_code: 200 + } + ], + IdFormat: W3C, + Kind: Server + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs new file mode 100644 index 000000000..db0386145 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs @@ -0,0 +1,112 @@ +using System.Net.Http.Headers; +using Altinn.App.Api.Tests.Mocks; +using Altinn.App.Api.Tests.Utils; +using Altinn.App.Common.Tests; +using Altinn.App.Core.Features; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Altinn.App.Api.Tests.Middleware; + +public class TelemetryEnrichingMiddlewareTests : ApiTestBase, IClassFixture> +{ + public TelemetryEnrichingMiddlewareTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) + : base(factory, outputHelper) { } + + private (TelemetrySink Telemetry, Func Request) AnalyzeTelemetry( + string token, + bool includeTraceContext = false + ) + { + this.OverrideServicesForThisTest = (services) => + { + services.AddTelemetrySink( + shouldAlsoListenToActivities: (_, source) => source.Name == "Microsoft.AspNetCore" + ); + }; + + string org = "tdd"; + string app = "contributer-restriction"; + + HttpClient client = GetRootedClient(org, app, includeTraceContext); + var telemetry = this.Services.GetRequiredService(); + + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + return (telemetry, async () => await client.GetStringAsync($"/{org}/{app}/api/v1/applicationmetadata")); + } + + [Fact] + public async Task Should_Have_Root_AspNetCore_Trace_Org() + { + var org = Guid.NewGuid().ToString(); + string token = PrincipalUtil.GetOrgToken(org, "160694123", 4); + + var (telemetry, request) = AnalyzeTelemetry(token); + await request(); + var activities = telemetry.CapturedActivities; + var activity = Assert.Single( + activities, + a => a.TagObjects.Any(t => t.Key == Telemetry.Labels.OrganisationName && (t.Value as string) == org) + ); + Assert.True(activity.IsAllDataRequested); + Assert.True(activity.Recorded); + Assert.Equal("Microsoft.AspNetCore", activity.Source.Name); + Assert.Null(activity.Parent); + Assert.Null(activity.ParentId); + Assert.Equal(default, activity.ParentSpanId); + + await Verify(telemetry.GetSnapshot(activity)); + } + + [Fact] + public async Task Should_Have_Root_AspNetCore_Trace_User() + { + var partyId = Random.Shared.Next(); + var principal = PrincipalUtil.GetUserPrincipal(10, partyId, 4); + var token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); + + var (telemetry, request) = AnalyzeTelemetry(token); + await request(); + var activities = telemetry.CapturedActivities; + var activity = Assert.Single( + activities, + a => a.TagObjects.Any(t => t.Key == Telemetry.Labels.UserPartyId && (t.Value as int?) == partyId) + ); + Assert.True(activity.IsAllDataRequested); + Assert.True(activity.Recorded); + Assert.Equal("Microsoft.AspNetCore", activity.Source.Name); + Assert.Null(activity.Parent); + Assert.Null(activity.ParentId); + Assert.Equal(default, activity.ParentSpanId); + await Verify(telemetry.GetSnapshot(activity)).ScrubMember(Telemetry.Labels.UserPartyId); + } + + [Fact] + public async Task Should_Always_Be_A_Root_Trace() + { + var partyId = Random.Shared.Next(); + var principal = PrincipalUtil.GetUserPrincipal(10, partyId, 4); + var token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); + + var (telemetry, request) = AnalyzeTelemetry(token, includeTraceContext: true); + using (var parentActivity = telemetry.Object.ActivitySource.StartActivity("TestParentActivity")) + { + Assert.NotNull(parentActivity); + await request(); + } + var activities = telemetry.CapturedActivities; + var activity = Assert.Single( + activities, + a => a.TagObjects.Any(t => t.Key == Telemetry.Labels.UserPartyId && (t.Value as int?) == partyId) + ); + Assert.True(activity.IsAllDataRequested); + Assert.True(activity.Recorded); + Assert.Equal("Microsoft.AspNetCore", activity.Source.Name); + Assert.Null(activity.Parent); + Assert.Null(activity.ParentId); + Assert.Equal(default, activity.ParentSpanId); + await Verify(telemetry.GetSnapshot(activity)).ScrubMember(Telemetry.Labels.UserPartyId); + } +} diff --git a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs index 57480e308..f5ec05c64 100644 --- a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs +++ b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs @@ -17,7 +17,7 @@ namespace Altinn.App.Api.Tests.Mocks { - public class PepWithPDPAuthorizationMockSI : Common.PEP.Interfaces.IPDP + public class PepWithPDPAuthorizationMockSI : Altinn.Common.PEP.Interfaces.IPDP { private readonly IInstanceClient _instanceClient; diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs index 9374508cf..a8361a986 100644 --- a/test/Altinn.App.Api.Tests/Program.cs +++ b/test/Altinn.App.Api.Tests/Program.cs @@ -1,5 +1,6 @@ using Altinn.App.Api.Extensions; using Altinn.App.Api.Helpers; +using Altinn.App.Api.Tests; using Altinn.App.Api.Tests.Data; using Altinn.App.Api.Tests.Mocks; using Altinn.App.Api.Tests.Mocks.Authentication; @@ -36,10 +37,13 @@ } ); +ApiTestBase.ConfigureFakeLogging(builder.Logging); + builder.Configuration.AddJsonFile( Path.Join(TestData.GetTestDataRootDirectory(), "apps", "tdd", "contributer-restriction", "appsettings.json") ); builder.Configuration.GetSection("MetricsSettings:Enabled").Value = "false"; +builder.Configuration.GetSection("AppSettings:UseOpenTelemetry").Value = "true"; ConfigureServices(builder.Services, builder.Configuration); ConfigureMockServices(builder.Services, builder.Configuration); diff --git a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs index 9ecbec6f2..c2162fe26 100644 --- a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs +++ b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs @@ -23,13 +23,15 @@ public static ClaimsPrincipal GetUserPrincipal(int? userId, int? partyId, int au ); if (userId > 0) { - claims.Add(new Claim(AltinnCoreClaimTypes.UserId, userId.ToString()!, ClaimValueTypes.String, issuer)); + claims.Add( + new Claim(AltinnCoreClaimTypes.UserId, userId.Value.ToString(), ClaimValueTypes.String, issuer) + ); } if (partyId > 0) { claims.Add( - new Claim(AltinnCoreClaimTypes.PartyID, userId.ToString()!, ClaimValueTypes.Integer32, issuer) + new Claim(AltinnCoreClaimTypes.PartyID, partyId.Value.ToString(), ClaimValueTypes.Integer32, issuer) ); } diff --git a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj new file mode 100644 index 000000000..261873f17 --- /dev/null +++ b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj @@ -0,0 +1,43 @@ + + + + net8.0 + enable + enable + + false + true + + $(NoWarn);CS1591;CS0618 + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/test/Altinn.App.Common.Tests/TelemetryDITests.cs b/test/Altinn.App.Common.Tests/TelemetryDITests.cs new file mode 100644 index 000000000..21b1f2b6c --- /dev/null +++ b/test/Altinn.App.Common.Tests/TelemetryDITests.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.App.Common.Tests; + +public class TelemetryDITests +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TelemetryFake_Is_Disposed(bool materialize) + { + using var scope = TelemetrySink.CreateScope(); + scope.IsDisposed.Should().BeFalse(); + + var services = new ServiceCollection(); + services.AddTelemetrySink(); + var sp = services.BuildServiceProvider( + new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true, } + ); + + if (materialize) + { + var fake = sp.GetRequiredService(); + fake.IsDisposed.Should().BeFalse(); + scope.IsDisposed.Should().BeFalse(); + sp.Dispose(); + fake.IsDisposed.Should().BeTrue(); + scope.IsDisposed.Should().BeTrue(); + } + else + { + scope.IsDisposed.Should().BeFalse(); + sp.Dispose(); + scope.IsDisposed.Should().BeFalse(); + } + } +} diff --git a/test/Altinn.App.Core.Tests/Mocks/TelemetrySink.cs b/test/Altinn.App.Common.Tests/TelemetrySink.cs similarity index 58% rename from test/Altinn.App.Core.Tests/Mocks/TelemetrySink.cs rename to test/Altinn.App.Common.Tests/TelemetrySink.cs index 94b87b905..dd0596137 100644 --- a/test/Altinn.App.Core.Tests/Mocks/TelemetrySink.cs +++ b/test/Altinn.App.Common.Tests/TelemetrySink.cs @@ -7,46 +7,87 @@ using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using static Altinn.App.Core.Tests.Mocks.TelemetrySink; +using static Altinn.App.Common.Tests.TelemetrySink; -namespace Altinn.App.Core.Tests.Mocks; +namespace Altinn.App.Common.Tests; -internal sealed record TelemetrySink : IDisposable +public static class TelemetryDI { - internal bool IsDisposed { get; private set; } + public static IServiceCollection AddTelemetrySink( + this IServiceCollection services, + string org = "ttd", + string name = "test", + string version = "v1", + Func? shouldAlsoListenToActivities = null, + Func? shouldAlsoListenToMetrics = null + ) + { + var telemetryRegistration = services.FirstOrDefault(s => s.ServiceType == typeof(Telemetry)); + if (telemetryRegistration is not null) + services.Remove(telemetryRegistration); + + services.AddSingleton(sp => new TelemetrySink( + sp, + org, + name, + version, + telemetry: null, + shouldAlsoListenToActivities, + shouldAlsoListenToMetrics + )); + services.AddSingleton(sp => sp.GetRequiredService().Object); + + return services; + } +} + +public sealed record TelemetrySink : IDisposable +{ + public bool IsDisposed { get; private set; } - internal static ConcurrentDictionary Scopes { get; } = []; + public static ConcurrentDictionary Scopes { get; } = []; - internal Telemetry Object { get; } + public Telemetry Object { get; } - internal ActivityListener ActivityListener { get; } + public ActivityListener ActivityListener { get; } - internal MeterListener MeterListener { get; } + public MeterListener MeterListener { get; } private readonly ConcurrentBag _activities = []; private readonly ConcurrentDictionary> _metricValues = []; - internal readonly record struct MetricMeasurement(long Value, IReadOnlyDictionary Tags); + public readonly record struct MetricMeasurement(long Value, IReadOnlyDictionary Tags); + + public IEnumerable CapturedActivities => _activities; - internal IEnumerable CapturedActivities => _activities; + public IReadOnlyDictionary> CapturedMetrics => _metricValues; - internal IReadOnlyDictionary> CapturedMetrics => _metricValues; + public TelemetrySnapshot GetSnapshot() => new(CapturedActivities, CapturedMetrics); - internal TelemetrySnapshot GetSnapshot() => new(CapturedActivities, CapturedMetrics); + public TelemetrySnapshot GetSnapshot(Activity activity) => + new([activity], new Dictionary>()); - internal TelemetrySink(string org = "ttd", string name = "test", string version = "v1") + public TelemetrySink( + IServiceProvider? serviceProvider = null, + string org = "ttd", + string name = "test", + string version = "v1", + Telemetry? telemetry = null, + Func? shouldAlsoListenToActivities = null, + Func? shouldAlsoListenToMetrics = null + ) { var appId = new AppIdentifier(org, name); var options = new AppSettings { AppVersion = version, }; - Object = new Telemetry(appId, Options.Create(options)); + Object = telemetry ?? new Telemetry(appId, Options.Create(options)); ActivityListener = new ActivityListener() { ShouldListenTo = (activitySource) => { var sameSource = ReferenceEquals(activitySource, Object.ActivitySource); - return sameSource; + return sameSource || (shouldAlsoListenToActivities?.Invoke(serviceProvider!, activitySource) ?? false); }, Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded, @@ -62,7 +103,13 @@ internal TelemetrySink(string org = "ttd", string name = "test", string version InstrumentPublished = (instrument, listener) => { var sameSource = ReferenceEquals(instrument.Meter, Object.Meter); - if (!sameSource) + if ( + !sameSource + && ( + shouldAlsoListenToMetrics is null + || !shouldAlsoListenToMetrics(serviceProvider!, instrument.Meter) + ) + ) { return; } @@ -108,14 +155,14 @@ public void Dispose() } } - internal sealed class Scope : IDisposable + public sealed class Scope : IDisposable { - public bool IsDisposed { get; internal set; } + public bool IsDisposed { get; set; } public void Dispose() => Scopes.TryRemove(this, out _).Should().BeTrue(); } - internal static Scope CreateScope() + public static Scope CreateScope() { var scope = new Scope(); Scopes.TryAdd(scope, default).Should().BeTrue(); @@ -123,7 +170,7 @@ internal static Scope CreateScope() } } -internal class TelemetrySnapshot( +public class TelemetrySnapshot( IEnumerable? activities, IReadOnlyDictionary>? metrics ) @@ -142,47 +189,3 @@ internal class TelemetrySnapshot( ?.Select(m => new KeyValuePair>(m.Key, m.Value)) .Where(x => x.Value.Count != 0); } - -internal static class TelemetryDI -{ - internal static IServiceCollection AddTelemetrySink(this IServiceCollection services) - { - services.AddSingleton(_ => new TelemetrySink()); - services.AddSingleton(sp => sp.GetRequiredService().Object); - return services; - } -} - -public class TelemetryDITests -{ - [Theory] - [InlineData(true)] - [InlineData(false)] - public void TelemetryFake_Is_Disposed(bool materialize) - { - using var scope = TelemetrySink.CreateScope(); - scope.IsDisposed.Should().BeFalse(); - - var services = new ServiceCollection(); - services.AddTelemetrySink(); - var sp = services.BuildServiceProvider( - new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true, } - ); - - if (materialize) - { - var fake = sp.GetRequiredService(); - fake.IsDisposed.Should().BeFalse(); - scope.IsDisposed.Should().BeFalse(); - sp.Dispose(); - fake.IsDisposed.Should().BeTrue(); - scope.IsDisposed.Should().BeTrue(); - } - else - { - scope.IsDisposed.Should().BeFalse(); - sp.Dispose(); - scope.IsDisposed.Should().BeFalse(); - } - } -} diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index 93956770d..bb7c20f0b 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -60,6 +60,7 @@ + diff --git a/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs b/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs index fa61a88f8..71874d8e1 100644 --- a/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Notifications/Email/EmailNotificationClientTests.cs @@ -4,13 +4,13 @@ namespace Altinn.App.Core.Tests.Features.Notifications.Email; using System.Net.Http; using System.Text; using System.Text.Json; +using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Notifications.Email; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Notifications.Email; -using Altinn.App.Core.Tests.Mocks; using Altinn.Common.AccessTokenClient.Services; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; diff --git a/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs index 8e8de71b1..fa16226a1 100644 --- a/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs @@ -4,13 +4,13 @@ namespace Altinn.App.Core.Tests.Features.Notifications.Sms; using System.Net.Http; using System.Text; using System.Text.Json; +using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Notifications.Sms; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Notifications.Sms; -using Altinn.App.Core.Tests.Mocks; using Altinn.Common.AccessTokenClient.Services; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; diff --git a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs index 72f63d3e1..0d650e99c 100644 --- a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs @@ -1,9 +1,9 @@ #nullable disable +using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; using Altinn.App.Core.Implementation; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; -using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; diff --git a/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs b/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs index bf2ef0863..84e49f365 100644 --- a/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs +++ b/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs @@ -1,13 +1,12 @@ #nullable disable using System.Net; using System.Text.Json; +using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients.Events; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; -using Altinn.App.Core.Tests.Mocks; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; diff --git a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.AddCompleteConfirmation_SuccessfulCallToStorage.verified.txt b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.AddCompleteConfirmation_SuccessfulCallToStorage.verified.txt index 7fe13848a..ea382db8c 100644 --- a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.AddCompleteConfirmation_SuccessfulCallToStorage.verified.txt +++ b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.AddCompleteConfirmation_SuccessfulCallToStorage.verified.txt @@ -7,7 +7,7 @@ instance.guid: Guid_1 }, { - instance.owner_party_id: 1337 + instance.owner.party.id: 1337 } ], IdFormat: W3C diff --git a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs index 8e1c471fb..2a5c332d5 100644 --- a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs @@ -1,11 +1,11 @@ #nullable disable using System.Net; using System.Text; +using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients.Storage; using Altinn.App.Core.Models; -using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs index 00e762c56..900150b28 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs @@ -1,9 +1,9 @@ #nullable disable using System.Security.Claims; using System.Text.Json; +using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; using Altinn.App.Core.Infrastructure.Clients.Authorization; -using Altinn.App.Core.Tests.Mocks; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Interfaces; using Altinn.Platform.Storage.Interface.Models; diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs index c622665d1..1b204e9ef 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs @@ -2,13 +2,13 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text; +using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients.Storage; using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; using Altinn.App.Core.Tests.Infrastructure.Clients.Storage.TestData; -using Altinn.App.Core.Tests.Mocks; using Altinn.App.PlatformServices.Tests.Data; using Altinn.App.PlatformServices.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; diff --git a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs index 5af60298f..bf618b42a 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs @@ -1,11 +1,11 @@ #nullable disable using System.Text.Encodings.Web; using System.Text.Json; +using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; -using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.Extensions.Options; diff --git a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs index 598c10895..e198a65f8 100644 --- a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Altinn.App.Common.Tests; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; using Altinn.App.Core.Internal.Auth; @@ -6,7 +7,6 @@ using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; -using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; diff --git a/test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.cs index 274a8260e..49eba51d9 100644 --- a/test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Patch/PatchServiceTests.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Altinn.App.Common.Tests; using Altinn.App.Core.Features; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; @@ -7,7 +8,6 @@ using Altinn.App.Core.Internal.Validation; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; -using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Json.Patch; diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index 9207fb561..079ad52cd 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -1,13 +1,12 @@ using System.Net; +using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Features; using Altinn.App.Core.Infrastructure.Clients.Pdf; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Pdf; using Altinn.App.Core.Internal.Profile; -using Altinn.App.Core.Tests.Mocks; using Altinn.App.PlatformServices.Tests.Helpers; using Altinn.App.PlatformServices.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; @@ -15,7 +14,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Moq; -using Xunit; namespace Altinn.App.PlatformServices.Tests.Internal.Pdf { diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index c59d7e38b..67cc00fae 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Altinn.App.Common.Tests; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; @@ -8,7 +9,6 @@ using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.UserAction; -using Altinn.App.Core.Tests.Mocks; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Enums; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs index 7e7f5b630..eb4586c86 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs @@ -1,12 +1,10 @@ -using Altinn.App.Core.Features; +using Altinn.App.Common.Tests; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Internal.Process.Elements.Base; using Altinn.App.Core.Tests.Internal.Process.TestUtils; -using Altinn.App.Core.Tests.Mocks; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs b/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs index 2dd3b69df..4c195975f 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs @@ -1,6 +1,5 @@ -using Altinn.App.Core.Features; +using Altinn.App.Common.Tests; using Altinn.App.Core.Internal.Process; -using Altinn.App.Core.Tests.Mocks; using Moq; namespace Altinn.App.Core.Tests.Internal.Process.TestUtils; From c36fc3582084dd521685cc2612ecfee0363b6f72 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Thu, 30 May 2024 09:46:53 +0200 Subject: [PATCH 20/22] Attempt to fix some flaky telemetry tests (#664) --- .../TelemetryEnrichingMiddlewareTests.cs | 3 +++ .../Altinn.App.Common.Tests.csproj | 1 + test/Altinn.App.Common.Tests/TelemetrySink.cs | 24 ++++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs index db0386145..c07aa6f5e 100644 --- a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs +++ b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs @@ -45,6 +45,7 @@ public async Task Should_Have_Root_AspNetCore_Trace_Org() var (telemetry, request) = AnalyzeTelemetry(token); await request(); + telemetry.TryFlush(); var activities = telemetry.CapturedActivities; var activity = Assert.Single( activities, @@ -69,6 +70,7 @@ public async Task Should_Have_Root_AspNetCore_Trace_User() var (telemetry, request) = AnalyzeTelemetry(token); await request(); + telemetry.TryFlush(); var activities = telemetry.CapturedActivities; var activity = Assert.Single( activities, @@ -96,6 +98,7 @@ public async Task Should_Always_Be_A_Root_Trace() Assert.NotNull(parentActivity); await request(); } + telemetry.TryFlush(); var activities = telemetry.CapturedActivities; var activity = Assert.Single( activities, diff --git a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj index 261873f17..184bb174a 100644 --- a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj +++ b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj @@ -30,6 +30,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/Altinn.App.Common.Tests/TelemetrySink.cs b/test/Altinn.App.Common.Tests/TelemetrySink.cs index dd0596137..ebe4e2e27 100644 --- a/test/Altinn.App.Common.Tests/TelemetrySink.cs +++ b/test/Altinn.App.Common.Tests/TelemetrySink.cs @@ -7,6 +7,8 @@ using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; using static Altinn.App.Common.Tests.TelemetrySink; namespace Altinn.App.Common.Tests; @@ -55,6 +57,7 @@ public sealed record TelemetrySink : IDisposable private readonly ConcurrentBag _activities = []; private readonly ConcurrentDictionary> _metricValues = []; + private readonly IServiceProvider? _serviceProvider; public readonly record struct MetricMeasurement(long Value, IReadOnlyDictionary Tags); @@ -67,6 +70,19 @@ public sealed record TelemetrySink : IDisposable public TelemetrySnapshot GetSnapshot(Activity activity) => new([activity], new Dictionary>()); + public void TryFlush() + { + Assert.NotNull(_serviceProvider); + + var meterProvider = _serviceProvider.GetService(); + var traceProvider = _serviceProvider.GetService(); + Assert.NotNull(meterProvider); + Assert.NotNull(traceProvider); + + _ = meterProvider.ForceFlush(25); + _ = traceProvider.ForceFlush(25); + } + public TelemetrySink( IServiceProvider? serviceProvider = null, string org = "ttd", @@ -77,6 +93,12 @@ public TelemetrySink( Func? shouldAlsoListenToMetrics = null ) { + _serviceProvider = serviceProvider; + if (shouldAlsoListenToActivities is not null) + Assert.NotNull(_serviceProvider); + if (shouldAlsoListenToMetrics is not null) + Assert.NotNull(_serviceProvider); + var appId = new AppIdentifier(org, name); var options = new AppSettings { AppVersion = version, }; @@ -87,7 +109,7 @@ public TelemetrySink( ShouldListenTo = (activitySource) => { var sameSource = ReferenceEquals(activitySource, Object.ActivitySource); - return sameSource || (shouldAlsoListenToActivities?.Invoke(serviceProvider!, activitySource) ?? false); + return sameSource || (shouldAlsoListenToActivities?.Invoke(_serviceProvider!, activitySource) ?? false); }, Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded, From f1ed9343c6345cf3f72bd3b9b38946176c377013 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Thu, 30 May 2024 09:56:35 +0200 Subject: [PATCH 21/22] Misc project cleanup - props, editorconfig, refactorings, docs (#661) --- .editorconfig | 97 +- .github/workflows/publish-release.yml | 3 +- CONTRIBUTING.md | 90 + Directory.Build.props | 34 +- LICENSE | 27 + README.md | 21 +- src/Altinn.App.Api/Altinn.App.Api.csproj | 8 +- .../Controllers/AccountController.cs | 49 +- .../Controllers/ActionsController.cs | 1 - .../ApplicationLanguageController.cs | 71 +- .../ApplicationMetadataController.cs | 175 +- .../ApplicationSettingsController.cs | 66 +- .../Controllers/AuthenticationController.cs | 99 +- .../Controllers/AuthorizationController.cs | 182 +- .../Controllers/DataController.cs | 1685 +++++++------- .../Controllers/DataListsController.cs | 132 +- .../Controllers/DataTagsController.cs | 284 ++- .../DisableFormValueModelBindingAttribute.cs | 41 +- .../Controllers/EventsReceiverController.cs | 132 +- .../Controllers/FileScanController.cs | 99 +- .../Controllers/HomeController.cs | 242 +- .../Controllers/InstancesController.cs | 1974 ++++++++--------- .../Controllers/OptionsController.cs | 165 +- .../Controllers/PagesController.cs | 119 +- .../Controllers/PartiesController.cs | 259 ++- .../Controllers/PdfController.cs | 297 ++- .../Controllers/ProcessController.cs | 1192 +++++----- .../Controllers/ProfileController.cs | 77 +- .../Controllers/RedirectController.cs | 117 +- .../Controllers/ResourceController.cs | 310 ++- .../Controllers/StatelessDataController.cs | 597 +++-- .../Controllers/StatelessPagesController.cs | 99 +- .../Controllers/TextsController.cs | 79 +- .../Controllers/ValidateController.cs | 241 +- ...rityHeadersApplicationBuilderExtensions.cs | 23 +- .../Extensions/ServiceCollectionExtensions.cs | 704 +++--- .../DataRestrictionValidation.cs | 293 ++- .../RequestHandling/MultipartRequestReader.cs | 211 +- .../Helpers/RequestHandling/RequestPart.cs | 49 +- .../RequestHandling/RequestPartValidator.cs | 159 +- ...teAntiforgeryTokenIfAuthCookieAttribute.cs | 57 +- ...eryTokenIfAuthCookieAuthorizationFilter.cs | 123 +- .../Infrastructure/Health/HealthCheck.cs | 31 +- .../Middleware/SecurityHeadersMiddleware.cs | 63 +- .../TelemetryEnrichingMiddleware.cs | 6 +- .../Telemetry/CustomTelemetryInitializer.cs | 25 +- .../Telemetry/HealthTelemetryFilter.cs | 55 +- .../Telemetry/IdentityTelemetryFilter.cs | 78 +- .../Mappers/SimpleInstanceMapper.cs | 71 +- .../Models/DataElementFileScanResult.cs | 31 +- .../Models/InstanceFileScanResult.cs | 117 +- .../Models/InstansiationInstance.cs | 57 +- src/Altinn.App.Api/Models/TagsList.cs | 19 +- src/Altinn.App.Core/Altinn.App.Core.csproj | 8 +- .../Configuration/AppSettings.cs | 410 ++-- .../Configuration/CacheSettings.cs | 17 +- .../Configuration/FrontEndSettings.cs | 15 +- .../Configuration/GeneralSettings.cs | 119 +- .../Configuration/PlatformSettings.cs | 105 +- .../Constants/AuthzConstants.cs | 41 +- src/Altinn.App.Core/Constants/General.cs | 57 +- .../EFormidling/EformidlingConstants.cs | 19 +- .../EFormidling/EformidlingStartup.cs | 103 +- .../EformidlingDeliveryException.cs | 31 +- .../EformidlingStatusCheckEventHandler.cs | 402 ++-- .../EformidlingStatusCheckEventHandler2.cs | 358 ++- .../Extensions/ClaimsPrincipalExtensions.cs | 95 +- .../ConfigurationBuilderExtensions.cs | 57 +- .../Extensions/DataProtectionConfiguration.cs | 55 +- .../Extensions/DictionaryExtensions.cs | 43 +- .../Extensions/HttpClientExtension.cs | 193 +- .../Extensions/HttpContextExtensions.cs | 33 +- .../Extensions/ServiceCollectionExtensions.cs | 485 ++-- .../Extensions/StringExtensions.cs | 71 +- .../Extensions/XmlToLinqExtensions.cs | 472 ++-- .../Features/Action/PaymentUserAction.cs | 120 +- .../Features/Action/SigningUserAction.cs | 1 - .../Features/Action/UserActionService.cs | 2 - .../Features/DataLists/DataListsFactory.cs | 53 +- .../Features/DataLists/DataListsService.cs | 85 +- .../Features/DataLists/IDataListsService.cs | 62 +- .../DataLists/InstanceDataListsFactory.cs | 53 +- .../DataLists/NullDataListProvider.cs | 34 +- .../DataLists/NullInstanceDataListProvider.cs | 42 +- .../FileAnalyzis/FileAnalyserFactory.cs | 37 +- .../FileAnalyzis/FileAnalysisResult.cs | 63 +- .../FileAnalyzis/FileAnalysisService.cs | 75 +- .../Features/FileAnalyzis/IFileAnalyser.cs | 29 +- .../FileAnalyzis/IFileAnalyserFactory.cs | 17 +- .../FileAnalyzis/IFileAnalysisService.cs | 23 +- .../Features/IAppOptionsProvider.cs | 35 +- .../Features/IDataListProvider.cs | 40 +- src/Altinn.App.Core/Features/IEventHandler.cs | 25 +- .../Features/IInstanceAppOptionsProvider.cs | 45 +- .../Features/IInstanceDataListProvider.cs | 50 +- .../Features/IInstanceValidator.cs | 1 - src/Altinn.App.Core/Features/IPageOrder.cs | 47 +- .../Email/EmailNotificationClient.cs | 1 - .../Sms/SmsNotificationClient.cs | 1 - .../Altinn2CodeListProvider.cs | 173 +- .../Altinn2MetadataApiClient.cs | 65 +- .../MetadataCodelistResponse.cs | 109 +- .../Features/Options/AppOptionsFactory.cs | 81 +- .../Features/Options/AppOptionsFileHandler.cs | 60 +- .../Features/Options/AppOptionsService.cs | 85 +- ...tionProviderServiceCollectionExtensions.cs | 101 +- .../Options/DefaultAppOptionsProvider.cs | 73 +- .../Options/IAppOptionsFileHandler.cs | 21 +- .../Features/Options/IAppOptionsService.cs | 57 +- .../Options/InstanceAppOptionsFactory.cs | 51 +- .../Options/NullInstanceAppOptionsProvider.cs | 37 +- .../Features/PageOrder/DefaultPageOrder.cs | 65 +- .../Payment/Exceptions/PaymentException.cs | 21 +- .../Payment/Models/PaymentInformation.cs | 41 +- .../Features/Payment/Models/PaymentStatus.cs | 59 +- .../Payment/Processors/Nets/INetsClient.cs | 45 +- .../Processors/Nets/Models/HttpApiResult.cs | 5 +- .../Nets/Models/NetsCreatePayment.cs | 2 - .../Processors/Nets/Models/NetsOrderItem.cs | 115 +- .../Nets/Models/NetsPaymentMethod.cs | 25 +- .../Models/NetsPaymentMethodConfiguration.cs | 33 +- .../Processors/Nets/NetsPaymentProcessor.cs | 1 - .../Payment/Services/IPaymentService.cs | 57 +- .../Features/Pdf/NullPdfFormatter.cs | 25 +- .../Telemetry.ApplicationMetadata.Client.cs | 8 +- .../Telemetry.ApplicationMetadata.Service.cs | 46 +- .../Telemetry.Authorization.Client.cs | 12 +- .../Telemetry.Authorization.Service.cs | 12 +- .../Features/Telemetry.Data.cs | 4 +- .../Features/Telemetry.DataClient.cs | 32 +- .../Features/Telemetry.Instances.cs | 26 +- .../Features/Telemetry.Prefill.Service.cs | 6 +- .../Features/Telemetry.Processes.cs | 8 +- .../Features/Telemetry.Validation.cs | 14 +- .../Default/DefaultTaskValidator.cs | 2 - .../LegacyIInstanceValidatorTaskValidator.cs | 1 - .../Validation/GenericFormDataValidator.cs | 12 +- .../Features/Validation/IFileValidator.cs | 31 +- src/Altinn.App.Core/Helpers/AppTextHelper.cs | 67 +- .../Helpers/AuthenticationHelper.cs | 37 +- .../CamelCaseExceptDictionaryResolver.cs | 19 +- src/Altinn.App.Core/Helpers/DataHelper.cs | 275 ++- .../Helpers/DataModel/DataModel.cs | 8 +- .../Helpers/DataModel/DataModelException.cs | 2 - .../Helpers/Extensions/StringExtensions.cs | 45 +- .../Extensions/Utf8JsonReaderExtentions.cs | 4 +- src/Altinn.App.Core/Helpers/IDataModel.cs | 5 - .../Helpers/InstantiationHelper.cs | 329 ++- src/Altinn.App.Core/Helpers/JsonHelper.cs | 353 ++- src/Altinn.App.Core/Helpers/MimeTypeMap.cs | 1263 ++++++----- src/Altinn.App.Core/Helpers/ObjectUtils.cs | 1 - src/Altinn.App.Core/Helpers/PathHelper.cs | 49 +- .../Helpers/PlatformHttpException.cs | 59 +- src/Altinn.App.Core/Helpers/ProcessError.cs | 27 +- src/Altinn.App.Core/Helpers/ProcessHelper.cs | 201 +- src/Altinn.App.Core/Helpers/SelfLinkHelper.cs | 141 +- .../Serialization/ModelDeserializer.cs | 233 +- .../Helpers/ServiceException.cs | 58 +- .../Helpers/ShadowFieldsConverter.cs | 61 +- src/Altinn.App.Core/Helpers/UserHelper.cs | 123 +- .../Implementation/AppResourcesSI.cs | 718 +++--- .../Implementation/PrefillSI.cs | 502 +++-- .../Implementation/UserTokenProvider.cs | 59 +- .../Authentication/AuthenticationClient.cs | 89 +- .../Authorization/AuthorizationClient.cs | 281 ++- .../Clients/Events/EventsClient.cs | 167 +- .../Events/EventsSubscriptionClient.cs | 153 +- .../Clients/Events/Subscription.cs | 99 +- .../Clients/Events/SubscriptionRequest.cs | 33 +- .../Clients/KeyVault/SecretsClient.cs | 111 +- .../Clients/KeyVault/SecretsLocalClient.cs | 115 +- .../Maskinporten/IX509CertificateProvider.cs | 25 +- .../Clients/Pdf/PdfGeneratorClient.cs | 1 - .../Clients/Profile/ProfileClient.cs | 146 +- .../Profile/ProfileClientCachingDecorator.cs | 77 +- .../Clients/Register/AltinnPartyClient.cs | 230 +- .../Clients/Register/PersonClient.cs | 141 +- .../Clients/Register/RegisterERClient.cs | 145 +- .../Clients/Storage/ApplicationClient.cs | 86 +- .../Clients/Storage/DataClient.cs | 934 ++++---- .../Clients/Storage/InstanceClient.cs | 582 +++-- .../Clients/Storage/InstanceEventClient.cs | 188 +- .../Clients/Storage/ProcessClient.cs | 140 +- .../Clients/Storage/SignClient.cs | 119 +- .../Clients/Storage/TextClient.cs | 132 +- .../Interface/IAppResources.cs | 353 ++- src/Altinn.App.Core/Interface/IApplication.cs | 23 +- .../Interface/IAuthentication.cs | 21 +- .../Interface/IAuthorization.cs | 69 +- src/Altinn.App.Core/Interface/IDSF.cs | 26 +- src/Altinn.App.Core/Interface/IData.cs | 392 ++-- src/Altinn.App.Core/Interface/IER.cs | 23 +- src/Altinn.App.Core/Interface/IEvents.cs | 19 +- src/Altinn.App.Core/Interface/IInstance.cs | 243 +- .../Interface/IInstanceEvent.cs | 43 +- .../Interface/IPersonLookup.cs | 35 +- .../Interface/IPersonRetriever.cs | 35 +- src/Altinn.App.Core/Interface/IPrefill.cs | 55 +- src/Altinn.App.Core/Interface/IProcess.cs | 29 +- src/Altinn.App.Core/Interface/IProfile.cs | 23 +- src/Altinn.App.Core/Interface/IRegister.cs | 35 +- src/Altinn.App.Core/Interface/ISecrets.cs | 57 +- .../Interface/IUserTokenProvider.cs | 25 +- .../Internal/App/AppMetadata.cs | 193 +- .../App/ApplicationConfigException.cs | 43 +- .../Internal/App/FrontendFeatures.cs | 43 +- .../Internal/App/IAppMetadata.cs | 45 +- .../Internal/App/IAppResources.cs | 313 ++- .../Internal/App/IApplicationClient.cs | 21 +- .../Internal/App/IFrontendFeatures.cs | 19 +- .../Internal/Auth/IAuthenticationClient.cs | 19 +- .../Internal/Auth/IAuthorizationClient.cs | 85 +- .../Internal/Auth/IAuthorizationService.cs | 83 +- .../Internal/Auth/IUserTokenProvider.cs | 23 +- .../Internal/Data/DataService.cs | 209 +- .../Internal/Data/IDataClient.cs | 450 ++-- .../Internal/Data/IDataService.cs | 101 +- .../Internal/Events/EventHandlerResolver.cs | 41 +- .../Internal/Events/IEventHandlerResolver.cs | 21 +- .../Events/IEventSecretCodeProvider.cs | 25 +- .../Internal/Events/IEventsClient.cs | 17 +- .../Internal/Events/IEventsSubscription.cs | 17 +- .../Events/KeyVaultSecretCodeProvider.cs | 61 +- .../Events/SubscriptionValidationHandler.cs | 27 +- .../Internal/Events/UnhandledEventHandler.cs | 29 +- .../Expressions/ExpressionEvaluator.cs | 10 +- .../ExpressionEvaluatorTypeErrorException.cs | 2 - .../Internal/Instances/IInstanceClient.cs | 241 +- .../Instances/IInstanceEventClient.cs | 41 +- .../Internal/Language/ApplicationLanguage.cs | 103 +- .../Internal/Language/IApplicationLanguage.cs | 19 +- .../Maskinporten/MaskinportenExtensions.cs | 41 +- .../MaskinportenJwkTokenProvider.cs | 135 +- .../Internal/Pdf/IPdfService.cs | 39 +- .../Internal/Pdf/PdfGenerationException.cs | 43 +- .../Internal/Pdf/PdfService.cs | 1 - .../Internal/Prefill/IPrefill.cs | 53 +- .../AltinnExtensionProperties/AltinnAction.cs | 111 +- .../AltinnGatewayExtension.cs | 19 +- .../AltinnPaymentConfiguration.cs | 75 +- .../AltinnTaskExtension.cs | 53 +- .../Internal/Process/Elements/Definitions.cs | 43 +- .../Internal/Process/Elements/EndEvent.cs | 22 +- .../Process/Elements/ExclusiveGateway.cs | 43 +- .../Process/Elements/ExtensionElements.cs | 29 +- .../Internal/Process/Elements/Process.cs | 87 +- .../Internal/Process/Elements/ProcessTask.cs | 33 +- .../Internal/Process/Elements/SequenceFlow.cs | 59 +- .../Internal/Process/Elements/StartEvent.cs | 22 +- .../Internal/Process/Elements/UserAction.cs | 42 +- .../EventHandlers/EndEventEventHandler.cs | 73 +- .../Interfaces/IEndEventEventHandler.cs | 17 +- .../ProcessTask/AbandonTaskEventHandler.cs | 53 +- .../ProcessTask/EndTaskEventHandler.cs | 117 +- .../Interfaces/IAbandonTaskEventHandler.cs | 25 +- .../Interfaces/IEndTaskEventHandler.cs | 25 +- .../Interfaces/IStartTaskEventHandler.cs | 17 +- .../ProcessTask/StartTaskEventHandler.cs | 91 +- .../Process/ExpressionsExclusiveGateway.cs | 321 ++- .../Process/Interfaces/IProcessClient.cs | 27 +- .../Process/Interfaces/IProcessEngine.cs | 47 +- .../IProcessEventHandlerDelegator.cs | 25 +- .../Process/Interfaces/IProcessNavigator.cs | 25 +- .../Process/ProcessEventHandlingDelegator.cs | 191 +- .../Internal/Process/ProcessException.cs | 21 +- .../Process/ProcessSequenceFlowType.cs | 33 +- .../Interfaces/IProcessTaskDataLocker.cs | 37 +- .../Interfaces/IProcessTaskFinalizer.cs | 23 +- .../Interfaces/IProcessTaskInitializer.cs | 23 +- .../Common/ProcessTaskInitializer.cs | 1 - .../ProcessTasks/ConfirmationProcessTask.cs | 45 +- .../Process/ProcessTasks/DataProcessTask.cs | 45 +- .../ProcessTasks/FeedbackProcessTask.cs | 45 +- .../ProcessTasks/Interfaces/IProcessTask.cs | 41 +- .../ProcessTasks/PaymentProcessTask.cs | 139 +- .../ProcessTasks/SigningProcessTask.cs | 43 +- .../Internal/Profile/IProfileClient.cs | 21 +- .../Internal/Registers/IAltinnPartyClient.cs | 33 +- .../Internal/Registers/IOrganizationClient.cs | 21 +- .../Internal/Registers/IPersonClient.cs | 33 +- .../Internal/Secrets/ISecretsClient.cs | 55 +- .../Internal/Sign/ISignClient.cs | 2 - src/Altinn.App.Core/Internal/Texts/IText.cs | 27 +- .../Validation/FileValidationService.cs | 77 +- .../Validation/FileValidatorFactory.cs | 37 +- .../Validation/IFileValidationService.cs | 23 +- .../Validation/IFileValidatorFactory.cs | 17 +- .../Internal/Validation/IValidatorFactory.cs | 2 - src/Altinn.App.Core/Models/AppIdentifier.cs | 227 +- src/Altinn.App.Core/Models/AppOption.cs | 336 ++- src/Altinn.App.Core/Models/AppOptions.cs | 37 +- .../Models/ApplicationLanguage.cs | 21 +- .../Models/ApplicationMetadata.cs | 114 +- src/Altinn.App.Core/Models/Attachment.cs | 33 +- src/Altinn.App.Core/Models/AttachmentList.cs | 25 +- .../Models/CalculationResult.cs | 95 +- src/Altinn.App.Core/Models/CloudEvent.cs | 115 +- src/Altinn.App.Core/Models/Components.cs | 17 +- src/Altinn.App.Core/Models/DataList.cs | 31 +- .../Models/DataListMetadata.cs | 53 +- .../Models/InstanceIdentifier.cs | 255 ++- .../Models/InstanceSelection.cs | 61 +- .../Models/Layout/Components/BaseComponent.cs | 1 - .../Layout/Components/GroupComponent.cs | 1 - .../Layout/Components/OptionsComponent.cs | 3 - .../Layout/Components/SummaryComponent.cs | 3 - .../Models/Layout/LayoutModel.cs | 3 - .../Models/Layout/PageComponentConverter.cs | 16 +- src/Altinn.App.Core/Models/LayoutSet.cs | 33 +- src/Altinn.App.Core/Models/LayoutSets.cs | 17 +- src/Altinn.App.Core/Models/LayoutSettings.cs | 25 +- src/Altinn.App.Core/Models/Logo.cs | 43 +- src/Altinn.App.Core/Models/OnEntry.cs | 19 +- src/Altinn.App.Core/Models/Pages.cs | 25 +- .../Models/Process/ProcessChangeResult.cs | 105 +- .../Models/Process/ProcessStateChange.cs | 33 +- src/Altinn.App.Core/Models/QueryResponse.cs | 49 +- src/Altinn.App.Core/Models/StylesConfig.cs | 25 +- src/Altinn.App.Core/Models/UserContext.cs | 87 +- .../Models/Validation/ExpressionValidation.cs | 67 +- .../Validation/FrontendSeverityConverter.cs | 65 +- .../InstantiationValidationResult.cs | 33 +- .../Models/Validation/ValidationException.cs | 45 +- .../Models/Validation/ValidationIssue.cs | 155 +- .../Models/Validation/ValidationIssueCodes.cs | 101 +- .../Validation/ValidationIssueSeverity.cs | 59 +- .../Validation/ValidationIssueSource.cs | 49 +- src/Directory.Build.props | 30 +- .../Altinn.App.Api.Tests.csproj | 90 +- .../Constants/XacmlRequestAttribute.cs | 95 +- .../Controllers/ActionsControllerTests.cs | 1 - .../ApplicationMetadataControllerTests.cs | 1 - .../Controllers/DataControllerTests.cs | 407 ++-- .../Controllers/DataController_PatchTests.cs | 1 - .../Controllers/DataController_PutTests.cs | 2 - .../EventsReceiverControllerTests.cs | 141 +- .../Controllers/FileScanControllerTests.cs | 123 +- ...nstancesController_ActiveInstancesTests.cs | 1 - .../InstancesController_CopyInstanceTests.cs | 1 - .../InstancesController_PostNewInstance.cs | 1 - .../Controllers/OptionsControllerTests.cs | 390 ++-- .../Controllers/PdfControllerTests.cs | 610 +++-- .../Controllers/ProcessControllerTests.cs | 1 - .../StatelessDataControllerTests.cs | 1 - .../StatelessPagesControllerTests.cs | 1 - .../Controllers/TextsControllerTests.cs | 1 - .../Controllers/ValidateControllerTests.cs | 1 - .../ValidateControllerValidateDataTests.cs | 1 - ...alidateController_ValidateInstanceTests.cs | 1 - test/Altinn.App.Api.Tests/Data/TestData.cs | 2 - .../apps/tdd/eformidling-app/logic/App.cs | 61 +- .../logic/EFormidlingMetadata.cs | 100 +- .../apps/tdd/eformidling-app/models/Skjema.cs | 22 +- ...EformidlingStatusCheckEventHandlerTests.cs | 1 - .../DataRestrictionValidationTests.cs | 4 - .../Helpers/StartupHelperTests.cs | 2 - .../Mappers/SimpleInstanceMapperTests.cs | 3 - .../SecurityHeadersMiddlewareTests.cs | 5 - .../Mocks/AppMetadataMock.cs | 253 ++- .../ConfigurationManagerStub.cs | 65 +- .../Authentication/ISigningKeysRetriever.cs | 23 +- .../JwtCookiePostConfigureOptionsStub.cs | 54 +- .../SigningKeysRetrieverStub.cs | 23 +- .../Mocks/AuthorizationMock.cs | 103 +- .../Mocks/DataClientMock.cs | 896 ++++---- .../Mocks/Event/DummyFailureEventHandler.cs | 16 +- .../Mocks/Event/DummySuccessEventHandler.cs | 16 +- .../Event/EventSecretCodeProviderStub.cs | 12 +- .../Mocks/InstanceClientMockSi.cs | 923 ++++---- .../Mocks/JwtTokenMock.cs | 109 +- .../Mocks/PepWithPDPAuthorizationMockSI.cs | 511 +++-- .../Models/InstanceFileScanResultTests.cs | 271 +-- .../Models/XacmlResourceAttributes.cs | 55 +- .../OpenApi/OpenApiSpecChangeDetection.cs | 1 - .../Telemetry/TelemetryConfigurationTests.cs | 1 - .../SwaggerIncludeXmlCommentsTestDouble.cs | 3 - test/Altinn.App.Api.Tests/Utils/JsonUtils.cs | 1 - .../Utils/PrincipalUtil.cs | 203 +- .../Altinn.App.Common.Tests.csproj | 2 +- .../Altinn.App.Core.Tests.csproj | 2 +- .../DataLists/DataListsFactoryTest.cs | 82 +- .../DataLists/InstanceDataListsFactoryTest.cs | 92 +- .../DataLists/NullDataListProviderTest.cs | 20 +- .../NullInstanceDataListProviderTest.cs | 28 +- .../DefaultEFormidlingServiceTests.cs | 1 - .../Extensions/DictionaryExtensionsTests.cs | 102 +- .../InstanceEventExtensionsTests.cs | 1 - .../Extensions/ProcessStateExtensionTests.cs | 1 - .../Extensions/ServiceCollectionTests.cs | 30 +- .../Extensions/StringExtensionTests.cs | 220 +- .../Features/Action/PaymentUserActionTests.cs | 10 - .../Features/Action/SigningUserActionTests.cs | 1 - .../Action/UniqueSignatureAuthorizerTests.cs | 3 - .../Features/Action/UserActionServiceTests.cs | 1 - .../GenericDataProcessorTests.cs | 1 - .../NullInstantiationProcessTests.cs | 1 - .../Sms/SmsNotificationClientTests.cs | 1 - ...2MetadataApiClientHttpMessageHandlerMoq.cs | 79 +- .../Altinn2OptionsCacheTests.cs | 162 +- .../Altinn2Provider/Altinn2OptionsTests.cs | 258 ++- .../Options/AppOptionsFactoryTests.cs | 192 +- .../Options/InstanceAppOptionsFactoryTests.cs | 80 +- .../Features/Options/JoinedAppOptionsTests.cs | 1 - .../NullInstanceAppOptionsProviderTests.cs | 28 +- .../Providers/Nets/HttpApiResultTests.cs | 1 - .../Payment/Providers/Nets/NetsMapperTests.cs | 1 - .../Nets/NetsPaymentProcessorTests.cs | 3 - .../Default/DataAnnotationValidatorTests.cs | 1 - .../Default/DefaultTaskValidatorTests.cs | 1 - .../Default/ExpressionValidatorTests.cs | 2 - .../Default/LegacyIValidationFormDataTests.cs | 270 ++- .../Validators/GenericValidatorTests.cs | 1 - .../NullInstantiationValidatorTests.cs | 1 - .../Validators/ValidationServiceOldTests.cs | 1 - .../Validators/ValidationServiceTests.cs | 1 - .../Helpers/JsonHelperTests.cs | 1 - .../JsonSerializerIgnorePrefixTests.cs | 1 - .../Helpers/LinqExpressionHelpersTests.cs | 1 - .../Helpers/MimeTypeMapTests.cs | 1 - .../Helpers/MultiDecisionHelperTests.cs | 1 - .../Helpers/ObjectUtilsTests.cs | 1 - .../ObjectUtils_XmlSerializationTests.cs | 1 - .../Helpers/PathHelperTests.cs | 62 +- .../Helpers/ProcessHelperTests.cs | 4 - .../Helpers/RemoveBomExtentionsTests.cs | 1 - .../Helpers/SelfLinkHelperTests.cs | 42 +- .../Helpers/ShadowFieldsConverterTests.cs | 1 - .../Helpers/Utf8JsonReaderExtensionsTests.cs | 1 - .../Implementation/AppResourcesSITests.cs | 1 - .../Implementation/DefaultPageOrderTest.cs | 136 +- .../Implementation/EventsClientTest.cs | 355 ++- .../Implementation/InstanceClientTests.cs | 875 ++++---- .../Implementation/NullPdfFormatterTests.cs | 1 - .../Implementation/PersonClientTests.cs | 282 ++- .../AppDataModel/ModelWithShadowFields.cs | 117 +- .../Implementation/TextClientTest.cs | 258 ++- .../Clients/EventsSubscriptionClientTests.cs | 122 +- .../Clients/Storage/DataClientTests.cs | 1639 +++++++------- .../Clients/Storage/SignClientTests.cs | 1 - .../Internal/App/AppMedataTest.cs | 984 ++++---- .../Internal/App/FrontendFeaturesTest.cs | 58 +- .../Auth/AuthorizationServiceTests.cs | 1 - .../Internal/Data/DataServiceTests.cs | 365 ++- .../Events/EventHandlerResolverTests.cs | 59 +- .../Events/UnhandledEventHandlerTests.cs | 18 +- .../MaskinportenExtensionsTests.cs | 1 - .../MaskinportenJwkTokenProviderTests.cs | 2 - .../Internal/Pdf/PdfServiceTests.cs | 425 ++-- .../TestData/UserActionAuthorizerStub.cs | 11 +- ...thorizerServiceCollectionExtensionTests.cs | 1 - .../Process/Elements/AppProcessStateTests.cs | 1 - .../AbandonTaskEventHandlerTests.cs | 1 - .../ProcessTask/EndTaskEventHandlerTests.cs | 1 - .../ProcessTask/StartTaskEventHandlerTests.cs | 1 - .../ExpressionsExclusiveGatewayTests.cs | 1 - .../Internal/Process/ProcessEngineTest.cs | 1 - .../Process/ProcessEventHandlingTests.cs | 1 - .../Internal/Process/ProcessNavigatorTests.cs | 1 - .../Common/ProcessTaskDataLockerTests.cs | 284 ++- .../Common/ProcessTaskFinalizerTests.cs | 134 +- .../ProcessTasks/PaymentProcessTaskTests.cs | 1 - .../EformidlingServiceTaskTests.cs | 1 - .../ServiceTasks/PdfServiceTaskTests.cs | 1 - .../ServiceTasks/TestData/DummyDataType.cs | 7 +- .../Internal/Process/TestData/DummyModel.cs | 11 +- .../TestBackendExclusiveFunctions.cs | 1 - .../CommonTests/TestContextList.cs | 1 - .../CommonTests/TestFunctions.cs | 1 - .../CommonTests/TestInvalid.cs | 1 - .../FullTests/Test1/RunTest1.cs | 1 - .../FullTests/Test2/RunTest2.cs | 1 - .../FullTests/Test3/RunTest3.cs | 1 - .../LayoutExpressions/TestDataModel.cs | 1 - .../Mocks/DelegatingHandlerStub.cs | 37 +- .../Mocks/RequestInterceptor.cs | 71 +- .../Models/AppIdentifierTests.cs | 208 +- .../Models/ApplicationMetdataTests.cs | 1 - .../Models/InstanceIdentifierTests.cs | 136 +- .../Models/MimeTypeTests.cs | 1 - .../Models/PageComponentConverterTests.cs | 1 - .../Models/Result/ServiceResultTests.cs | 1 - test/Directory.Build.props | 2 +- 482 files changed, 23949 insertions(+), 24873 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE diff --git a/.editorconfig b/.editorconfig index 5243a2812..32314fd23 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,56 +10,77 @@ indent_style = space indent_size = 4 max_line_length = 120 +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + [*.{cs,vb}] dotnet_sort_system_directives_first = true dotnet_separate_import_directive_groups = false -#### Naming styles #### +end_of_line = crlf -# Naming rules +#### Naming styles #### -dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +# Interfaces start with 'I' +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case +dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i -dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +# Types should be PascalCase +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum, namespace, delegate +dotnet_naming_rule.types_should_be_pascal_case.severity = warning dotnet_naming_rule.types_should_be_pascal_case.symbols = types dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case - -# Symbol specifications - -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = - -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = - -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = - -# Naming styles - -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case +# Non-private members should be PascalCase +dotnet_naming_symbols.non_private_members.applicable_kinds = property, event, field +dotnet_naming_symbols.non_private_members.applicable_accessibilities = public, internal, protected, protected_internal +dotnet_naming_rule.non_private_members_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_private_members_should_be_pascal_case.symbols = non_private_members +dotnet_naming_rule.non_private_members_should_be_pascal_case.style = pascal_case + +# All methods should be PascalCase +dotnet_naming_symbols.methods.applicable_kinds = method, local_function +dotnet_naming_rule.methods_should_be_pascal_case.severity = warning +dotnet_naming_rule.methods_should_be_pascal_case.symbols = methods +dotnet_naming_rule.methods_should_be_pascal_case.style = pascal_case + +# Private member should be '_' prefixed and camelCase +dotnet_naming_symbols.private_members.applicable_kinds = property, event, field +dotnet_naming_symbols.private_members.applicable_accessibilities = private, private_protected +dotnet_naming_style.__prefixed_camel_case.required_prefix = _ +dotnet_naming_style.__prefixed_camel_case.required_suffix = +dotnet_naming_style.__prefixed_camel_case.word_separator = +dotnet_naming_style.__prefixed_camel_case.capitalization = camel_case +dotnet_naming_rule.private_members_should_be___prefixed_camel_case.severity = warning +dotnet_naming_rule.private_members_should_be___prefixed_camel_case.symbols = private_members +dotnet_naming_rule.private_members_should_be___prefixed_camel_case.style = __prefixed_camel_case + +# Const fields should be PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = warning +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const + + +# General naming styles dotnet_naming_style.pascal_case.required_prefix = dotnet_naming_style.pascal_case.required_suffix = dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion @@ -67,16 +88,11 @@ dotnet_style_prefer_auto_properties = true:silent dotnet_style_object_initializer = true:suggestion dotnet_style_collection_initializer = true:suggestion dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -indent_size = 4 -max_line_length = 160 -end_of_line = crlf dotnet_style_prefer_simplified_boolean_expressions = true:suggestion csharp_using_directive_placement = outside_namespace:silent csharp_prefer_simple_using_statement = true:suggestion csharp_prefer_braces = true:silent -csharp_style_namespace_declarations= file_scoped:suggestion csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_top_level_statements = true:silent csharp_style_expression_bodied_methods = false:silent @@ -89,6 +105,13 @@ csharp_style_expression_bodied_lambdas = true:silent csharp_style_expression_bodied_local_functions = false:silent csharp_indent_labels = one_less_than_current csharp_style_prefer_primary_constructors = false:suggestion +csharp_style_namespace_declarations = file_scoped:error + +# Naming rule violation +dotnet_diagnostic.IDE1006.severity = error + +# Unused usings +dotnet_diagnostic.IDE0005.severity = suggestion # CA1848: Use the LoggerMessage delegates dotnet_diagnostic.CA1848.severity = none diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index acc77d37c..2993b1073 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -27,11 +27,10 @@ jobs: dotnet build --configuration Release --no-restore -p:Deterministic=true -p:BuildNumber=${{ github.run_number }} - name: Pack run: | - dotnet pack AppLibDotnet.sln --configuration Release --no-restore --no-build -p:BuildNumber=${{ github.run_number }} -p:Deterministic=true + dotnet pack AppLibDotnet.sln --configuration Release --no-restore --no-build -p:Deterministic=true -p:BuildNumber=${{ github.run_number }} - name: Versions run: | dotnet --version - name: Publish run: | dotnet nuget push src/**/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} - \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..f0d6f8af6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,90 @@ +# How to contribute + +Developer documentation for Altinn.App .NET libraries. + +Here are some important resources: + + * [Team Apps Github board](https://github.com/orgs/Altinn/projects/39/views/2) + * [Altinn Studio docs](https://docs.altinn.studio/) + +## Reporting Issues + +Open [our Github issue tracker](https://github.com/Altinn/app-lib-dotnet/issues/new/choose) +and choose an appropriate issue template. + +Feel free to query existing issues before creating a new one. + +## Contributing Changes + +* Fork and/or create branch +* Push changes +* Test your changes, see the testing changes below +* Create PR - fill in the required sections + * Try to provide reasoning/rationale for the changes if it hasn't already been discussed + * Attach appropriate tags according to the change (`feature`, `kind/feature-request`, `bugfix`, `kind/bug`, `kind/chore`) + * If you work on team Apps, attach the `Team Apps` project, add it to a sprint and input an estimate (if an issue isn't already added) +* Make sure coding style is consistent + * Csharpier for formatting (`editor.formatOnSave` is on by default, there should be an extension for your editor/IDE) + * EditorConfig is configured, you should use an editor or IDE that supports it (that should cover other conventions) +* We require 1 approval to merge changes + * Make sure Sonar / CodeQL and other static analysis issues are resolved + * We don't need 100% code coverage, effort and risk must be weighed + * Use squash merge + * Use a descriptive PR title, as it is used for release notes generation + +### Versioning + +We use semantic versioning. So we avoid breaking changes for anything that might break builds or change behavior. + +### Telemetry + +The Altinn app libraries are instrumented with `System.Diagnostics`, and telemetry is shipped via OpenTelemetry. +When developing new features and code, failure modes should be considered carefully, and telemetry should be added +such that we can observe that the code works correctly when running locally and in test and production environments. +See existing code for tips and clues. + +Parts of the telemetry is considered public contract. +Consumers may build alerting and dashboards based on this telemetry, so if we change names and tags +that may break things downstream. Names and tags are in the `Telemetry` class. + +### Testing + +We have automated tests in the `test/` folder using mainly xUnit, FluentAssertions, Moq and Verify. +Some tests invoke classes directly (while mocking dependencies as needed), +while some construct adhoc DI containers or use ASP.NET Core `WebApplicationFactory<>`. +The following resources are currently snapshot tested (some with Verify) + +* OpenAPI doc +* Telemetry output (activities/spans and metrics) + +In addition to automated testing, we should do manual tests for any non-trivial change as well. + +#### Manual testing + +To manually test changes, simply make your app reference the libraries directly. See the diff below, +and make sure the relative directory paths work for your setup. + +```csproj + + + lib\$(TargetFramework)\*.xml + + +``` + +Make sure [localtest](https://github.com/Altinn/app-localtest) is running +Run your app + +```shell +dotnet run --project App/ +``` + +To debug changes in the libraries, you can + +* Add the `altinn-lib-donet/src` projects to your apps `App.sln` file +* Create a `code-workspace` file in the case of VSCode + +Debug breakpoints should then work as normal diff --git a/Directory.Build.props b/Directory.Build.props index ad238e3a6..b945e8251 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,20 +1,20 @@ - - true - + + true + - - latest - true - Minimum - strict - - + + latest + true + Minimum + strict + + - - - all - runtime; build; native; contentfiles; analyzers - - - + + + all + runtime; build; native; contentfiles; analyzers + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..e36cdaa11 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017, Altinn +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Altinn nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md index f39260bb0..318522072 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,27 @@ -# app-template-dotnet -Altinn application template using .NET 6 +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/Altinn/app-lib-dotnet/dotnet-test.yml?branch=main)](https://github.com/Altinn/app-lib-dotnet/actions) +[![GitHub](https://img.shields.io/github/license/Altinn/app-lib-dotnet?style=flat-square)](https://github.com/Altinn/app-lib-dotnet/blob/main/LICENSE)
+[![NuGet / Altinn.App.Core](https://img.shields.io/nuget/v/Altinn.App.Core?label=Altinn.App.Core)](https://www.nuget.org/packages/Altinn.App.Core) +[![NuGet / Altinn.App.Api](https://img.shields.io/nuget/v/Altinn.App.Api?label=Altinn.App.Api)](https://www.nuget.org/packages/Altinn.App.Api) -## Goal for template +# Altinn.App .NET libraries -The goal for this template is to give application owners using Altinn Studio a application template that covers common functional and technical needs. +Altinn.App .NET libraries for apps running on Altinn 3, part of the [Altinn Studio product](https://docs.altinn.studio/altinn-studio/about/). -- BPMN Process Support -- Api for app-frontend and end user system +These libraries support the runtime of Altinn apps by +* Exposing APIs used by service owners and the [frontend (app-frontend-react)](https://github.com/Altinn/app-frontend-react) of an Altinn app +* Exposing abstractions to interact with features of Altinn Studio and Altinn 3 Platform services + +## Contributing + +See [/CONTRIBUTING.md](/CONTRIBUTING.md). ## Architecture This template is built using .NET -The below diagram shows the different layers. +The below diagram shows the different layers. ![Template](https://raw.githubusercontent.com/Altinn/app-template-dotnet/main/apptemplate.drawio.svg) diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index 51bc24cec..f6a040ad9 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -8,13 +8,7 @@ This class library holds all the API controllers used by a standard Altinn 3 App. - https://github.com/Altinn/app-lib-dotnet/releases - Altinn Platform Contributors - git - https://github.com/Altinn/app-lib-dotnet true - enable - enable {E8F29FE8-6B62-41F1-A08C-2A318DD08BB4} @@ -39,4 +33,4 @@
- + \ No newline at end of file diff --git a/src/Altinn.App.Api/Controllers/AccountController.cs b/src/Altinn.App.Api/Controllers/AccountController.cs index 687a54c01..ae1b2b210 100644 --- a/src/Altinn.App.Api/Controllers/AccountController.cs +++ b/src/Altinn.App.Api/Controllers/AccountController.cs @@ -3,35 +3,34 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// To handle authentication related operations +/// +public class AccountController : Controller { + private readonly PlatformSettings _platformSettings; + private readonly ILogger _logger; + /// - /// To handle authentication related operations + /// Initializes a new instance of the class /// - public class AccountController : Controller + /// the configuration for platform + /// the logger + public AccountController(IOptions platformSettings, ILogger logger) { - private readonly PlatformSettings _platformSettings; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class - /// - /// the configuration for platform - /// the logger - public AccountController(IOptions platformSettings, ILogger logger) - { - _platformSettings = platformSettings.Value; - _logger = logger; - } + _platformSettings = platformSettings.Value; + _logger = logger; + } - /// - /// Redirects to login - /// - /// - public IActionResult Login(string goToUrl) - { - _logger.LogInformation($"Account login - gotourl - {goToUrl}"); - return Redirect($"{_platformSettings.ApiAuthenticationEndpoint}authentication?goto={goToUrl}"); - } + /// + /// Redirects to login + /// + /// + public IActionResult Login(string goToUrl) + { + _logger.LogInformation($"Account login - gotourl - {goToUrl}"); + return Redirect($"{_platformSettings.ApiAuthenticationEndpoint}authentication?goto={goToUrl}"); } } diff --git a/src/Altinn.App.Api/Controllers/ActionsController.cs b/src/Altinn.App.Api/Controllers/ActionsController.cs index c0a94906f..7f1684461 100644 --- a/src/Altinn.App.Api/Controllers/ActionsController.cs +++ b/src/Altinn.App.Api/Controllers/ActionsController.cs @@ -7,7 +7,6 @@ using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Instances; -using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Validation; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Process; diff --git a/src/Altinn.App.Api/Controllers/ApplicationLanguageController.cs b/src/Altinn.App.Api/Controllers/ApplicationLanguageController.cs index 0899fa15a..eeca4dcbf 100644 --- a/src/Altinn.App.Api/Controllers/ApplicationLanguageController.cs +++ b/src/Altinn.App.Api/Controllers/ApplicationLanguageController.cs @@ -4,50 +4,49 @@ using Microsoft.AspNetCore.Mvc; using ApplicationLanguage = Altinn.App.Core.Models.ApplicationLanguage; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Represents the Application language API giving access to the different languages supported by the application. +/// +[Route("{org}/{app}/api/v1/applicationlanguages")] +[Authorize] +public class ApplicationLanguageController : ControllerBase { + private readonly IApplicationLanguage _applicationLanguage; + private readonly ILogger _logger; + /// - /// Represents the Application language API giving access to the different languages supported by the application. + /// Initializes a new instance of the class. /// - [Route("{org}/{app}/api/v1/applicationlanguages")] - [Authorize] - public class ApplicationLanguageController : ControllerBase + /// An implementation with access to application languages. + /// The logger. + public ApplicationLanguageController( + IApplicationLanguage applicationLanguage, + ILogger logger + ) { - private readonly IApplicationLanguage _applicationLanguage; - private readonly ILogger _logger; + _applicationLanguage = applicationLanguage; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// An implementation with access to application languages. - /// The logger. - public ApplicationLanguageController( - IApplicationLanguage applicationLanguage, - ILogger logger - ) + /// + /// Method to retrieve the supported languages from the application. + /// + /// Returns a list of ApplicationLanguages. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetLanguages() + { + try { - _applicationLanguage = applicationLanguage; - _logger = logger; + return await _applicationLanguage.GetApplicationLanguages(); } - - /// - /// Method to retrieve the supported languages from the application. - /// - /// Returns a list of ApplicationLanguages. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetLanguages() + catch (DirectoryNotFoundException) { - try - { - return await _applicationLanguage.GetApplicationLanguages(); - } - catch (DirectoryNotFoundException) - { - _logger.LogWarning("Something went wrong while trying to fetch the application language"); - return NotFound(); - } + _logger.LogWarning("Something went wrong while trying to fetch the application language"); + return NotFound(); } } } diff --git a/src/Altinn.App.Api/Controllers/ApplicationMetadataController.cs b/src/Altinn.App.Api/Controllers/ApplicationMetadataController.cs index 88ef66ab2..02d86b06e 100644 --- a/src/Altinn.App.Api/Controllers/ApplicationMetadataController.cs +++ b/src/Altinn.App.Api/Controllers/ApplicationMetadataController.cs @@ -3,116 +3,115 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Hanldes application metadata +/// AllowAnonymous, because this is static known information and used from LocalTest +/// +[AllowAnonymous] +[ApiController] +public class ApplicationMetadataController : ControllerBase { + private readonly IAppMetadata _appMetadata; + private readonly ILogger _logger; + /// - /// Hanldes application metadata - /// AllowAnonymous, because this is static known information and used from LocalTest + /// Initializes a new instance of the class + /// The IAppMetadata service + /// Logger for ApplicationMetadataController /// - [AllowAnonymous] - [ApiController] - public class ApplicationMetadataController : ControllerBase + public ApplicationMetadataController(IAppMetadata appMetadata, ILogger logger) { - private readonly IAppMetadata _appMetadata; - private readonly ILogger _logger; + _appMetadata = appMetadata; + _logger = logger; + } - /// - /// Initializes a new instance of the class - /// The IAppMetadata service - /// Logger for ApplicationMetadataController - /// - public ApplicationMetadataController(IAppMetadata appMetadata, ILogger logger) + /// + /// Get the application metadata https://altinncdn.no/schemas/json/application/application-metadata.schema.v1.json + /// + /// If org and app does not match, this returns a 409 Conflict response + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// Boolean get parameter to skip verification of correct org/app + /// Application metadata + [HttpGet("{org}/{app}/api/v1/applicationmetadata")] + public async Task> GetAction( + string org, + string app, + [FromQuery] bool checkOrgApp = true + ) + { + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + + string wantedAppId = $"{org}/{app}"; + + if (!checkOrgApp || application.Id.Equals(wantedAppId)) { - _appMetadata = appMetadata; - _logger = logger; + return Ok(application); } - /// - /// Get the application metadata https://altinncdn.no/schemas/json/application/application-metadata.schema.v1.json - /// - /// If org and app does not match, this returns a 409 Conflict response - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// Boolean get parameter to skip verification of correct org/app - /// Application metadata - [HttpGet("{org}/{app}/api/v1/applicationmetadata")] - public async Task> GetAction( - string org, - string app, - [FromQuery] bool checkOrgApp = true - ) - { - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + return Conflict($"This is {application.Id}, and not the app you are looking for: {wantedAppId}!"); + } + /// + /// Get the application XACML policy file + /// + /// If org and app does not match, this returns a 409 Conflict response + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// XACML policy file + [HttpGet("{org}/{app}/api/v1/meta/authorizationpolicy")] + public async Task> GetPolicy(string org, string app) + { + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + try + { + string policy = await _appMetadata.GetApplicationXACMLPolicy(); string wantedAppId = $"{org}/{app}"; - if (!checkOrgApp || application.Id.Equals(wantedAppId)) + if (application.Id.Equals(wantedAppId)) { - return Ok(application); + return Content(policy, "text/xml", System.Text.Encoding.UTF8); } return Conflict($"This is {application.Id}, and not the app you are looking for: {wantedAppId}!"); } - - /// - /// Get the application XACML policy file - /// - /// If org and app does not match, this returns a 409 Conflict response - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// XACML policy file - [HttpGet("{org}/{app}/api/v1/meta/authorizationpolicy")] - public async Task> GetPolicy(string org, string app) + catch (FileNotFoundException) { - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - try - { - string policy = await _appMetadata.GetApplicationXACMLPolicy(); - string wantedAppId = $"{org}/{app}"; - - if (application.Id.Equals(wantedAppId)) - { - return Content(policy, "text/xml", System.Text.Encoding.UTF8); - } - - return Conflict($"This is {application.Id}, and not the app you are looking for: {wantedAppId}!"); - } - catch (FileNotFoundException) - { - return NotFound(); - } + return NotFound(); } + } - /// - /// Get the application BPMN process file - /// - /// If org and app does not match, this returns a 409 Conflict response - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// BPMN process file - [HttpGet("{org}/{app}/api/v1/meta/process")] - public async Task> GetProcess(string org, string app) + /// + /// Get the application BPMN process file + /// + /// If org and app does not match, this returns a 409 Conflict response + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// BPMN process file + [HttpGet("{org}/{app}/api/v1/meta/process")] + public async Task> GetProcess(string org, string app) + { + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + string wantedAppId = $"{org}/{app}"; + try { - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - string wantedAppId = $"{org}/{app}"; - try - { - if (application.Id.Equals(wantedAppId)) - { - string process = await _appMetadata.GetApplicationBPMNProcess(); - return Content(process, "text/xml", System.Text.Encoding.UTF8); - } - - return Conflict($"This is {application.Id}, and not the app you are looking for: {wantedAppId}!"); - } - catch (ApplicationConfigException ex) + if (application.Id.Equals(wantedAppId)) { - _logger.LogError(ex, "Failed to read process from file for appId: ${WantedApp}", wantedAppId); - return NotFound(); + string process = await _appMetadata.GetApplicationBPMNProcess(); + return Content(process, "text/xml", System.Text.Encoding.UTF8); } + + return Conflict($"This is {application.Id}, and not the app you are looking for: {wantedAppId}!"); + } + catch (ApplicationConfigException ex) + { + _logger.LogError(ex, "Failed to read process from file for appId: ${WantedApp}", wantedAppId); + return NotFound(); } } } diff --git a/src/Altinn.App.Api/Controllers/ApplicationSettingsController.cs b/src/Altinn.App.Api/Controllers/ApplicationSettingsController.cs index d1657d6eb..b42e6c0f6 100644 --- a/src/Altinn.App.Api/Controllers/ApplicationSettingsController.cs +++ b/src/Altinn.App.Api/Controllers/ApplicationSettingsController.cs @@ -4,50 +4,46 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Controller that exposes application config +/// +[ApiController] +public class ApplicationSettingsController : ControllerBase { + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() { DictionaryKeyPolicy = JsonNamingPolicy.CamelCase }; + + private readonly AppSettings _appSettings; + private readonly FrontEndSettings _frontEndSettings; + /// - /// Controller that exposes application config + /// Controller that exposes a subset of app setings /// - [ApiController] - public class ApplicationSettingsController : ControllerBase + public ApplicationSettingsController(IOptions appSettings, IOptions frontEndSettings) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() { DictionaryKeyPolicy = JsonNamingPolicy.CamelCase }; + _appSettings = appSettings.Value; + _frontEndSettings = frontEndSettings.Value; + } - private readonly AppSettings _appSettings; - private readonly FrontEndSettings _frontEndSettings; + /// + /// Returns the application settings + /// + [HttpGet("{org}/{app}/api/v1/applicationsettings")] + public IActionResult GetAction(string org, string app) + { + FrontEndSettings frontEndSettings = _frontEndSettings; - /// - /// Controller that exposes a subset of app setings - /// - public ApplicationSettingsController( - IOptions appSettings, - IOptions frontEndSettings + // Adding key from _appSettings to be backwards compatible. + if ( + !frontEndSettings.ContainsKey(nameof(_appSettings.AppOidcProvider)) + && !string.IsNullOrEmpty(_appSettings.AppOidcProvider) ) { - _appSettings = appSettings.Value; - _frontEndSettings = frontEndSettings.Value; + frontEndSettings.Add(nameof(_appSettings.AppOidcProvider), _appSettings.AppOidcProvider); } - /// - /// Returns the application settings - /// - [HttpGet("{org}/{app}/api/v1/applicationsettings")] - public IActionResult GetAction(string org, string app) - { - FrontEndSettings frontEndSettings = _frontEndSettings; - - // Adding key from _appSettings to be backwards compatible. - if ( - !frontEndSettings.ContainsKey(nameof(_appSettings.AppOidcProvider)) - && !string.IsNullOrEmpty(_appSettings.AppOidcProvider) - ) - { - frontEndSettings.Add(nameof(_appSettings.AppOidcProvider), _appSettings.AppOidcProvider); - } - - return new JsonResult(frontEndSettings, _jsonSerializerOptions); - } + return new JsonResult(frontEndSettings, _jsonSerializerOptions); } } diff --git a/src/Altinn.App.Api/Controllers/AuthenticationController.cs b/src/Altinn.App.Api/Controllers/AuthenticationController.cs index 4096c3a50..a58e42428 100644 --- a/src/Altinn.App.Api/Controllers/AuthenticationController.cs +++ b/src/Altinn.App.Api/Controllers/AuthenticationController.cs @@ -6,66 +6,65 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Exposes API endpoints related to authentication. +/// +public class AuthenticationController : ControllerBase { + private readonly IAuthenticationClient _authenticationClient; + private readonly GeneralSettings _settings; + /// - /// Exposes API endpoints related to authentication. + /// Initializes a new instance of the class /// - public class AuthenticationController : ControllerBase + public AuthenticationController(IAuthenticationClient authenticationClient, IOptions settings) { - private readonly IAuthenticationClient _authenticationClient; - private readonly GeneralSettings _settings; + _authenticationClient = authenticationClient; + _settings = settings.Value; + } - /// - /// Initializes a new instance of the class - /// - public AuthenticationController(IAuthenticationClient authenticationClient, IOptions settings) - { - _authenticationClient = authenticationClient; - _settings = settings.Value; - } + /// + /// Refreshes the AltinnStudioRuntime JwtToken when not in AltinnStudio mode. + /// + /// Ok result with updated token. + [Authorize] + [HttpGet("{org}/{app}/api/[controller]/keepAlive")] + public async Task KeepAlive() + { + string token = await _authenticationClient.RefreshToken(); - /// - /// Refreshes the AltinnStudioRuntime JwtToken when not in AltinnStudio mode. - /// - /// Ok result with updated token. - [Authorize] - [HttpGet("{org}/{app}/api/[controller]/keepAlive")] - public async Task KeepAlive() + CookieOptions runtimeCookieSetting = new CookieOptions { - string token = await _authenticationClient.RefreshToken(); - - CookieOptions runtimeCookieSetting = new CookieOptions - { - Domain = _settings.HostName, - HttpOnly = true, - Secure = true, - IsEssential = true, - SameSite = SameSiteMode.Lax - }; + Domain = _settings.HostName, + HttpOnly = true, + Secure = true, + IsEssential = true, + SameSite = SameSiteMode.Lax + }; - if (!string.IsNullOrWhiteSpace(token)) - { - HttpContext.Response.Cookies.Append(General.RuntimeCookieName, token, runtimeCookieSetting); - return Ok(); - } - - return BadRequest(); - } - - /// - /// Invalidates the AltinnStudioRuntime cookie. - /// - /// Ok result with invalidated cookie. - [Authorize] - [HttpPut("{org}/{app}/api/[controller]/invalidatecookie")] - public IActionResult InvalidateCookie() + if (!string.IsNullOrWhiteSpace(token)) { - HttpContext.Response.Cookies.Delete( - General.RuntimeCookieName, - new CookieOptions { Domain = _settings.HostName } - ); + HttpContext.Response.Cookies.Append(General.RuntimeCookieName, token, runtimeCookieSetting); return Ok(); } + + return BadRequest(); + } + + /// + /// Invalidates the AltinnStudioRuntime cookie. + /// + /// Ok result with invalidated cookie. + [Authorize] + [HttpPut("{org}/{app}/api/[controller]/invalidatecookie")] + public IActionResult InvalidateCookie() + { + HttpContext.Response.Cookies.Delete( + General.RuntimeCookieName, + new CookieOptions { Domain = _settings.HostName } + ); + return Ok(); } } diff --git a/src/Altinn.App.Api/Controllers/AuthorizationController.cs b/src/Altinn.App.Api/Controllers/AuthorizationController.cs index 9ecd28829..13f4e04cc 100644 --- a/src/Altinn.App.Api/Controllers/AuthorizationController.cs +++ b/src/Altinn.App.Api/Controllers/AuthorizationController.cs @@ -8,122 +8,118 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Exposes API endpoints related to authorization +/// +public class AuthorizationController : Controller { + private readonly IAuthorizationClient _authorization; + private readonly UserHelper _userHelper; + private readonly GeneralSettings _settings; + /// - /// Exposes API endpoints related to authorization + /// Initializes a new instance of the class /// - public class AuthorizationController : Controller + public AuthorizationController( + IAuthorizationClient authorization, + IProfileClient profileClient, + IAltinnPartyClient altinnPartyClientClient, + IOptions settings + ) { - private readonly IAuthorizationClient _authorization; - private readonly UserHelper _userHelper; - private readonly GeneralSettings _settings; + _userHelper = new UserHelper(profileClient, altinnPartyClientClient, settings); + _authorization = authorization; + _settings = settings.Value; + } - /// - /// Initializes a new instance of the class - /// - public AuthorizationController( - IAuthorizationClient authorization, - IProfileClient profileClient, - IAltinnPartyClient altinnPartyClientClient, - IOptions settings - ) - { - _userHelper = new UserHelper(profileClient, altinnPartyClientClient, settings); - _authorization = authorization; - _settings = settings.Value; - } + /// + /// Gets current party by reading cookie value and validating. + /// + /// Party id for selected party. If invalid, partyId for logged in user is returned. + [Authorize] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + [HttpGet("{org}/{app}/api/authorization/parties/current")] + public async Task GetCurrentParty(bool returnPartyObject = false) + { + UserContext userContext = await _userHelper.GetUserContext(HttpContext); + int userId = userContext.UserId; - /// - /// Gets current party by reading cookie value and validating. - /// - /// Party id for selected party. If invalid, partyId for logged in user is returned. - [Authorize] - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - [HttpGet("{org}/{app}/api/authorization/parties/current")] - public async Task GetCurrentParty(bool returnPartyObject = false) + // If selected party is different than party for user self need to verify + if (userContext.UserParty == null || userContext.PartyId != userContext.UserParty.PartyId) { - UserContext userContext = await _userHelper.GetUserContext(HttpContext); - int userId = userContext.UserId; + bool? isValid = await _authorization.ValidateSelectedParty(userId, userContext.PartyId); - // If selected party is different than party for user self need to verify - if (userContext.UserParty == null || userContext.PartyId != userContext.UserParty.PartyId) + if (isValid == true) { - bool? isValid = await _authorization.ValidateSelectedParty(userId, userContext.PartyId); - - if (isValid == true) - { - if (returnPartyObject) - { - return Ok(userContext.Party); - } - - return Ok(userContext.PartyId); - } - else if (userContext.UserParty != null) - { - userContext.Party = userContext.UserParty; - userContext.PartyId = userContext.UserParty.PartyId; - } - else + if (returnPartyObject) { - userContext.Party = null; - userContext.PartyId = 0; + return Ok(userContext.Party); } - } - string? cookieValue = Request.Cookies[_settings.GetAltinnPartyCookieName]; - if (!int.TryParse(cookieValue, out int partyIdFromCookie)) - { - partyIdFromCookie = 0; + return Ok(userContext.PartyId); } - - // Setting cookie to partyID of logged in user if it varies from previus value. - if (partyIdFromCookie != userContext.PartyId) + else if (userContext.UserParty != null) { - Response.Cookies.Append( - _settings.GetAltinnPartyCookieName, - userContext.PartyId.ToString(), - new CookieOptions { Domain = _settings.HostName } - ); + userContext.Party = userContext.UserParty; + userContext.PartyId = userContext.UserParty.PartyId; } - - if (returnPartyObject) + else { - return Ok(userContext.Party); + userContext.Party = null; + userContext.PartyId = 0; } + } - return Ok(userContext.PartyId); + string? cookieValue = Request.Cookies[_settings.GetAltinnPartyCookieName]; + if (!int.TryParse(cookieValue, out int partyIdFromCookie)) + { + partyIdFromCookie = 0; } - /// - /// Checks if the user can represent the selected party. - /// - /// The userId - /// The partyId - /// Boolean indicating if the selected party is valid. - [Authorize] - [HttpGet] - public async Task ValidateSelectedParty(int userId, int partyId) + // Setting cookie to partyID of logged in user if it varies from previus value. + if (partyIdFromCookie != userContext.PartyId) { - if (partyId == 0 || userId == 0) - { - return BadRequest("Both userId and partyId must be provided."); - } + Response.Cookies.Append( + _settings.GetAltinnPartyCookieName, + userContext.PartyId.ToString(), + new CookieOptions { Domain = _settings.HostName } + ); + } - bool? result = await _authorization.ValidateSelectedParty(userId, partyId); + if (returnPartyObject) + { + return Ok(userContext.Party); + } - if (result != null) - { - return Ok(result); - } - else - { - return StatusCode( - 500, - $"Something went wrong when trying to validate party {partyId} for user {userId}" - ); - } + return Ok(userContext.PartyId); + } + + /// + /// Checks if the user can represent the selected party. + /// + /// The userId + /// The partyId + /// Boolean indicating if the selected party is valid. + [Authorize] + [HttpGet] + public async Task ValidateSelectedParty(int userId, int partyId) + { + if (partyId == 0 || userId == 0) + { + return BadRequest("Both userId and partyId must be provided."); + } + + bool? result = await _authorization.ValidateSelectedParty(userId, partyId); + + if (result != null) + { + return Ok(result); + } + else + { + return StatusCode(500, $"Something went wrong when trying to validate party {partyId} for user {userId}"); } } } diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index d1931c270..4f51ea025 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -29,1028 +29,1015 @@ using Microsoft.FeatureManagement; using Microsoft.Net.Http.Headers; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// The data controller handles creation, update, validation and calculation of data elements. +/// +[AutoValidateAntiforgeryTokenIfAuthCookie] +[ApiController] +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data")] +public class DataController : ControllerBase { + private readonly ILogger _logger; + private readonly IDataClient _dataClient; + private readonly IEnumerable _dataProcessors; + private readonly IInstanceClient _instanceClient; + private readonly IInstantiationProcessor _instantiationProcessor; + private readonly IAppModel _appModel; + private readonly IAppResources _appResourcesService; + private readonly IAppMetadata _appMetadata; + private readonly IPrefill _prefillService; + private readonly IFileAnalysisService _fileAnalyserService; + private readonly IFileValidationService _fileValidationService; + private readonly IFeatureManager _featureManager; + private readonly IPatchService _patchService; + + private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; + /// - /// The data controller handles creation, update, validation and calculation of data elements. + /// The data controller is responsible for adding business logic to the data elements. /// - [AutoValidateAntiforgeryTokenIfAuthCookie] - [ApiController] - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data")] - public class DataController : ControllerBase + /// logger + /// instance service to store instances + /// Instantiation processor + /// A service with access to data storage. + /// Services implementing logic during data read/write + /// Service for generating app model + /// The apps resource service + /// The app metadata service + /// The feature manager controlling enabled features. + /// A service with prefill related logic. + /// Service used to analyse files uploaded. + /// Service used to validate files uploaded. + /// Service for applying a json patch to a json serializable object + public DataController( + ILogger logger, + IInstanceClient instanceClient, + IInstantiationProcessor instantiationProcessor, + IDataClient dataClient, + IEnumerable dataProcessors, + IAppModel appModel, + IAppResources appResourcesService, + IPrefill prefillService, + IFileAnalysisService fileAnalyserService, + IFileValidationService fileValidationService, + IAppMetadata appMetadata, + IFeatureManager featureManager, + IPatchService patchService + ) { - private readonly ILogger _logger; - private readonly IDataClient _dataClient; - private readonly IEnumerable _dataProcessors; - private readonly IInstanceClient _instanceClient; - private readonly IInstantiationProcessor _instantiationProcessor; - private readonly IAppModel _appModel; - private readonly IAppResources _appResourcesService; - private readonly IAppMetadata _appMetadata; - private readonly IPrefill _prefillService; - private readonly IFileAnalysisService _fileAnalyserService; - private readonly IFileValidationService _fileValidationService; - private readonly IFeatureManager _featureManager; - private readonly IPatchService _patchService; - - private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; - - /// - /// The data controller is responsible for adding business logic to the data elements. - /// - /// logger - /// instance service to store instances - /// Instantiation processor - /// A service with access to data storage. - /// Services implementing logic during data read/write - /// Service for generating app model - /// The apps resource service - /// The app metadata service - /// The feature manager controlling enabled features. - /// A service with prefill related logic. - /// Service used to analyse files uploaded. - /// Service used to validate files uploaded. - /// Service for applying a json patch to a json serializable object - public DataController( - ILogger logger, - IInstanceClient instanceClient, - IInstantiationProcessor instantiationProcessor, - IDataClient dataClient, - IEnumerable dataProcessors, - IAppModel appModel, - IAppResources appResourcesService, - IPrefill prefillService, - IFileAnalysisService fileAnalyserService, - IFileValidationService fileValidationService, - IAppMetadata appMetadata, - IFeatureManager featureManager, - IPatchService patchService - ) - { - _logger = logger; - - _instanceClient = instanceClient; - _instantiationProcessor = instantiationProcessor; - _dataClient = dataClient; - _dataProcessors = dataProcessors; - _appModel = appModel; - _appResourcesService = appResourcesService; - _appMetadata = appMetadata; - _prefillService = prefillService; - _fileAnalyserService = fileAnalyserService; - _fileValidationService = fileValidationService; - _featureManager = featureManager; - _patchService = patchService; - } + _logger = logger; + + _instanceClient = instanceClient; + _instantiationProcessor = instantiationProcessor; + _dataClient = dataClient; + _dataProcessors = dataProcessors; + _appModel = appModel; + _appResourcesService = appResourcesService; + _appMetadata = appMetadata; + _prefillService = prefillService; + _fileAnalyserService = fileAnalyserService; + _fileValidationService = fileValidationService; + _featureManager = featureManager; + _patchService = patchService; + } - /// - /// Creates and instantiates a data element of a given element-type. Clients can upload the data element in the request content. - /// - /// unique identfier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that this the owner of the instance - /// unique id to identify the instance - /// identifies the data element type to create - /// A list is returned if multiple elements are created. - [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] - [HttpPost] - [DisableFormValueModelBinding] - [RequestSizeLimit(REQUEST_SIZE_LIMIT)] - [ProducesResponseType(typeof(DataElement), 201)] - public async Task Create( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromQuery] string dataType - ) + /// + /// Creates and instantiates a data element of a given element-type. Clients can upload the data element in the request content. + /// + /// unique identfier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that this the owner of the instance + /// unique id to identify the instance + /// identifies the data element type to create + /// A list is returned if multiple elements are created. + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] + [HttpPost] + [DisableFormValueModelBinding] + [RequestSizeLimit(REQUEST_SIZE_LIMIT)] + [ProducesResponseType(typeof(DataElement), 201)] + public async Task Create( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromQuery] string dataType + ) + { + /* The Body of the request is read much later when it has been made sure it is worth it. */ + + try { - /* The Body of the request is read much later when it has been made sure it is worth it. */ + Application application = await _appMetadata.GetApplicationMetadata(); - try + DataType? dataTypeFromMetadata = application.DataTypes.First(e => + e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase) + ); + + if (dataTypeFromMetadata == null) + { + return BadRequest( + $"Element type {dataType} not allowed for instance {instanceOwnerPartyId}/{instanceGuid}." + ); + } + + if (!IsValidContributer(dataTypeFromMetadata, User)) { - Application application = await _appMetadata.GetApplicationMetadata(); + return Forbid(); + } + + bool appLogic = dataTypeFromMetadata.AppLogic?.ClassRef != null; - DataType? dataTypeFromMetadata = application.DataTypes.First(e => - e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase) + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + if (instance == null) + { + return NotFound($"Did not find instance {instance}"); + } + + if (!InstanceIsActive(instance)) + { + return Conflict( + $"Cannot upload data for archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}" ); + } - if (dataTypeFromMetadata == null) + if (appLogic) + { + return await CreateAppModelData(org, app, instance, dataType); + } + else + { + (bool validationRestrictionSuccess, List errors) = + DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata); + if (!validationRestrictionSuccess) { - return BadRequest( - $"Element type {dataType} not allowed for instance {instanceOwnerPartyId}/{instanceGuid}." - ); + return BadRequest(await GetErrorDetails(errors)); } - if (!IsValidContributer(dataTypeFromMetadata, User)) + StreamContent streamContent = Request.CreateContentStream(); + + using Stream fileStream = new MemoryStream(); + await streamContent.CopyToAsync(fileStream); + if (fileStream.Length == 0) { - return Forbid(); + const string errorMessage = "Invalid data provided. Error: The file is zero bytes."; + var error = new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Severity = ValidationIssueSeverity.Error, + Description = errorMessage + }; + _logger.LogError(errorMessage); + return BadRequest(await GetErrorDetails(new List { error })); } - bool appLogic = dataTypeFromMetadata.AppLogic?.ClassRef != null; + bool parseSuccess = Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues); + string? filename = parseSuccess ? DataRestrictionValidation.GetFileNameFromHeader(headerValues) : null; - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (instance == null) + IEnumerable fileAnalysisResults = new List(); + if (FileAnalysisEnabledForDataType(dataTypeFromMetadata)) { - return NotFound($"Did not find instance {instance}"); + fileAnalysisResults = await _fileAnalyserService.Analyse( + dataTypeFromMetadata, + fileStream, + filename + ); } - if (!InstanceIsActive(instance)) + bool fileValidationSuccess = true; + List validationIssues = new(); + if (FileValidationEnabledForDataType(dataTypeFromMetadata)) { - return Conflict( - $"Cannot upload data for archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}" + (fileValidationSuccess, validationIssues) = await _fileValidationService.Validate( + dataTypeFromMetadata, + fileAnalysisResults ); } - if (appLogic) + if (!fileValidationSuccess) { - return await CreateAppModelData(org, app, instance, dataType); + return BadRequest(await GetErrorDetails(validationIssues)); } - else + + if (streamContent.Headers.ContentType is null) { - (bool validationRestrictionSuccess, List errors) = - DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata); - if (!validationRestrictionSuccess) - { - return BadRequest(await GetErrorDetails(errors)); - } + return StatusCode(500, "Content-Type not defined"); + } - StreamContent streamContent = Request.CreateContentStream(); + fileStream.Seek(0, SeekOrigin.Begin); + return await CreateBinaryData( + instance, + dataType, + streamContent.Headers.ContentType.ToString(), + filename, + fileStream + ); + } + } + catch (PlatformHttpException e) + { + return HandlePlatformHttpException( + e, + $"Cannot create data element of {dataType} for {instanceOwnerPartyId}/{instanceGuid}" + ); + } + } - using Stream fileStream = new MemoryStream(); - await streamContent.CopyToAsync(fileStream); - if (fileStream.Length == 0) - { - const string errorMessage = "Invalid data provided. Error: The file is zero bytes."; - var error = new ValidationIssue - { - Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, - Severity = ValidationIssueSeverity.Error, - Description = errorMessage - }; - _logger.LogError(errorMessage); - return BadRequest(await GetErrorDetails(new List { error })); - } + /// + /// File validation requires json object in response and is introduced in the + /// the methods above validating files. In order to be consistent for the return types + /// of this controller, old methods are updated to return json object in response. + /// Since this is a breaking change, a feature flag is introduced to control the behaviour, + /// and the developer need to opt-in to the new behaviour. Json object are by default + /// returned as part of file validation which is a new feature. + /// + private async Task GetErrorDetails(List errors) + { + return await _featureManager.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse) + ? errors + : string.Join(";", errors.Select(x => x.Description)); + } - bool parseSuccess = Request.Headers.TryGetValue( - "Content-Disposition", - out StringValues headerValues - ); - string? filename = parseSuccess - ? DataRestrictionValidation.GetFileNameFromHeader(headerValues) - : null; + private static bool FileAnalysisEnabledForDataType(DataType dataTypeFromMetadata) + { + return dataTypeFromMetadata.EnabledFileAnalysers != null && dataTypeFromMetadata.EnabledFileAnalysers.Count > 0; + } - IEnumerable fileAnalysisResults = new List(); - if (FileAnalysisEnabledForDataType(dataTypeFromMetadata)) - { - fileAnalysisResults = await _fileAnalyserService.Analyse( - dataTypeFromMetadata, - fileStream, - filename - ); - } + private static bool FileValidationEnabledForDataType(DataType dataTypeFromMetadata) + { + return dataTypeFromMetadata.EnabledFileValidators != null + && dataTypeFromMetadata.EnabledFileValidators.Count > 0; + } - bool fileValidationSuccess = true; - List validationIssues = new(); - if (FileValidationEnabledForDataType(dataTypeFromMetadata)) - { - (fileValidationSuccess, validationIssues) = await _fileValidationService.Validate( - dataTypeFromMetadata, - fileAnalysisResults - ); - } + /// + /// Gets a data element from storage and applies business logic if nessesary. + /// + /// unique identfier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// unique id to identify the data element to get + /// Whether to initialize or remove AltinnRowId fields in the model + /// The language selected by the user. + /// The data element is returned in the body of the response + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_READ)] + [HttpGet("{dataGuid:guid}")] + public async Task Get( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] Guid dataGuid, + [FromQuery] bool includeRowId = false, + [FromQuery] string? language = null + ) + { + try + { + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + if (instance == null) + { + return NotFound($"Did not find instance {instance}"); + } - if (!fileValidationSuccess) - { - return BadRequest(await GetErrorDetails(validationIssues)); - } + DataElement? dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); - if (streamContent.Headers.ContentType is null) - { - return StatusCode(500, "Content-Type not defined"); - } + if (dataElement == null) + { + return NotFound("Did not find data element"); + } - fileStream.Seek(0, SeekOrigin.Begin); - return await CreateBinaryData( - instance, - dataType, - streamContent.Headers.ContentType.ToString(), - filename, - fileStream - ); - } + DataType? dataType = await GetDataType(dataElement); + + if (dataType is null) + { + string error = $"Could not determine if {dataType} requires app logic for application {org}/{app}"; + _logger.LogError(error); + return BadRequest(error); } - catch (PlatformHttpException e) + else if (dataType.AppLogic?.ClassRef is not null) { - return HandlePlatformHttpException( - e, - $"Cannot create data element of {dataType} for {instanceOwnerPartyId}/{instanceGuid}" + return await GetFormData( + org, + app, + instanceOwnerPartyId, + instanceGuid, + instance, + dataGuid, + dataElement, + dataType, + includeRowId, + language ); } - } - /// - /// File validation requires json object in response and is introduced in the - /// the methods above validating files. In order to be consistent for the return types - /// of this controller, old methods are updated to return json object in response. - /// Since this is a breaking change, a feature flag is introduced to control the behaviour, - /// and the developer need to opt-in to the new behaviour. Json object are by default - /// returned as part of file validation which is a new feature. - /// - private async Task GetErrorDetails(List errors) - { - return await _featureManager.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse) - ? errors - : string.Join(";", errors.Select(x => x.Description)); + return await GetBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, dataElement); } - - private static bool FileAnalysisEnabledForDataType(DataType dataTypeFromMetadata) + catch (PlatformHttpException e) { - return dataTypeFromMetadata.EnabledFileAnalysers != null - && dataTypeFromMetadata.EnabledFileAnalysers.Count > 0; + return HandlePlatformHttpException( + e, + $"Cannot get data element of {dataGuid} for {instanceOwnerPartyId}/{instanceGuid}" + ); } + } - private static bool FileValidationEnabledForDataType(DataType dataTypeFromMetadata) + /// + /// Updates an existing data element with new content. + /// + /// unique identfier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// unique id to identify the data element to update + /// The language selected by the user. + /// The updated data element, including the changed fields in the event of a calculation that changed data. + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] + [HttpPut("{dataGuid:guid}")] + [DisableFormValueModelBinding] + [RequestSizeLimit(REQUEST_SIZE_LIMIT)] + [ProducesResponseType(typeof(DataElement), 201)] + [ProducesResponseType(typeof(CalculationResult), 200)] + public async Task Put( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] Guid dataGuid, + [FromQuery] string? language = null + ) + { + try { - return dataTypeFromMetadata.EnabledFileValidators != null - && dataTypeFromMetadata.EnabledFileValidators.Count > 0; - } + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - /// - /// Gets a data element from storage and applies business logic if nessesary. - /// - /// unique identfier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// unique id to identify the data element to get - /// Whether to initialize or remove AltinnRowId fields in the model - /// The language selected by the user. - /// The data element is returned in the body of the response - [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_READ)] - [HttpGet("{dataGuid:guid}")] - public async Task Get( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromRoute] Guid dataGuid, - [FromQuery] bool includeRowId = false, - [FromQuery] string? language = null - ) - { - try + if (!InstanceIsActive(instance)) { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (instance == null) - { - return NotFound($"Did not find instance {instance}"); - } - - DataElement? dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); + return Conflict( + $"Cannot update data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}" + ); + } - if (dataElement == null) - { - return NotFound("Did not find data element"); - } + DataElement? dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); - DataType? dataType = await GetDataType(dataElement); + if (dataElement == null) + { + return NotFound("Did not find data element"); + } - if (dataType is null) - { - string error = $"Could not determine if {dataType} requires app logic for application {org}/{app}"; - _logger.LogError(error); - return BadRequest(error); - } - else if (dataType.AppLogic?.ClassRef is not null) - { - return await GetFormData( - org, - app, - instanceOwnerPartyId, - instanceGuid, - instance, - dataGuid, - dataElement, - dataType, - includeRowId, - language - ); - } + DataType? dataType = await GetDataType(dataElement); - return await GetBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, dataElement); - } - catch (PlatformHttpException e) + if (dataType is null) { - return HandlePlatformHttpException( - e, - $"Cannot get data element of {dataGuid} for {instanceOwnerPartyId}/{instanceGuid}" + _logger.LogError( + "Could not determine if {dataType} requires app logic for application {org}/{app}", + dataType, + org, + app ); + return BadRequest($"Could not determine if data type {dataType} requires application logic."); } - } - - /// - /// Updates an existing data element with new content. - /// - /// unique identfier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// unique id to identify the data element to update - /// The language selected by the user. - /// The updated data element, including the changed fields in the event of a calculation that changed data. - [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] - [HttpPut("{dataGuid:guid}")] - [DisableFormValueModelBinding] - [RequestSizeLimit(REQUEST_SIZE_LIMIT)] - [ProducesResponseType(typeof(DataElement), 201)] - [ProducesResponseType(typeof(CalculationResult), 200)] - public async Task Put( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromRoute] Guid dataGuid, - [FromQuery] string? language = null - ) - { - try + else if (dataType.AppLogic?.ClassRef is not null) { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - - if (!InstanceIsActive(instance)) - { - return Conflict( - $"Cannot update data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}" - ); - } - - DataElement? dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); - - if (dataElement == null) - { - return NotFound("Did not find data element"); - } - - DataType? dataType = await GetDataType(dataElement); - - if (dataType is null) - { - _logger.LogError( - "Could not determine if {dataType} requires app logic for application {org}/{app}", - dataType, - org, - app - ); - return BadRequest($"Could not determine if data type {dataType} requires application logic."); - } - else if (dataType.AppLogic?.ClassRef is not null) - { - return await PutFormData(org, app, instance, dataGuid, dataType, language); - } - - (bool validationRestrictionSuccess, List errors) = - DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataType); - if (!validationRestrictionSuccess) - { - return BadRequest(await GetErrorDetails(errors)); - } - - return await PutBinaryData(instanceOwnerPartyId, instanceGuid, dataGuid); + return await PutFormData(org, app, instance, dataGuid, dataType, language); } - catch (PlatformHttpException e) + + (bool validationRestrictionSuccess, List errors) = + DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataType); + if (!validationRestrictionSuccess) { - return HandlePlatformHttpException( - e, - $"Unable to update data element {dataGuid} for instance {instanceOwnerPartyId}/{instanceGuid}" - ); + return BadRequest(await GetErrorDetails(errors)); } + + return await PutBinaryData(instanceOwnerPartyId, instanceGuid, dataGuid); + } + catch (PlatformHttpException e) + { + return HandlePlatformHttpException( + e, + $"Unable to update data element {dataGuid} for instance {instanceOwnerPartyId}/{instanceGuid}" + ); } + } - /// - /// Updates an existing form data element with a patch of changes. - /// - /// unique identfier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// unique id to identify the data element to update - /// Container object for the and list of ignored validators - /// The language selected by the user. - /// A response object with the new full model and validation issues from all the groups that run - [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] - [HttpPatch("{dataGuid:guid}")] - [ProducesResponseType(typeof(DataPatchResponse), 200)] - [ProducesResponseType(typeof(ProblemDetails), 409)] - [ProducesResponseType(typeof(ProblemDetails), 422)] - public async Task> PatchFormData( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromRoute] Guid dataGuid, - [FromBody] DataPatchRequest dataPatchRequest, - [FromQuery] string? language = null - ) + /// + /// Updates an existing form data element with a patch of changes. + /// + /// unique identfier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// unique id to identify the data element to update + /// Container object for the and list of ignored validators + /// The language selected by the user. + /// A response object with the new full model and validation issues from all the groups that run + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] + [HttpPatch("{dataGuid:guid}")] + [ProducesResponseType(typeof(DataPatchResponse), 200)] + [ProducesResponseType(typeof(ProblemDetails), 409)] + [ProducesResponseType(typeof(ProblemDetails), 422)] + public async Task> PatchFormData( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] Guid dataGuid, + [FromBody] DataPatchRequest dataPatchRequest, + [FromQuery] string? language = null + ) + { + try { - try - { - var instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + var instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (!InstanceIsActive(instance)) - { - return Conflict( - $"Cannot update data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}" - ); - } + if (!InstanceIsActive(instance)) + { + return Conflict( + $"Cannot update data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}" + ); + } - var dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); + var dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); - if (dataElement == null) - { - return NotFound("Did not find data element"); - } - - var dataType = await GetDataType(dataElement); + if (dataElement == null) + { + return NotFound("Did not find data element"); + } - if (dataType?.AppLogic?.ClassRef is null) - { - _logger.LogError( - "Could not determine if {dataType} requires app logic for application {org}/{app}", - dataType, - org, - app - ); - return BadRequest($"Could not determine if data type {dataType?.Id} requires application logic."); - } + var dataType = await GetDataType(dataElement); - ServiceResult res = await _patchService.ApplyPatch( - instance, + if (dataType?.AppLogic?.ClassRef is null) + { + _logger.LogError( + "Could not determine if {dataType} requires app logic for application {org}/{app}", dataType, - dataElement, - dataPatchRequest.Patch, - language, - dataPatchRequest.IgnoredValidators + org, + app ); + return BadRequest($"Could not determine if data type {dataType?.Id} requires application logic."); + } - if (res.Success) - { - await UpdateDataValuesOnInstance(instance, dataType.Id, res.Ok.NewDataModel); - await UpdatePresentationTextsOnInstance(instance, dataType.Id, res.Ok.NewDataModel); - - return Ok( - new DataPatchResponse - { - NewDataModel = res.Ok.NewDataModel, - ValidationIssues = res.Ok.ValidationIssues - } - ); - } + ServiceResult res = await _patchService.ApplyPatch( + instance, + dataType, + dataElement, + dataPatchRequest.Patch, + language, + dataPatchRequest.IgnoredValidators + ); - return Problem(res.Error); - } - catch (PlatformHttpException e) + if (res.Success) { - return HandlePlatformHttpException( - e, - $"Unable to update data element {dataGuid} for instance {instanceOwnerPartyId}/{instanceGuid}" + await UpdateDataValuesOnInstance(instance, dataType.Id, res.Ok.NewDataModel); + await UpdatePresentationTextsOnInstance(instance, dataType.Id, res.Ok.NewDataModel); + + return Ok( + new DataPatchResponse + { + NewDataModel = res.Ok.NewDataModel, + ValidationIssues = res.Ok.ValidationIssues + } ); } + + return Problem(res.Error); + } + catch (PlatformHttpException e) + { + return HandlePlatformHttpException( + e, + $"Unable to update data element {dataGuid} for instance {instanceOwnerPartyId}/{instanceGuid}" + ); } + } - /// - /// Delete a data element. - /// - /// unique identfier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// unique id to identify the data element to update - /// The updated data element. - [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] - [HttpDelete("{dataGuid:guid}")] - public async Task Delete( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromRoute] Guid dataGuid - ) + /// + /// Delete a data element. + /// + /// unique identfier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// unique id to identify the data element to update + /// The updated data element. + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] + [HttpDelete("{dataGuid:guid}")] + public async Task Delete( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] Guid dataGuid + ) + { + try { - try + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + if (instance == null) { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (instance == null) - { - return NotFound("Did not find instance"); - } - - if (!InstanceIsActive(instance)) - { - return Conflict( - $"Cannot delete data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}" - ); - } - - DataElement? dataElement = instance.Data.Find(m => m.Id.Equals(dataGuid.ToString())); - - if (dataElement == null) - { - return NotFound("Did not find data element"); - } + return NotFound("Did not find instance"); + } - DataType? dataType = await GetDataType(dataElement); + if (!InstanceIsActive(instance)) + { + return Conflict( + $"Cannot delete data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}" + ); + } - if (dataType == null) - { - string errorMsg = - $"Could not determine if {dataElement.DataType} requires app logic for application {org}/{app}"; - _logger.LogError(errorMsg); - return BadRequest(errorMsg); - } - else if (dataType.AppLogic?.ClassRef is not null) - { - // trying deleting a form element - return BadRequest("Deleting form data is not possible at this moment."); - } + DataElement? dataElement = instance.Data.Find(m => m.Id.Equals(dataGuid.ToString())); - return await DeleteBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); - } - catch (PlatformHttpException e) + if (dataElement == null) { - return HandlePlatformHttpException( - e, - $"Cannot delete data element {dataGuid} for {instanceOwnerPartyId}/{instanceGuid}" - ); + return NotFound("Did not find data element"); } - } - private ObjectResult ExceptionResponse(Exception exception, string message) - { - _logger.LogError(exception, message); + DataType? dataType = await GetDataType(dataElement); - if (exception is PlatformHttpException phe) + if (dataType == null) { - return StatusCode((int)phe.Response.StatusCode, phe.Message); + string errorMsg = + $"Could not determine if {dataElement.DataType} requires app logic for application {org}/{app}"; + _logger.LogError(errorMsg); + return BadRequest(errorMsg); } - else if (exception is ServiceException se) + else if (dataType.AppLogic?.ClassRef is not null) { - return StatusCode((int)se.StatusCode, se.Message); + // trying deleting a form element + return BadRequest("Deleting form data is not possible at this moment."); } - return StatusCode(500, $"{message}"); + return await DeleteBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); } - - private async Task CreateBinaryData( - Instance instanceBefore, - string dataType, - string contentType, - string? filename, - Stream fileStream - ) + catch (PlatformHttpException e) { - int instanceOwnerPartyId = int.Parse(instanceBefore.Id.Split("/")[0]); - Guid instanceGuid = Guid.Parse(instanceBefore.Id.Split("/")[1]); - - DataElement dataElement = await _dataClient.InsertBinaryData( - instanceBefore.Id, - dataType, - contentType, - filename, - fileStream + return HandlePlatformHttpException( + e, + $"Cannot delete data element {dataGuid} for {instanceOwnerPartyId}/{instanceGuid}" ); + } + } - if (Guid.Parse(dataElement.Id) == Guid.Empty) - { - return StatusCode( - 500, - $"Cannot store form attachment on instance {instanceOwnerPartyId}/{instanceGuid}" - ); - } + private ObjectResult ExceptionResponse(Exception exception, string message) + { + _logger.LogError(exception, message); - SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request); - return Created(dataElement.SelfLinks.Apps, dataElement); + if (exception is PlatformHttpException phe) + { + return StatusCode((int)phe.Response.StatusCode, phe.Message); } - - private async Task CreateAppModelData(string org, string app, Instance instance, string dataType) + else if (exception is ServiceException se) { - Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - - object? appModel; - - string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); - - if (Request.ContentType == null) - { - appModel = _appModel.Create(classRef); - } - else - { - ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - - if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) - { - return BadRequest(deserializer.Error); - } - } + return StatusCode((int)se.StatusCode, se.Message); + } - // runs prefill from repo configuration if config exists - await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, dataType, appModel); + return StatusCode(500, $"{message}"); + } - await _instantiationProcessor.DataCreation(instance, appModel, null); + private async Task CreateBinaryData( + Instance instanceBefore, + string dataType, + string contentType, + string? filename, + Stream fileStream + ) + { + int instanceOwnerPartyId = int.Parse(instanceBefore.Id.Split("/")[0]); + Guid instanceGuid = Guid.Parse(instanceBefore.Id.Split("/")[1]); + + DataElement dataElement = await _dataClient.InsertBinaryData( + instanceBefore.Id, + dataType, + contentType, + filename, + fileStream + ); + + if (Guid.Parse(dataElement.Id) == Guid.Empty) + { + return StatusCode(500, $"Cannot store form attachment on instance {instanceOwnerPartyId}/{instanceGuid}"); + } - await UpdatePresentationTextsOnInstance(instance, dataType, appModel); - await UpdateDataValuesOnInstance(instance, dataType, appModel); + SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request); + return Created(dataElement.SelfLinks.Apps, dataElement); + } - int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); + private async Task CreateAppModelData(string org, string app, Instance instance, string dataType) + { + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - ObjectUtils.InitializeAltinnRowId(appModel); - ObjectUtils.PrepareModelForXmlStorage(appModel); - - DataElement dataElement = await _dataClient.InsertFormData( - appModel, - instanceGuid, - _appModel.GetModelType(classRef), - org, - app, - instanceOwnerPartyId, - dataType - ); - SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request); + object? appModel; - return Created(dataElement.SelfLinks.Apps, dataElement); - } + string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); - /// - /// Gets a data element from storage. - /// - /// The data element is returned in the body of the response - private async Task GetBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid, - DataElement dataElement - ) + if (Request.ContentType == null) { - Stream dataStream = await _dataClient.GetBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); - - if (dataStream != null) - { - string? userOrgClaim = User.GetOrg(); - if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.InvariantCultureIgnoreCase)) - { - await _instanceClient.UpdateReadStatus(instanceOwnerPartyId, instanceGuid, "read"); - } - - return File(dataStream, dataElement.ContentType, dataElement.Filename); - } - else - { - return NotFound(); - } + appModel = _appModel.Create(classRef); } - - private async Task DeleteBinaryData( - string org, - string app, - int instanceOwnerId, - Guid instanceGuid, - Guid dataGuid - ) + else { - bool successfullyDeleted = await _dataClient.DeleteData( - org, - app, - instanceOwnerId, - instanceGuid, - dataGuid, - false - ); + ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); + appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (successfullyDeleted) + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) { - return Ok(); - } - else - { - return StatusCode( - 500, - $"Something went wrong when deleting data element {dataGuid} for instance {instanceGuid}" - ); + return BadRequest(deserializer.Error); } } - private async Task GetDataType(DataElement element) - { - Application application = await _appMetadata.GetApplicationMetadata(); - return application?.DataTypes.Find(e => e.Id == element.DataType); - } + // runs prefill from repo configuration if config exists + await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, dataType, appModel); - /// - /// Gets a data element (form data) from storage and performs business logic on it (e.g. to calculate certain fields) before it is returned. - /// If more there are more data elements of the same dataType only the first one is returned. In that case use the more spesific - /// GET method to fetch a particular data element. - /// - /// data element is returned in response body - private async Task GetFormData( - string org, - string app, - int instanceOwnerId, - Guid instanceGuid, - Instance instance, - Guid dataGuid, - DataElement dataElement, - DataType dataType, - bool includeRowId, - string? language - ) - { - // Get Form Data from data service. Assumes that the data element is form data. - object appModel = await _dataClient.GetFormData( - instanceGuid, - _appModel.GetModelType(dataType.AppLogic.ClassRef), - org, - app, - instanceOwnerId, - dataGuid - ); + await _instantiationProcessor.DataCreation(instance, appModel, null); - if (appModel == null) - { - return BadRequest($"Did not find form data for data element {dataGuid}"); - } + await UpdatePresentationTextsOnInstance(instance, dataType, appModel); + await UpdateDataValuesOnInstance(instance, dataType, appModel); - // we need to save a copy to detect changes if dataProcessRead changes the model - byte[] beforeProcessDataRead = JsonSerializer.SerializeToUtf8Bytes(appModel); + int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - foreach (var dataProcessor in _dataProcessors) - { - _logger.LogInformation( - "ProcessDataRead for {modelType} using {dataProcesor}", - appModel.GetType().Name, - dataProcessor.GetType().Name - ); - await dataProcessor.ProcessDataRead(instance, dataGuid, appModel, language); - } + ObjectUtils.InitializeAltinnRowId(appModel); + ObjectUtils.PrepareModelForXmlStorage(appModel); - if (includeRowId) - { - ObjectUtils.InitializeAltinnRowId(appModel); - } + DataElement dataElement = await _dataClient.InsertFormData( + appModel, + instanceGuid, + _appModel.GetModelType(classRef), + org, + app, + instanceOwnerPartyId, + dataType + ); + SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request); - // Save back the changes if dataProcessRead has changed the model and the element is not locked - if ( - !dataElement.Locked - && !beforeProcessDataRead.SequenceEqual(JsonSerializer.SerializeToUtf8Bytes(appModel)) - ) - { - try - { - await _dataClient.UpdateData( - appModel, - instanceGuid, - appModel.GetType(), - org, - app, - instanceOwnerId, - dataGuid - ); - } - catch (PlatformHttpException e) when (e.Response.StatusCode == HttpStatusCode.Forbidden) - { - _logger.LogInformation("User does not have write access to the data element. Skipping update."); - } - } + return Created(dataElement.SelfLinks.Apps, dataElement); + } - if (!includeRowId) - { - // If the consumer does not request AltinnRowId to be initialized, we remove it from the model - ObjectUtils.RemoveAltinnRowId(appModel); - } + /// + /// Gets a data element from storage. + /// + /// The data element is returned in the body of the response + private async Task GetBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid, + DataElement dataElement + ) + { + Stream dataStream = await _dataClient.GetBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); - // This is likely not required as the instance is already read + if (dataStream != null) + { string? userOrgClaim = User.GetOrg(); if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.InvariantCultureIgnoreCase)) { - await _instanceClient.UpdateReadStatus(instanceOwnerId, instanceGuid, "read"); + await _instanceClient.UpdateReadStatus(instanceOwnerPartyId, instanceGuid, "read"); } - return Ok(appModel); + return File(dataStream, dataElement.ContentType, dataElement.Filename); } + else + { + return NotFound(); + } + } - private async Task PutBinaryData(int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid) + private async Task DeleteBinaryData( + string org, + string app, + int instanceOwnerId, + Guid instanceGuid, + Guid dataGuid + ) + { + bool successfullyDeleted = await _dataClient.DeleteData( + org, + app, + instanceOwnerId, + instanceGuid, + dataGuid, + false + ); + + if (successfullyDeleted) { - if (Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) - { - var contentDispositionHeader = ContentDispositionHeaderValue.Parse(headerValues.ToString()); - _logger.LogInformation("Content-Disposition: {ContentDisposition}", headerValues.ToString()); - DataElement dataElement = await _dataClient.UpdateBinaryData( - new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), - Request.ContentType, - contentDispositionHeader.FileName.ToString(), - dataGuid, - Request.Body - ); - SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request); + return Ok(); + } + else + { + return StatusCode( + 500, + $"Something went wrong when deleting data element {dataGuid} for instance {instanceGuid}" + ); + } + } - return Created(dataElement.SelfLinks.Apps, dataElement); - } + private async Task GetDataType(DataElement element) + { + Application application = await _appMetadata.GetApplicationMetadata(); + return application?.DataTypes.Find(e => e.Id == element.DataType); + } - return BadRequest("Invalid data provided. Error: The request must include a Content-Disposition header"); + /// + /// Gets a data element (form data) from storage and performs business logic on it (e.g. to calculate certain fields) before it is returned. + /// If more there are more data elements of the same dataType only the first one is returned. In that case use the more spesific + /// GET method to fetch a particular data element. + /// + /// data element is returned in response body + private async Task GetFormData( + string org, + string app, + int instanceOwnerId, + Guid instanceGuid, + Instance instance, + Guid dataGuid, + DataElement dataElement, + DataType dataType, + bool includeRowId, + string? language + ) + { + // Get Form Data from data service. Assumes that the data element is form data. + object appModel = await _dataClient.GetFormData( + instanceGuid, + _appModel.GetModelType(dataType.AppLogic.ClassRef), + org, + app, + instanceOwnerId, + dataGuid + ); + + if (appModel == null) + { + return BadRequest($"Did not find form data for data element {dataGuid}"); } - private async Task PutFormData( - string org, - string app, - Instance instance, - Guid dataGuid, - DataType dataType, - string? language - ) - { - int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); + // we need to save a copy to detect changes if dataProcessRead changes the model + byte[] beforeProcessDataRead = JsonSerializer.SerializeToUtf8Bytes(appModel); - string classRef = dataType.AppLogic.ClassRef; - Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + foreach (var dataProcessor in _dataProcessors) + { + _logger.LogInformation( + "ProcessDataRead for {modelType} using {dataProcesor}", + appModel.GetType().Name, + dataProcessor.GetType().Name + ); + await dataProcessor.ProcessDataRead(instance, dataGuid, appModel, language); + } - ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - object? serviceModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + if (includeRowId) + { + ObjectUtils.InitializeAltinnRowId(appModel); + } - if (!string.IsNullOrEmpty(deserializer.Error)) + // Save back the changes if dataProcessRead has changed the model and the element is not locked + if (!dataElement.Locked && !beforeProcessDataRead.SequenceEqual(JsonSerializer.SerializeToUtf8Bytes(appModel))) + { + try { - return BadRequest(deserializer.Error); + await _dataClient.UpdateData( + appModel, + instanceGuid, + appModel.GetType(), + org, + app, + instanceOwnerId, + dataGuid + ); } - - if (serviceModel == null) + catch (PlatformHttpException e) when (e.Response.StatusCode == HttpStatusCode.Forbidden) { - return BadRequest("No data found in content"); + _logger.LogInformation("User does not have write access to the data element. Skipping update."); } + } - Dictionary? changedFields = await JsonHelper.ProcessDataWriteWithDiff( - instance, + if (!includeRowId) + { + // If the consumer does not request AltinnRowId to be initialized, we remove it from the model + ObjectUtils.RemoveAltinnRowId(appModel); + } + + // This is likely not required as the instance is already read + string? userOrgClaim = User.GetOrg(); + if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.InvariantCultureIgnoreCase)) + { + await _instanceClient.UpdateReadStatus(instanceOwnerId, instanceGuid, "read"); + } + + return Ok(appModel); + } + + private async Task PutBinaryData(int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid) + { + if (Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) + { + var contentDispositionHeader = ContentDispositionHeaderValue.Parse(headerValues.ToString()); + _logger.LogInformation("Content-Disposition: {ContentDisposition}", headerValues.ToString()); + DataElement dataElement = await _dataClient.UpdateBinaryData( + new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), + Request.ContentType, + contentDispositionHeader.FileName.ToString(), dataGuid, - serviceModel, - language, - _dataProcessors, - _logger + Request.Body ); + SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request); - await UpdatePresentationTextsOnInstance(instance, dataType.Id, serviceModel); - await UpdateDataValuesOnInstance(instance, dataType.Id, serviceModel); - - ObjectUtils.InitializeAltinnRowId(serviceModel); - ObjectUtils.PrepareModelForXmlStorage(serviceModel); - - // Save Formdata to database - DataElement updatedDataElement = await _dataClient.UpdateData( - serviceModel, - instanceGuid, - _appModel.GetModelType(classRef), - org, - app, - instanceOwnerPartyId, - dataGuid - ); + return Created(dataElement.SelfLinks.Apps, dataElement); + } - SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, updatedDataElement, Request); + return BadRequest("Invalid data provided. Error: The request must include a Content-Disposition header"); + } - string dataUrl = updatedDataElement.SelfLinks.Apps; - if (changedFields is not null) - { - CalculationResult calculationResult = new(updatedDataElement) { ChangedFields = changedFields }; - return Ok(calculationResult); - } + private async Task PutFormData( + string org, + string app, + Instance instance, + Guid dataGuid, + DataType dataType, + string? language + ) + { + int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - return Created(dataUrl, updatedDataElement); + string classRef = dataType.AppLogic.ClassRef; + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + + ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); + object? serviceModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + + if (!string.IsNullOrEmpty(deserializer.Error)) + { + return BadRequest(deserializer.Error); } - private async Task UpdatePresentationTextsOnInstance(Instance instance, string dataType, object serviceModel) + if (serviceModel == null) { - var updatedValues = DataHelper.GetUpdatedDataValues( - (await _appMetadata.GetApplicationMetadata()).PresentationFields, - instance.PresentationTexts, - dataType, - serviceModel - ); + return BadRequest("No data found in content"); + } - if (updatedValues.Count > 0) - { - await _instanceClient.UpdatePresentationTexts( - int.Parse(instance.Id.Split("/")[0]), - Guid.Parse(instance.Id.Split("/")[1]), - new PresentationTexts { Texts = updatedValues } - ); - } + Dictionary? changedFields = await JsonHelper.ProcessDataWriteWithDiff( + instance, + dataGuid, + serviceModel, + language, + _dataProcessors, + _logger + ); + + await UpdatePresentationTextsOnInstance(instance, dataType.Id, serviceModel); + await UpdateDataValuesOnInstance(instance, dataType.Id, serviceModel); + + ObjectUtils.InitializeAltinnRowId(serviceModel); + ObjectUtils.PrepareModelForXmlStorage(serviceModel); + + // Save Formdata to database + DataElement updatedDataElement = await _dataClient.UpdateData( + serviceModel, + instanceGuid, + _appModel.GetModelType(classRef), + org, + app, + instanceOwnerPartyId, + dataGuid + ); + + SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, updatedDataElement, Request); + + string dataUrl = updatedDataElement.SelfLinks.Apps; + if (changedFields is not null) + { + CalculationResult calculationResult = new(updatedDataElement) { ChangedFields = changedFields }; + return Ok(calculationResult); } - private async Task UpdateDataValuesOnInstance(Instance instance, string dataType, object serviceModel) + return Created(dataUrl, updatedDataElement); + } + + private async Task UpdatePresentationTextsOnInstance(Instance instance, string dataType, object serviceModel) + { + var updatedValues = DataHelper.GetUpdatedDataValues( + (await _appMetadata.GetApplicationMetadata()).PresentationFields, + instance.PresentationTexts, + dataType, + serviceModel + ); + + if (updatedValues.Count > 0) { - var updatedValues = DataHelper.GetUpdatedDataValues( - (await _appMetadata.GetApplicationMetadata()).DataFields, - instance.DataValues, - dataType, - serviceModel + await _instanceClient.UpdatePresentationTexts( + int.Parse(instance.Id.Split("/")[0]), + Guid.Parse(instance.Id.Split("/")[1]), + new PresentationTexts { Texts = updatedValues } ); + } + } - if (updatedValues.Count > 0) - { - await _instanceClient.UpdateDataValues( - int.Parse(instance.Id.Split("/")[0]), - Guid.Parse(instance.Id.Split("/")[1]), - new DataValues { Values = updatedValues } - ); - } + private async Task UpdateDataValuesOnInstance(Instance instance, string dataType, object serviceModel) + { + var updatedValues = DataHelper.GetUpdatedDataValues( + (await _appMetadata.GetApplicationMetadata()).DataFields, + instance.DataValues, + dataType, + serviceModel + ); + + if (updatedValues.Count > 0) + { + await _instanceClient.UpdateDataValues( + int.Parse(instance.Id.Split("/")[0]), + Guid.Parse(instance.Id.Split("/")[1]), + new DataValues { Values = updatedValues } + ); } + } - private ActionResult HandlePlatformHttpException(PlatformHttpException e, string defaultMessage) + private ActionResult HandlePlatformHttpException(PlatformHttpException e, string defaultMessage) + { + if (e.Response.StatusCode == HttpStatusCode.Forbidden) { - if (e.Response.StatusCode == HttpStatusCode.Forbidden) - { - return Forbid(); - } - else if (e.Response.StatusCode == HttpStatusCode.NotFound) - { - return NotFound(); - } - else if (e.Response.StatusCode == HttpStatusCode.Conflict) - { - return Conflict(); - } - else - { - return ExceptionResponse(e, defaultMessage); - } + return Forbid(); + } + else if (e.Response.StatusCode == HttpStatusCode.NotFound) + { + return NotFound(); } + else if (e.Response.StatusCode == HttpStatusCode.Conflict) + { + return Conflict(); + } + else + { + return ExceptionResponse(e, defaultMessage); + } + } - private static bool InstanceIsActive(Instance i) + private static bool InstanceIsActive(Instance i) + { + if (i?.Status?.Archived != null || i?.Status?.SoftDeleted != null || i?.Status?.HardDeleted != null) { - if (i?.Status?.Archived != null || i?.Status?.SoftDeleted != null || i?.Status?.HardDeleted != null) - { - return false; - } + return false; + } + + return true; + } + private static bool IsValidContributer(DataType dataType, ClaimsPrincipal user) + { + if (dataType.AllowedContributers == null || dataType.AllowedContributers.Count == 0) + { return true; } - private static bool IsValidContributer(DataType dataType, ClaimsPrincipal user) + foreach (string item in dataType.AllowedContributers) { - if (dataType.AllowedContributers == null || dataType.AllowedContributers.Count == 0) - { - return true; - } + string key = item.Split(':')[0]; + string value = item.Split(':')[1]; - foreach (string item in dataType.AllowedContributers) + switch (key.ToLower()) { - string key = item.Split(':')[0]; - string value = item.Split(':')[1]; + case "org": + if (value.Equals(user.GetOrg(), StringComparison.OrdinalIgnoreCase)) + { + return true; + } - switch (key.ToLower()) - { - case "org": - if (value.Equals(user.GetOrg(), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - break; - case "orgno": - if (value.Equals(user.GetOrgNumber().ToString())) - { - return true; - } - - break; - default: - break; - } - } + break; + case "orgno": + if (value.Equals(user.GetOrgNumber().ToString())) + { + return true; + } - return false; + break; + default: + break; + } } - private ObjectResult Problem(DataPatchError error) + return false; + } + + private ObjectResult Problem(DataPatchError error) + { + int code = error.ErrorType switch { - int code = error.ErrorType switch + DataPatchErrorType.PatchTestFailed => (int)HttpStatusCode.Conflict, + DataPatchErrorType.DeserializationFailed => (int)HttpStatusCode.UnprocessableContent, + _ => (int)HttpStatusCode.InternalServerError + }; + return StatusCode( + code, + new ProblemDetails() { - DataPatchErrorType.PatchTestFailed => (int)HttpStatusCode.Conflict, - DataPatchErrorType.DeserializationFailed => (int)HttpStatusCode.UnprocessableContent, - _ => (int)HttpStatusCode.InternalServerError - }; - return StatusCode( - code, - new ProblemDetails() - { - Title = error.Title, - Detail = error.Detail, - Type = "https://datatracker.ietf.org/doc/html/rfc6902/", - Status = code, - Extensions = error.Extensions ?? new Dictionary(StringComparer.Ordinal) - } - ); - } + Title = error.Title, + Detail = error.Detail, + Type = "https://datatracker.ietf.org/doc/html/rfc6902/", + Status = code, + Extensions = error.Extensions ?? new Dictionary(StringComparer.Ordinal) + } + ); } } diff --git a/src/Altinn.App.Api/Controllers/DataListsController.cs b/src/Altinn.App.Api/Controllers/DataListsController.cs index 0c625a448..8b1f2f233 100644 --- a/src/Altinn.App.Api/Controllers/DataListsController.cs +++ b/src/Altinn.App.Api/Controllers/DataListsController.cs @@ -3,86 +3,80 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Represents the DataLists API. +/// +[ApiController] +public class DataListsController : ControllerBase { + private readonly IDataListsService _dataListsService; + /// - /// Represents the DataLists API. + /// Initializes a new instance of the class. /// - [ApiController] - public class DataListsController : ControllerBase + /// Service for handling datalists + public DataListsController(IDataListsService dataListsService) { - private readonly IDataListsService _dataListsService; + _dataListsService = dataListsService; + } - /// - /// Initializes a new instance of the class. - /// - /// Service for handling datalists - public DataListsController(IDataListsService dataListsService) + /// + /// Api that exposes app related datalists + /// + /// The listId + /// Query parameteres supplied + /// The language selected by the user. + /// The data list + [HttpGet] + [Route("/{org}/{app}/api/datalists/{id}")] + public async Task Get( + [FromRoute] string id, + [FromQuery] Dictionary queryParams, + [FromQuery] string? language = null + ) + { + DataList dataLists = await _dataListsService.GetDataListAsync(id, language, queryParams); + if (dataLists.ListItems == null) { - _dataListsService = dataListsService; + return NotFound(); } - /// - /// Api that exposes app related datalists - /// - /// The listId - /// Query parameteres supplied - /// The language selected by the user. - /// The data list - [HttpGet] - [Route("/{org}/{app}/api/datalists/{id}")] - public async Task Get( - [FromRoute] string id, - [FromQuery] Dictionary queryParams, - [FromQuery] string? language = null - ) - { - DataList dataLists = await _dataListsService.GetDataListAsync(id, language, queryParams); - if (dataLists.ListItems == null) - { - return NotFound(); - } - - return Ok(dataLists); - } - - /// - /// Exposes datalists related to the app and logged in user - /// - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// The datalistId - /// Query parameteres supplied - /// The language selected by the user. - /// A representing the result of the asynchronous operation. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [Authorize(Policy = "InstanceRead")] - [Route("/{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/datalists/{id}")] - public async Task Get( - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromRoute] string id, - [FromQuery] Dictionary queryParams, - [FromQuery] string? language = null - ) - { - var instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId, instanceGuid); + return Ok(dataLists); + } - DataList dataLists = await _dataListsService.GetDataListAsync( - instanceIdentifier, - id, - language, - queryParams - ); + /// + /// Exposes datalists related to the app and logged in user + /// + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// The datalistId + /// Query parameteres supplied + /// The language selected by the user. + /// A representing the result of the asynchronous operation. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = "InstanceRead")] + [Route("/{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/datalists/{id}")] + public async Task Get( + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] string id, + [FromQuery] Dictionary queryParams, + [FromQuery] string? language = null + ) + { + var instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId, instanceGuid); - if (dataLists.ListItems == null) - { - return NotFound(); - } + DataList dataLists = await _dataListsService.GetDataListAsync(instanceIdentifier, id, language, queryParams); - return Ok(dataLists); + if (dataLists.ListItems == null) + { + return NotFound(); } + + return Ok(dataLists); } } diff --git a/src/Altinn.App.Api/Controllers/DataTagsController.cs b/src/Altinn.App.Api/Controllers/DataTagsController.cs index 49b3cabcb..9b9116984 100644 --- a/src/Altinn.App.Api/Controllers/DataTagsController.cs +++ b/src/Altinn.App.Api/Controllers/DataTagsController.cs @@ -3,177 +3,175 @@ using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Models; using Altinn.App.Core.Constants; -using Altinn.App.Core.Infrastructure.Clients; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Instances; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// This controller class provides action methods for endpoints related to the tags resource on data elements. +/// +[AutoValidateAntiforgeryTokenIfAuthCookie] +[Produces(MediaTypeNames.Application.Json)] +[Consumes(MediaTypeNames.Application.Json)] +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data/{dataGuid:guid}/tags")] +public class DataTagsController : ControllerBase { + private readonly IInstanceClient _instanceClient; + private readonly IDataClient _dataClient; + + /// + /// Initialize a new instance of with the given services. + /// + /// A client that can be used to send instance requests to storage. + /// A client that can be used to send data requests to storage. + public DataTagsController(IInstanceClient instanceClient, IDataClient dataClient) + { + _instanceClient = instanceClient; + _dataClient = dataClient; + } + /// - /// This controller class provides action methods for endpoints related to the tags resource on data elements. + /// Retrieves all tags associated with the given data element. /// - [AutoValidateAntiforgeryTokenIfAuthCookie] - [Produces(MediaTypeNames.Application.Json)] - [Consumes(MediaTypeNames.Application.Json)] - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data/{dataGuid:guid}/tags")] - public class DataTagsController : ControllerBase + /// The short name for the application owner. + /// The name of the application. + /// The party id of the owner of the instance. + /// The id of the instance. + /// The id of the data element. + /// A object with a list of tags. + [HttpGet] + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_READ)] + [ProducesResponseType(typeof(TagsList), StatusCodes.Status200OK)] + public async Task> Get( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] Guid dataGuid + ) { - private readonly IInstanceClient _instanceClient; - private readonly IDataClient _dataClient; - - /// - /// Initialize a new instance of with the given services. - /// - /// A client that can be used to send instance requests to storage. - /// A client that can be used to send data requests to storage. - public DataTagsController(IInstanceClient instanceClient, IDataClient dataClient) + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + if (instance == null) { - _instanceClient = instanceClient; - _dataClient = dataClient; + return NotFound($"Unable to find instance based on the given parameters."); } - /// - /// Retrieves all tags associated with the given data element. - /// - /// The short name for the application owner. - /// The name of the application. - /// The party id of the owner of the instance. - /// The id of the instance. - /// The id of the data element. - /// A object with a list of tags. - [HttpGet] - [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_READ)] - [ProducesResponseType(typeof(TagsList), StatusCodes.Status200OK)] - public async Task> Get( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromRoute] Guid dataGuid - ) + DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + + if (dataElement == null) { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (instance == null) - { - return NotFound($"Unable to find instance based on the given parameters."); - } + return NotFound("Unable to find data element based on the given parameters."); + } - DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + TagsList tagsList = new TagsList { Tags = dataElement.Tags }; + + return tagsList; + } + + /// + /// Adds a new tag to a data element. + /// + /// The short name for the application owner. + /// The name of the application. + /// The party id of the owner of the instance. + /// The id of the instance. + /// The id of the data element. + /// The new tag to be added. + /// A object with a list of tags. + [HttpPost] + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] + [ProducesResponseType(typeof(TagsList), StatusCodes.Status201Created)] + public async Task> Add( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] Guid dataGuid, + [FromBody] string tag + ) + { + if (tag == null || !Regex.Match(tag, "^[\\p{L}\\-_]+$").Success) + { + return BadRequest("The new tag must consist of letters."); + } - if (dataElement == null) - { - return NotFound("Unable to find data element based on the given parameters."); - } + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + if (instance == null) + { + return NotFound("Unable to find instance based on the given parameters."); + } - TagsList tagsList = new TagsList { Tags = dataElement.Tags }; + DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); - return tagsList; + if (dataElement == null) + { + return NotFound("Unable to find data element based on the given parameters."); } - /// - /// Adds a new tag to a data element. - /// - /// The short name for the application owner. - /// The name of the application. - /// The party id of the owner of the instance. - /// The id of the instance. - /// The id of the data element. - /// The new tag to be added. - /// A object with a list of tags. - [HttpPost] - [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] - [ProducesResponseType(typeof(TagsList), StatusCodes.Status201Created)] - public async Task> Add( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromRoute] Guid dataGuid, - [FromBody] string tag - ) + if (!dataElement.Tags.Contains(tag)) { - if (tag == null || !Regex.Match(tag, "^[\\p{L}\\-_]+$").Success) - { - return BadRequest("The new tag must consist of letters."); - } - - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (instance == null) - { - return NotFound("Unable to find instance based on the given parameters."); - } - - DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); - - if (dataElement == null) - { - return NotFound("Unable to find data element based on the given parameters."); - } - - if (!dataElement.Tags.Contains(tag)) - { - dataElement.Tags.Add(tag); - dataElement = await _dataClient.Update(instance, dataElement); - } - - TagsList tagsList = new TagsList { Tags = dataElement.Tags }; - - // There is no endpoint to GET a specific tag. Using the tags list endpoint. - var routeValues = new - { - org, - app, - instanceOwnerPartyId, - instanceGuid, - dataGuid - }; - return CreatedAtAction(nameof(Get), routeValues, tagsList); + dataElement.Tags.Add(tag); + dataElement = await _dataClient.Update(instance, dataElement); } - /// - /// Removes a tag from a data element. - /// - /// The short name for the application owner. - /// The name of the application. - /// The party id of the owner of the instance. - /// The id of the instance. - /// The id of the data element. - /// The name of the tag to be removed. - [HttpDelete("{tag}")] - [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task Delete( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromRoute] Guid dataGuid, - [FromRoute] string tag - ) + TagsList tagsList = new TagsList { Tags = dataElement.Tags }; + + // There is no endpoint to GET a specific tag. Using the tags list endpoint. + var routeValues = new { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (instance == null) - { - return NotFound("Unable to find instance based on the given parameters."); - } + org, + app, + instanceOwnerPartyId, + instanceGuid, + dataGuid + }; + return CreatedAtAction(nameof(Get), routeValues, tagsList); + } - DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + /// + /// Removes a tag from a data element. + /// + /// The short name for the application owner. + /// The name of the application. + /// The party id of the owner of the instance. + /// The id of the instance. + /// The id of the data element. + /// The name of the tag to be removed. + [HttpDelete("{tag}")] + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task Delete( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] Guid dataGuid, + [FromRoute] string tag + ) + { + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + if (instance == null) + { + return NotFound("Unable to find instance based on the given parameters."); + } - if (dataElement == null) - { - return NotFound("Unable to find data element based on the given parameters."); - } + DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); - if (dataElement.Tags.Remove(tag)) - { - await _dataClient.Update(instance, dataElement); - } + if (dataElement == null) + { + return NotFound("Unable to find data element based on the given parameters."); + } - return NoContent(); + if (dataElement.Tags.Remove(tag)) + { + await _dataClient.Update(instance, dataElement); } + + return NoContent(); } } diff --git a/src/Altinn.App.Api/Controllers/DisableFormValueModelBindingAttribute.cs b/src/Altinn.App.Api/Controllers/DisableFormValueModelBindingAttribute.cs index f42d75ab4..9a5739c86 100644 --- a/src/Altinn.App.Api/Controllers/DisableFormValueModelBindingAttribute.cs +++ b/src/Altinn.App.Api/Controllers/DisableFormValueModelBindingAttribute.cs @@ -2,30 +2,29 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Turns of binding of attachement +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter { /// - /// Turns of binding of attachement + /// Called before resource is processed and turns of formvalue provider an jquery provider /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter + /// the execution context + public void OnResourceExecuting(ResourceExecutingContext context) { - /// - /// Called before resource is processed and turns of formvalue provider an jquery provider - /// - /// the execution context - public void OnResourceExecuting(ResourceExecutingContext context) - { - var factories = context.ValueProviderFactories; - factories.RemoveType(); - factories.RemoveType(); - factories.RemoveType(); - } - - /// - /// Called after resource is processed. Does nothing. - /// - /// the execution context - public void OnResourceExecuted(ResourceExecutedContext context) { } + var factories = context.ValueProviderFactories; + factories.RemoveType(); + factories.RemoveType(); + factories.RemoveType(); } + + /// + /// Called after resource is processed. Does nothing. + /// + /// the execution context + public void OnResourceExecuted(ResourceExecutedContext context) { } } diff --git a/src/Altinn.App.Api/Controllers/EventsReceiverController.cs b/src/Altinn.App.Api/Controllers/EventsReceiverController.cs index b5fc89b01..b40a3af49 100644 --- a/src/Altinn.App.Api/Controllers/EventsReceiverController.cs +++ b/src/Altinn.App.Api/Controllers/EventsReceiverController.cs @@ -1,91 +1,87 @@ #nullable disable using System.Text.Json; -using System.Threading.Tasks; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Models; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Controller for handling inbound events from the event system +/// +[Route("{org}/{app}/api/v1/eventsreceiver")] +public class EventsReceiverController : ControllerBase { + private readonly IEventHandlerResolver _eventHandlerResolver; + private readonly ILogger _logger; + private readonly IEventSecretCodeProvider _secretCodeProvider; + /// - /// Controller for handling inbound events from the event system + /// Initializes a new instance of the class. /// - [Route("{org}/{app}/api/v1/eventsreceiver")] - public class EventsReceiverController : ControllerBase + public EventsReceiverController( + IEventHandlerResolver eventHandlerResolver, + ILogger logger, + IOptions options, + IEventSecretCodeProvider secretCodeProvider + ) { - private readonly IEventHandlerResolver _eventHandlerResolver; - private readonly ILogger _logger; - private readonly IEventSecretCodeProvider _secretCodeProvider; + _eventHandlerResolver = eventHandlerResolver; + _logger = logger; + _secretCodeProvider = secretCodeProvider; + } - /// - /// Initializes a new instance of the class. - /// - public EventsReceiverController( - IEventHandlerResolver eventHandlerResolver, - ILogger logger, - IOptions options, - IEventSecretCodeProvider secretCodeProvider - ) + /// + /// Create a new inbound event for the app to process. + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(425)] + [ProducesResponseType(500)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task Post([FromQuery] string code, [FromBody] CloudEvent cloudEvent) + { + if (await _secretCodeProvider.GetSecretCode() != code) { - _eventHandlerResolver = eventHandlerResolver; - _logger = logger; - _secretCodeProvider = secretCodeProvider; + return Unauthorized(); } - /// - /// Create a new inbound event for the app to process. - /// - [HttpPost] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(425)] - [ProducesResponseType(500)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task Post([FromQuery] string code, [FromBody] CloudEvent cloudEvent) + if (cloudEvent.Type == null) { - if (await _secretCodeProvider.GetSecretCode() != code) - { - return Unauthorized(); - } - - if (cloudEvent.Type == null) - { - _logger.LogError( - "CloudEvent.Type is null, unable to process event! Data received: {data}", - JsonSerializer.Serialize(cloudEvent) - ); - return BadRequest(); - } + _logger.LogError( + "CloudEvent.Type is null, unable to process event! Data received: {data}", + JsonSerializer.Serialize(cloudEvent) + ); + return BadRequest(); + } - IEventHandler eventHandler = _eventHandlerResolver.ResolveEventHandler(cloudEvent.Type); - try - { - bool eventSuccessfullyProcessed = await eventHandler.ProcessEvent(cloudEvent); + IEventHandler eventHandler = _eventHandlerResolver.ResolveEventHandler(cloudEvent.Type); + try + { + bool eventSuccessfullyProcessed = await eventHandler.ProcessEvent(cloudEvent); - // A return value of 425 will ensure the event system triggers the retry mecanism. - // Actually any other return value than 200 Ok will do this, but this is the "correct way" - // of saying we would like to be reminded again later. - return eventSuccessfullyProcessed ? Ok() : new StatusCodeResult(425); - } - catch (NotImplementedException) - { - return BadRequest($"No eventhandler found that supports {cloudEvent.Type}"); - } - catch (Exception ex) - { - _logger.LogError( - ex, - "Unable to process event {eventType}. An exception was raised while processing message {messageid}. Exception thrown {exceptionMessage}", - cloudEvent.Type, - cloudEvent.Id, - ex.Message - ); - return new StatusCodeResult(500); - } + // A return value of 425 will ensure the event system triggers the retry mecanism. + // Actually any other return value than 200 Ok will do this, but this is the "correct way" + // of saying we would like to be reminded again later. + return eventSuccessfullyProcessed ? Ok() : new StatusCodeResult(425); + } + catch (NotImplementedException) + { + return BadRequest($"No eventhandler found that supports {cloudEvent.Type}"); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Unable to process event {eventType}. An exception was raised while processing message {messageid}. Exception thrown {exceptionMessage}", + cloudEvent.Type, + cloudEvent.Id, + ex.Message + ); + return new StatusCodeResult(500); } } } diff --git a/src/Altinn.App.Api/Controllers/FileScanController.cs b/src/Altinn.App.Api/Controllers/FileScanController.cs index c93199ecb..48c871a01 100644 --- a/src/Altinn.App.Api/Controllers/FileScanController.cs +++ b/src/Altinn.App.Api/Controllers/FileScanController.cs @@ -6,71 +6,66 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Contains actions for checking status on file scans. +/// +[Authorize] +[ApiController] +public class FileScanController : ControllerBase { + private readonly IInstanceClient _instanceClient; + /// - /// Contains actions for checking status on file scans. + /// Initialises a new instance of the class /// - [Authorize] - [ApiController] - public class FileScanController : ControllerBase + public FileScanController(IInstanceClient instanceClient) { - private readonly IInstanceClient _instanceClient; + _instanceClient = instanceClient; + } - /// - /// Initialises a new instance of the class - /// - public FileScanController(IInstanceClient instanceClient) + /// + /// Checks that file scan result for an instance and it's data elements. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation + /// Unique id of the party that is the owner of the instance. + /// Unique id to identify the instance + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/filescanresult")] + public async Task> GetFileScanResults( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid + ) + { + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + if (instance == null) { - _instanceClient = instanceClient; + return NotFound(); } - /// - /// Checks that file scan result for an instance and it's data elements. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation - /// Unique id of the party that is the owner of the instance. - /// Unique id to identify the instance - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/filescanresult")] - public async Task> GetFileScanResults( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid - ) - { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (instance == null) - { - return NotFound(); - } + return Ok(MapFromInstance(instance)); + } - return Ok(MapFromInstance(instance)); - } + private static InstanceFileScanResult MapFromInstance(Instance instance) + { + var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(instance)); - private static InstanceFileScanResult MapFromInstance(Instance instance) + if (instance.Data != null) { - var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(instance)); - - if (instance.Data != null) + foreach (var dataElement in instance.Data) { - foreach (var dataElement in instance.Data) - { - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = dataElement.Id, - FileScanResult = dataElement.FileScanResult - } - ); - } + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = dataElement.Id, FileScanResult = dataElement.FileScanResult } + ); } - - return instanceFileScanResult; } + + return instanceFileScanResult; } } diff --git a/src/Altinn.App.Api/Controllers/HomeController.cs b/src/Altinn.App.Api/Controllers/HomeController.cs index 03b041266..974d64a75 100644 --- a/src/Altinn.App.Api/Controllers/HomeController.cs +++ b/src/Altinn.App.Api/Controllers/HomeController.cs @@ -8,152 +8,148 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Provides access to the default home view. +/// +public class HomeController : Controller { + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + private readonly IAntiforgery _antiforgery; + private readonly PlatformSettings _platformSettings; + private readonly IWebHostEnvironment _env; + private readonly AppSettings _appSettings; + private readonly IAppResources _appResources; + private readonly IAppMetadata _appMetadata; + private readonly List _onEntryWithInstance = new List { "new-instance", "select-instance" }; + + /// + /// Initialize a new instance of the class. + /// + /// The anti forgery service. + /// The platform settings. + /// The current environment. + /// The application settings + /// The application resources service + /// The application metadata service + public HomeController( + IAntiforgery antiforgery, + IOptions platformSettings, + IWebHostEnvironment env, + IOptions appSettings, + IAppResources appResources, + IAppMetadata appMetadata + ) + { + _antiforgery = antiforgery; + _platformSettings = platformSettings.Value; + _env = env; + _appSettings = appSettings.Value; + _appResources = appResources; + _appMetadata = appMetadata; + } + /// - /// Provides access to the default home view. + /// Returns the index view with references to the React app. /// - public class HomeController : Controller + /// The application owner short name. + /// The name of the app + /// Parameter to indicate disabling of reportee selection in Altinn Portal. + [HttpGet] + [Route("{org}/{app}/")] + public async Task Index( + [FromRoute] string org, + [FromRoute] string app, + [FromQuery] bool dontChooseReportee + ) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - private readonly IAntiforgery _antiforgery; - private readonly PlatformSettings _platformSettings; - private readonly IWebHostEnvironment _env; - private readonly AppSettings _appSettings; - private readonly IAppResources _appResources; - private readonly IAppMetadata _appMetadata; - private readonly List _onEntryWithInstance = new List { "new-instance", "select-instance" }; - - /// - /// Initialize a new instance of the class. - /// - /// The anti forgery service. - /// The platform settings. - /// The current environment. - /// The application settings - /// The application resources service - /// The application metadata service - public HomeController( - IAntiforgery antiforgery, - IOptions platformSettings, - IWebHostEnvironment env, - IOptions appSettings, - IAppResources appResources, - IAppMetadata appMetadata - ) + // See comments in the configuration of Antiforgery in MvcConfiguration.cs. + var tokens = _antiforgery.GetAndStoreTokens(HttpContext); + if (tokens.RequestToken != null) { - _antiforgery = antiforgery; - _platformSettings = platformSettings.Value; - _env = env; - _appSettings = appSettings.Value; - _appResources = appResources; - _appMetadata = appMetadata; + HttpContext.Response.Cookies.Append( + "XSRF-TOKEN", + tokens.RequestToken, + new CookieOptions + { + HttpOnly = false // Make this cookie readable by Javascript. + } + ); } - /// - /// Returns the index view with references to the React app. - /// - /// The application owner short name. - /// The name of the app - /// Parameter to indicate disabling of reportee selection in Altinn Portal. - [HttpGet] - [Route("{org}/{app}/")] - public async Task Index( - [FromRoute] string org, - [FromRoute] string app, - [FromQuery] bool dontChooseReportee - ) + if (await ShouldShowAppView()) { - // See comments in the configuration of Antiforgery in MvcConfiguration.cs. - var tokens = _antiforgery.GetAndStoreTokens(HttpContext); - if (tokens.RequestToken != null) - { - HttpContext.Response.Cookies.Append( - "XSRF-TOKEN", - tokens.RequestToken, - new CookieOptions - { - HttpOnly = false // Make this cookie readable by Javascript. - } - ); - } - - if (await ShouldShowAppView()) - { - ViewBag.org = org; - ViewBag.app = app; - return PartialView("Index"); - } - - string scheme = _env.IsDevelopment() ? "http" : "https"; - string goToUrl = HttpUtility.UrlEncode($"{scheme}://{Request.Host}/{org}/{app}"); - - string redirectUrl = $"{_platformSettings.ApiAuthenticationEndpoint}authentication?goto={goToUrl}"; - - if (!string.IsNullOrEmpty(_appSettings.AppOidcProvider)) - { - redirectUrl += "&iss=" + _appSettings.AppOidcProvider; - } - - if (dontChooseReportee) - { - redirectUrl += "&DontChooseReportee=true"; - } - - return Redirect(redirectUrl); + ViewBag.org = org; + ViewBag.app = app; + return PartialView("Index"); } - private async Task ShouldShowAppView() + string scheme = _env.IsDevelopment() ? "http" : "https"; + string goToUrl = HttpUtility.UrlEncode($"{scheme}://{Request.Host}/{org}/{app}"); + + string redirectUrl = $"{_platformSettings.ApiAuthenticationEndpoint}authentication?goto={goToUrl}"; + + if (!string.IsNullOrEmpty(_appSettings.AppOidcProvider)) { - if (User?.Identity?.IsAuthenticated == true) - { - return true; - } + redirectUrl += "&iss=" + _appSettings.AppOidcProvider; + } - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - if (!IsStatelessApp(application)) - { - return false; - } + if (dontChooseReportee) + { + redirectUrl += "&DontChooseReportee=true"; + } - DataType? dataType = GetStatelessDataType(application); + return Redirect(redirectUrl); + } - if (dataType != null && dataType.AppLogic.AllowAnonymousOnStateless) - { - return true; - } + private async Task ShouldShowAppView() + { + if (User?.Identity?.IsAuthenticated == true) + { + return true; + } + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + if (!IsStatelessApp(application)) + { return false; } - private bool IsStatelessApp(ApplicationMetadata application) + DataType? dataType = GetStatelessDataType(application); + + if (dataType != null && dataType.AppLogic.AllowAnonymousOnStateless) { - if (application?.OnEntry == null) - { - return false; - } + return true; + } + + return false; + } - return !_onEntryWithInstance.Contains(application.OnEntry.Show); + private bool IsStatelessApp(ApplicationMetadata application) + { + if (application?.OnEntry == null) + { + return false; } - private DataType? GetStatelessDataType(ApplicationMetadata application) + return !_onEntryWithInstance.Contains(application.OnEntry.Show); + } + + private DataType? GetStatelessDataType(ApplicationMetadata application) + { + string layoutSetsString = _appResources.GetLayoutSets(); + + // Stateless apps only work with layousets + if (!string.IsNullOrEmpty(layoutSetsString)) { - string layoutSetsString = _appResources.GetLayoutSets(); - - // Stateless apps only work with layousets - if (!string.IsNullOrEmpty(layoutSetsString)) - { - LayoutSets? layoutSets = JsonSerializer.Deserialize( - layoutSetsString, - _jsonSerializerOptions - ); - string? dataTypeId = layoutSets?.Sets?.Find(set => set.Id == application.OnEntry?.Show)?.DataType; - return application.DataTypes.Find(d => d.Id == dataTypeId); - } - - return null; + LayoutSets? layoutSets = JsonSerializer.Deserialize(layoutSetsString, _jsonSerializerOptions); + string? dataTypeId = layoutSets?.Sets?.Find(set => set.Id == application.OnEntry?.Show)?.DataType; + return application.DataTypes.Find(d => d.Id == dataTypeId); } + + return null; } } diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index 2e895eb7d..cdf5651c9 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -35,1227 +35,1213 @@ using Microsoft.Extensions.Primitives; using Newtonsoft.Json; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Controller for application instances for app-backend. +/// You can create a new instance (POST), update it (PUT) and retrieve a specific instance (GET). +/// +[Authorize] +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[AutoValidateAntiforgeryTokenIfAuthCookie] +[Route("{org}/{app}/instances")] +[ApiController] +public class InstancesController : ControllerBase { + private readonly ILogger _logger; + + private readonly IInstanceClient _instanceClient; + private readonly IDataClient _dataClient; + private readonly IAltinnPartyClient _altinnPartyClientClient; + private readonly IEventsClient _eventsClient; + private readonly IProfileClient _profileClient; + + private readonly IAppMetadata _appMetadata; + private readonly IAppModel _appModel; + private readonly IInstantiationProcessor _instantiationProcessor; + private readonly IInstantiationValidator _instantiationValidator; + private readonly IPDP _pdp; + private readonly IPrefill _prefillService; + private readonly AppSettings _appSettings; + private readonly IProcessEngine _processEngine; + private readonly IOrganizationClient _orgClient; + + private const long RequestSizeLimit = 2000 * 1024 * 1024; + /// - /// Controller for application instances for app-backend. - /// You can create a new instance (POST), update it (PUT) and retrieve a specific instance (GET). + /// Initializes a new instance of the class /// + public InstancesController( + ILogger logger, + IAltinnPartyClient altinnPartyClientClient, + IInstanceClient instanceClient, + IDataClient dataClient, + IAppMetadata appMetadata, + IAppModel appModel, + IInstantiationProcessor instantiationProcessor, + IInstantiationValidator instantiationValidator, + IPDP pdp, + IEventsClient eventsClient, + IOptions appSettings, + IPrefill prefillService, + IProfileClient profileClient, + IProcessEngine processEngine, + IOrganizationClient orgClient + ) + { + _logger = logger; + _instanceClient = instanceClient; + _dataClient = dataClient; + _appMetadata = appMetadata; + _altinnPartyClientClient = altinnPartyClientClient; + _appModel = appModel; + _instantiationProcessor = instantiationProcessor; + _instantiationValidator = instantiationValidator; + _pdp = pdp; + _eventsClient = eventsClient; + _appSettings = appSettings.Value; + _prefillService = prefillService; + _profileClient = profileClient; + _processEngine = processEngine; + _orgClient = orgClient; + } + + /// + /// Gets an instance object from storage. + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// the instance [Authorize] - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - [AutoValidateAntiforgeryTokenIfAuthCookie] - [Route("{org}/{app}/instances")] - [ApiController] - public class InstancesController : ControllerBase + [HttpGet("{instanceOwnerPartyId:int}/{instanceGuid:guid}")] + [Produces("application/json")] + [ProducesResponseType(typeof(Instance), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task Get( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid + ) { - private readonly ILogger _logger; - - private readonly IInstanceClient _instanceClient; - private readonly IDataClient _dataClient; - private readonly IAltinnPartyClient _altinnPartyClientClient; - private readonly IEventsClient _eventsClient; - private readonly IProfileClient _profileClient; - - private readonly IAppMetadata _appMetadata; - private readonly IAppModel _appModel; - private readonly IInstantiationProcessor _instantiationProcessor; - private readonly IInstantiationValidator _instantiationValidator; - private readonly IPDP _pdp; - private readonly IPrefill _prefillService; - private readonly AppSettings _appSettings; - private readonly IProcessEngine _processEngine; - private readonly IOrganizationClient _orgClient; - - private const long RequestSizeLimit = 2000 * 1024 * 1024; - - /// - /// Initializes a new instance of the class - /// - public InstancesController( - ILogger logger, - IAltinnPartyClient altinnPartyClientClient, - IInstanceClient instanceClient, - IDataClient dataClient, - IAppMetadata appMetadata, - IAppModel appModel, - IInstantiationProcessor instantiationProcessor, - IInstantiationValidator instantiationValidator, - IPDP pdp, - IEventsClient eventsClient, - IOptions appSettings, - IPrefill prefillService, - IProfileClient profileClient, - IProcessEngine processEngine, - IOrganizationClient orgClient - ) + EnforcementResult enforcementResult = await AuthorizeAction( + org, + app, + instanceOwnerPartyId, + instanceGuid, + "read" + ); + + if (!enforcementResult.Authorized) { - _logger = logger; - _instanceClient = instanceClient; - _dataClient = dataClient; - _appMetadata = appMetadata; - _altinnPartyClientClient = altinnPartyClientClient; - _appModel = appModel; - _instantiationProcessor = instantiationProcessor; - _instantiationValidator = instantiationValidator; - _pdp = pdp; - _eventsClient = eventsClient; - _appSettings = appSettings.Value; - _prefillService = prefillService; - _profileClient = profileClient; - _processEngine = processEngine; - _orgClient = orgClient; - } - - /// - /// Gets an instance object from storage. - /// - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// the instance - [Authorize] - [HttpGet("{instanceOwnerPartyId:int}/{instanceGuid:guid}")] - [Produces("application/json")] - [ProducesResponseType(typeof(Instance), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task Get( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid - ) + return Forbidden(enforcementResult); + } + + try { - EnforcementResult enforcementResult = await AuthorizeAction( - org, - app, - instanceOwnerPartyId, - instanceGuid, - "read" - ); + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request); - if (!enforcementResult.Authorized) - { - return Forbidden(enforcementResult); - } + string? userOrgClaim = User.GetOrg(); - try + if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.InvariantCultureIgnoreCase)) { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request); + await _instanceClient.UpdateReadStatus(instanceOwnerPartyId, instanceGuid, "read"); + } - string? userOrgClaim = User.GetOrg(); + return Ok(instance); + } + catch (Exception exception) + { + return ExceptionResponse(exception, $"Get instance {instanceOwnerPartyId}/{instanceGuid} failed"); + } + } - if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.InvariantCultureIgnoreCase)) - { - await _instanceClient.UpdateReadStatus(instanceOwnerPartyId, instanceGuid, "read"); - } + /// + /// Creates a new instance of an application in platform storage. Clients can send an instance as json or send a + /// multipart form-data with the instance in the first part named "instance" and the prefill data in the next parts, with + /// names that correspond to the element types defined in the application metadata. + /// The data elements are stored. Currently calculate and validate is not implemented. + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// the created instance + [HttpPost] + [DisableFormValueModelBinding] + [Produces("application/json")] + [ProducesResponseType(typeof(Instance), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [RequestSizeLimit(RequestSizeLimit)] + public async Task> Post( + [FromRoute] string org, + [FromRoute] string app, + [FromQuery] int? instanceOwnerPartyId + ) + { + if (string.IsNullOrEmpty(org)) + { + return BadRequest("The path parameter 'org' cannot be empty"); + } - return Ok(instance); - } - catch (Exception exception) - { - return ExceptionResponse(exception, $"Get instance {instanceOwnerPartyId}/{instanceGuid} failed"); - } + if (string.IsNullOrEmpty(app)) + { + return BadRequest("The path parameter 'app' cannot be empty"); } - /// - /// Creates a new instance of an application in platform storage. Clients can send an instance as json or send a - /// multipart form-data with the instance in the first part named "instance" and the prefill data in the next parts, with - /// names that correspond to the element types defined in the application metadata. - /// The data elements are stored. Currently calculate and validate is not implemented. - /// - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// the created instance - [HttpPost] - [DisableFormValueModelBinding] - [Produces("application/json")] - [ProducesResponseType(typeof(Instance), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [RequestSizeLimit(RequestSizeLimit)] - public async Task> Post( - [FromRoute] string org, - [FromRoute] string app, - [FromQuery] int? instanceOwnerPartyId - ) + MultipartRequestReader parsedRequest = new MultipartRequestReader(Request); + await parsedRequest.Read(); + + if (parsedRequest.Errors.Count != 0) { - if (string.IsNullOrEmpty(org)) - { - return BadRequest("The path parameter 'org' cannot be empty"); - } + return BadRequest($"Error when reading content: {JsonConvert.SerializeObject(parsedRequest.Errors)}"); + } - if (string.IsNullOrEmpty(app)) - { - return BadRequest("The path parameter 'app' cannot be empty"); - } + Instance? instanceTemplate = await ExtractInstanceTemplate(parsedRequest); - MultipartRequestReader parsedRequest = new MultipartRequestReader(Request); - await parsedRequest.Read(); + if (instanceOwnerPartyId is null && instanceTemplate is null) + { + return BadRequest( + "Cannot create an instance without an instanceOwner.partyId. Either provide instanceOwner party Id as a query parameter or an instanceTemplate object in the body." + ); + } - if (parsedRequest.Errors.Count != 0) - { - return BadRequest($"Error when reading content: {JsonConvert.SerializeObject(parsedRequest.Errors)}"); - } + if (instanceOwnerPartyId is not null && instanceTemplate?.InstanceOwner?.PartyId is not null) + { + return BadRequest( + "You cannot provide an instanceOwnerPartyId as a query param as well as an instance template in the body. Choose one or the other." + ); + } - Instance? instanceTemplate = await ExtractInstanceTemplate(parsedRequest); + if (instanceTemplate is not null) + { + InstanceOwner lookup = instanceTemplate.InstanceOwner; - if (instanceOwnerPartyId is null && instanceTemplate is null) + if ( + lookup == null + || (lookup.PersonNumber == null && lookup.OrganisationNumber == null && lookup.PartyId == null) + ) { return BadRequest( - "Cannot create an instance without an instanceOwner.partyId. Either provide instanceOwner party Id as a query parameter or an instanceTemplate object in the body." + "Error: instanceOwnerPartyId query parameter is empty and InstanceOwner is missing from instance template. You must populate instanceOwnerPartyId or InstanceOwner" ); } - - if (instanceOwnerPartyId is not null && instanceTemplate?.InstanceOwner?.PartyId is not null) + } + else + { + if (instanceOwnerPartyId is null) { - return BadRequest( - "You cannot provide an instanceOwnerPartyId as a query param as well as an instance template in the body. Choose one or the other." - ); + return StatusCode(500, "Can't create minimal instance when Instance owner Party ID is null"); } - if (instanceTemplate is not null) + // create minimum instance template + instanceTemplate = new Instance { - InstanceOwner lookup = instanceTemplate.InstanceOwner; + InstanceOwner = new InstanceOwner { PartyId = instanceOwnerPartyId.Value.ToString() } + }; + } - if ( - lookup == null - || (lookup.PersonNumber == null && lookup.OrganisationNumber == null && lookup.PartyId == null) - ) - { - return BadRequest( - "Error: instanceOwnerPartyId query parameter is empty and InstanceOwner is missing from instance template. You must populate instanceOwnerPartyId or InstanceOwner" - ); - } - } - else + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + RequestPartValidator requestValidator = new(application); + string? multipartError = requestValidator.ValidateParts(parsedRequest.Parts); + + if (!string.IsNullOrEmpty(multipartError)) + { + return BadRequest($"Error when comparing content to application metadata: {multipartError}"); + } + + Party party; + try + { + party = await LookupParty(instanceTemplate.InstanceOwner) ?? throw new Exception("Unknown party"); + instanceTemplate.InstanceOwner = InstantiationHelper.PartyToInstanceOwner(party); + } + catch (Exception partyLookupException) + { + if (partyLookupException is ServiceException sexp) { - if (instanceOwnerPartyId is null) + if (sexp.StatusCode.Equals(HttpStatusCode.Unauthorized)) { - return StatusCode(500, "Can't create minimal instance when Instance owner Party ID is null"); + return StatusCode((int)HttpStatusCode.Forbidden); } - - // create minimum instance template - instanceTemplate = new Instance - { - InstanceOwner = new InstanceOwner { PartyId = instanceOwnerPartyId.Value.ToString() } - }; } - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - RequestPartValidator requestValidator = new(application); - string? multipartError = requestValidator.ValidateParts(parsedRequest.Parts); + return NotFound($"Cannot lookup party: {partyLookupException.Message}"); + } - if (!string.IsNullOrEmpty(multipartError)) - { - return BadRequest($"Error when comparing content to application metadata: {multipartError}"); - } + EnforcementResult enforcementResult = await AuthorizeAction(org, app, party.PartyId, null, "instantiate"); + if (!enforcementResult.Authorized) + { + return Forbidden(enforcementResult); + } - Party party; - try - { - party = await LookupParty(instanceTemplate.InstanceOwner) ?? throw new Exception("Unknown party"); - instanceTemplate.InstanceOwner = InstantiationHelper.PartyToInstanceOwner(party); - } - catch (Exception partyLookupException) - { - if (partyLookupException is ServiceException sexp) - { - if (sexp.StatusCode.Equals(HttpStatusCode.Unauthorized)) - { - return StatusCode((int)HttpStatusCode.Forbidden); - } - } + if (!InstantiationHelper.IsPartyAllowedToInstantiate(party, application.PartyTypesAllowed)) + { + return StatusCode( + (int)HttpStatusCode.Forbidden, + $"Party {party.PartyId} is not allowed to instantiate this application {org}/{app}" + ); + } - return NotFound($"Cannot lookup party: {partyLookupException.Message}"); - } + // Run custom app logic to validate instantiation + InstantiationValidationResult? validationResult = await _instantiationValidator.Validate(instanceTemplate); + if (validationResult != null && !validationResult.Valid) + { + return StatusCode((int)HttpStatusCode.Forbidden, validationResult); + } - EnforcementResult enforcementResult = await AuthorizeAction(org, app, party.PartyId, null, "instantiate"); - if (!enforcementResult.Authorized) - { - return Forbidden(enforcementResult); - } + instanceTemplate.Org = application.Org; + ConditionallySetReadStatus(instanceTemplate); - if (!InstantiationHelper.IsPartyAllowedToInstantiate(party, application.PartyTypesAllowed)) - { - return StatusCode( - (int)HttpStatusCode.Forbidden, - $"Party {party.PartyId} is not allowed to instantiate this application {org}/{app}" - ); - } + Instance instance; + instanceTemplate.Process = null; + ProcessStateChange? change = null; + + try + { + // start process and goto next task + ProcessStartRequest processStartRequest = new() { Instance = instanceTemplate, User = User }; - // Run custom app logic to validate instantiation - InstantiationValidationResult? validationResult = await _instantiationValidator.Validate(instanceTemplate); - if (validationResult != null && !validationResult.Valid) + ProcessChangeResult result = await _processEngine.GenerateProcessStartEvents(processStartRequest); + if (!result.Success) { - return StatusCode((int)HttpStatusCode.Forbidden, validationResult); + return Conflict(result.ErrorMessage); } - instanceTemplate.Org = application.Org; - ConditionallySetReadStatus(instanceTemplate); - - Instance instance; - instanceTemplate.Process = null; - ProcessStateChange? change = null; - - try - { - // start process and goto next task - ProcessStartRequest processStartRequest = new() { Instance = instanceTemplate, User = User }; + change = result.ProcessStateChange; - ProcessChangeResult result = await _processEngine.GenerateProcessStartEvents(processStartRequest); - if (!result.Success) - { - return Conflict(result.ErrorMessage); - } + // create the instance + instance = await _instanceClient.CreateInstance(org, app, instanceTemplate); + } + catch (Exception exception) + { + return ExceptionResponse( + exception, + $"Instantiation of appId {org}/{app} failed for party {instanceTemplate.InstanceOwner?.PartyId}" + ); + } - change = result.ProcessStateChange; + try + { + await StorePrefillParts(instance, application, parsedRequest.Parts); - // create the instance - instance = await _instanceClient.CreateInstance(org, app, instanceTemplate); - } - catch (Exception exception) - { - return ExceptionResponse( - exception, - $"Instantiation of appId {org}/{app} failed for party {instanceTemplate.InstanceOwner?.PartyId}" - ); - } + // get the updated instance + instance = await _instanceClient.GetInstance( + app, + org, + int.Parse(instance.InstanceOwner.PartyId), + Guid.Parse(instance.Id.Split("/")[1]) + ); - try - { - await StorePrefillParts(instance, application, parsedRequest.Parts); + // notify app and store events + _logger.LogInformation("Events sent to process engine: {Events}", change?.Events); + await _processEngine.HandleEventsAndUpdateStorage(instance, null, change?.Events); + } + catch (Exception exception) + { + return ExceptionResponse( + exception, + $"Instantiation of data elements failed for instance {instance.Id} for party {instanceTemplate.InstanceOwner?.PartyId}" + ); + } - // get the updated instance - instance = await _instanceClient.GetInstance( - app, - org, - int.Parse(instance.InstanceOwner.PartyId), - Guid.Parse(instance.Id.Split("/")[1]) - ); + await RegisterEvent("app.instance.created", instance); - // notify app and store events - _logger.LogInformation("Events sent to process engine: {Events}", change?.Events); - await _processEngine.HandleEventsAndUpdateStorage(instance, null, change?.Events); - } - catch (Exception exception) - { - return ExceptionResponse( - exception, - $"Instantiation of data elements failed for instance {instance.Id} for party {instanceTemplate.InstanceOwner?.PartyId}" - ); - } + SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request); + string url = instance.SelfLinks.Apps; - await RegisterEvent("app.instance.created", instance); + return Created(url, instance); + } - SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request); - string url = instance.SelfLinks.Apps; - - return Created(url, instance); - } - - /// - /// Simplified Instanciation with support for fieldprefill - /// - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// instansiation information - /// The new instance - [HttpPost("create")] - [DisableFormValueModelBinding] - [Produces("application/json")] - [ProducesResponseType(typeof(Instance), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [RequestSizeLimit(RequestSizeLimit)] - public async Task> PostSimplified( - [FromRoute] string org, - [FromRoute] string app, - [FromBody] InstansiationInstance instansiationInstance - ) + /// + /// Simplified Instanciation with support for fieldprefill + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// instansiation information + /// The new instance + [HttpPost("create")] + [DisableFormValueModelBinding] + [Produces("application/json")] + [ProducesResponseType(typeof(Instance), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [RequestSizeLimit(RequestSizeLimit)] + public async Task> PostSimplified( + [FromRoute] string org, + [FromRoute] string app, + [FromBody] InstansiationInstance instansiationInstance + ) + { + if (string.IsNullOrEmpty(org)) { - if (string.IsNullOrEmpty(org)) - { - return BadRequest("The path parameter 'org' cannot be empty"); - } + return BadRequest("The path parameter 'org' cannot be empty"); + } - if (string.IsNullOrEmpty(app)) - { - return BadRequest("The path parameter 'app' cannot be empty"); - } + if (string.IsNullOrEmpty(app)) + { + return BadRequest("The path parameter 'app' cannot be empty"); + } - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - bool copySourceInstance = !string.IsNullOrEmpty(instansiationInstance.SourceInstanceId); - if (copySourceInstance && application.CopyInstanceSettings?.Enabled != true) - { - return BadRequest( - "Creating instance based on a copy from an archived instance is not enabled for this app." - ); - } + bool copySourceInstance = !string.IsNullOrEmpty(instansiationInstance.SourceInstanceId); + if (copySourceInstance && application.CopyInstanceSettings?.Enabled != true) + { + return BadRequest( + "Creating instance based on a copy from an archived instance is not enabled for this app." + ); + } - InstanceOwner lookup = instansiationInstance.InstanceOwner; + InstanceOwner lookup = instansiationInstance.InstanceOwner; - if ( - lookup == null - || (lookup.PersonNumber == null && lookup.OrganisationNumber == null && lookup.PartyId == null) - ) - { - return BadRequest( - "Error: instanceOwnerPartyId query parameter is empty and InstanceOwner is missing from instance template. You must populate instanceOwnerPartyId or InstanceOwner" - ); - } + if ( + lookup == null + || (lookup.PersonNumber == null && lookup.OrganisationNumber == null && lookup.PartyId == null) + ) + { + return BadRequest( + "Error: instanceOwnerPartyId query parameter is empty and InstanceOwner is missing from instance template. You must populate instanceOwnerPartyId or InstanceOwner" + ); + } - Party party; - try - { - party = await LookupParty(instansiationInstance.InstanceOwner) ?? throw new Exception("Unknown party"); - instansiationInstance.InstanceOwner = InstantiationHelper.PartyToInstanceOwner(party); - } - catch (Exception partyLookupException) + Party party; + try + { + party = await LookupParty(instansiationInstance.InstanceOwner) ?? throw new Exception("Unknown party"); + instansiationInstance.InstanceOwner = InstantiationHelper.PartyToInstanceOwner(party); + } + catch (Exception partyLookupException) + { + if (partyLookupException is ServiceException sexp) { - if (partyLookupException is ServiceException sexp) + if (sexp.StatusCode.Equals(HttpStatusCode.Unauthorized)) { - if (sexp.StatusCode.Equals(HttpStatusCode.Unauthorized)) - { - return StatusCode((int)HttpStatusCode.Forbidden); - } + return StatusCode((int)HttpStatusCode.Forbidden); } - - return NotFound($"Cannot lookup party: {partyLookupException.Message}"); } - if (copySourceInstance && party.PartyId.ToString() != instansiationInstance.SourceInstanceId.Split("/")[0]) - { - return BadRequest("It is not possible to copy instances between instance owners."); - } + return NotFound($"Cannot lookup party: {partyLookupException.Message}"); + } - EnforcementResult enforcementResult = await AuthorizeAction(org, app, party.PartyId, null, "instantiate"); + if (copySourceInstance && party.PartyId.ToString() != instansiationInstance.SourceInstanceId.Split("/")[0]) + { + return BadRequest("It is not possible to copy instances between instance owners."); + } - if (!enforcementResult.Authorized) - { - return Forbidden(enforcementResult); - } + EnforcementResult enforcementResult = await AuthorizeAction(org, app, party.PartyId, null, "instantiate"); + + if (!enforcementResult.Authorized) + { + return Forbidden(enforcementResult); + } + + if (!InstantiationHelper.IsPartyAllowedToInstantiate(party, application.PartyTypesAllowed)) + { + return StatusCode( + (int)HttpStatusCode.Forbidden, + $"Party {party.PartyId} is not allowed to instantiate this application {org}/{app}" + ); + } - if (!InstantiationHelper.IsPartyAllowedToInstantiate(party, application.PartyTypesAllowed)) + Instance instanceTemplate = + new() { - return StatusCode( - (int)HttpStatusCode.Forbidden, - $"Party {party.PartyId} is not allowed to instantiate this application {org}/{app}" - ); - } + InstanceOwner = instansiationInstance.InstanceOwner, + VisibleAfter = instansiationInstance.VisibleAfter, + DueBefore = instansiationInstance.DueBefore, + Org = application.Org + }; - Instance instanceTemplate = - new() - { - InstanceOwner = instansiationInstance.InstanceOwner, - VisibleAfter = instansiationInstance.VisibleAfter, - DueBefore = instansiationInstance.DueBefore, - Org = application.Org - }; + ConditionallySetReadStatus(instanceTemplate); - ConditionallySetReadStatus(instanceTemplate); + // Run custom app logic to validate instantiation + InstantiationValidationResult? validationResult = await _instantiationValidator.Validate(instanceTemplate); + if (validationResult != null && !validationResult.Valid) + { + return StatusCode((int)HttpStatusCode.Forbidden, validationResult); + } - // Run custom app logic to validate instantiation - InstantiationValidationResult? validationResult = await _instantiationValidator.Validate(instanceTemplate); - if (validationResult != null && !validationResult.Valid) - { - return StatusCode((int)HttpStatusCode.Forbidden, validationResult); - } + Instance instance; + ProcessChangeResult processResult; + try + { + // start process and goto next task + instanceTemplate.Process = null; - Instance instance; - ProcessChangeResult processResult; - try + var request = new ProcessStartRequest() { - // start process and goto next task - instanceTemplate.Process = null; + Instance = instanceTemplate, + User = User, + Prefill = instansiationInstance.Prefill + }; - var request = new ProcessStartRequest() - { - Instance = instanceTemplate, - User = User, - Prefill = instansiationInstance.Prefill - }; + processResult = await _processEngine.GenerateProcessStartEvents(request); - processResult = await _processEngine.GenerateProcessStartEvents(request); + Instance? source = null; - Instance? source = null; + if (copySourceInstance) + { + string[] sourceSplit = instansiationInstance.SourceInstanceId.Split("/"); + Guid sourceInstanceGuid = Guid.Parse(sourceSplit[1]); - if (copySourceInstance) + try { - string[] sourceSplit = instansiationInstance.SourceInstanceId.Split("/"); - Guid sourceInstanceGuid = Guid.Parse(sourceSplit[1]); - - try - { - source = await _instanceClient.GetInstance(app, org, party.PartyId, sourceInstanceGuid); - } - catch (PlatformHttpException exception) - { - return StatusCode( - 500, - $"Retrieving source instance failed with status code {exception.Response.StatusCode}" - ); - } - - if (!source.Status.IsArchived) - { - return BadRequest("It is not possible to copy an instance that isn't archived."); - } + source = await _instanceClient.GetInstance(app, org, party.PartyId, sourceInstanceGuid); } - - instance = await _instanceClient.CreateInstance(org, app, instanceTemplate); - - if (copySourceInstance && source is not null) + catch (PlatformHttpException exception) { - await CopyDataFromSourceInstance(application, instance, source); + return StatusCode( + 500, + $"Retrieving source instance failed with status code {exception.Response.StatusCode}" + ); } - instance = await _instanceClient.GetInstance(instance); - await _processEngine.HandleEventsAndUpdateStorage( - instance, - instansiationInstance.Prefill, - processResult.ProcessStateChange?.Events - ); - } - catch (Exception exception) - { - return ExceptionResponse( - exception, - $"Instantiation of appId {org}/{app} failed for party {instanceTemplate.InstanceOwner?.PartyId}" - ); + if (!source.Status.IsArchived) + { + return BadRequest("It is not possible to copy an instance that isn't archived."); + } } - await RegisterEvent("app.instance.created", instance); + instance = await _instanceClient.CreateInstance(org, app, instanceTemplate); - SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request); - string url = instance.SelfLinks.Apps; - - return Created(url, instance); - } - - /// - /// This method handles the copy endpoint for when a user wants to create a copy of an existing instance. - /// The endpoint will primarily be accessed directly by a user clicking the copy button for an archived instance. - /// - /// Unique identifier of the organisation responsible for the app - /// Application identifier which is unique within an organisation - /// Unique id of the party that is the owner of the instance - /// Unique id to identify the instance - /// A representing the result of the asynchronous operation. - /// - /// The endpoint will return a redirect to the new instance if the copy operation was successful. - /// - [Obsolete("This endpoint will be removed in a future release of the app template packages.")] - [ApiExplorerSettings(IgnoreApi = true)] - [Authorize] - [HttpGet("/{org}/{app}/legacy/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/copy")] - [ProducesResponseType(typeof(Instance), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task CopyInstance( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid - ) - { - // This endpoint should be used exclusively by end users. Ideally from a browser as a request after clicking - // a button in the message box, but for now we simply just exclude app owner(s). - string? orgClaim = User.GetOrg(); - if (orgClaim is not null) + if (copySourceInstance && source is not null) { - return Forbid(); + await CopyDataFromSourceInstance(application, instance, source); } - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + instance = await _instanceClient.GetInstance(instance); + await _processEngine.HandleEventsAndUpdateStorage( + instance, + instansiationInstance.Prefill, + processResult.ProcessStateChange?.Events + ); + } + catch (Exception exception) + { + return ExceptionResponse( + exception, + $"Instantiation of appId {org}/{app} failed for party {instanceTemplate.InstanceOwner?.PartyId}" + ); + } - if (application.CopyInstanceSettings?.Enabled is null or false) - { - return BadRequest( - "Creating instance based on a copy from an archived instance is not enabled for this app." - ); - } + await RegisterEvent("app.instance.created", instance); - EnforcementResult readAccess = await AuthorizeAction(org, app, instanceOwnerPartyId, instanceGuid, "read"); + SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request); + string url = instance.SelfLinks.Apps; - if (!readAccess.Authorized) - { - return Forbidden(readAccess); - } + return Created(url, instance); + } - Instance? sourceInstance = await GetInstance(org, app, instanceOwnerPartyId, instanceGuid); + /// + /// This method handles the copy endpoint for when a user wants to create a copy of an existing instance. + /// The endpoint will primarily be accessed directly by a user clicking the copy button for an archived instance. + /// + /// Unique identifier of the organisation responsible for the app + /// Application identifier which is unique within an organisation + /// Unique id of the party that is the owner of the instance + /// Unique id to identify the instance + /// A representing the result of the asynchronous operation. + /// + /// The endpoint will return a redirect to the new instance if the copy operation was successful. + /// + [Obsolete("This endpoint will be removed in a future release of the app template packages.")] + [ApiExplorerSettings(IgnoreApi = true)] + [Authorize] + [HttpGet("/{org}/{app}/legacy/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/copy")] + [ProducesResponseType(typeof(Instance), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task CopyInstance( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid + ) + { + // This endpoint should be used exclusively by end users. Ideally from a browser as a request after clicking + // a button in the message box, but for now we simply just exclude app owner(s). + string? orgClaim = User.GetOrg(); + if (orgClaim is not null) + { + return Forbid(); + } - if (sourceInstance?.Status?.IsArchived is null or false) - { - return BadRequest("The instance being copied must be archived."); - } + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - EnforcementResult instantiateAccess = await AuthorizeAction( - org, - app, - instanceOwnerPartyId, - null, - "instantiate" + if (application.CopyInstanceSettings?.Enabled is null or false) + { + return BadRequest( + "Creating instance based on a copy from an archived instance is not enabled for this app." ); + } + + EnforcementResult readAccess = await AuthorizeAction(org, app, instanceOwnerPartyId, instanceGuid, "read"); + + if (!readAccess.Authorized) + { + return Forbidden(readAccess); + } + + Instance? sourceInstance = await GetInstance(org, app, instanceOwnerPartyId, instanceGuid); + + if (sourceInstance?.Status?.IsArchived is null or false) + { + return BadRequest("The instance being copied must be archived."); + } + + EnforcementResult instantiateAccess = await AuthorizeAction( + org, + app, + instanceOwnerPartyId, + null, + "instantiate" + ); + + if (!instantiateAccess.Authorized) + { + return Forbidden(instantiateAccess); + } - if (!instantiateAccess.Authorized) + // Multiple properties like Org and AppId will be set by Storage + Instance targetInstance = + new() { - return Forbidden(instantiateAccess); - } + InstanceOwner = sourceInstance.InstanceOwner, + VisibleAfter = sourceInstance.VisibleAfter, + Status = new() { ReadStatus = ReadStatus.Read } + }; - // Multiple properties like Org and AppId will be set by Storage - Instance targetInstance = - new() - { - InstanceOwner = sourceInstance.InstanceOwner, - VisibleAfter = sourceInstance.VisibleAfter, - Status = new() { ReadStatus = ReadStatus.Read } - }; + InstantiationValidationResult? validationResult = await _instantiationValidator.Validate(targetInstance); + if (validationResult != null && !validationResult.Valid) + { + return StatusCode((int)HttpStatusCode.Forbidden, validationResult); + } + + ProcessStartRequest processStartRequest = new() { Instance = targetInstance, User = User }; + + ProcessChangeResult startResult = await _processEngine.GenerateProcessStartEvents(processStartRequest); - InstantiationValidationResult? validationResult = await _instantiationValidator.Validate(targetInstance); - if (validationResult != null && !validationResult.Valid) - { - return StatusCode((int)HttpStatusCode.Forbidden, validationResult); - } + targetInstance = await _instanceClient.CreateInstance(org, app, targetInstance); - ProcessStartRequest processStartRequest = new() { Instance = targetInstance, User = User }; + await CopyDataFromSourceInstance(application, targetInstance, sourceInstance); - ProcessChangeResult startResult = await _processEngine.GenerateProcessStartEvents(processStartRequest); + targetInstance = await _instanceClient.GetInstance(targetInstance); - targetInstance = await _instanceClient.CreateInstance(org, app, targetInstance); + await _processEngine.HandleEventsAndUpdateStorage(targetInstance, null, startResult.ProcessStateChange?.Events); - await CopyDataFromSourceInstance(application, targetInstance, sourceInstance); + await RegisterEvent("app.instance.created", targetInstance); - targetInstance = await _instanceClient.GetInstance(targetInstance); + string url = SelfLinkHelper.BuildFrontendSelfLink(targetInstance, Request); - await _processEngine.HandleEventsAndUpdateStorage( - targetInstance, - null, - startResult.ProcessStateChange?.Events - ); + return Redirect(url); + } - await RegisterEvent("app.instance.created", targetInstance); - - string url = SelfLinkHelper.BuildFrontendSelfLink(targetInstance, Request); - - return Redirect(url); - } - - /// - /// Add complete confirmation. - /// - /// - /// Add to an instance that a given stakeholder considers the instance as no longer needed by them. The stakeholder has - /// collected all the data and information they needed from the instance and expect no additional data to be added to it. - /// The body of the request isn't used for anything despite this being a POST operation. - /// - /// The party id of the instance owner. - /// The id of the instance to confirm as complete. - /// Returns the instance with updated list of confirmations. - [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_COMPLETE)] - [HttpPost("{instanceOwnerPartyId:int}/{instanceGuid:guid}/complete")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Produces("application/json")] - public async Task> AddCompleteConfirmation( - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid - ) + /// + /// Add complete confirmation. + /// + /// + /// Add to an instance that a given stakeholder considers the instance as no longer needed by them. The stakeholder has + /// collected all the data and information they needed from the instance and expect no additional data to be added to it. + /// The body of the request isn't used for anything despite this being a POST operation. + /// + /// The party id of the instance owner. + /// The id of the instance to confirm as complete. + /// Returns the instance with updated list of confirmations. + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_COMPLETE)] + [HttpPost("{instanceOwnerPartyId:int}/{instanceGuid:guid}/complete")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Produces("application/json")] + public async Task> AddCompleteConfirmation( + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid + ) + { + try { - try - { - Instance instance = await _instanceClient.AddCompleteConfirmation(instanceOwnerPartyId, instanceGuid); - SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request); + Instance instance = await _instanceClient.AddCompleteConfirmation(instanceOwnerPartyId, instanceGuid); + SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request); - return Ok(instance); - } - catch (Exception exception) - { - return ExceptionResponse( - exception, - $"Adding complete confirmation to instance {instanceOwnerPartyId}/{instanceGuid} failed" - ); - } + return Ok(instance); + } + catch (Exception exception) + { + return ExceptionResponse( + exception, + $"Adding complete confirmation to instance {instanceOwnerPartyId}/{instanceGuid} failed" + ); } + } - /// - /// Allows an app owner to update the substatus of an instance. - /// - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// The party id of the instance owner. - /// The id of the instance to update. - /// The new substatus of the instance. - /// Returns the instance with updated substatus. - [Authorize] - [HttpPut("{instanceOwnerPartyId:int}/{instanceGuid:guid}/substatus")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [Produces("application/json")] - public async Task> UpdateSubstatus( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromBody] Substatus substatus - ) + /// + /// Allows an app owner to update the substatus of an instance. + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// The party id of the instance owner. + /// The id of the instance to update. + /// The new substatus of the instance. + /// Returns the instance with updated substatus. + [Authorize] + [HttpPut("{instanceOwnerPartyId:int}/{instanceGuid:guid}/substatus")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [Produces("application/json")] + public async Task> UpdateSubstatus( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromBody] Substatus substatus + ) + { + if (substatus == null || string.IsNullOrEmpty(substatus.Label)) { - if (substatus == null || string.IsNullOrEmpty(substatus.Label)) - { - return BadRequest( - $"Invalid sub status: {JsonConvert.SerializeObject(substatus)}. Substatus must be defined and include a label." - ); - } + return BadRequest( + $"Invalid sub status: {JsonConvert.SerializeObject(substatus)}. Substatus must be defined and include a label." + ); + } - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - string? orgClaim = User.GetOrg(); - if (!instance.Org.Equals(orgClaim)) - { - return Forbid(); - } + string? orgClaim = User.GetOrg(); + if (!instance.Org.Equals(orgClaim)) + { + return Forbid(); + } - try - { - Instance updatedInstance = await _instanceClient.UpdateSubstatus( - instanceOwnerPartyId, - instanceGuid, - substatus - ); - SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request); + try + { + Instance updatedInstance = await _instanceClient.UpdateSubstatus( + instanceOwnerPartyId, + instanceGuid, + substatus + ); + SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request); - await RegisterEvent("app.instance.substatus.changed", instance); + await RegisterEvent("app.instance.substatus.changed", instance); - return Ok(updatedInstance); - } - catch (Exception exception) - { - return ExceptionResponse( - exception, - $"Updating substatus for instance {instanceOwnerPartyId}/{instanceGuid} failed." - ); - } + return Ok(updatedInstance); } - - /// - /// Deletes an instance. - /// - /// The party id of the instance owner. - /// The id of the instance to delete. - /// A value indicating whether the instance should be unrecoverable. - /// Returns the deleted instance. - [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_DELETE)] - [HttpDelete("{instanceOwnerPartyId:int}/{instanceGuid:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Produces("application/json")] - public async Task> DeleteInstance( - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromQuery] bool hard - ) + catch (Exception exception) { - try - { - Instance deletedInstance = await _instanceClient.DeleteInstance( - instanceOwnerPartyId, - instanceGuid, - hard - ); - SelfLinkHelper.SetInstanceAppSelfLinks(deletedInstance, Request); - - return Ok(deletedInstance); - } - catch (Exception exception) - { - return ExceptionResponse(exception, $"Deleting instance {instanceOwnerPartyId}/{instanceGuid} failed."); - } + return ExceptionResponse( + exception, + $"Updating substatus for instance {instanceOwnerPartyId}/{instanceGuid} failed." + ); } + } - /// - /// Retrieves all active instances that fulfull the org, app, and instanceOwnerParty Id combination. - /// - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// The party id of the instance owner. - /// A list of light weight instance objects that contains instanceId, lastChanged and lastChangedBy (full name). - [Authorize] - [HttpGet("{instanceOwnerPartyId:int}/active")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Produces("application/json")] - public async Task>> GetActiveInstances( - [FromRoute] string org, - [FromRoute] string app, - int instanceOwnerPartyId - ) + /// + /// Deletes an instance. + /// + /// The party id of the instance owner. + /// The id of the instance to delete. + /// A value indicating whether the instance should be unrecoverable. + /// Returns the deleted instance. + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_DELETE)] + [HttpDelete("{instanceOwnerPartyId:int}/{instanceGuid:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Produces("application/json")] + public async Task> DeleteInstance( + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromQuery] bool hard + ) + { + try { - Dictionary queryParams = - new() - { - { "appId", $"{org}/{app}" }, - { "instanceOwner.partyId", instanceOwnerPartyId.ToString() }, - { "status.isArchived", "false" }, - { "status.isSoftDeleted", "false" } - }; + Instance deletedInstance = await _instanceClient.DeleteInstance(instanceOwnerPartyId, instanceGuid, hard); + SelfLinkHelper.SetInstanceAppSelfLinks(deletedInstance, Request); - List activeInstances = await _instanceClient.GetInstances(queryParams); + return Ok(deletedInstance); + } + catch (Exception exception) + { + return ExceptionResponse(exception, $"Deleting instance {instanceOwnerPartyId}/{instanceGuid} failed."); + } + } - if (activeInstances.Count == 0) + /// + /// Retrieves all active instances that fulfull the org, app, and instanceOwnerParty Id combination. + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// The party id of the instance owner. + /// A list of light weight instance objects that contains instanceId, lastChanged and lastChangedBy (full name). + [Authorize] + [HttpGet("{instanceOwnerPartyId:int}/active")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Produces("application/json")] + public async Task>> GetActiveInstances( + [FromRoute] string org, + [FromRoute] string app, + int instanceOwnerPartyId + ) + { + Dictionary queryParams = + new() { - return Ok(new List()); - } + { "appId", $"{org}/{app}" }, + { "instanceOwner.partyId", instanceOwnerPartyId.ToString() }, + { "status.isArchived", "false" }, + { "status.isSoftDeleted", "false" } + }; - var lastChangedByValues = activeInstances.Select(i => i.LastChangedBy).Distinct(); + List activeInstances = await _instanceClient.GetInstances(queryParams); - Dictionary userAndOrgLookup = new Dictionary(); + if (activeInstances.Count == 0) + { + return Ok(new List()); + } - foreach (string lastChangedBy in lastChangedByValues) - { - if (lastChangedBy?.Length == 9) - { - Organization? organization = await _orgClient.GetOrganization(lastChangedBy); - if (organization is not null && !string.IsNullOrEmpty(organization.Name)) - { - userAndOrgLookup.Add(lastChangedBy, organization.Name); - } - } - else if (int.TryParse(lastChangedBy, out int lastChangedByInt)) - { - UserProfile? user = await _profileClient.GetUserProfile(lastChangedByInt); - if (user is not null && user.Party is not null && !string.IsNullOrEmpty(user.Party.Name)) - { - userAndOrgLookup.Add(lastChangedBy, user.Party.Name); - } - } - } + var lastChangedByValues = activeInstances.Select(i => i.LastChangedBy).Distinct(); - return Ok(SimpleInstanceMapper.MapInstanceListToSimpleInstanceList(activeInstances, userAndOrgLookup)); - } + Dictionary userAndOrgLookup = new Dictionary(); - private async Task GetInstance(string org, string app, int instanceOwnerPartyId, Guid instanceGuid) + foreach (string lastChangedBy in lastChangedByValues) { - try + if (lastChangedBy?.Length == 9) { - return await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + Organization? organization = await _orgClient.GetOrganization(lastChangedBy); + if (organization is not null && !string.IsNullOrEmpty(organization.Name)) + { + userAndOrgLookup.Add(lastChangedBy, organization.Name); + } } - catch (PlatformHttpException platformHttpException) + else if (int.TryParse(lastChangedBy, out int lastChangedByInt)) { - switch (platformHttpException.Response.StatusCode) + UserProfile? user = await _profileClient.GetUserProfile(lastChangedByInt); + if (user is not null && user.Party is not null && !string.IsNullOrEmpty(user.Party.Name)) { - case HttpStatusCode.Forbidden: // Storage returns 403 for non-existing instances - case HttpStatusCode.NotFound: - return null; - default: - throw; + userAndOrgLookup.Add(lastChangedBy, user.Party.Name); } } } - private void ConditionallySetReadStatus(Instance instance) - { - string? orgClaimValue = User.GetOrg(); + return Ok(SimpleInstanceMapper.MapInstanceListToSimpleInstanceList(activeInstances, userAndOrgLookup)); + } - if (orgClaimValue == instance.Org) + private async Task GetInstance(string org, string app, int instanceOwnerPartyId, Guid instanceGuid) + { + try + { + return await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + } + catch (PlatformHttpException platformHttpException) + { + switch (platformHttpException.Response.StatusCode) { - // Default value for ReadStatus is "not read" - return; + case HttpStatusCode.Forbidden: // Storage returns 403 for non-existing instances + case HttpStatusCode.NotFound: + return null; + default: + throw; } - - instance.Status ??= new InstanceStatus(); - instance.Status.ReadStatus = ReadStatus.Read; } + } - private async Task CopyDataFromSourceInstance( - ApplicationMetadata application, - Instance targetInstance, - Instance sourceInstance - ) + private void ConditionallySetReadStatus(Instance instance) + { + string? orgClaimValue = User.GetOrg(); + + if (orgClaimValue == instance.Org) { - string org = application.Org; - string app = application.AppIdentifier.App; - int instanceOwnerPartyId = int.Parse(targetInstance.InstanceOwner.PartyId); + // Default value for ReadStatus is "not read" + return; + } - string[] sourceSplit = sourceInstance.Id.Split("/"); - Guid sourceInstanceGuid = Guid.Parse(sourceSplit[1]); + instance.Status ??= new InstanceStatus(); + instance.Status.ReadStatus = ReadStatus.Read; + } + + private async Task CopyDataFromSourceInstance( + ApplicationMetadata application, + Instance targetInstance, + Instance sourceInstance + ) + { + string org = application.Org; + string app = application.AppIdentifier.App; + int instanceOwnerPartyId = int.Parse(targetInstance.InstanceOwner.PartyId); + + string[] sourceSplit = sourceInstance.Id.Split("/"); + Guid sourceInstanceGuid = Guid.Parse(sourceSplit[1]); - List dts = application - .DataTypes.Where(dt => dt.AppLogic?.ClassRef != null) - .Where(dt => dt.TaskId != null && dt.TaskId.Equals(targetInstance.Process.CurrentTask.ElementId)) - .ToList(); - List excludedDataTypes = application.CopyInstanceSettings.ExcludedDataTypes; + List dts = application + .DataTypes.Where(dt => dt.AppLogic?.ClassRef != null) + .Where(dt => dt.TaskId != null && dt.TaskId.Equals(targetInstance.Process.CurrentTask.ElementId)) + .ToList(); + List excludedDataTypes = application.CopyInstanceSettings.ExcludedDataTypes; + + foreach (DataElement de in sourceInstance.Data) + { + if (excludedDataTypes != null && excludedDataTypes.Contains(de.DataType)) + { + continue; + } - foreach (DataElement de in sourceInstance.Data) + if (dts.Any(dts => dts.Id.Equals(de.DataType))) { - if (excludedDataTypes != null && excludedDataTypes.Contains(de.DataType)) + DataType dt = dts.First(dt => dt.Id.Equals(de.DataType)); + + Type type; + try { - continue; + type = _appModel.GetModelType(dt.AppLogic.ClassRef); } - - if (dts.Any(dts => dts.Id.Equals(de.DataType))) + catch (Exception altinnAppException) { - DataType dt = dts.First(dt => dt.Id.Equals(de.DataType)); - - Type type; - try - { - type = _appModel.GetModelType(dt.AppLogic.ClassRef); - } - catch (Exception altinnAppException) - { - throw new ServiceException( - HttpStatusCode.InternalServerError, - $"App.GetAppModelType failed: {altinnAppException.Message}", - altinnAppException - ); - } - - object data = await _dataClient.GetFormData( - sourceInstanceGuid, - type, - org, - app, - instanceOwnerPartyId, - Guid.Parse(de.Id) + throw new ServiceException( + HttpStatusCode.InternalServerError, + $"App.GetAppModelType failed: {altinnAppException.Message}", + altinnAppException ); + } - if (application.CopyInstanceSettings.ExcludedDataFields != null) - { - DataHelper.ResetDataFields(application.CopyInstanceSettings.ExcludedDataFields, data); - } + object data = await _dataClient.GetFormData( + sourceInstanceGuid, + type, + org, + app, + instanceOwnerPartyId, + Guid.Parse(de.Id) + ); - await _prefillService.PrefillDataModel(instanceOwnerPartyId.ToString(), dt.Id, data); + if (application.CopyInstanceSettings.ExcludedDataFields != null) + { + DataHelper.ResetDataFields(application.CopyInstanceSettings.ExcludedDataFields, data); + } - await _instantiationProcessor.DataCreation(targetInstance, data, null); + await _prefillService.PrefillDataModel(instanceOwnerPartyId.ToString(), dt.Id, data); - ObjectUtils.InitializeAltinnRowId(data); + await _instantiationProcessor.DataCreation(targetInstance, data, null); - await _dataClient.InsertFormData( - data, - Guid.Parse(targetInstance.Id.Split("/")[1]), - type, - org, - app, - instanceOwnerPartyId, - dt.Id - ); + ObjectUtils.InitializeAltinnRowId(data); - await UpdatePresentationTextsOnInstance( - application.PresentationFields, - targetInstance, - dt.Id, - data - ); - await UpdateDataValuesOnInstance(application.DataFields, targetInstance, dt.Id, data); - } + await _dataClient.InsertFormData( + data, + Guid.Parse(targetInstance.Id.Split("/")[1]), + type, + org, + app, + instanceOwnerPartyId, + dt.Id + ); + + await UpdatePresentationTextsOnInstance(application.PresentationFields, targetInstance, dt.Id, data); + await UpdateDataValuesOnInstance(application.DataFields, targetInstance, dt.Id, data); } } + } - private ActionResult ExceptionResponse(Exception exception, string message) - { - _logger.LogError(exception, message); - - if (exception is PlatformHttpException platformHttpException) - { - return platformHttpException.Response.StatusCode switch - { - HttpStatusCode.Forbidden => Forbid(), - HttpStatusCode.NotFound => NotFound(), - HttpStatusCode.Conflict => Conflict(), - _ => StatusCode((int)platformHttpException.Response.StatusCode, platformHttpException.Message), - }; - } + private ActionResult ExceptionResponse(Exception exception, string message) + { + _logger.LogError(exception, message); - if (exception is ServiceException se) + if (exception is PlatformHttpException platformHttpException) + { + return platformHttpException.Response.StatusCode switch { - return StatusCode((int)se.StatusCode, se.Message); - } + HttpStatusCode.Forbidden => Forbid(), + HttpStatusCode.NotFound => NotFound(), + HttpStatusCode.Conflict => Conflict(), + _ => StatusCode((int)platformHttpException.Response.StatusCode, platformHttpException.Message), + }; + } - return StatusCode(500, $"{message}"); + if (exception is ServiceException se) + { + return StatusCode((int)se.StatusCode, se.Message); } - private async Task AuthorizeAction( - string org, - string app, - int partyId, - Guid? instanceGuid, - string action - ) + return StatusCode(500, $"{message}"); + } + + private async Task AuthorizeAction( + string org, + string app, + int partyId, + Guid? instanceGuid, + string action + ) + { + EnforcementResult enforcementResult = new EnforcementResult(); + XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest( + org, + app, + HttpContext.User, + action, + partyId, + instanceGuid + ); + XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + + if (response?.Response == null) { - EnforcementResult enforcementResult = new EnforcementResult(); - XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest( - org, - app, - HttpContext.User, + string serializedRequest = JsonConvert.SerializeObject(request); + _logger.LogInformation( + "// Instances Controller // Authorization of action {action} failed with request: {serializedRequest}.", action, - partyId, - instanceGuid + serializedRequest ); - XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + return enforcementResult; + } + + enforcementResult = DecisionHelper.ValidatePdpDecisionDetailed(response.Response, HttpContext.User); + return enforcementResult; + } - if (response?.Response == null) + private async Task LookupParty(InstanceOwner instanceOwner) + { + if (instanceOwner.PartyId != null) + { + try { - string serializedRequest = JsonConvert.SerializeObject(request); - _logger.LogInformation( - "// Instances Controller // Authorization of action {action} failed with request: {serializedRequest}.", - action, - serializedRequest + return await _altinnPartyClientClient.GetParty(int.Parse(instanceOwner.PartyId)); + } + catch (Exception e) when (e is not ServiceException) + { + _logger.LogWarning(e, "Failed to lookup party by partyId: {partyId}", instanceOwner.PartyId); + throw new ServiceException( + HttpStatusCode.BadRequest, + $"Failed to lookup party by partyId: {instanceOwner.PartyId}. The exception was: {e.Message}", + e ); - return enforcementResult; } - - enforcementResult = DecisionHelper.ValidatePdpDecisionDetailed(response.Response, HttpContext.User); - return enforcementResult; } - - private async Task LookupParty(InstanceOwner instanceOwner) + else { - if (instanceOwner.PartyId != null) + string lookupNumber = "personNumber or organisationNumber"; + string personOrOrganisationNumber = instanceOwner.PersonNumber ?? instanceOwner.OrganisationNumber; + try { - try - { - return await _altinnPartyClientClient.GetParty(int.Parse(instanceOwner.PartyId)); - } - catch (Exception e) when (e is not ServiceException) + if (!string.IsNullOrEmpty(instanceOwner.PersonNumber)) { - _logger.LogWarning(e, "Failed to lookup party by partyId: {partyId}", instanceOwner.PartyId); - throw new ServiceException( - HttpStatusCode.BadRequest, - $"Failed to lookup party by partyId: {instanceOwner.PartyId}. The exception was: {e.Message}", - e + lookupNumber = "personNumber"; + return await _altinnPartyClientClient.LookupParty( + new PartyLookup { Ssn = instanceOwner.PersonNumber } ); } - } - else - { - string lookupNumber = "personNumber or organisationNumber"; - string personOrOrganisationNumber = instanceOwner.PersonNumber ?? instanceOwner.OrganisationNumber; - try + else if (!string.IsNullOrEmpty(instanceOwner.OrganisationNumber)) { - if (!string.IsNullOrEmpty(instanceOwner.PersonNumber)) - { - lookupNumber = "personNumber"; - return await _altinnPartyClientClient.LookupParty( - new PartyLookup { Ssn = instanceOwner.PersonNumber } - ); - } - else if (!string.IsNullOrEmpty(instanceOwner.OrganisationNumber)) - { - lookupNumber = "organisationNumber"; - return await _altinnPartyClientClient.LookupParty( - new PartyLookup { OrgNo = instanceOwner.OrganisationNumber } - ); - } - else - { - throw new ServiceException( - HttpStatusCode.BadRequest, - "Neither personNumber or organisationNumber has value in instanceOwner" - ); - } + lookupNumber = "organisationNumber"; + return await _altinnPartyClientClient.LookupParty( + new PartyLookup { OrgNo = instanceOwner.OrganisationNumber } + ); } - catch (Exception e) + else { - _logger.LogWarning( - e, - "Failed to lookup party by {lookupNumber}: {personOrOrganisationNumber}", - lookupNumber, - personOrOrganisationNumber - ); throw new ServiceException( HttpStatusCode.BadRequest, - $"Failed to lookup party by {lookupNumber}: {personOrOrganisationNumber}. The exception was: {e.Message}", - e + "Neither personNumber or organisationNumber has value in instanceOwner" ); } } + catch (Exception e) + { + _logger.LogWarning( + e, + "Failed to lookup party by {lookupNumber}: {personOrOrganisationNumber}", + lookupNumber, + personOrOrganisationNumber + ); + throw new ServiceException( + HttpStatusCode.BadRequest, + $"Failed to lookup party by {lookupNumber}: {personOrOrganisationNumber}. The exception was: {e.Message}", + e + ); + } } + } - private async Task StorePrefillParts(Instance instance, ApplicationMetadata appInfo, List parts) - { - Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - int instanceOwnerIdAsInt = int.Parse(instance.InstanceOwner.PartyId); - string org = instance.Org; - string app = instance.AppId.Split("/")[1]; + private async Task StorePrefillParts(Instance instance, ApplicationMetadata appInfo, List parts) + { + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + int instanceOwnerIdAsInt = int.Parse(instance.InstanceOwner.PartyId); + string org = instance.Org; + string app = instance.AppId.Split("/")[1]; - foreach (RequestPart part in parts) + foreach (RequestPart part in parts) + { + // NOTE: part.Name is nullable on the type here, but `RequestPartValidator.ValidatePart` which is called + // further up the stack will error out if it actually null, so we just sanity-check here + // and throw if it is null. + // TODO: improve the modelling of this type. + if (part.Name is null) { - // NOTE: part.Name is nullable on the type here, but `RequestPartValidator.ValidatePart` which is called - // further up the stack will error out if it actually null, so we just sanity-check here - // and throw if it is null. - // TODO: improve the modelling of this type. - if (part.Name is null) - { - throw new InvalidOperationException("Unexpected state - part name is null"); - } + throw new InvalidOperationException("Unexpected state - part name is null"); + } + + DataType? dataType = appInfo.DataTypes.Find(d => d.Id == part.Name); - DataType? dataType = appInfo.DataTypes.Find(d => d.Id == part.Name); + DataElement dataElement; + if (dataType?.AppLogic?.ClassRef != null) + { + _logger.LogInformation("Storing part {partName}", part.Name); - DataElement dataElement; - if (dataType?.AppLogic?.ClassRef != null) + Type type; + try { - _logger.LogInformation("Storing part {partName}", part.Name); - - Type type; - try - { - type = _appModel.GetModelType(dataType.AppLogic.ClassRef); - } - catch (Exception altinnAppException) - { - throw new ServiceException( - HttpStatusCode.InternalServerError, - $"App.GetAppModelType failed: {altinnAppException.Message}", - altinnAppException - ); - } - - ModelDeserializer deserializer = new ModelDeserializer(_logger, type); - object? data = await deserializer.DeserializeAsync(part.Stream, part.ContentType); - - if (!string.IsNullOrEmpty(deserializer.Error) || data is null) - { - throw new InvalidOperationException(deserializer.Error); - } - - await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, part.Name, data); - - await _instantiationProcessor.DataCreation(instance, data, null); - - ObjectUtils.InitializeAltinnRowId(data); - - dataElement = await _dataClient.InsertFormData( - data, - instanceGuid, - type, - org, - app, - instanceOwnerIdAsInt, - part.Name - ); + type = _appModel.GetModelType(dataType.AppLogic.ClassRef); } - else + catch (Exception altinnAppException) { - dataElement = await _dataClient.InsertBinaryData( - instance.Id, - part.Name, - part.ContentType, - part.FileName, - part.Stream + throw new ServiceException( + HttpStatusCode.InternalServerError, + $"App.GetAppModelType failed: {altinnAppException.Message}", + altinnAppException ); } - if (dataElement == null) + ModelDeserializer deserializer = new ModelDeserializer(_logger, type); + object? data = await deserializer.DeserializeAsync(part.Stream, part.ContentType); + + if (!string.IsNullOrEmpty(deserializer.Error) || data is null) { - throw new ServiceException( - HttpStatusCode.InternalServerError, - $"Data service did not return a valid instance metadata when attempt to store data element {part.Name}" - ); + throw new InvalidOperationException(deserializer.Error); } - } - } - /// - /// Extracts the instance template from a multipart reader, which contains a number of parts. If the reader contains - /// only one part and it has no name and contentType application/json it is assumed to be an instance template. - /// - /// If found the method removes the part corresponding to the instance template form the parts list. - /// - /// multipart reader object - /// the instance template or null if none is found - private static async Task ExtractInstanceTemplate(MultipartRequestReader reader) - { - Instance? instanceTemplate = null; + await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, part.Name, data); - RequestPart? instancePart = reader.Parts.Find(part => part.Name == "instance"); + await _instantiationProcessor.DataCreation(instance, data, null); - // assume that first part with no name is an instanceTemplate - if ( - instancePart == null - && reader.Parts.Count == 1 - && reader.Parts[0].ContentType.Contains("application/json") - && reader.Parts[0].Name == null - ) + ObjectUtils.InitializeAltinnRowId(data); + + dataElement = await _dataClient.InsertFormData( + data, + instanceGuid, + type, + org, + app, + instanceOwnerIdAsInt, + part.Name + ); + } + else { - instancePart = reader.Parts[0]; + dataElement = await _dataClient.InsertBinaryData( + instance.Id, + part.Name, + part.ContentType, + part.FileName, + part.Stream + ); } - if (instancePart != null) + if (dataElement == null) { - reader.Parts.Remove(instancePart); + throw new ServiceException( + HttpStatusCode.InternalServerError, + $"Data service did not return a valid instance metadata when attempt to store data element {part.Name}" + ); + } + } + } - using StreamReader streamReader = new StreamReader(instancePart.Stream, Encoding.UTF8); - string content = await streamReader.ReadToEndAsync(); + /// + /// Extracts the instance template from a multipart reader, which contains a number of parts. If the reader contains + /// only one part and it has no name and contentType application/json it is assumed to be an instance template. + /// + /// If found the method removes the part corresponding to the instance template form the parts list. + /// + /// multipart reader object + /// the instance template or null if none is found + private static async Task ExtractInstanceTemplate(MultipartRequestReader reader) + { + Instance? instanceTemplate = null; - instanceTemplate = JsonConvert.DeserializeObject(content); - } + RequestPart? instancePart = reader.Parts.Find(part => part.Name == "instance"); - return instanceTemplate; + // assume that first part with no name is an instanceTemplate + if ( + instancePart == null + && reader.Parts.Count == 1 + && reader.Parts[0].ContentType.Contains("application/json") + && reader.Parts[0].Name == null + ) + { + instancePart = reader.Parts[0]; } - private async Task RegisterEvent(string eventType, Instance instance) + if (instancePart != null) { - if (_appSettings.RegisterEventsWithEventsComponent) - { - try - { - await _eventsClient.AddEvent(eventType, instance); - } - catch (Exception exception) - { - _logger.LogWarning(exception, "Exception when sending event with the Events component."); - } - } + reader.Parts.Remove(instancePart); + + using StreamReader streamReader = new StreamReader(instancePart.Stream, Encoding.UTF8); + string content = await streamReader.ReadToEndAsync(); + + instanceTemplate = JsonConvert.DeserializeObject(content); } - private ActionResult Forbidden(EnforcementResult enforcementResult) + return instanceTemplate; + } + + private async Task RegisterEvent(string eventType, Instance instance) + { + if (_appSettings.RegisterEventsWithEventsComponent) { - if (enforcementResult.FailedObligations != null && enforcementResult.FailedObligations.Count > 0) + try { - return StatusCode((int)HttpStatusCode.Forbidden, enforcementResult.FailedObligations); + await _eventsClient.AddEvent(eventType, instance); } + catch (Exception exception) + { + _logger.LogWarning(exception, "Exception when sending event with the Events component."); + } + } + } - return StatusCode((int)HttpStatusCode.Forbidden); + private ActionResult Forbidden(EnforcementResult enforcementResult) + { + if (enforcementResult.FailedObligations != null && enforcementResult.FailedObligations.Count > 0) + { + return StatusCode((int)HttpStatusCode.Forbidden, enforcementResult.FailedObligations); } - private async Task UpdatePresentationTextsOnInstance( - List presentationFields, - Instance instance, - string dataType, - object data - ) + return StatusCode((int)HttpStatusCode.Forbidden); + } + + private async Task UpdatePresentationTextsOnInstance( + List presentationFields, + Instance instance, + string dataType, + object data + ) + { + var updatedValues = DataHelper.GetUpdatedDataValues( + presentationFields, + instance.PresentationTexts, + dataType, + data + ); + + if (updatedValues.Count > 0) { - var updatedValues = DataHelper.GetUpdatedDataValues( - presentationFields, - instance.PresentationTexts, - dataType, - data + await _instanceClient.UpdatePresentationTexts( + int.Parse(instance.Id.Split("/")[0]), + Guid.Parse(instance.Id.Split("/")[1]), + new PresentationTexts { Texts = updatedValues } ); - - if (updatedValues.Count > 0) - { - await _instanceClient.UpdatePresentationTexts( - int.Parse(instance.Id.Split("/")[0]), - Guid.Parse(instance.Id.Split("/")[1]), - new PresentationTexts { Texts = updatedValues } - ); - } } + } - private async Task UpdateDataValuesOnInstance( - List dataFields, - Instance instance, - string dataType, - object data - ) - { - var updatedValues = DataHelper.GetUpdatedDataValues(dataFields, instance.DataValues, dataType, data); + private async Task UpdateDataValuesOnInstance( + List dataFields, + Instance instance, + string dataType, + object data + ) + { + var updatedValues = DataHelper.GetUpdatedDataValues(dataFields, instance.DataValues, dataType, data); - if (updatedValues.Count > 0) - { - await _instanceClient.UpdateDataValues( - int.Parse(instance.Id.Split("/")[0]), - Guid.Parse(instance.Id.Split("/")[1]), - new DataValues { Values = updatedValues } - ); - } + if (updatedValues.Count > 0) + { + await _instanceClient.UpdateDataValues( + int.Parse(instance.Id.Split("/")[0]), + Guid.Parse(instance.Id.Split("/")[1]), + new DataValues { Values = updatedValues } + ); } } } diff --git a/src/Altinn.App.Api/Controllers/OptionsController.cs b/src/Altinn.App.Api/Controllers/OptionsController.cs index 8f0a743e9..383fe63ca 100644 --- a/src/Altinn.App.Api/Controllers/OptionsController.cs +++ b/src/Altinn.App.Api/Controllers/OptionsController.cs @@ -4,102 +4,101 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Represents the Options API. +/// +[Route("{org}/{app}/api/options")] +[ApiController] +public class OptionsController : ControllerBase { + private readonly IAppOptionsService _appOptionsService; + /// - /// Represents the Options API. + /// Initializes a new instance of the class. /// - [Route("{org}/{app}/api/options")] - [ApiController] - public class OptionsController : ControllerBase + /// Service for handling app options + public OptionsController(IAppOptionsService appOptionsService) { - private readonly IAppOptionsService _appOptionsService; + _appOptionsService = appOptionsService; + } - /// - /// Initializes a new instance of the class. - /// - /// Service for handling app options - public OptionsController(IAppOptionsService appOptionsService) + /// + /// Api that exposes app related options + /// + /// The optionsId + /// Query parameters supplied + /// The language selected by the user. + /// The options list + [HttpGet("{optionsId}")] + public async Task Get( + [FromRoute] string optionsId, + [FromQuery] Dictionary queryParams, + [FromQuery] string? language = null + ) + { + AppOptions appOptions = await _appOptionsService.GetOptionsAsync(optionsId, language, queryParams); + if (appOptions?.Options == null) { - _appOptionsService = appOptionsService; + return NotFound(); } - /// - /// Api that exposes app related options - /// - /// The optionsId - /// Query parameters supplied - /// The language selected by the user. - /// The options list - [HttpGet("{optionsId}")] - public async Task Get( - [FromRoute] string optionsId, - [FromQuery] Dictionary queryParams, - [FromQuery] string? language = null - ) - { - AppOptions appOptions = await _appOptionsService.GetOptionsAsync(optionsId, language, queryParams); - if (appOptions?.Options == null) - { - return NotFound(); - } - - HttpContext.Response.Headers.Append( - "Altinn-DownstreamParameters", - appOptions.Parameters.ToUrlEncodedNameValueString(',') - ); + HttpContext.Response.Headers.Append( + "Altinn-DownstreamParameters", + appOptions.Parameters.ToUrlEncodedNameValueString(',') + ); - return Ok(appOptions.Options); - } + return Ok(appOptions.Options); + } - /// - /// Exposes options related to the app and logged in user - /// - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// The optionsId - /// The language selected by the user. - /// Query parameteres supplied - /// A representing the result of the asynchronous operation. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [Authorize(Policy = "InstanceRead")] - [Route("/{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/options/{optionsId}")] - public async Task Get( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromRoute] string optionsId, - [FromQuery] string? language, - [FromQuery] Dictionary queryParams - ) - { - var instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId, instanceGuid); + /// + /// Exposes options related to the app and logged in user + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// The optionsId + /// The language selected by the user. + /// Query parameteres supplied + /// A representing the result of the asynchronous operation. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = "InstanceRead")] + [Route("/{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/options/{optionsId}")] + public async Task Get( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] string optionsId, + [FromQuery] string? language, + [FromQuery] Dictionary queryParams + ) + { + var instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId, instanceGuid); - AppOptions appOptions = await _appOptionsService.GetOptionsAsync( - instanceIdentifier, - optionsId, - language ?? "nb", - queryParams - ); + AppOptions appOptions = await _appOptionsService.GetOptionsAsync( + instanceIdentifier, + optionsId, + language ?? "nb", + queryParams + ); - // Only return NotFound if we can't find an options provider. - // If we find the options provider, but it doesnt' have values, return empty list. - if (appOptions.Options == null) - { - return NotFound(); - } + // Only return NotFound if we can't find an options provider. + // If we find the options provider, but it doesnt' have values, return empty list. + if (appOptions.Options == null) + { + return NotFound(); + } - HttpContext.Response.Headers.Append( - "Altinn-DownstreamParameters", - appOptions.Parameters.ToUrlEncodedNameValueString(',') - ); + HttpContext.Response.Headers.Append( + "Altinn-DownstreamParameters", + appOptions.Parameters.ToUrlEncodedNameValueString(',') + ); - return Ok(appOptions.Options); - } + return Ok(appOptions.Options); } } diff --git a/src/Altinn.App.Api/Controllers/PagesController.cs b/src/Altinn.App.Api/Controllers/PagesController.cs index f4a0a3c01..ca0ecb451 100644 --- a/src/Altinn.App.Api/Controllers/PagesController.cs +++ b/src/Altinn.App.Api/Controllers/PagesController.cs @@ -7,74 +7,73 @@ using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Handles page related operations +/// +[Authorize] +[ApiController] +[Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/pages")] +[Obsolete("IPageOrder does not work with frontend version 4")] +public class PagesController : ControllerBase { + private readonly IAppModel _appModel; + private readonly IAppResources _resources; + private readonly IPageOrder _pageOrder; + private readonly ILogger _logger; + /// - /// Handles page related operations + /// Initializes a new instance of the class. /// - [Authorize] - [ApiController] - [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/pages")] - [Obsolete("IPageOrder does not work with frontend version 4")] - public class PagesController : ControllerBase + /// The current App Core used to interface with custom logic + /// The app resource service + /// A logger provided by the logging framework. + /// The page order service + public PagesController( + IAppModel appModel, + IAppResources resources, + IPageOrder pageOrder, + ILogger logger + ) { - private readonly IAppModel _appModel; - private readonly IAppResources _resources; - private readonly IPageOrder _pageOrder; - private readonly ILogger _logger; + _appModel = appModel; + _resources = resources; + _pageOrder = pageOrder; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// The current App Core used to interface with custom logic - /// The app resource service - /// A logger provided by the logging framework. - /// The page order service - public PagesController( - IAppModel appModel, - IAppResources resources, - IPageOrder pageOrder, - ILogger logger - ) + /// + /// Get the page order based on the current state of the instance + /// + /// The pages sorted in the correct order + [HttpPost("order")] + public async Task>> GetPageOrder( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromQuery] string layoutSetId, + [FromQuery] string currentPage, + [FromQuery] string dataTypeId, + [FromBody] dynamic formData + ) + { + if (string.IsNullOrEmpty(dataTypeId)) { - _appModel = appModel; - _resources = resources; - _pageOrder = pageOrder; - _logger = logger; + return BadRequest("Query parameter `dataTypeId` must be defined"); } - /// - /// Get the page order based on the current state of the instance - /// - /// The pages sorted in the correct order - [HttpPost("order")] - public async Task>> GetPageOrder( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromQuery] string layoutSetId, - [FromQuery] string currentPage, - [FromQuery] string dataTypeId, - [FromBody] dynamic formData - ) - { - if (string.IsNullOrEmpty(dataTypeId)) - { - return BadRequest("Query parameter `dataTypeId` must be defined"); - } - - string classRef = _resources.GetClassRefForLogicDataType(dataTypeId); + string classRef = _resources.GetClassRefForLogicDataType(dataTypeId); - object data = JsonConvert.DeserializeObject(formData.ToString(), _appModel.GetModelType(classRef)); - return await _pageOrder.GetPageOrder( - new AppIdentifier(org, app), - new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), - layoutSetId, - currentPage, - dataTypeId, - data - ); - } + object data = JsonConvert.DeserializeObject(formData.ToString(), _appModel.GetModelType(classRef)); + return await _pageOrder.GetPageOrder( + new AppIdentifier(org, app), + new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), + layoutSetId, + currentPage, + dataTypeId, + data + ); } } diff --git a/src/Altinn.App.Api/Controllers/PartiesController.cs b/src/Altinn.App.Api/Controllers/PartiesController.cs index d221d0e1a..39ce416bb 100644 --- a/src/Altinn.App.Api/Controllers/PartiesController.cs +++ b/src/Altinn.App.Api/Controllers/PartiesController.cs @@ -13,131 +13,101 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Handles party related operations +/// +[Authorize] +[ApiController] +public class PartiesController : ControllerBase { + private readonly IAuthorizationClient _authorizationClient; + private readonly UserHelper _userHelper; + private readonly IProfileClient _profileClient; + private readonly GeneralSettings _settings; + private readonly IAppMetadata _appMetadata; + /// - /// Handles party related operations + /// Initializes a new instance of the class /// - [Authorize] - [ApiController] - public class PartiesController : ControllerBase + public PartiesController( + IAuthorizationClient authorizationClient, + IProfileClient profileClient, + IAltinnPartyClient altinnPartyClientClient, + IOptions settings, + IAppMetadata appMetadata + ) { - private readonly IAuthorizationClient _authorizationClient; - private readonly UserHelper _userHelper; - private readonly IProfileClient _profileClient; - private readonly GeneralSettings _settings; - private readonly IAppMetadata _appMetadata; - - /// - /// Initializes a new instance of the class - /// - public PartiesController( - IAuthorizationClient authorizationClient, - IProfileClient profileClient, - IAltinnPartyClient altinnPartyClientClient, - IOptions settings, - IAppMetadata appMetadata - ) - { - _authorizationClient = authorizationClient; - _userHelper = new UserHelper(profileClient, altinnPartyClientClient, settings); - _profileClient = profileClient; - _settings = settings.Value; - _appMetadata = appMetadata; - } - - /// - /// Gets the list of parties the user can represent - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// when set to true returns parties that are allowed to instantiate - /// parties - [Authorize] - [HttpGet("{org}/{app}/api/v1/parties")] - public async Task Get(string org, string app, bool allowedToInstantiateFilter = false) - { - UserContext userContext = await _userHelper.GetUserContext(HttpContext); - List? partyList = await _authorizationClient.GetPartyList(userContext.UserId); - - if (allowedToInstantiateFilter) - { - Application application = await _appMetadata.GetApplicationMetadata(); - List validParties = InstantiationHelper.FilterPartiesByAllowedPartyTypes( - partyList, - application.PartyTypesAllowed - ); - return Ok(validParties); - } + _authorizationClient = authorizationClient; + _userHelper = new UserHelper(profileClient, altinnPartyClientClient, settings); + _profileClient = profileClient; + _settings = settings.Value; + _appMetadata = appMetadata; + } - return Ok(partyList); - } + /// + /// Gets the list of parties the user can represent + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// when set to true returns parties that are allowed to instantiate + /// parties + [Authorize] + [HttpGet("{org}/{app}/api/v1/parties")] + public async Task Get(string org, string app, bool allowedToInstantiateFilter = false) + { + UserContext userContext = await _userHelper.GetUserContext(HttpContext); + List? partyList = await _authorizationClient.GetPartyList(userContext.UserId); - /// - /// Validates party and profile settings before the end user is allowed to instantiate a new app instance - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The selected partyId - /// A validation status - [Authorize] - [HttpPost("{org}/{app}/api/v1/parties/validateInstantiation")] - public async Task ValidateInstantiation(string org, string app, [FromQuery] int partyId) + if (allowedToInstantiateFilter) { - UserContext userContext = await _userHelper.GetUserContext(HttpContext); - UserProfile? user = await _profileClient.GetUserProfile(userContext.UserId); - if (user is null) - { - return StatusCode(500, "Could not get user profile while validating instantiation"); - } - List? partyList = await _authorizationClient.GetPartyList(userContext.UserId); Application application = await _appMetadata.GetApplicationMetadata(); + List validParties = InstantiationHelper.FilterPartiesByAllowedPartyTypes( + partyList, + application.PartyTypesAllowed + ); + return Ok(validParties); + } - PartyTypesAllowed partyTypesAllowed = application.PartyTypesAllowed; - Party? partyUserRepresents = null; - - // Check if the user can represent the supplied partyId - if (partyId != user.PartyId) - { - Party? represents = InstantiationHelper.GetPartyByPartyId(partyList, partyId); - if (represents == null) - { - // the user does not represent the chosen party id, is not allowed to initiate - return Ok( - new InstantiationValidationResult - { - Valid = false, - Message = "The user does not represent the supplied party", - ValidParties = InstantiationHelper.FilterPartiesByAllowedPartyTypes( - partyList, - partyTypesAllowed - ) - } - ); - } - - partyUserRepresents = represents; - } + return Ok(partyList); + } - if (partyUserRepresents == null) - { - // if not set, the user represents itself - partyUserRepresents = user.Party; - } + /// + /// Validates party and profile settings before the end user is allowed to instantiate a new app instance + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The selected partyId + /// A validation status + [Authorize] + [HttpPost("{org}/{app}/api/v1/parties/validateInstantiation")] + public async Task ValidateInstantiation(string org, string app, [FromQuery] int partyId) + { + UserContext userContext = await _userHelper.GetUserContext(HttpContext); + UserProfile? user = await _profileClient.GetUserProfile(userContext.UserId); + if (user is null) + { + return StatusCode(500, "Could not get user profile while validating instantiation"); + } + List? partyList = await _authorizationClient.GetPartyList(userContext.UserId); + Application application = await _appMetadata.GetApplicationMetadata(); - // Check if the application can be initiated with the party chosen - bool canInstantiate = InstantiationHelper.IsPartyAllowedToInstantiate( - partyUserRepresents, - partyTypesAllowed - ); + PartyTypesAllowed partyTypesAllowed = application.PartyTypesAllowed; + Party? partyUserRepresents = null; - if (!canInstantiate) + // Check if the user can represent the supplied partyId + if (partyId != user.PartyId) + { + Party? represents = InstantiationHelper.GetPartyByPartyId(partyList, partyId); + if (represents == null) { + // the user does not represent the chosen party id, is not allowed to initiate return Ok( new InstantiationValidationResult { Valid = false, - Message = "The supplied party is not allowed to instantiate the application", + Message = "The user does not represent the supplied party", ValidParties = InstantiationHelper.FilterPartiesByAllowedPartyTypes( partyList, partyTypesAllowed @@ -146,38 +116,61 @@ public async Task ValidateInstantiation(string org, string app, [ ); } - return Ok(new InstantiationValidationResult { Valid = true, }); + partyUserRepresents = represents; } - /// - /// Updates the party the user represents - /// - /// Status code - [Authorize] - [HttpPut("{org}/{app}/api/v1/parties/{partyId}")] - public async Task UpdateSelectedParty(int partyId) + if (partyUserRepresents == null) { - UserContext userContext = await _userHelper.GetUserContext(HttpContext); - int userId = userContext.UserId; - - bool? isValid = await _authorizationClient.ValidateSelectedParty(userId, partyId); + // if not set, the user represents itself + partyUserRepresents = user.Party; + } - if (!isValid.HasValue) - { - return StatusCode(500, "Something went wrong when trying to update selectedparty."); - } - else if (isValid.Value == false) - { - return BadRequest($"User {userId} cannot represent party {partyId}."); - } + // Check if the application can be initiated with the party chosen + bool canInstantiate = InstantiationHelper.IsPartyAllowedToInstantiate(partyUserRepresents, partyTypesAllowed); - Response.Cookies.Append( - _settings.GetAltinnPartyCookieName, - partyId.ToString(), - new CookieOptions { Domain = _settings.HostName } + if (!canInstantiate) + { + return Ok( + new InstantiationValidationResult + { + Valid = false, + Message = "The supplied party is not allowed to instantiate the application", + ValidParties = InstantiationHelper.FilterPartiesByAllowedPartyTypes(partyList, partyTypesAllowed) + } ); + } + + return Ok(new InstantiationValidationResult { Valid = true, }); + } - return Ok("Party successfully updated"); + /// + /// Updates the party the user represents + /// + /// Status code + [Authorize] + [HttpPut("{org}/{app}/api/v1/parties/{partyId}")] + public async Task UpdateSelectedParty(int partyId) + { + UserContext userContext = await _userHelper.GetUserContext(HttpContext); + int userId = userContext.UserId; + + bool? isValid = await _authorizationClient.ValidateSelectedParty(userId, partyId); + + if (!isValid.HasValue) + { + return StatusCode(500, "Something went wrong when trying to update selectedparty."); } + else if (isValid.Value == false) + { + return BadRequest($"User {userId} cannot represent party {partyId}."); + } + + Response.Cookies.Append( + _settings.GetAltinnPartyCookieName, + partyId.ToString(), + new CookieOptions { Domain = _settings.HostName } + ); + + return Ok("Party successfully updated"); } } diff --git a/src/Altinn.App.Api/Controllers/PdfController.cs b/src/Altinn.App.Api/Controllers/PdfController.cs index 81ea4944a..29434aabb 100644 --- a/src/Altinn.App.Api/Controllers/PdfController.cs +++ b/src/Altinn.App.Api/Controllers/PdfController.cs @@ -10,170 +10,169 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Handles PDF related operations +/// +[Authorize] +[ApiController] +public class PdfController : ControllerBase { + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + private readonly IInstanceClient _instanceClient; +#pragma warning disable CS0618 // Type or member is obsolete + private readonly IPdfFormatter _pdfFormatter; + private readonly IAppResources _resources; + private readonly IAppModel _appModel; + private readonly IDataClient _dataClient; + private readonly IWebHostEnvironment _env; + private readonly IPdfService _pdfService; + /// - /// Handles PDF related operations + /// Initializes a new instance of the class. /// - [Authorize] - [ApiController] - public class PdfController : ControllerBase + /// The instance client + /// The pdf formatter service + /// The app resource service + /// The app model service + /// The data client + /// The environment + /// The PDF service + public PdfController( + IInstanceClient instanceClient, +#pragma warning disable CS0618 // Type or member is obsolete + IPdfFormatter pdfFormatter, + IAppResources resources, + IAppModel appModel, + IDataClient dataClient, + IWebHostEnvironment env, + IPdfService pdfService + ) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + _instanceClient = instanceClient; + _pdfFormatter = pdfFormatter; + _resources = resources; + _appModel = appModel; + _dataClient = dataClient; + _env = env; + _pdfService = pdfService; + } - private readonly IInstanceClient _instanceClient; -#pragma warning disable CS0618 // Type or member is obsolete - private readonly IPdfFormatter _pdfFormatter; - private readonly IAppResources _resources; - private readonly IAppModel _appModel; - private readonly IDataClient _dataClient; - private readonly IWebHostEnvironment _env; - private readonly IPdfService _pdfService; - - /// - /// Initializes a new instance of the class. - /// - /// The instance client - /// The pdf formatter service - /// The app resource service - /// The app model service - /// The data client - /// The environment - /// The PDF service - public PdfController( - IInstanceClient instanceClient, -#pragma warning disable CS0618 // Type or member is obsolete - IPdfFormatter pdfFormatter, - IAppResources resources, - IAppModel appModel, - IDataClient dataClient, - IWebHostEnvironment env, - IPdfService pdfService - ) + /// + /// Generate a preview of the PDF for the current task + /// + [ApiExplorerSettings(IgnoreApi = true)] + [HttpGet("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/pdf/preview")] + public async Task GetPdfPreview( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid + ) + { + if (_env.IsProduction()) + { + return NotFound(); + } + + var instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + string? taskId = instance.Process?.CurrentTask?.ElementId; + if (instance == null || taskId == null) + { + return NotFound("Did not find instance or task"); + } + + Stream pdfContent = await _pdfService.GeneratePdf(instance, taskId, CancellationToken.None); + return new FileStreamResult(pdfContent, "application/pdf"); + } + + /// + /// Get the pdf formatting + /// + /// The lists of pages/components to exclude from PDF + [HttpGet("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data/{dataGuid}/pdf/format")] + public async Task GetPdfFormat( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] Guid dataGuid + ) + { + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + if (instance == null) { - _instanceClient = instanceClient; - _pdfFormatter = pdfFormatter; - _resources = resources; - _appModel = appModel; - _dataClient = dataClient; - _env = env; - _pdfService = pdfService; + return NotFound("Did not find instance"); } - /// - /// Generate a preview of the PDF for the current task - /// - [ApiExplorerSettings(IgnoreApi = true)] - [HttpGet("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/pdf/preview")] - public async Task GetPdfPreview( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid - ) + string? taskId = instance.Process?.CurrentTask?.ElementId; + if (taskId == null) { - if (_env.IsProduction()) - { - return NotFound(); - } - - var instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - string? taskId = instance.Process?.CurrentTask?.ElementId; - if (instance == null || taskId == null) - { - return NotFound("Did not find instance or task"); - } - - Stream pdfContent = await _pdfService.GeneratePdf(instance, taskId, CancellationToken.None); - return new FileStreamResult(pdfContent, "application/pdf"); + return Conflict("Instance does not have a valid currentTask"); } - /// - /// Get the pdf formatting - /// - /// The lists of pages/components to exclude from PDF - [HttpGet("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data/{dataGuid}/pdf/format")] - public async Task GetPdfFormat( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromRoute] Guid dataGuid - ) + DataElement? dataElement = instance.Data.FirstOrDefault(d => d.Id == dataGuid.ToString()); + if (dataElement == null) { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (instance == null) - { - return NotFound("Did not find instance"); - } - - string? taskId = instance.Process?.CurrentTask?.ElementId; - if (taskId == null) - { - return Conflict("Instance does not have a valid currentTask"); - } - - DataElement? dataElement = instance.Data.FirstOrDefault(d => d.Id == dataGuid.ToString()); - if (dataElement == null) - { - return NotFound("Did not find data element"); - } - - string appModelclassRef = _resources.GetClassRefForLogicDataType(dataElement.DataType); - Type dataType = _appModel.GetModelType(appModelclassRef); - - string layoutSetsString = _resources.GetLayoutSets(); - LayoutSets? layoutSets = null; - LayoutSet? layoutSet = null; - if (!string.IsNullOrEmpty(layoutSetsString)) - { - layoutSets = - JsonSerializer.Deserialize(layoutSetsString, _jsonSerializerOptions) - ?? throw new JsonException("Could not deserialize LayoutSets"); - layoutSet = layoutSets.Sets?.FirstOrDefault(t => - t.DataType.Equals(dataElement.DataType) && t.Tasks.Contains(taskId) - ); - } - - string? layoutSettingsFileContent = - layoutSet == null - ? _resources.GetLayoutSettingsString() - : _resources.GetLayoutSettingsStringForSet(layoutSet.Id); - - LayoutSettings? layoutSettings = null; - if (!string.IsNullOrEmpty(layoutSettingsFileContent)) - { - layoutSettings = JsonSerializer.Deserialize( - layoutSettingsFileContent, - _jsonSerializerOptions - ); - } - - // Ensure layoutsettings are initialized in FormatPdf - layoutSettings ??= new(); - layoutSettings.Pages ??= new(); - layoutSettings.Pages.ExcludeFromPdf ??= new(); - layoutSettings.Components ??= new(); - layoutSettings.Components.ExcludeFromPdf ??= new(); - - object data = await _dataClient.GetFormData( - instanceGuid, - dataType, - org, - app, - instanceOwnerPartyId, - new Guid(dataElement.Id) + return NotFound("Did not find data element"); + } + + string appModelclassRef = _resources.GetClassRefForLogicDataType(dataElement.DataType); + Type dataType = _appModel.GetModelType(appModelclassRef); + + string layoutSetsString = _resources.GetLayoutSets(); + LayoutSets? layoutSets = null; + LayoutSet? layoutSet = null; + if (!string.IsNullOrEmpty(layoutSetsString)) + { + layoutSets = + JsonSerializer.Deserialize(layoutSetsString, _jsonSerializerOptions) + ?? throw new JsonException("Could not deserialize LayoutSets"); + layoutSet = layoutSets.Sets?.FirstOrDefault(t => + t.DataType.Equals(dataElement.DataType) && t.Tasks.Contains(taskId) ); + } - layoutSettings = await _pdfFormatter.FormatPdf(layoutSettings, data, instance, layoutSet); + string? layoutSettingsFileContent = + layoutSet == null + ? _resources.GetLayoutSettingsString() + : _resources.GetLayoutSettingsStringForSet(layoutSet.Id); - var result = new - { - ExcludedPages = layoutSettings?.Pages?.ExcludeFromPdf ?? new List(), - ExcludedComponents = layoutSettings?.Components?.ExcludeFromPdf ?? new List(), - }; - return Ok(result); + LayoutSettings? layoutSettings = null; + if (!string.IsNullOrEmpty(layoutSettingsFileContent)) + { + layoutSettings = JsonSerializer.Deserialize( + layoutSettingsFileContent, + _jsonSerializerOptions + ); } + + // Ensure layoutsettings are initialized in FormatPdf + layoutSettings ??= new(); + layoutSettings.Pages ??= new(); + layoutSettings.Pages.ExcludeFromPdf ??= new(); + layoutSettings.Components ??= new(); + layoutSettings.Components.ExcludeFromPdf ??= new(); + + object data = await _dataClient.GetFormData( + instanceGuid, + dataType, + org, + app, + instanceOwnerPartyId, + new Guid(dataElement.Id) + ); + + layoutSettings = await _pdfFormatter.FormatPdf(layoutSettings, data, instance, layoutSet); + + var result = new + { + ExcludedPages = layoutSettings?.Pages?.ExcludeFromPdf ?? new List(), + ExcludedComponents = layoutSettings?.Components?.ExcludeFromPdf ?? new List(), + }; + return Ok(result); } } diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 190ed80c5..dc46a040f 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -13,721 +13,713 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; using AppProcessState = Altinn.App.Core.Internal.Process.Elements.AppProcessState; using IAuthorizationService = Altinn.App.Core.Internal.Auth.IAuthorizationService; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Controller for setting and moving process flow of an instance. +/// +[Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/process")] +[ApiController] +[Authorize] +[AutoValidateAntiforgeryTokenIfAuthCookie] +public class ProcessController : ControllerBase { + private const int MaxIterationsAllowed = 100; + + private readonly ILogger _logger; + private readonly IInstanceClient _instanceClient; + private readonly IProcessClient _processClient; + private readonly IValidationService _validationService; + private readonly IAuthorizationService _authorization; + private readonly IProcessEngine _processEngine; + private readonly IProcessReader _processReader; + /// - /// Controller for setting and moving process flow of an instance. + /// Initializes a new instance of the /// - [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/process")] - [ApiController] - [Authorize] - [AutoValidateAntiforgeryTokenIfAuthCookie] - public class ProcessController : ControllerBase + public ProcessController( + ILogger logger, + IInstanceClient instanceClient, + IProcessClient processClient, + IValidationService validationService, + IAuthorizationService authorization, + IProcessReader processReader, + IProcessEngine processEngine + ) { - private const int MaxIterationsAllowed = 100; - - private readonly ILogger _logger; - private readonly IInstanceClient _instanceClient; - private readonly IProcessClient _processClient; - private readonly IValidationService _validationService; - private readonly IAuthorizationService _authorization; - private readonly IProcessEngine _processEngine; - private readonly IProcessReader _processReader; - - /// - /// Initializes a new instance of the - /// - public ProcessController( - ILogger logger, - IInstanceClient instanceClient, - IProcessClient processClient, - IValidationService validationService, - IAuthorizationService authorization, - IProcessReader processReader, - IProcessEngine processEngine - ) - { - _logger = logger; - _instanceClient = instanceClient; - _processClient = processClient; - _validationService = validationService; - _authorization = authorization; - _processReader = processReader; - _processEngine = processEngine; - } - - /// - /// Get the process state of an instance. - /// - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// the instance's process state - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [Authorize(Policy = "InstanceRead")] - public async Task> GetProcessState( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid - ) + _logger = logger; + _instanceClient = instanceClient; + _processClient = processClient; + _validationService = validationService; + _authorization = authorization; + _processReader = processReader; + _processEngine = processEngine; + } + + /// + /// Get the process state of an instance. + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// the instance's process state + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = "InstanceRead")] + public async Task> GetProcessState( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid + ) + { + try { - try - { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process); + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process); - return Ok(appProcessState); - } - catch (PlatformHttpException e) - { - return HandlePlatformHttpException( - e, - $"Failed to access process for {instanceOwnerPartyId}/{instanceGuid}" - ); - } - catch (Exception exception) - { - _logger.LogError($"Failed to access process for {instanceOwnerPartyId}/{instanceGuid}"); - return ExceptionResponse( - exception, - $"Failed to access process for {instanceOwnerPartyId}/{instanceGuid}" - ); - } + return Ok(appProcessState); } - - /// - /// Starts the process of an instance. - /// - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// a specific start event id to start the process, must be used if there are more than one start events - /// The process state - [HttpPost("start")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - [Authorize(Policy = "InstanceInstantiate")] - public async Task> StartProcess( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromQuery] string? startEvent = null - ) + catch (PlatformHttpException e) { - Instance? instance = null; - - try - { - instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + return HandlePlatformHttpException( + e, + $"Failed to access process for {instanceOwnerPartyId}/{instanceGuid}" + ); + } + catch (Exception exception) + { + _logger.LogError($"Failed to access process for {instanceOwnerPartyId}/{instanceGuid}"); + return ExceptionResponse(exception, $"Failed to access process for {instanceOwnerPartyId}/{instanceGuid}"); + } + } - var request = new ProcessStartRequest() - { - Instance = instance, - StartEventId = startEvent, - User = User - }; - ProcessChangeResult result = await _processEngine.GenerateProcessStartEvents(request); - if (!result.Success) - { - return Conflict(result.ErrorMessage); - } + /// + /// Starts the process of an instance. + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// a specific start event id to start the process, must be used if there are more than one start events + /// The process state + [HttpPost("start")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [Authorize(Policy = "InstanceInstantiate")] + public async Task> StartProcess( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromQuery] string? startEvent = null + ) + { + Instance? instance = null; - await _processEngine.HandleEventsAndUpdateStorage(instance, null, result.ProcessStateChange?.Events); + try + { + instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - AppProcessState appProcessState = await ConvertAndAuthorizeActions( - instance, - result.ProcessStateChange?.NewProcessState - ); - return Ok(appProcessState); - } - catch (PlatformHttpException e) + var request = new ProcessStartRequest() { - return HandlePlatformHttpException( - e, - $"Unable to start the process for instance {instance?.Id} of {instance?.AppId}" - ); - } - catch (Exception startException) + Instance = instance, + StartEventId = startEvent, + User = User + }; + ProcessChangeResult result = await _processEngine.GenerateProcessStartEvents(request); + if (!result.Success) { - _logger.LogError( - $"Unable to start the process for instance {instance?.Id} of {instance?.AppId}. Due to {startException}" - ); - return ExceptionResponse( - startException, - $"Unable to start the process for instance {instance?.Id} of {instance?.AppId}" - ); + return Conflict(result.ErrorMessage); } - } - /// - /// Gets a list of the next process elements that can be reached from the current process element. - /// If process is not started it returns the possible start events. - /// - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// list of next process element identifiers (tasks or events) - [Authorize(Policy = "InstanceRead")] - [HttpGet("next")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - [Obsolete( - "From v8 of nuget package navigation is done by sending performed action to the next api. Available actions are returned in the GET /process endpoint" - )] - public async Task>> GetNextElements( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid - ) - { - Instance? instance = null; - string? currentTaskId = null; - - try - { - instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + await _processEngine.HandleEventsAndUpdateStorage(instance, null, result.ProcessStateChange?.Events); - if (instance.Process == null) - { - return Ok(_processReader.GetStartEventIds()); - } + AppProcessState appProcessState = await ConvertAndAuthorizeActions( + instance, + result.ProcessStateChange?.NewProcessState + ); + return Ok(appProcessState); + } + catch (PlatformHttpException e) + { + return HandlePlatformHttpException( + e, + $"Unable to start the process for instance {instance?.Id} of {instance?.AppId}" + ); + } + catch (Exception startException) + { + _logger.LogError( + $"Unable to start the process for instance {instance?.Id} of {instance?.AppId}. Due to {startException}" + ); + return ExceptionResponse( + startException, + $"Unable to start the process for instance {instance?.Id} of {instance?.AppId}" + ); + } + } - currentTaskId = instance.Process.CurrentTask?.ElementId; + /// + /// Gets a list of the next process elements that can be reached from the current process element. + /// If process is not started it returns the possible start events. + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// list of next process element identifiers (tasks or events) + [Authorize(Policy = "InstanceRead")] + [HttpGet("next")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [Obsolete( + "From v8 of nuget package navigation is done by sending performed action to the next api. Available actions are returned in the GET /process endpoint" + )] + public async Task>> GetNextElements( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid + ) + { + Instance? instance = null; + string? currentTaskId = null; - if (currentTaskId == null) - { - return Conflict($"Instance does not have valid info about currentTask"); - } + try + { + instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - return Ok(new List()); - } - catch (PlatformHttpException e) - { - return HandlePlatformHttpException( - e, - $"Unable to find next process element for instance {instance?.Id} and current task {currentTaskId}. Exception was {e.Message}. Is the process file OK?" - ); - } - catch (Exception processException) + if (instance.Process == null) { - _logger.LogError( - $"Unable to find next process element for instance {instance?.Id} and current task {currentTaskId}. {processException}" - ); - return ExceptionResponse( - processException, - $"Unable to find next process element for instance {instance?.Id} and current task {currentTaskId}. Exception was {processException.Message}. Is the process file OK?" - ); + return Ok(_processReader.GetStartEventIds()); } - } - private async Task GetValidationProblemDetails( - Instance instance, - string currentTaskId, - string? language - ) - { - var validationIssues = await _validationService.ValidateInstanceAtTask(instance, currentTaskId, language); - var success = validationIssues.TrueForAll(v => v.Severity != ValidationIssueSeverity.Error); + currentTaskId = instance.Process.CurrentTask?.ElementId; - if (!success) + if (currentTaskId == null) { - var errorCount = validationIssues.Count(v => v.Severity == ValidationIssueSeverity.Error); - return new ProblemDetails() - { - Detail = $"{errorCount} validation errors found for task {currentTaskId}", - Status = (int)HttpStatusCode.Conflict, - Title = "Validation failed for task", - Extensions = new Dictionary() { { "validationIssues", validationIssues }, }, - }; + return Conflict($"Instance does not have valid info about currentTask"); } - return null; - } - - /// - /// Change the instance's process state to next process element in accordance with process definition. - /// - /// new process state - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// obsolete: alias for action - /// Signal the language to use for pdf generation, error messages... - /// The body of the request containing possible actions to perform before advancing the process - [HttpPut("next")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> NextElement( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromQuery] string? elementId = null, - [FromQuery] string? language = null, - [FromBody] ProcessNext? processNext = null - ) + return Ok(new List()); + } + catch (PlatformHttpException e) { - try - { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - - var currentTaskId = instance.Process.CurrentTask?.ElementId; - - if (currentTaskId is null) - { - return Conflict( - new ProblemDetails() - { - Status = (int)HttpStatusCode.Conflict, - Title = "Process is not started. Use start!", - } - ); - } - - if (instance.Process.Ended.HasValue) - { - return Conflict( - new ProblemDetails() { Status = (int)HttpStatusCode.Conflict, Title = "Process is ended." } - ); - } - - string? altinnTaskType = instance.Process.CurrentTask?.AltinnTaskType; - - if (altinnTaskType == null) - { - return Conflict( - new ProblemDetails() - { - Status = (int)HttpStatusCode.Conflict, - Title = "Instance does not have current altinn task type information!", - } - ); - } + return HandlePlatformHttpException( + e, + $"Unable to find next process element for instance {instance?.Id} and current task {currentTaskId}. Exception was {e.Message}. Is the process file OK?" + ); + } + catch (Exception processException) + { + _logger.LogError( + $"Unable to find next process element for instance {instance?.Id} and current task {currentTaskId}. {processException}" + ); + return ExceptionResponse( + processException, + $"Unable to find next process element for instance {instance?.Id} and current task {currentTaskId}. Exception was {processException.Message}. Is the process file OK?" + ); + } + } - string? checkedAction = EnsureActionNotTaskType(processNext?.Action ?? altinnTaskType); - bool authorized = await AuthorizeAction( - checkedAction, - org, - app, - instanceOwnerPartyId, - instanceGuid, - currentTaskId - ); + private async Task GetValidationProblemDetails( + Instance instance, + string currentTaskId, + string? language + ) + { + var validationIssues = await _validationService.ValidateInstanceAtTask(instance, currentTaskId, language); + var success = validationIssues.TrueForAll(v => v.Severity != ValidationIssueSeverity.Error); - if (!authorized) - { - return StatusCode( - 403, - new ProblemDetails() - { - Status = (int)HttpStatusCode.Forbidden, - Detail = - $"User is not authorized to perform action {checkedAction} on task {currentTaskId}", - Title = "Unauthorized", - } - ); - } + if (!success) + { + var errorCount = validationIssues.Count(v => v.Severity == ValidationIssueSeverity.Error); + return new ProblemDetails() + { + Detail = $"{errorCount} validation errors found for task {currentTaskId}", + Status = (int)HttpStatusCode.Conflict, + Title = "Validation failed for task", + Extensions = new Dictionary() { { "validationIssues", validationIssues }, }, + }; + } - _logger.LogDebug("User is authorized to perform action {Action}", checkedAction); - var request = new ProcessNextRequest() - { - Instance = instance, - User = User, - Action = checkedAction - }; - var validationProblem = await GetValidationProblemDetails(instance, currentTaskId, language); - if (validationProblem is not null) - { - return Conflict(validationProblem); - } + return null; + } - var result = await _processEngine.Next(request); - if (!result.Success) - { - return GetResultForError(result); - } + /// + /// Change the instance's process state to next process element in accordance with process definition. + /// + /// new process state + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// obsolete: alias for action + /// Signal the language to use for pdf generation, error messages... + /// The body of the request containing possible actions to perform before advancing the process + [HttpPut("next")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task> NextElement( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromQuery] string? elementId = null, + [FromQuery] string? language = null, + [FromBody] ProcessNext? processNext = null + ) + { + try + { + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - AppProcessState appProcessState = await ConvertAndAuthorizeActions( - instance, - result.ProcessStateChange.NewProcessState - ); + var currentTaskId = instance.Process.CurrentTask?.ElementId; - return Ok(appProcessState); - } - catch (PlatformHttpException e) - { - _logger.LogError("Platform exception when processing next. {message}", e.Message); - return HandlePlatformHttpException(e, "Process next failed."); - } - catch (Exception exception) + if (currentTaskId is null) { - return ExceptionResponse(exception, "Process next failed."); + return Conflict( + new ProblemDetails() + { + Status = (int)HttpStatusCode.Conflict, + Title = "Process is not started. Use start!", + } + ); } - } - private ActionResult GetResultForError(ProcessChangeResult result) - { - switch (result.ErrorType) + if (instance.Process.Ended.HasValue) { - case ProcessErrorType.Conflict: - return Conflict( - new ProblemDetails() - { - Detail = result.ErrorMessage, - Status = (int)HttpStatusCode.Conflict, - Title = "Conflict", - } - ); - case ProcessErrorType.Internal: - return StatusCode( - 500, - new ProblemDetails() - { - Detail = result.ErrorMessage, - Status = (int)HttpStatusCode.InternalServerError, - Title = "Internal server error", - } - ); - case ProcessErrorType.Unauthorized: - return StatusCode( - 403, - new ProblemDetails() - { - Detail = result.ErrorMessage, - Status = (int)HttpStatusCode.Forbidden, - Title = "Unauthorized", - } - ); - default: - return StatusCode( - 500, - new ProblemDetails() - { - Detail = $"Unknown ProcessErrorType {result.ErrorType}", - Status = (int)HttpStatusCode.InternalServerError, - Title = "Internal server error", - } - ); + return Conflict( + new ProblemDetails() { Status = (int)HttpStatusCode.Conflict, Title = "Process is ended." } + ); } - } - /// - /// Attemts to end the process by running next until an end event is reached. - /// Notice that process must have been started. - /// - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// unique id of the party that is the owner of the instance - /// unique id to identify the instance - /// The currently used language by the user (or null if not available) - /// current process status - [HttpPut("completeProcess")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> CompleteProcess( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromQuery] string? language = null - ) - { - Instance instance; + string? altinnTaskType = instance.Process.CurrentTask?.AltinnTaskType; - try - { - instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - } - catch (PlatformHttpException e) - { - return HandlePlatformHttpException(e, "Could not complete process."); - } - - if (instance.Process == null) + if (altinnTaskType == null) { return Conflict( new ProblemDetails() { Status = (int)HttpStatusCode.Conflict, - Title = "Process is not started. Use start!", + Title = "Instance does not have current altinn task type information!", } ); } - else + + string? checkedAction = EnsureActionNotTaskType(processNext?.Action ?? altinnTaskType); + bool authorized = await AuthorizeAction( + checkedAction, + org, + app, + instanceOwnerPartyId, + instanceGuid, + currentTaskId + ); + + if (!authorized) { - if (instance.Process.Ended.HasValue) - { - return Conflict($"Process is ended. It cannot be restarted."); - } + return StatusCode( + 403, + new ProblemDetails() + { + Status = (int)HttpStatusCode.Forbidden, + Detail = $"User is not authorized to perform action {checkedAction} on task {currentTaskId}", + Title = "Unauthorized", + } + ); } - string currentTaskId = instance.Process.CurrentTask?.ElementId ?? instance.Process.StartEvent; + _logger.LogDebug("User is authorized to perform action {Action}", checkedAction); + var request = new ProcessNextRequest() + { + Instance = instance, + User = User, + Action = checkedAction + }; + var validationProblem = await GetValidationProblemDetails(instance, currentTaskId, language); + if (validationProblem is not null) + { + return Conflict(validationProblem); + } - if (currentTaskId == null) + var result = await _processEngine.Next(request); + if (!result.Success) { - return Conflict($"Instance does not have valid currentTask"); + return GetResultForError(result); } - // do next until end event is reached or task cannot be completed. - int counter = 0; + AppProcessState appProcessState = await ConvertAndAuthorizeActions( + instance, + result.ProcessStateChange.NewProcessState + ); - while ( - instance.Process.EndEvent is null - && instance.Process.CurrentTask is not null - && counter++ < MaxIterationsAllowed - ) - { - string altinnTaskType = EnsureActionNotTaskType(instance.Process.CurrentTask.AltinnTaskType); - - bool authorized = await AuthorizeAction( - altinnTaskType, - org, - app, - instanceOwnerPartyId, - instanceGuid, - instance.Process.CurrentTask.ElementId - ); - if (!authorized) - { - return Forbid(); - } + return Ok(appProcessState); + } + catch (PlatformHttpException e) + { + _logger.LogError("Platform exception when processing next. {message}", e.Message); + return HandlePlatformHttpException(e, "Process next failed."); + } + catch (Exception exception) + { + return ExceptionResponse(exception, "Process next failed."); + } + } - var validationProblem = await GetValidationProblemDetails( - instance, - instance.Process.CurrentTask.ElementId, - language + private ActionResult GetResultForError(ProcessChangeResult result) + { + switch (result.ErrorType) + { + case ProcessErrorType.Conflict: + return Conflict( + new ProblemDetails() + { + Detail = result.ErrorMessage, + Status = (int)HttpStatusCode.Conflict, + Title = "Conflict", + } ); - if (validationProblem is not null) - { - return Conflict(validationProblem); - } - - try - { - ProcessNextRequest request = new ProcessNextRequest() + case ProcessErrorType.Internal: + return StatusCode( + 500, + new ProblemDetails() { - Instance = instance, - User = User, - Action = altinnTaskType - }; - var result = await _processEngine.Next(request); - - if (!result.Success) + Detail = result.ErrorMessage, + Status = (int)HttpStatusCode.InternalServerError, + Title = "Internal server error", + } + ); + case ProcessErrorType.Unauthorized: + return StatusCode( + 403, + new ProblemDetails() { - return GetResultForError(result); + Detail = result.ErrorMessage, + Status = (int)HttpStatusCode.Forbidden, + Title = "Unauthorized", } - } - catch (Exception ex) - { - return ExceptionResponse(ex, "Complete process failed."); - } - } - - if (counter >= MaxIterationsAllowed) - { - _logger.LogError( - $"More than {MaxIterationsAllowed} iterations detected in process. Possible loop. Fix app's process definition!" ); + default: return StatusCode( 500, - $"More than {counter} iterations detected in process. Possible loop. Fix app process definition!" + new ProblemDetails() + { + Detail = $"Unknown ProcessErrorType {result.ErrorType}", + Status = (int)HttpStatusCode.InternalServerError, + Title = "Internal server error", + } ); - } + } + } - AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process); - return Ok(appProcessState); + /// + /// Attemts to end the process by running next until an end event is reached. + /// Notice that process must have been started. + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// The currently used language by the user (or null if not available) + /// current process status + [HttpPut("completeProcess")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task> CompleteProcess( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromQuery] string? language = null + ) + { + Instance instance; + + try + { + instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + } + catch (PlatformHttpException e) + { + return HandlePlatformHttpException(e, "Could not complete process."); } - /// - /// Get the process history for an instance. - /// - /// Returns a list of the process events. - [HttpGet("history")] - [Authorize(Policy = "InstanceRead")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task GetProcessHistory( - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid - ) + if (instance.Process == null) { - try + return Conflict( + new ProblemDetails() + { + Status = (int)HttpStatusCode.Conflict, + Title = "Process is not started. Use start!", + } + ); + } + else + { + if (instance.Process.Ended.HasValue) { - return Ok( - await _processClient.GetProcessHistory(instanceGuid.ToString(), instanceOwnerPartyId.ToString()) - ); + return Conflict($"Process is ended. It cannot be restarted."); } - catch (PlatformHttpException e) + } + + string currentTaskId = instance.Process.CurrentTask?.ElementId ?? instance.Process.StartEvent; + + if (currentTaskId == null) + { + return Conflict($"Instance does not have valid currentTask"); + } + + // do next until end event is reached or task cannot be completed. + int counter = 0; + + while ( + instance.Process.EndEvent is null + && instance.Process.CurrentTask is not null + && counter++ < MaxIterationsAllowed + ) + { + string altinnTaskType = EnsureActionNotTaskType(instance.Process.CurrentTask.AltinnTaskType); + + bool authorized = await AuthorizeAction( + altinnTaskType, + org, + app, + instanceOwnerPartyId, + instanceGuid, + instance.Process.CurrentTask.ElementId + ); + if (!authorized) { - return HandlePlatformHttpException( - e, - $"Unable to find retrieve process history for instance {instanceOwnerPartyId}/{instanceGuid}. Exception: {e}" - ); + return Forbid(); } - catch (Exception processException) + + var validationProblem = await GetValidationProblemDetails( + instance, + instance.Process.CurrentTask.ElementId, + language + ); + if (validationProblem is not null) { - _logger.LogError( - $"Unable to find retrieve process history for instance {instanceOwnerPartyId}/{instanceGuid}. Exception: {processException}" - ); - return ExceptionResponse( - processException, - $"Unable to find retrieve process history for instance {instanceOwnerPartyId}/{instanceGuid}. Exception: {processException}" - ); + return Conflict(validationProblem); } - } - private async Task ConvertAndAuthorizeActions(Instance instance, ProcessState? processState) - { - AppProcessState appProcessState = new AppProcessState(processState); - if (appProcessState.CurrentTask?.ElementId != null) + try { - var flowElement = _processReader.GetFlowElement(appProcessState.CurrentTask.ElementId); - if (flowElement is ProcessTask processTask) + ProcessNextRequest request = new ProcessNextRequest() + { + Instance = instance, + User = User, + Action = altinnTaskType + }; + var result = await _processEngine.Next(request); + + if (!result.Success) { - appProcessState.CurrentTask.Actions = new Dictionary(); - List actions = new List() { new("read"), new("write") }; - actions.AddRange( - processTask.ExtensionElements?.TaskExtension?.AltinnActions ?? new List() - ); - var authDecisions = await AuthorizeActions(actions, instance); - appProcessState.CurrentTask.Actions = authDecisions - .Where(a => a.ActionType == ActionType.ProcessAction) - .ToDictionary(a => a.Id, a => a.Authorized); - appProcessState.CurrentTask.HasReadAccess = authDecisions.Single(a => a.Id == "read").Authorized; - appProcessState.CurrentTask.HasWriteAccess = authDecisions.Single(a => a.Id == "write").Authorized; - appProcessState.CurrentTask.UserActions = authDecisions; + return GetResultForError(result); } } - - var processTasks = new List(); - foreach (var processElement in _processReader.GetAllFlowElements().OfType()) + catch (Exception ex) { - processTasks.Add( - new AppProcessTaskTypeInfo - { - ElementId = processElement.Id, - AltinnTaskType = processElement.ExtensionElements?.TaskExtension?.TaskType - } - ); + return ExceptionResponse(ex, "Complete process failed."); } + } - appProcessState.ProcessTasks = processTasks; - - return appProcessState; + if (counter >= MaxIterationsAllowed) + { + _logger.LogError( + $"More than {MaxIterationsAllowed} iterations detected in process. Possible loop. Fix app's process definition!" + ); + return StatusCode( + 500, + $"More than {counter} iterations detected in process. Possible loop. Fix app process definition!" + ); } - private ObjectResult ExceptionResponse(Exception exception, string message) + AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process); + return Ok(appProcessState); + } + + /// + /// Get the process history for an instance. + /// + /// Returns a list of the process events. + [HttpGet("history")] + [Authorize(Policy = "InstanceRead")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetProcessHistory( + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid + ) + { + try + { + return Ok(await _processClient.GetProcessHistory(instanceGuid.ToString(), instanceOwnerPartyId.ToString())); + } + catch (PlatformHttpException e) { - _logger.LogError(exception, message); + return HandlePlatformHttpException( + e, + $"Unable to find retrieve process history for instance {instanceOwnerPartyId}/{instanceGuid}. Exception: {e}" + ); + } + catch (Exception processException) + { + _logger.LogError( + $"Unable to find retrieve process history for instance {instanceOwnerPartyId}/{instanceGuid}. Exception: {processException}" + ); + return ExceptionResponse( + processException, + $"Unable to find retrieve process history for instance {instanceOwnerPartyId}/{instanceGuid}. Exception: {processException}" + ); + } + } - if (exception is PlatformHttpException phe) + private async Task ConvertAndAuthorizeActions(Instance instance, ProcessState? processState) + { + AppProcessState appProcessState = new AppProcessState(processState); + if (appProcessState.CurrentTask?.ElementId != null) + { + var flowElement = _processReader.GetFlowElement(appProcessState.CurrentTask.ElementId); + if (flowElement is ProcessTask processTask) { - return StatusCode( - (int)phe.Response.StatusCode, - new ProblemDetails() - { - Detail = phe.Message, - Status = (int)phe.Response.StatusCode, - Title = message - } + appProcessState.CurrentTask.Actions = new Dictionary(); + List actions = new List() { new("read"), new("write") }; + actions.AddRange( + processTask.ExtensionElements?.TaskExtension?.AltinnActions ?? new List() ); + var authDecisions = await AuthorizeActions(actions, instance); + appProcessState.CurrentTask.Actions = authDecisions + .Where(a => a.ActionType == ActionType.ProcessAction) + .ToDictionary(a => a.Id, a => a.Authorized); + appProcessState.CurrentTask.HasReadAccess = authDecisions.Single(a => a.Id == "read").Authorized; + appProcessState.CurrentTask.HasWriteAccess = authDecisions.Single(a => a.Id == "write").Authorized; + appProcessState.CurrentTask.UserActions = authDecisions; } + } - if (exception is ServiceException se) - { - return StatusCode( - (int)se.StatusCode, - new ProblemDetails() - { - Detail = se.Message, - Status = (int)se.StatusCode, - Title = message - } - ); - } + var processTasks = new List(); + foreach (var processElement in _processReader.GetAllFlowElements().OfType()) + { + processTasks.Add( + new AppProcessTaskTypeInfo + { + ElementId = processElement.Id, + AltinnTaskType = processElement.ExtensionElements?.TaskExtension?.TaskType + } + ); + } + + appProcessState.ProcessTasks = processTasks; + + return appProcessState; + } + private ObjectResult ExceptionResponse(Exception exception, string message) + { + _logger.LogError(exception, message); + + if (exception is PlatformHttpException phe) + { return StatusCode( - 500, + (int)phe.Response.StatusCode, new ProblemDetails() { - Detail = exception.Message, - Status = 500, + Detail = phe.Message, + Status = (int)phe.Response.StatusCode, Title = message } ); } - private async Task AuthorizeAction( - string action, - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - string? taskId = null - ) + if (exception is ServiceException se) { - return await _authorization.AuthorizeAction( - new AppIdentifier(org, app), - new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), - HttpContext.User, - action, - taskId + return StatusCode( + (int)se.StatusCode, + new ProblemDetails() + { + Detail = se.Message, + Status = (int)se.StatusCode, + Title = message + } ); } - private async Task> AuthorizeActions(List actions, Instance instance) + return StatusCode( + 500, + new ProblemDetails() + { + Detail = exception.Message, + Status = 500, + Title = message + } + ); + } + + private async Task AuthorizeAction( + string action, + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + string? taskId = null + ) + { + return await _authorization.AuthorizeAction( + new AppIdentifier(org, app), + new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), + HttpContext.User, + action, + taskId + ); + } + + private async Task> AuthorizeActions(List actions, Instance instance) + { + return await _authorization.AuthorizeActions(instance, HttpContext.User, actions); + } + + private static string EnsureActionNotTaskType(string actionOrTaskType) + { + switch (actionOrTaskType) { - return await _authorization.AuthorizeActions(instance, HttpContext.User, actions); + case "data": + case "feedback": + return "write"; + case "confirmation": + return "confirm"; + default: + // Not any known task type, so assume it is an action type + return actionOrTaskType; } + } - private static string EnsureActionNotTaskType(string actionOrTaskType) + private ActionResult HandlePlatformHttpException(PlatformHttpException e, string defaultMessage) + { + if (e.Response.StatusCode == HttpStatusCode.Forbidden) { - switch (actionOrTaskType) - { - case "data": - case "feedback": - return "write"; - case "confirmation": - return "confirm"; - default: - // Not any known task type, so assume it is an action type - return actionOrTaskType; - } + return Forbid(); } - private ActionResult HandlePlatformHttpException(PlatformHttpException e, string defaultMessage) + if (e.Response.StatusCode == HttpStatusCode.NotFound) { - if (e.Response.StatusCode == HttpStatusCode.Forbidden) - { - return Forbid(); - } - - if (e.Response.StatusCode == HttpStatusCode.NotFound) - { - return NotFound(); - } - - if (e.Response.StatusCode == HttpStatusCode.Conflict) - { - return Conflict(); - } + return NotFound(); + } - return ExceptionResponse(e, defaultMessage); + if (e.Response.StatusCode == HttpStatusCode.Conflict) + { + return Conflict(); } + + return ExceptionResponse(e, defaultMessage); } } diff --git a/src/Altinn.App.Api/Controllers/ProfileController.cs b/src/Altinn.App.Api/Controllers/ProfileController.cs index 7207b61be..8be647500 100644 --- a/src/Altinn.App.Api/Controllers/ProfileController.cs +++ b/src/Altinn.App.Api/Controllers/ProfileController.cs @@ -3,57 +3,56 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Controller that exposes profile +/// +[Authorize] +[Route("{org}/{app}/api/v1/profile")] +[ApiController] +public class ProfileController : Controller { + private readonly IProfileClient _profileClient; + private readonly ILogger _logger; + /// - /// Controller that exposes profile + /// Initializes a new instance of the class /// - [Authorize] - [Route("{org}/{app}/api/v1/profile")] - [ApiController] - public class ProfileController : Controller + public ProfileController(IProfileClient profileClient, ILogger logger) { - private readonly IProfileClient _profileClient; - private readonly ILogger _logger; + _profileClient = profileClient; + _logger = logger; + } - /// - /// Initializes a new instance of the class - /// - public ProfileController(IProfileClient profileClient, ILogger logger) + /// + /// Method that returns the user information about the user that is logged in + /// + [Authorize] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + [HttpGet("user")] + public async Task GetUser() + { + int userId = AuthenticationHelper.GetUserId(HttpContext); + if (userId == 0) { - _profileClient = profileClient; - _logger = logger; + return BadRequest("The userId is not proviced in the context."); } - /// - /// Method that returns the user information about the user that is logged in - /// - [Authorize] - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - [HttpGet("user")] - public async Task GetUser() + try { - int userId = AuthenticationHelper.GetUserId(HttpContext); - if (userId == 0) - { - return BadRequest("The userId is not proviced in the context."); - } - - try - { - var user = await _profileClient.GetUserProfile(userId); - - if (user == null) - { - return NotFound(); - } + var user = await _profileClient.GetUserProfile(userId); - return Ok(user); - } - catch (Exception e) + if (user == null) { - return StatusCode(500, e.Message); + return NotFound(); } + + return Ok(user); + } + catch (Exception e) + { + return StatusCode(500, e.Message); } } } diff --git a/src/Altinn.App.Api/Controllers/RedirectController.cs b/src/Altinn.App.Api/Controllers/RedirectController.cs index 2d154e891..e71dc4bc9 100644 --- a/src/Altinn.App.Api/Controllers/RedirectController.cs +++ b/src/Altinn.App.Api/Controllers/RedirectController.cs @@ -7,80 +7,79 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Options; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Controller for redirect and validation of URL +/// +[AllowAnonymous] +[AutoValidateAntiforgeryTokenIfAuthCookie] +[Route("{org}/{app}/api/v1/redirect")] +[ApiController] +public class RedirectController : ControllerBase { + private readonly GeneralSettings _settings; + /// - /// Controller for redirect and validation of URL + /// Initializes a new instance of the class. /// - [AllowAnonymous] - [AutoValidateAntiforgeryTokenIfAuthCookie] - [Route("{org}/{app}/api/v1/redirect")] - [ApiController] - public class RedirectController : ControllerBase + /// The general settings. + public RedirectController(IOptions settings) { - private readonly GeneralSettings _settings; + _settings = settings.Value; + } - /// - /// Initializes a new instance of the class. - /// - /// The general settings. - public RedirectController(IOptions settings) + /// + /// Validates URL used for redirection + /// + /// Base64 encoded string of the URL to validate + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public ActionResult ValidateUrl([BindRequired, FromQuery] string url) + { + if (string.IsNullOrEmpty(url)) { - _settings = settings.Value; + return BadRequest( + $"Invalid value of query parameter {nameof(url)}. The query parameter {nameof(url)} must not be empty or null." + ); } - /// - /// Validates URL used for redirection - /// - /// Base64 encoded string of the URL to validate - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public ActionResult ValidateUrl([BindRequired, FromQuery] string url) + try { - if (string.IsNullOrEmpty(url)) - { - return BadRequest( - $"Invalid value of query parameter {nameof(url)}. The query parameter {nameof(url)} must not be empty or null." - ); - } + var byteArrayUri = Convert.FromBase64String(url); + var convertedUri = Encoding.UTF8.GetString(byteArrayUri); + Uri uri = new Uri(convertedUri); - try + if (!IsValidRedirectUri(uri.Host)) { - var byteArrayUri = Convert.FromBase64String(url); - var convertedUri = Encoding.UTF8.GetString(byteArrayUri); - Uri uri = new Uri(convertedUri); - - if (!IsValidRedirectUri(uri.Host)) - { - string errorMessage = $"Invalid domain from query parameter {nameof(url)}."; - ModelState.AddModelError(nameof(url), errorMessage); - return ValidationProblem(); - } - - return Ok(convertedUri); + string errorMessage = $"Invalid domain from query parameter {nameof(url)}."; + ModelState.AddModelError(nameof(url), errorMessage); + return ValidationProblem(); } - catch (FormatException) - { - return BadRequest( - $"Invalid format of query parameter {nameof(url)}. The query parameter {nameof(url)} must be a valid base64 encoded string" - ); - } - } - private bool IsValidRedirectUri(string urlHost) + return Ok(convertedUri); + } + catch (FormatException) { - string validHost = _settings.HostName; - int segments = _settings.HostName.Split('.').Length; + return BadRequest( + $"Invalid format of query parameter {nameof(url)}. The query parameter {nameof(url)} must be a valid base64 encoded string" + ); + } + } + + private bool IsValidRedirectUri(string urlHost) + { + string validHost = _settings.HostName; + int segments = _settings.HostName.Split('.').Length; - List goToList = Enumerable - .Reverse(new List(urlHost.Split('.'))) - .Take(segments) - .Reverse() - .ToList(); - string redirectHost = string.Join(".", goToList); + List goToList = Enumerable + .Reverse(new List(urlHost.Split('.'))) + .Take(segments) + .Reverse() + .ToList(); + string redirectHost = string.Join(".", goToList); - return validHost.Equals(redirectHost); - } + return validHost.Equals(redirectHost); } } diff --git a/src/Altinn.App.Api/Controllers/ResourceController.cs b/src/Altinn.App.Api/Controllers/ResourceController.cs index c4e5999a0..fd658059d 100644 --- a/src/Altinn.App.Api/Controllers/ResourceController.cs +++ b/src/Altinn.App.Api/Controllers/ResourceController.cs @@ -1,185 +1,181 @@ -using System.Globalization; -using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Controller to handle resources like css, images, javascript included in an app +/// +public class ResourceController : ControllerBase { + private readonly IAppResources _appResourceService; + /// - /// Controller to handle resources like css, images, javascript included in an app + /// Initializes a new instance of the class /// - public class ResourceController : ControllerBase + /// The execution service + public ResourceController(IAppResources appResourcesService) { - private readonly IAppResources _appResourceService; + _appResourceService = appResourcesService; + } - /// - /// Initializes a new instance of the class - /// - /// The execution service - public ResourceController(IAppResources appResourcesService) - { - _appResourceService = appResourcesService; - } + /// + /// Get the json schema for the model + /// + /// Unique identifier of the model to fetch json schema for. + /// The model json schema. + [HttpGet] + [Route("{org}/{app}/api/jsonschema/{id}")] + public ActionResult GetModelJsonSchema([FromRoute] string id) + { + string schema = _appResourceService.GetModelJsonSchema(id); + return Ok(schema); + } - /// - /// Get the json schema for the model - /// - /// Unique identifier of the model to fetch json schema for. - /// The model json schema. - [HttpGet] - [Route("{org}/{app}/api/jsonschema/{id}")] - public ActionResult GetModelJsonSchema([FromRoute] string id) - { - string schema = _appResourceService.GetModelJsonSchema(id); - return Ok(schema); - } + /// + /// Get the form layout + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// A collection of FormLayout objects in JSON format. + /// + [HttpGet] + [Route("{org}/{app}/api/layouts")] + public ActionResult GetLayouts(string org, string app) + { + string layouts = _appResourceService.GetLayouts(); + return Ok(layouts); + } - /// - /// Get the form layout - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// A collection of FormLayout objects in JSON format. - /// - [HttpGet] - [Route("{org}/{app}/api/layouts")] - public ActionResult GetLayouts(string org, string app) - { - string layouts = _appResourceService.GetLayouts(); - return Ok(layouts); - } + /// + /// Get the form layout + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The layoutset id + /// A collection of FormLayout objects in JSON format. + [HttpGet] + [Route("{org}/{app}/api/layouts/{id}")] + public ActionResult GetLayouts(string org, string app, string id) + { + string layouts = _appResourceService.GetLayoutsForSet(id); + return Ok(layouts); + } - /// - /// Get the form layout - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The layoutset id - /// A collection of FormLayout objects in JSON format. - [HttpGet] - [Route("{org}/{app}/api/layouts/{id}")] - public ActionResult GetLayouts(string org, string app, string id) - { - string layouts = _appResourceService.GetLayoutsForSet(id); - return Ok(layouts); - } + /// + /// Get the layout settings. + /// + /// The application owner short name + /// The application name + /// The settings in the form of a string. + [HttpGet] + [Route("{org}/{app}/api/layoutsettings")] + public ActionResult GetLayoutSettings(string org, string app) + { + string? settings = _appResourceService.GetLayoutSettingsString(); + return Ok(settings); + } - /// - /// Get the layout settings. - /// - /// The application owner short name - /// The application name - /// The settings in the form of a string. - [HttpGet] - [Route("{org}/{app}/api/layoutsettings")] - public ActionResult GetLayoutSettings(string org, string app) - { - string? settings = _appResourceService.GetLayoutSettingsString(); - return Ok(settings); - } + /// + /// Get the layout settings. + /// + /// The application owner short name + /// The application name + /// The layoutset id + /// The settings in the form of a string. + [HttpGet] + [Route("{org}/{app}/api/layoutsettings/{id}")] + public ActionResult GetLayoutSettings(string org, string app, string id) + { + string? settings = _appResourceService.GetLayoutSettingsStringForSet(id); + return Ok(settings); + } - /// - /// Get the layout settings. - /// - /// The application owner short name - /// The application name - /// The layoutset id - /// The settings in the form of a string. - [HttpGet] - [Route("{org}/{app}/api/layoutsettings/{id}")] - public ActionResult GetLayoutSettings(string org, string app, string id) - { - string? settings = _appResourceService.GetLayoutSettingsStringForSet(id); - return Ok(settings); - } + /// + /// Get the layout-sets + /// + /// The application owner short name + /// The application name + /// The settings in the form of a string. + [HttpGet] + [Route("{org}/{app}/api/layoutsets")] + public ActionResult GetLayoutSets(string org, string app) + { + string settings = _appResourceService.GetLayoutSets(); + return Ok(settings); + } - /// - /// Get the layout-sets - /// - /// The application owner short name - /// The application name - /// The settings in the form of a string. - [HttpGet] - [Route("{org}/{app}/api/layoutsets")] - public ActionResult GetLayoutSets(string org, string app) + /// + /// Get the rule settings + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The layoutset id + /// A collection of FormLayout objects in JSON format. + /// + [HttpGet] + [Route("{org}/{app}/api/rulehandler/{id}")] + public ActionResult GetRulehandler(string org, string app, string id) + { + byte[] fileContent = _appResourceService.GetRuleHandlerForSet(id); + if (fileContent != null) { - string settings = _appResourceService.GetLayoutSets(); - return Ok(settings); + return new FileContentResult(fileContent, MimeTypeMap.GetMimeType(".ts").ToString()); } - /// - /// Get the rule settings - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The layoutset id - /// A collection of FormLayout objects in JSON format. - /// - [HttpGet] - [Route("{org}/{app}/api/rulehandler/{id}")] - public ActionResult GetRulehandler(string org, string app, string id) - { - byte[] fileContent = _appResourceService.GetRuleHandlerForSet(id); - if (fileContent != null) - { - return new FileContentResult(fileContent, MimeTypeMap.GetMimeType(".ts").ToString()); - } + return NoContent(); + } + /// + /// Get the ruleconfiguration. + /// + /// The application owner short name + /// The application name + /// The layoutset id + /// The settings in the form of a string. + [HttpGet] + [Route("{org}/{app}/api/ruleconfiguration/{id}")] + public ActionResult GetRuleConfiguration(string org, string app, string id) + { + byte[] fileContent = _appResourceService.GetRuleConfigurationForSet(id); + if (fileContent == null) + { return NoContent(); } - /// - /// Get the ruleconfiguration. - /// - /// The application owner short name - /// The application name - /// The layoutset id - /// The settings in the form of a string. - [HttpGet] - [Route("{org}/{app}/api/ruleconfiguration/{id}")] - public ActionResult GetRuleConfiguration(string org, string app, string id) - { - byte[] fileContent = _appResourceService.GetRuleConfigurationForSet(id); - if (fileContent == null) - { - return NoContent(); - } - - return new FileContentResult(fileContent, MimeTypeMap.GetMimeType(".json").ToString()); - } + return new FileContentResult(fileContent, MimeTypeMap.GetMimeType(".json").ToString()); + } - /// - /// Get the footer layout - /// - /// The application owner short name - /// The application name - /// The footer layout in the form of a string. - [HttpGet] - [Route("{org}/{app}/api/v1/footer")] - public async Task GetFooterLayout(string org, string app) + /// + /// Get the footer layout + /// + /// The application owner short name + /// The application name + /// The footer layout in the form of a string. + [HttpGet] + [Route("{org}/{app}/api/v1/footer")] + public async Task GetFooterLayout(string org, string app) + { + string? layout = await _appResourceService.GetFooter(); + if (layout is null) { - string? layout = await _appResourceService.GetFooter(); - if (layout is null) - { - return NoContent(); - } - - return Ok(layout); + return NoContent(); } - /// - /// Get validation configuration file. - /// - /// The application owner short name - /// The application name - /// Unique identifier of the model to fetch validations for. - /// The validation configuration file as json. - [HttpGet] - [Route("{org}/{app}/api/validationconfig/{id}")] - public ActionResult GetValidationConfiguration(string org, string app, string id) - { - string? validationConfiguration = _appResourceService.GetValidationConfiguration(id); - return Ok(validationConfiguration); - } + return Ok(layout); + } + + /// + /// Get validation configuration file. + /// + /// The application owner short name + /// The application name + /// Unique identifier of the model to fetch validations for. + /// The validation configuration file as json. + [HttpGet] + [Route("{org}/{app}/api/validationconfig/{id}")] + public ActionResult GetValidationConfiguration(string org, string app, string id) + { + string? validationConfiguration = _appResourceService.GetValidationConfiguration(id); + return Ok(validationConfiguration); } } diff --git a/src/Altinn.App.Api/Controllers/StatelessDataController.cs b/src/Altinn.App.Api/Controllers/StatelessDataController.cs index a91d28d98..49df9bd01 100644 --- a/src/Altinn.App.Api/Controllers/StatelessDataController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessDataController.cs @@ -18,375 +18,364 @@ using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// The stateless data controller handles creation and calculation of data elements not related to an instance. +/// +[AutoValidateAntiforgeryTokenIfAuthCookie] +[Route("{org}/{app}/v1/data")] +public class StatelessDataController : ControllerBase { + private readonly ILogger _logger; + private readonly IAppModel _appModel; + private readonly IAppResources _appResourcesService; + private readonly IEnumerable _dataProcessors; + private readonly IPrefill _prefillService; + private readonly IAltinnPartyClient _altinnPartyClientClient; + private readonly IPDP _pdp; + + private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; + + private const string PartyPrefix = "partyid"; + private const string PersonPrefix = "person"; + private const string OrgPrefix = "org"; + /// - /// The stateless data controller handles creation and calculation of data elements not related to an instance. + /// The stateless data controller is responsible for creating and updating stateless data elements. /// - [AutoValidateAntiforgeryTokenIfAuthCookie] - [Route("{org}/{app}/v1/data")] - public class StatelessDataController : ControllerBase + public StatelessDataController( + ILogger logger, + IAppModel appModel, + IAppResources appResourcesService, + IPrefill prefillService, + IAltinnPartyClient altinnPartyClientClient, + IPDP pdp, + IEnumerable dataProcessors + ) { - private readonly ILogger _logger; - private readonly IAppModel _appModel; - private readonly IAppResources _appResourcesService; - private readonly IEnumerable _dataProcessors; - private readonly IPrefill _prefillService; - private readonly IAltinnPartyClient _altinnPartyClientClient; - private readonly IPDP _pdp; - - private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; - - private const string PartyPrefix = "partyid"; - private const string PersonPrefix = "person"; - private const string OrgPrefix = "org"; - - /// - /// The stateless data controller is responsible for creating and updating stateless data elements. - /// - public StatelessDataController( - ILogger logger, - IAppModel appModel, - IAppResources appResourcesService, - IPrefill prefillService, - IAltinnPartyClient altinnPartyClientClient, - IPDP pdp, - IEnumerable dataProcessors - ) - { - _logger = logger; - _appModel = appModel; - _appResourcesService = appResourcesService; - _dataProcessors = dataProcessors; - _prefillService = prefillService; - _altinnPartyClientClient = altinnPartyClientClient; - _pdp = pdp; - } + _logger = logger; + _appModel = appModel; + _appResourcesService = appResourcesService; + _dataProcessors = dataProcessors; + _prefillService = prefillService; + _altinnPartyClientClient = altinnPartyClientClient; + _pdp = pdp; + } - /// - /// Create a new data object of the defined data type - /// - /// unique identfier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// The data type id - /// The party that should be represented with prefix "partyId:", "person:" or "org:" (eg: "partyId:123") - /// Currently selected language by the user (if available) - /// Return a new instance of the data object including prefill and initial calculations - [Authorize] - [HttpGet] - [DisableFormValueModelBinding] - [RequestSizeLimit(REQUEST_SIZE_LIMIT)] - [ProducesResponseType(typeof(DataElement), 200)] - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public async Task Get( - [FromRoute] string org, - [FromRoute] string app, - [FromQuery] string dataType, - [FromHeader(Name = "party")] string partyFromHeader, - [FromQuery] string? language = null - ) + /// + /// Create a new data object of the defined data type + /// + /// unique identfier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// The data type id + /// The party that should be represented with prefix "partyId:", "person:" or "org:" (eg: "partyId:123") + /// Currently selected language by the user (if available) + /// Return a new instance of the data object including prefill and initial calculations + [Authorize] + [HttpGet] + [DisableFormValueModelBinding] + [RequestSizeLimit(REQUEST_SIZE_LIMIT)] + [ProducesResponseType(typeof(DataElement), 200)] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public async Task Get( + [FromRoute] string org, + [FromRoute] string app, + [FromQuery] string dataType, + [FromHeader(Name = "party")] string partyFromHeader, + [FromQuery] string? language = null + ) + { + if (string.IsNullOrEmpty(dataType)) { - if (string.IsNullOrEmpty(dataType)) - { - return BadRequest( - $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." - ); - } - - string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); + return BadRequest( + $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." + ); + } - if (string.IsNullOrEmpty(classRef)) - { - return BadRequest( - $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." - ); - } + string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); - InstanceOwner? owner = await GetInstanceOwner(partyFromHeader); - if (owner is null) - { - return BadRequest( - $"Invalid party header. Please provide a party header on the form partyid:123, org:[orgnr] or person:[ssn]" - ); - } + if (string.IsNullOrEmpty(classRef)) + { + return BadRequest( + $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." + ); + } - EnforcementResult enforcementResult = await AuthorizeAction( - org, - app, - Convert.ToInt32(owner.PartyId), - "read" + InstanceOwner? owner = await GetInstanceOwner(partyFromHeader); + if (owner is null) + { + return BadRequest( + $"Invalid party header. Please provide a party header on the form partyid:123, org:[orgnr] or person:[ssn]" ); + } - if (!enforcementResult.Authorized) - { - return Forbidden(enforcementResult); - } + EnforcementResult enforcementResult = await AuthorizeAction(org, app, Convert.ToInt32(owner.PartyId), "read"); - object appModel = _appModel.Create(classRef); + if (!enforcementResult.Authorized) + { + return Forbidden(enforcementResult); + } - // runs prefill from repo configuration if config exists - await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel); + object appModel = _appModel.Create(classRef); - Instance virtualInstance = new Instance() { InstanceOwner = owner }; - await ProcessAllDataRead(virtualInstance, appModel, language); + // runs prefill from repo configuration if config exists + await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel); - return Ok(appModel); - } + Instance virtualInstance = new Instance() { InstanceOwner = owner }; + await ProcessAllDataRead(virtualInstance, appModel, language); + + return Ok(appModel); + } - private async Task ProcessAllDataRead(Instance virtualInstance, object appModel, string? language) + private async Task ProcessAllDataRead(Instance virtualInstance, object appModel, string? language) + { + foreach (var dataProcessor in _dataProcessors) { - foreach (var dataProcessor in _dataProcessors) - { - _logger.LogInformation( - "ProcessDataRead for {modelType} using {dataProcesor}", - appModel.GetType().Name, - dataProcessor.GetType().Name - ); - await dataProcessor.ProcessDataRead(virtualInstance, null, appModel, language); - } + _logger.LogInformation( + "ProcessDataRead for {modelType} using {dataProcesor}", + appModel.GetType().Name, + dataProcessor.GetType().Name + ); + await dataProcessor.ProcessDataRead(virtualInstance, null, appModel, language); } + } - /// - /// Create a new data object of the defined data type - /// - /// The data type id - /// The language selected by the user. - /// Return a new instance of the data object including prefill and initial calculations - [AllowAnonymous] - [HttpGet] - [DisableFormValueModelBinding] - [RequestSizeLimit(REQUEST_SIZE_LIMIT)] - [ProducesResponseType(typeof(DataElement), 200)] - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - [Route("anonymous")] - public async Task GetAnonymous([FromQuery] string dataType, [FromQuery] string? language = null) + /// + /// Create a new data object of the defined data type + /// + /// The data type id + /// The language selected by the user. + /// Return a new instance of the data object including prefill and initial calculations + [AllowAnonymous] + [HttpGet] + [DisableFormValueModelBinding] + [RequestSizeLimit(REQUEST_SIZE_LIMIT)] + [ProducesResponseType(typeof(DataElement), 200)] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + [Route("anonymous")] + public async Task GetAnonymous([FromQuery] string dataType, [FromQuery] string? language = null) + { + if (string.IsNullOrEmpty(dataType)) { - if (string.IsNullOrEmpty(dataType)) - { - return BadRequest( - $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." - ); - } + return BadRequest( + $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." + ); + } - string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); + string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); - if (string.IsNullOrEmpty(classRef)) - { - return BadRequest( - $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." - ); - } + if (string.IsNullOrEmpty(classRef)) + { + return BadRequest( + $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." + ); + } - object appModel = _appModel.Create(classRef); - var virtualInstance = new Instance(); - await ProcessAllDataRead(virtualInstance, appModel, language); + object appModel = _appModel.Create(classRef); + var virtualInstance = new Instance(); + await ProcessAllDataRead(virtualInstance, appModel, language); - return Ok(appModel); - } + return Ok(appModel); + } - /// - /// Create a new data object of the defined data type - /// - /// unique identifier of the organisation responsible for the app - /// application identifier which is unique within an organisation - /// The data type id - /// The party that should be represented with prefix "partyId:", "person:" or "org:" (eg: "partyId:123") - /// The language selected by the user. - /// Return a new instance of the data object including prefill and initial calculations - [Authorize] - [HttpPost] - [DisableFormValueModelBinding] - [RequestSizeLimit(REQUEST_SIZE_LIMIT)] - [ProducesResponseType(typeof(DataElement), 200)] - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public async Task Post( - [FromRoute] string org, - [FromRoute] string app, - [FromQuery] string dataType, - [FromHeader(Name = "party")] string partyFromHeader, - [FromQuery] string? language = null - ) + /// + /// Create a new data object of the defined data type + /// + /// unique identifier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// The data type id + /// The party that should be represented with prefix "partyId:", "person:" or "org:" (eg: "partyId:123") + /// The language selected by the user. + /// Return a new instance of the data object including prefill and initial calculations + [Authorize] + [HttpPost] + [DisableFormValueModelBinding] + [RequestSizeLimit(REQUEST_SIZE_LIMIT)] + [ProducesResponseType(typeof(DataElement), 200)] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public async Task Post( + [FromRoute] string org, + [FromRoute] string app, + [FromQuery] string dataType, + [FromHeader(Name = "party")] string partyFromHeader, + [FromQuery] string? language = null + ) + { + if (string.IsNullOrEmpty(dataType)) { - if (string.IsNullOrEmpty(dataType)) - { - return BadRequest( - $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." - ); - } + return BadRequest( + $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." + ); + } - string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); + string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); - if (string.IsNullOrEmpty(classRef)) - { - return BadRequest( - $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." - ); - } + if (string.IsNullOrEmpty(classRef)) + { + return BadRequest( + $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." + ); + } - InstanceOwner? owner = await GetInstanceOwner(partyFromHeader); + InstanceOwner? owner = await GetInstanceOwner(partyFromHeader); - if (owner is null) - { - return BadRequest($"Invalid party header"); - } + if (owner is null) + { + return BadRequest($"Invalid party header"); + } - EnforcementResult enforcementResult = await AuthorizeAction( - org, - app, - Convert.ToInt32(owner.PartyId), - "read" - ); + EnforcementResult enforcementResult = await AuthorizeAction(org, app, Convert.ToInt32(owner.PartyId), "read"); - if (!enforcementResult.Authorized) - { - return Forbidden(enforcementResult); - } + if (!enforcementResult.Authorized) + { + return Forbidden(enforcementResult); + } - ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); + object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) - { - return BadRequest(deserializer.Error); - } + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) + { + return BadRequest(deserializer.Error); + } - // runs prefill from repo configuration if config exists - await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel); + // runs prefill from repo configuration if config exists + await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel); - Instance virtualInstance = new Instance() { InstanceOwner = owner }; - await ProcessAllDataRead(virtualInstance, appModel, language); + Instance virtualInstance = new Instance() { InstanceOwner = owner }; + await ProcessAllDataRead(virtualInstance, appModel, language); - return Ok(appModel); - } + return Ok(appModel); + } - /// - /// Create a new data object of the defined data type - /// - /// The data type id - /// The language selected by the user. - /// Return a new instance of the data object including prefill and initial calculations - [AllowAnonymous] - [HttpPost] - [DisableFormValueModelBinding] - [RequestSizeLimit(REQUEST_SIZE_LIMIT)] - [ProducesResponseType(typeof(DataElement), 200)] - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - [Route("anonymous")] - public async Task PostAnonymous([FromQuery] string dataType, [FromQuery] string? language = null) + /// + /// Create a new data object of the defined data type + /// + /// The data type id + /// The language selected by the user. + /// Return a new instance of the data object including prefill and initial calculations + [AllowAnonymous] + [HttpPost] + [DisableFormValueModelBinding] + [RequestSizeLimit(REQUEST_SIZE_LIMIT)] + [ProducesResponseType(typeof(DataElement), 200)] + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + [Route("anonymous")] + public async Task PostAnonymous([FromQuery] string dataType, [FromQuery] string? language = null) + { + if (string.IsNullOrEmpty(dataType)) { - if (string.IsNullOrEmpty(dataType)) - { - return BadRequest( - $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." - ); - } + return BadRequest( + $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." + ); + } - string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); + string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); - if (string.IsNullOrEmpty(classRef)) - { - return BadRequest( - $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." - ); - } + if (string.IsNullOrEmpty(classRef)) + { + return BadRequest( + $"Invalid dataType {dataType} provided. Please provide a valid dataType as query parameter." + ); + } - ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); + object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) - { - return BadRequest(deserializer.Error); - } + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) + { + return BadRequest(deserializer.Error); + } - Instance virtualInstance = new Instance(); - await ProcessAllDataRead(virtualInstance, appModel, language); + Instance virtualInstance = new Instance(); + await ProcessAllDataRead(virtualInstance, appModel, language); - return Ok(appModel); - } + return Ok(appModel); + } - private async Task GetInstanceOwner(string? partyFromHeader) + private async Task GetInstanceOwner(string? partyFromHeader) + { + // Use the party id of the logged in user, if no party id is given in the header + // Not sure if this is really used anywhere. It doesn't seem useful, as you'd + // always want to create an instance based on the selected party, not the person + // you happened to log in as. + if (partyFromHeader is null) { - // Use the party id of the logged in user, if no party id is given in the header - // Not sure if this is really used anywhere. It doesn't seem useful, as you'd - // always want to create an instance based on the selected party, not the person - // you happened to log in as. - if (partyFromHeader is null) + var partyId = Request.HttpContext.User.GetPartyIdAsInt(); + if (partyId is null) { - var partyId = Request.HttpContext.User.GetPartyIdAsInt(); - if (partyId is null) - { - return null; - } - - var partyFromUser = await _altinnPartyClientClient.GetParty(partyId.Value); - if (partyFromUser is null) - { - return null; - } - - return InstantiationHelper.PartyToInstanceOwner(partyFromUser); + return null; } - // Get the party as read in from the header. Authorization happens later. - var headerParts = partyFromHeader.Split(':'); - if (partyFromHeader.Contains(',') || headerParts.Length != 2) + var partyFromUser = await _altinnPartyClientClient.GetParty(partyId.Value); + if (partyFromUser is null) { return null; } - var id = headerParts[1]; - var idPrefix = headerParts[0].ToLowerInvariant(); - var party = idPrefix switch - { - PartyPrefix => await _altinnPartyClientClient.GetParty(int.TryParse(id, out var partyId) ? partyId : 0), + return InstantiationHelper.PartyToInstanceOwner(partyFromUser); + } - // Frontend seems to only use partyId, not orgnr or ssn. - PersonPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { Ssn = id }), - OrgPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { OrgNo = id }), - _ => null, - }; + // Get the party as read in from the header. Authorization happens later. + var headerParts = partyFromHeader.Split(':'); + if (partyFromHeader.Contains(',') || headerParts.Length != 2) + { + return null; + } - if (party is null || party.PartyId == 0) - { - return null; - } + var id = headerParts[1]; + var idPrefix = headerParts[0].ToLowerInvariant(); + var party = idPrefix switch + { + PartyPrefix => await _altinnPartyClientClient.GetParty(int.TryParse(id, out var partyId) ? partyId : 0), - return InstantiationHelper.PartyToInstanceOwner(party); - } + // Frontend seems to only use partyId, not orgnr or ssn. + PersonPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { Ssn = id }), + OrgPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { OrgNo = id }), + _ => null, + }; - private async Task AuthorizeAction(string org, string app, int partyId, string action) + if (party is null || party.PartyId == 0) { - EnforcementResult enforcementResult = new EnforcementResult(); - XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest( - org, - app, - HttpContext.User, - action, - partyId, - null - ); - XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + return null; + } - if (response?.Response == null) - { - _logger.LogInformation( - $"// Instances Controller // Authorization of action {action} failed with request: {JsonConvert.SerializeObject(request)}." - ); - return enforcementResult; - } + return InstantiationHelper.PartyToInstanceOwner(party); + } - enforcementResult = DecisionHelper.ValidatePdpDecisionDetailed(response.Response, HttpContext.User); + private async Task AuthorizeAction(string org, string app, int partyId, string action) + { + EnforcementResult enforcementResult = new EnforcementResult(); + XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest( + org, + app, + HttpContext.User, + action, + partyId, + null + ); + XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + + if (response?.Response == null) + { + _logger.LogInformation( + $"// Instances Controller // Authorization of action {action} failed with request: {JsonConvert.SerializeObject(request)}." + ); return enforcementResult; } - private ActionResult Forbidden(EnforcementResult enforcementResult) - { - if (enforcementResult.FailedObligations != null && enforcementResult.FailedObligations.Count > 0) - { - return StatusCode((int)HttpStatusCode.Forbidden, enforcementResult.FailedObligations); - } + enforcementResult = DecisionHelper.ValidatePdpDecisionDetailed(response.Response, HttpContext.User); + return enforcementResult; + } - return StatusCode((int)HttpStatusCode.Forbidden); + private ActionResult Forbidden(EnforcementResult enforcementResult) + { + if (enforcementResult.FailedObligations != null && enforcementResult.FailedObligations.Count > 0) + { + return StatusCode((int)HttpStatusCode.Forbidden, enforcementResult.FailedObligations); } + + return StatusCode((int)HttpStatusCode.Forbidden); } } diff --git a/src/Altinn.App.Api/Controllers/StatelessPagesController.cs b/src/Altinn.App.Api/Controllers/StatelessPagesController.cs index 92bf14137..df45c29da 100644 --- a/src/Altinn.App.Api/Controllers/StatelessPagesController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessPagesController.cs @@ -7,64 +7,63 @@ using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Handles page related operations +/// +[ApiController] +[Route("{org}/{app}/v1/pages")] +[AllowAnonymous] +[Obsolete("IPageOrder does not work with frontend version 4")] +public class StatelessPagesController : ControllerBase { + private readonly IAppModel _appModel; + private readonly IAppResources _resources; + private readonly IPageOrder _pageOrder; + /// - /// Handles page related operations + /// Initializes a new instance of the class. /// - [ApiController] - [Route("{org}/{app}/v1/pages")] - [AllowAnonymous] - [Obsolete("IPageOrder does not work with frontend version 4")] - public class StatelessPagesController : ControllerBase + /// The current appmodel implementation for getting the model Type + /// The app resource service + /// The page order service + public StatelessPagesController(IAppModel appModel, IAppResources resources, IPageOrder pageOrder) { - private readonly IAppModel _appModel; - private readonly IAppResources _resources; - private readonly IPageOrder _pageOrder; + _appModel = appModel; + _resources = resources; + _pageOrder = pageOrder; + } - /// - /// Initializes a new instance of the class. - /// - /// The current appmodel implementation for getting the model Type - /// The app resource service - /// The page order service - public StatelessPagesController(IAppModel appModel, IAppResources resources, IPageOrder pageOrder) + /// + /// Get the page order based on the current state of the instance + /// + /// The pages sorted in the correct order + [HttpPost("order")] + public async Task>> GetPageOrder( + [FromRoute] string org, + [FromRoute] string app, + [FromQuery] string layoutSetId, + [FromQuery] string currentPage, + [FromQuery] string dataTypeId, + [FromBody] dynamic formData + ) + { + if (string.IsNullOrEmpty(dataTypeId)) { - _appModel = appModel; - _resources = resources; - _pageOrder = pageOrder; + return BadRequest($"Query parameter `{nameof(dataTypeId)}` must be defined"); } - /// - /// Get the page order based on the current state of the instance - /// - /// The pages sorted in the correct order - [HttpPost("order")] - public async Task>> GetPageOrder( - [FromRoute] string org, - [FromRoute] string app, - [FromQuery] string layoutSetId, - [FromQuery] string currentPage, - [FromQuery] string dataTypeId, - [FromBody] dynamic formData - ) - { - if (string.IsNullOrEmpty(dataTypeId)) - { - return BadRequest($"Query parameter `{nameof(dataTypeId)}` must be defined"); - } - - string classRef = _resources.GetClassRefForLogicDataType(dataTypeId); + string classRef = _resources.GetClassRefForLogicDataType(dataTypeId); - object data = JsonConvert.DeserializeObject(formData.ToString(), _appModel.GetModelType(classRef)); - return await _pageOrder.GetPageOrder( - new AppIdentifier(org, app), - InstanceIdentifier.NoInstance, - layoutSetId, - currentPage, - dataTypeId, - data - ); - } + object data = JsonConvert.DeserializeObject(formData.ToString(), _appModel.GetModelType(classRef)); + return await _pageOrder.GetPageOrder( + new AppIdentifier(org, app), + InstanceIdentifier.NoInstance, + layoutSetId, + currentPage, + dataTypeId, + data + ); } } diff --git a/src/Altinn.App.Api/Controllers/TextsController.cs b/src/Altinn.App.Api/Controllers/TextsController.cs index 04cdd50f3..4ecc0204c 100644 --- a/src/Altinn.App.Api/Controllers/TextsController.cs +++ b/src/Altinn.App.Api/Controllers/TextsController.cs @@ -2,55 +2,54 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Mvc; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Represents the Text resources API giving access to texts in different languages. +/// +[Route("{org}/{app}/api/v1/texts/{language}")] +public class TextsController : ControllerBase { + private readonly IAppResources _appResources; + /// - /// Represents the Text resources API giving access to texts in different languages. + /// Initializes a new instance of the class. /// - [Route("{org}/{app}/api/v1/texts/{language}")] - public class TextsController : ControllerBase + /// A service with access to text resources. + public TextsController(IAppResources appResources) { - private readonly IAppResources _appResources; + _appResources = appResources; + } - /// - /// Initializes a new instance of the class. - /// - /// A service with access to text resources. - public TextsController(IAppResources appResources) + /// + /// Method to retrieve text resources + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The text language to use. + /// The text resource file content or 404 + [HttpGet] + public async Task> Get(string org, string app, [FromRoute] string language) + { + if (!string.IsNullOrEmpty(language) && language.Length != 2) { - _appResources = appResources; + return BadRequest( + $"Provided language {language} is invalid. Language code should consists of two characters." + ); } - /// - /// Method to retrieve text resources - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The text language to use. - /// The text resource file content or 404 - [HttpGet] - public async Task> Get(string org, string app, [FromRoute] string language) + TextResource? textResource = await _appResources.GetTexts(org, app, language); + + if (textResource == null && language != "nb") { - if (!string.IsNullOrEmpty(language) && language.Length != 2) - { - return BadRequest( - $"Provided language {language} is invalid. Language code should consists of two characters." - ); - } - - TextResource? textResource = await _appResources.GetTexts(org, app, language); - - if (textResource == null && language != "nb") - { - textResource = await _appResources.GetTexts(org, app, "nb"); - } - - if (textResource == null) - { - return NotFound(); - } - - return textResource; + textResource = await _appResources.GetTexts(org, app, "nb"); } + + if (textResource == null) + { + return NotFound(); + } + + return textResource; } } diff --git a/src/Altinn.App.Api/Controllers/ValidateController.cs b/src/Altinn.App.Api/Controllers/ValidateController.cs index 61b271647..260432607 100644 --- a/src/Altinn.App.Api/Controllers/ValidateController.cs +++ b/src/Altinn.App.Api/Controllers/ValidateController.cs @@ -7,153 +7,152 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Altinn.App.Api.Controllers +namespace Altinn.App.Api.Controllers; + +/// +/// Represents all actions related to validation of data and instances +/// +[Authorize] +[ApiController] +public class ValidateController : ControllerBase { + private readonly IInstanceClient _instanceClient; + private readonly IAppMetadata _appMetadata; + private readonly IValidationService _validationService; + + /// + /// Initialises a new instance of the class + /// + public ValidateController( + IInstanceClient instanceClient, + IValidationService validationService, + IAppMetadata appMetadata + ) + { + _instanceClient = instanceClient; + _validationService = validationService; + _appMetadata = appMetadata; + } + /// - /// Represents all actions related to validation of data and instances + /// Validate an app instance. This will validate all individual data elements, both the binary elements and the elements bound + /// to a model, and then finally the state of the instance. /// - [Authorize] - [ApiController] - public class ValidateController : ControllerBase + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation + /// Unique id of the party that is the owner of the instance. + /// Unique id to identify the instance + /// The currently used language by the user (or null if not available) + [HttpGet] + [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/validate")] + public async Task ValidateInstance( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromQuery] string? language = null + ) { - private readonly IInstanceClient _instanceClient; - private readonly IAppMetadata _appMetadata; - private readonly IValidationService _validationService; - - /// - /// Initialises a new instance of the class - /// - public ValidateController( - IInstanceClient instanceClient, - IValidationService validationService, - IAppMetadata appMetadata - ) + Instance? instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + if (instance == null) { - _instanceClient = instanceClient; - _validationService = validationService; - _appMetadata = appMetadata; + return NotFound(); } - /// - /// Validate an app instance. This will validate all individual data elements, both the binary elements and the elements bound - /// to a model, and then finally the state of the instance. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation - /// Unique id of the party that is the owner of the instance. - /// Unique id to identify the instance - /// The currently used language by the user (or null if not available) - [HttpGet] - [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/validate")] - public async Task ValidateInstance( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerPartyId, - [FromRoute] Guid instanceGuid, - [FromQuery] string? language = null - ) + string? taskId = instance.Process?.CurrentTask?.ElementId; + if (taskId == null) { - Instance? instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (instance == null) - { - return NotFound(); - } - - string? taskId = instance.Process?.CurrentTask?.ElementId; - if (taskId == null) - { - throw new ValidationException("Unable to validate instance without a started process."); - } + throw new ValidationException("Unable to validate instance without a started process."); + } - try + try + { + List messages = await _validationService.ValidateInstanceAtTask( + instance, + taskId, + language + ); + return Ok(messages); + } + catch (PlatformHttpException exception) + { + if (exception.Response.StatusCode == System.Net.HttpStatusCode.Forbidden) { - List messages = await _validationService.ValidateInstanceAtTask( - instance, - taskId, - language - ); - return Ok(messages); + return StatusCode(403); } - catch (PlatformHttpException exception) - { - if (exception.Response.StatusCode == System.Net.HttpStatusCode.Forbidden) - { - return StatusCode(403); - } - throw; - } + throw; } + } - /// - /// Validate an app instance. This will validate a single data element - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation - /// Unique id of the party that is the owner of the instance. - /// Unique id to identify the instance - /// Unique id identifying specific data element - /// The currently used language by the user (or null if not available) - [HttpGet] - [Route("{org}/{app}/instances/{instanceOwnerId:int}/{instanceId:guid}/data/{dataGuid:guid}/validate")] - public async Task ValidateData( - [FromRoute] string org, - [FromRoute] string app, - [FromRoute] int instanceOwnerId, - [FromRoute] Guid instanceId, - [FromRoute] Guid dataGuid, - [FromQuery] string? language = null - ) + /// + /// Validate an app instance. This will validate a single data element + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation + /// Unique id of the party that is the owner of the instance. + /// Unique id to identify the instance + /// Unique id identifying specific data element + /// The currently used language by the user (or null if not available) + [HttpGet] + [Route("{org}/{app}/instances/{instanceOwnerId:int}/{instanceId:guid}/data/{dataGuid:guid}/validate")] + public async Task ValidateData( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerId, + [FromRoute] Guid instanceId, + [FromRoute] Guid dataGuid, + [FromQuery] string? language = null + ) + { + Instance? instance = await _instanceClient.GetInstance(app, org, instanceOwnerId, instanceId); + if (instance == null) { - Instance? instance = await _instanceClient.GetInstance(app, org, instanceOwnerId, instanceId); - if (instance == null) - { - return NotFound(); - } + return NotFound(); + } - if (instance.Process?.CurrentTask?.ElementId == null) - { - throw new ValidationException("Unable to validate instance without a started process."); - } + if (instance.Process?.CurrentTask?.ElementId == null) + { + throw new ValidationException("Unable to validate instance without a started process."); + } - List messages = new List(); + List messages = new List(); - DataElement? element = instance.Data.FirstOrDefault(d => d.Id == dataGuid.ToString()); + DataElement? element = instance.Data.FirstOrDefault(d => d.Id == dataGuid.ToString()); - if (element == null) - { - throw new ValidationException("Unable to validate data element."); - } + if (element == null) + { + throw new ValidationException("Unable to validate data element."); + } - Application application = await _appMetadata.GetApplicationMetadata(); + Application application = await _appMetadata.GetApplicationMetadata(); - DataType? dataType = application.DataTypes.FirstOrDefault(et => et.Id == element.DataType); + DataType? dataType = application.DataTypes.FirstOrDefault(et => et.Id == element.DataType); - if (dataType == null) - { - throw new ValidationException("Unknown element type."); - } + if (dataType == null) + { + throw new ValidationException("Unknown element type."); + } - messages.AddRange(await _validationService.ValidateDataElement(instance, element, dataType, language)); + messages.AddRange(await _validationService.ValidateDataElement(instance, element, dataType, language)); - string taskId = instance.Process.CurrentTask.ElementId; + string taskId = instance.Process.CurrentTask.ElementId; - // Should this be a BadRequest instead? - if (!dataType.TaskId.Equals(taskId, StringComparison.OrdinalIgnoreCase)) + // Should this be a BadRequest instead? + if (!dataType.TaskId.Equals(taskId, StringComparison.OrdinalIgnoreCase)) + { + ValidationIssue message = new ValidationIssue { - ValidationIssue message = new ValidationIssue - { - Code = ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, - Severity = ValidationIssueSeverity.Warning, - DataElementId = element.Id, - Description = $"Data element for task {dataType.TaskId} validated while currentTask is {taskId}", - CustomTextKey = ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, - CustomTextParams = new List() { dataType.TaskId, taskId }, - }; - messages.Add(message); - } - - return Ok(messages); + Code = ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, + Severity = ValidationIssueSeverity.Warning, + DataElementId = element.Id, + Description = $"Data element for task {dataType.TaskId} validated while currentTask is {taskId}", + CustomTextKey = ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, + CustomTextParams = new List() { dataType.TaskId, taskId }, + }; + messages.Add(message); } + + return Ok(messages); } } diff --git a/src/Altinn.App.Api/Extensions/SecurityHeadersApplicationBuilderExtensions.cs b/src/Altinn.App.Api/Extensions/SecurityHeadersApplicationBuilderExtensions.cs index a0442f655..7a3fd6abc 100644 --- a/src/Altinn.App.Api/Extensions/SecurityHeadersApplicationBuilderExtensions.cs +++ b/src/Altinn.App.Api/Extensions/SecurityHeadersApplicationBuilderExtensions.cs @@ -1,21 +1,20 @@ #nullable disable using Altinn.App.Api.Infrastructure.Middleware; -namespace Altinn.App.Api.Extensions +namespace Altinn.App.Api.Extensions; + +/// +/// Extensions for adding default security headers middleware to the pipeline. +/// +public static class SecurityHeadersApplicationBuilderExtensions { /// - /// Extensions for adding default security headers middleware to the pipeline. + /// Adds the security headers to the pipeline. /// - public static class SecurityHeadersApplicationBuilderExtensions + /// The application builder + /// + public static IApplicationBuilder UseDefaultSecurityHeaders(this IApplicationBuilder builder) { - /// - /// Adds the security headers to the pipeline. - /// - /// The application builder - /// - public static IApplicationBuilder UseDefaultSecurityHeaders(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } + return builder.UseMiddleware(); } } diff --git a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs index ba6f55b53..c5687139d 100644 --- a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs @@ -23,437 +23,419 @@ using OpenTelemetry.Resources; using OpenTelemetry.Trace; -namespace Altinn.App.Api.Extensions +namespace Altinn.App.Api.Extensions; + +/// +/// Class for registering required services to run an Altinn application. +/// +public static class ServiceCollectionExtensions { /// - /// Class for registering required services to run an Altinn application. + /// Add the controllers and views used by an Altinn application /// - public static class ServiceCollectionExtensions + public static void AddAltinnAppControllersWithViews(this IServiceCollection services) { - /// - /// Add the controllers and views used by an Altinn application - /// - public static void AddAltinnAppControllersWithViews(this IServiceCollection services) - { - // Add API controllers from Altinn.App.Api - IMvcBuilder mvcBuilder = services.AddControllersWithViews(); - mvcBuilder - .AddApplicationPart(typeof(InstancesController).Assembly) - .AddXmlSerializerFormatters() - .AddJsonOptions(options => - { - options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; - }); - } + // Add API controllers from Altinn.App.Api + IMvcBuilder mvcBuilder = services.AddControllersWithViews(); + mvcBuilder + .AddApplicationPart(typeof(InstancesController).Assembly) + .AddXmlSerializerFormatters() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; + }); + } - /// - /// Adds all services to run an Altinn application. - /// - /// The being built. - /// A reference to the current object. - /// A reference to the current object. - public static void AddAltinnAppServices( - this IServiceCollection services, - IConfiguration config, - IWebHostEnvironment env - ) - { - services.AddMemoryCache(); - services.AddHealthChecks().AddCheck("default_health_check"); - services.AddFeatureManagement(); + /// + /// Adds all services to run an Altinn application. + /// + /// The being built. + /// A reference to the current object. + /// A reference to the current object. + public static void AddAltinnAppServices( + this IServiceCollection services, + IConfiguration config, + IWebHostEnvironment env + ) + { + services.AddMemoryCache(); + services.AddHealthChecks().AddCheck("default_health_check"); + services.AddFeatureManagement(); - services.AddPlatformServices(config, env); - services.AddAppServices(config, env); - services.ConfigureDataProtection(); + services.AddPlatformServices(config, env); + services.AddAppServices(config, env); + services.ConfigureDataProtection(); - var useOpenTelemetrySetting = config.GetValue("AppSettings:UseOpenTelemetry"); + var useOpenTelemetrySetting = config.GetValue("AppSettings:UseOpenTelemetry"); - // Use Application Insights as default, opt in to use Open Telemetry - if (useOpenTelemetrySetting is true) - { - AddOpenTelemetry(services, config, env); - } - else - { - AddApplicationInsights(services, config, env); - } + // Use Application Insights as default, opt in to use Open Telemetry + if (useOpenTelemetrySetting is true) + { + AddOpenTelemetry(services, config, env); + } + else + { + AddApplicationInsights(services, config, env); + } - AddAuthenticationScheme(services, config, env); - AddAuthorizationPolicies(services); - AddAntiforgery(services); + AddAuthenticationScheme(services, config, env); + AddAuthorizationPolicies(services); + AddAntiforgery(services); - services.AddSingleton(); + services.AddSingleton(); - services.Configure(options => - { - options.AllowSynchronousIO = true; - }); + services.Configure(options => + { + options.AllowSynchronousIO = true; + }); - // HttpClients for platform functionality. Registered as HttpClient so default HttpClientFactory is used - services.AddHttpClient(); + // HttpClients for platform functionality. Registered as HttpClient so default HttpClientFactory is used + services.AddHttpClient(); - services.AddSingleton(); - } + services.AddSingleton(); + } - /// - /// Adds Application Insights to the service collection. - /// - /// Services - /// Config - /// Environment - internal static void AddApplicationInsights( - IServiceCollection services, - IConfiguration config, - IWebHostEnvironment env - ) - { - var (applicationInsightsKey, applicationInsightsConnectionString) = GetAppInsightsConfig(config, env); + /// + /// Adds Application Insights to the service collection. + /// + /// Services + /// Config + /// Environment + internal static void AddApplicationInsights( + IServiceCollection services, + IConfiguration config, + IWebHostEnvironment env + ) + { + var (applicationInsightsKey, applicationInsightsConnectionString) = GetAppInsightsConfig(config, env); - if ( - !string.IsNullOrEmpty(applicationInsightsKey) - || !string.IsNullOrEmpty(applicationInsightsConnectionString) - ) - { - services.AddApplicationInsightsTelemetry( - (options) => + if (!string.IsNullOrEmpty(applicationInsightsKey) || !string.IsNullOrEmpty(applicationInsightsConnectionString)) + { + services.AddApplicationInsightsTelemetry( + (options) => + { + if (string.IsNullOrEmpty(applicationInsightsConnectionString)) { - if (string.IsNullOrEmpty(applicationInsightsConnectionString)) - { #pragma warning disable CS0618 // Type or member is obsolete - // Set instrumentationKey for compatibility if connectionString does not exist. - options.InstrumentationKey = applicationInsightsKey; + // Set instrumentationKey for compatibility if connectionString does not exist. + options.InstrumentationKey = applicationInsightsKey; #pragma warning restore CS0618 // Type or member is obsolete - } - else - { - options.ConnectionString = applicationInsightsConnectionString; - } } - ); - services.AddApplicationInsightsTelemetryProcessor(); - services.AddApplicationInsightsTelemetryProcessor(); - services.AddSingleton(); - } + else + { + options.ConnectionString = applicationInsightsConnectionString; + } + } + ); + services.AddApplicationInsightsTelemetryProcessor(); + services.AddApplicationInsightsTelemetryProcessor(); + services.AddSingleton(); } + } - private static void AddOpenTelemetry( - IServiceCollection services, - IConfiguration config, - IWebHostEnvironment env - ) + private static void AddOpenTelemetry(IServiceCollection services, IConfiguration config, IWebHostEnvironment env) + { + var appId = StartupHelper.GetApplicationId().Split("/")[1]; + var appVersion = config.GetSection("AppSettings").GetValue("AppVersion"); + if (string.IsNullOrWhiteSpace(appVersion)) { - var appId = StartupHelper.GetApplicationId().Split("/")[1]; - var appVersion = config.GetSection("AppSettings").GetValue("AppVersion"); - if (string.IsNullOrWhiteSpace(appVersion)) + appVersion = "Local"; + } + services.AddHostedService(); + services.AddSingleton(); + + // This bit of code makes ASP.NET Core spans always root. + // Depending on infrastructure used and how the application is exposed/called, + // it might be a good idea to be in control of the root span (and therefore the size, baggage etch) + // Taken from: https://github.com/open-telemetry/opentelemetry-dotnet-contrib/issues/1773 + _ = Sdk.SuppressInstrumentation; // Just to trigger static constructor. The static constructor in Sdk initializes Propagators.DefaultTextMapPropagator which we depend on below + Sdk.SetDefaultTextMapPropagator(new OtelPropagator(Propagators.DefaultTextMapPropagator)); + DistributedContextPropagator.Current = new AspNetCorePropagator(); + + var appInsightsConnectionString = GetAppInsightsConnectionStringForOtel(config, env); + + services + .AddOpenTelemetry() + .ConfigureResource(r => + r.AddService(serviceName: appId, serviceVersion: appVersion, serviceInstanceId: Environment.MachineName) + ) + .WithTracing(builder => { - appVersion = "Local"; - } - services.AddHostedService(); - services.AddSingleton(); - - // This bit of code makes ASP.NET Core spans always root. - // Depending on infrastructure used and how the application is exposed/called, - // it might be a good idea to be in control of the root span (and therefore the size, baggage etch) - // Taken from: https://github.com/open-telemetry/opentelemetry-dotnet-contrib/issues/1773 - _ = Sdk.SuppressInstrumentation; // Just to trigger static constructor. The static constructor in Sdk initializes Propagators.DefaultTextMapPropagator which we depend on below - Sdk.SetDefaultTextMapPropagator(new OtelPropagator(Propagators.DefaultTextMapPropagator)); - DistributedContextPropagator.Current = new AspNetCorePropagator(); - - var appInsightsConnectionString = GetAppInsightsConnectionStringForOtel(config, env); - - services - .AddOpenTelemetry() - .ConfigureResource(r => - r.AddService( - serviceName: appId, - serviceVersion: appVersion, - serviceInstanceId: Environment.MachineName - ) - ) - .WithTracing(builder => - { - builder = builder - .AddSource(appId) - .AddHttpClientInstrumentation(opts => - { - opts.RecordException = true; - }) - .AddAspNetCoreInstrumentation(opts => - { - opts.RecordException = true; - }); - - if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) + builder = builder + .AddSource(appId) + .AddHttpClientInstrumentation(opts => { - builder = builder.AddAzureMonitorTraceExporter(options => - { - options.ConnectionString = appInsightsConnectionString; - }); - } - else + opts.RecordException = true; + }) + .AddAspNetCoreInstrumentation(opts => { - builder = builder.AddOtlpExporter(); - } - }) - .WithMetrics(builder => - { - builder = builder - .AddMeter(appId) - .AddRuntimeInstrumentation() - .AddHttpClientInstrumentation() - .AddAspNetCoreInstrumentation(); + opts.RecordException = true; + }); - if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) - { - builder = builder.AddAzureMonitorMetricExporter(options => - { - options.ConnectionString = appInsightsConnectionString; - }); - } - else + if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) + { + builder = builder.AddAzureMonitorTraceExporter(options => { - builder = builder.AddOtlpExporter(); - } - }); - - services.AddLogging(logging => - { - logging.AddOpenTelemetry(options => + options.ConnectionString = appInsightsConnectionString; + }); + } + else { - options.IncludeFormattedMessage = true; + builder = builder.AddOtlpExporter(); + } + }) + .WithMetrics(builder => + { + builder = builder + .AddMeter(appId) + .AddRuntimeInstrumentation() + .AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation(); - if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) - { - options.AddAzureMonitorLogExporter(options => - { - options.ConnectionString = appInsightsConnectionString; - }); - } - else + if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) + { + builder = builder.AddAzureMonitorMetricExporter(options => { - options.AddOtlpExporter(); - } - }); + options.ConnectionString = appInsightsConnectionString; + }); + } + else + { + builder = builder.AddOtlpExporter(); + } }); - } - private sealed class TelemetryInitialization( - ILogger logger, - Telemetry telemetry, - MeterProvider meterProvider - ) : IHostedService + services.AddLogging(logging => { - public async Task StartAsync(CancellationToken cancellationToken) + logging.AddOpenTelemetry(options => { - // This codepath for initialization is here only because it makes it a lot easier to - // query the metrics from Prometheus using 'increase' without the appearance of a "missed" sample. - // 'increase' in Prometheus will not interpret 'none' -> 1 as a delta/increase, - // so when querying the increase within a range, there may be 1 less sample than expected. - // So here we let the metrics be initialized to 0, - // and then run collection/flush on the OTel MeterProvider to make sure they are exported. - // The first time we then increment the metric, it will count as a change from 0 -> 1 - telemetry.Init(); - try + options.IncludeFormattedMessage = true; + + if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) { - var task = Task.Factory.StartNew( - () => - { - if (!meterProvider.ForceFlush(10_000)) - { - logger.LogInformation("Failed to flush metrics after 10 seconds"); - } - }, - cancellationToken, - // Long running to avoid doing this blocking on a "normal" thread pool thread - TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, - TaskScheduler.Default - ); - if (await Task.WhenAny(task, Task.Delay(500, cancellationToken)) != task) + options.AddAzureMonitorLogExporter(options => { - logger.LogInformation( - "Tried to flush metrics within 0.5 seconds but it was taking too long, proceeding with startup" - ); - } + options.ConnectionString = appInsightsConnectionString; + }); } - catch (Exception ex) + else { - if (ex is OperationCanceledException) - return; - logger.LogWarning(ex, "Failed to flush metrics"); + options.AddOtlpExporter(); } - } + }); + }); + } - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + private sealed class TelemetryInitialization( + ILogger logger, + Telemetry telemetry, + MeterProvider meterProvider + ) : IHostedService + { + public async Task StartAsync(CancellationToken cancellationToken) + { + // This codepath for initialization is here only because it makes it a lot easier to + // query the metrics from Prometheus using 'increase' without the appearance of a "missed" sample. + // 'increase' in Prometheus will not interpret 'none' -> 1 as a delta/increase, + // so when querying the increase within a range, there may be 1 less sample than expected. + // So here we let the metrics be initialized to 0, + // and then run collection/flush on the OTel MeterProvider to make sure they are exported. + // The first time we then increment the metric, it will count as a change from 0 -> 1 + telemetry.Init(); + try + { + var task = Task.Factory.StartNew( + () => + { + if (!meterProvider.ForceFlush(10_000)) + { + logger.LogInformation("Failed to flush metrics after 10 seconds"); + } + }, + cancellationToken, + // Long running to avoid doing this blocking on a "normal" thread pool thread + TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, + TaskScheduler.Default + ); + if (await Task.WhenAny(task, Task.Delay(500, cancellationToken)) != task) + { + logger.LogInformation( + "Tried to flush metrics within 0.5 seconds but it was taking too long, proceeding with startup" + ); + } + } + catch (Exception ex) + { + if (ex is OperationCanceledException) + return; + logger.LogWarning(ex, "Failed to flush metrics"); + } } - internal sealed class OtelPropagator : TextMapPropagator - { - private readonly TextMapPropagator _inner; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } - public OtelPropagator(TextMapPropagator inner) => _inner = inner; + internal sealed class OtelPropagator : TextMapPropagator + { + private readonly TextMapPropagator _inner; - public override ISet Fields => _inner.Fields; + public OtelPropagator(TextMapPropagator inner) => _inner = inner; - public override PropagationContext Extract( - PropagationContext context, - T carrier, - Func> getter - ) - { - if (carrier is HttpRequest) - return default; - return _inner.Extract(context, carrier, getter); - } + public override ISet Fields => _inner.Fields; - public override void Inject(PropagationContext context, T carrier, Action setter) => - _inner.Inject(context, carrier, setter); + public override PropagationContext Extract( + PropagationContext context, + T carrier, + Func> getter + ) + { + if (carrier is HttpRequest) + return default; + return _inner.Extract(context, carrier, getter); } - internal sealed class AspNetCorePropagator : DistributedContextPropagator - { - private readonly DistributedContextPropagator _inner; + public override void Inject(PropagationContext context, T carrier, Action setter) => + _inner.Inject(context, carrier, setter); + } - public AspNetCorePropagator() => _inner = CreateDefaultPropagator(); + internal sealed class AspNetCorePropagator : DistributedContextPropagator + { + private readonly DistributedContextPropagator _inner; - public override IReadOnlyCollection Fields => _inner.Fields; + public AspNetCorePropagator() => _inner = CreateDefaultPropagator(); - public override IEnumerable>? ExtractBaggage( - object? carrier, - PropagatorGetterCallback? getter - ) - { - if (carrier is IHeaderDictionary) - return null; + public override IReadOnlyCollection Fields => _inner.Fields; - return _inner.ExtractBaggage(carrier, getter); - } + public override IEnumerable>? ExtractBaggage( + object? carrier, + PropagatorGetterCallback? getter + ) + { + if (carrier is IHeaderDictionary) + return null; - public override void ExtractTraceIdAndState( - object? carrier, - PropagatorGetterCallback? getter, - out string? traceId, - out string? traceState - ) - { - if (carrier is IHeaderDictionary) - { - traceId = null; - traceState = null; - return; - } + return _inner.ExtractBaggage(carrier, getter); + } - _inner.ExtractTraceIdAndState(carrier, getter, out traceId, out traceState); + public override void ExtractTraceIdAndState( + object? carrier, + PropagatorGetterCallback? getter, + out string? traceId, + out string? traceState + ) + { + if (carrier is IHeaderDictionary) + { + traceId = null; + traceState = null; + return; } - public override void Inject(Activity? activity, object? carrier, PropagatorSetterCallback? setter) => - _inner.Inject(activity, carrier, setter); + _inner.ExtractTraceIdAndState(carrier, getter, out traceId, out traceState); } - private static void AddAuthorizationPolicies(IServiceCollection services) + public override void Inject(Activity? activity, object? carrier, PropagatorSetterCallback? setter) => + _inner.Inject(activity, carrier, setter); + } + + private static void AddAuthorizationPolicies(IServiceCollection services) + { + services.AddAuthorization(options => { - services.AddAuthorization(options => + options.AddPolicy("InstanceRead", policy => policy.Requirements.Add(new AppAccessRequirement("read"))); + options.AddPolicy("InstanceWrite", policy => policy.Requirements.Add(new AppAccessRequirement("write"))); + options.AddPolicy("InstanceDelete", policy => policy.Requirements.Add(new AppAccessRequirement("delete"))); + options.AddPolicy( + "InstanceInstantiate", + policy => policy.Requirements.Add(new AppAccessRequirement("instantiate")) + ); + options.AddPolicy( + "InstanceComplete", + policy => policy.Requirements.Add(new AppAccessRequirement("complete")) + ); + }); + } + + private static void AddAuthenticationScheme( + IServiceCollection services, + IConfiguration config, + IWebHostEnvironment env + ) + { + services + .AddAuthentication(JwtCookieDefaults.AuthenticationScheme) + .AddJwtCookie(options => { - options.AddPolicy("InstanceRead", policy => policy.Requirements.Add(new AppAccessRequirement("read"))); - options.AddPolicy( - "InstanceWrite", - policy => policy.Requirements.Add(new AppAccessRequirement("write")) - ); - options.AddPolicy( - "InstanceDelete", - policy => policy.Requirements.Add(new AppAccessRequirement("delete")) - ); - options.AddPolicy( - "InstanceInstantiate", - policy => policy.Requirements.Add(new AppAccessRequirement("instantiate")) - ); - options.AddPolicy( - "InstanceComplete", - policy => policy.Requirements.Add(new AppAccessRequirement("complete")) - ); + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + ValidateIssuer = false, + ValidateAudience = false, + RequireExpirationTime = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + options.JwtCookieName = Altinn.App.Core.Constants.General.RuntimeCookieName; + options.MetadataAddress = config["AppSettings:OpenIdWellKnownEndpoint"]; + if (env.IsDevelopment()) + { + options.RequireHttpsMetadata = false; + } }); - } + } - private static void AddAuthenticationScheme( - IServiceCollection services, - IConfiguration config, - IWebHostEnvironment env - ) + private static void AddAntiforgery(IServiceCollection services) + { + services.AddAntiforgery(options => { - services - .AddAuthentication(JwtCookieDefaults.AuthenticationScheme) - .AddJwtCookie(options => - { - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - ValidateIssuer = false, - ValidateAudience = false, - RequireExpirationTime = true, - ValidateLifetime = true, - ClockSkew = TimeSpan.Zero - }; - options.JwtCookieName = Altinn.App.Core.Constants.General.RuntimeCookieName; - options.MetadataAddress = config["AppSettings:OpenIdWellKnownEndpoint"]; - if (env.IsDevelopment()) - { - options.RequireHttpsMetadata = false; - } - }); - } + // asp .net core expects two types of tokens: One that is attached to the request as header, and the other one as cookie. + // The values of the tokens are not the same and both need to be present and valid in a "unsafe" request. + + // Axios which we are using for client-side automatically extracts the value from the cookie named XSRF-TOKEN. We are setting this cookie in the UserController. + // We will therefore have two token cookies. One that contains the .net core cookie token; And one that is the request token and is added as a header in requests. + // The tokens are based on the logged-in user and must be updated if the user changes. + // https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-3.1 + // https://github.com/axios/axios/blob/master/lib/defaults.js + options.Cookie.Name = "AS-XSRF-TOKEN"; + options.HeaderName = "X-XSRF-TOKEN"; + }); + + services.TryAddSingleton(); + } - private static void AddAntiforgery(IServiceCollection services) - { - services.AddAntiforgery(options => - { - // asp .net core expects two types of tokens: One that is attached to the request as header, and the other one as cookie. - // The values of the tokens are not the same and both need to be present and valid in a "unsafe" request. - - // Axios which we are using for client-side automatically extracts the value from the cookie named XSRF-TOKEN. We are setting this cookie in the UserController. - // We will therefore have two token cookies. One that contains the .net core cookie token; And one that is the request token and is added as a header in requests. - // The tokens are based on the logged-in user and must be updated if the user changes. - // https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-3.1 - // https://github.com/axios/axios/blob/master/lib/defaults.js - options.Cookie.Name = "AS-XSRF-TOKEN"; - options.HeaderName = "X-XSRF-TOKEN"; - }); + private static (string? Key, string? ConnectionString) GetAppInsightsConfig( + IConfiguration config, + IHostEnvironment env + ) + { + var isDevelopment = env.IsDevelopment(); + string? key = isDevelopment + ? config["ApplicationInsights:InstrumentationKey"] + : Environment.GetEnvironmentVariable("ApplicationInsights__InstrumentationKey"); + string? connectionString = isDevelopment + ? config["ApplicationInsights:ConnectionString"] + : Environment.GetEnvironmentVariable("ApplicationInsights__ConnectionString"); + + return (key, connectionString); + } - services.TryAddSingleton(); + private static string? GetAppInsightsConnectionStringForOtel(IConfiguration config, IHostEnvironment env) + { + var (key, connString) = GetAppInsightsConfig(config, env); + if (string.IsNullOrWhiteSpace(connString)) + { + connString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING"); } - - private static (string? Key, string? ConnectionString) GetAppInsightsConfig( - IConfiguration config, - IHostEnvironment env - ) + if (!string.IsNullOrWhiteSpace(connString)) { - var isDevelopment = env.IsDevelopment(); - string? key = isDevelopment - ? config["ApplicationInsights:InstrumentationKey"] - : Environment.GetEnvironmentVariable("ApplicationInsights__InstrumentationKey"); - string? connectionString = isDevelopment - ? config["ApplicationInsights:ConnectionString"] - : Environment.GetEnvironmentVariable("ApplicationInsights__ConnectionString"); - - return (key, connectionString); + return connString; } - private static string? GetAppInsightsConnectionStringForOtel(IConfiguration config, IHostEnvironment env) + if (!Guid.TryParse(key, out _)) { - var (key, connString) = GetAppInsightsConfig(config, env); - if (string.IsNullOrWhiteSpace(connString)) - { - connString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING"); - } - if (!string.IsNullOrWhiteSpace(connString)) - { - return connString; - } - - if (!Guid.TryParse(key, out _)) - { - return null; - } - - return $"InstrumentationKey={key}"; + return null; } + + return $"InstrumentationKey={key}"; } } diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs b/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs index ce8fc8ab7..2f50bb34a 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs @@ -5,166 +5,163 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Primitives; -namespace Altinn.App.Api.Helpers.RequestHandling +namespace Altinn.App.Api.Helpers.RequestHandling; + +/// +/// Check datarestrictions on http requests +/// +public static class DataRestrictionValidation { /// - /// Check datarestrictions on http requests + /// Check if a data post/put request complies with restrictions agreed upon for the DataController /// - public static class DataRestrictionValidation + /// the original http request + /// datatype the files is beeing uploaded to + /// true with errorResponse = empty list if all is ok, false with errorResponse including errors if not + public static (bool Success, List Errors) CompliesWithDataRestrictions( + HttpRequest request, + DataType? dataType + ) { - /// - /// Check if a data post/put request complies with restrictions agreed upon for the DataController - /// - /// the original http request - /// datatype the files is beeing uploaded to - /// true with errorResponse = empty list if all is ok, false with errorResponse including errors if not - public static (bool Success, List Errors) CompliesWithDataRestrictions( - HttpRequest request, - DataType? dataType - ) + List errors = new(); + var errorBaseMessage = "Invalid data provided. Error:"; + if (!request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) { - List errors = new(); - var errorBaseMessage = "Invalid data provided. Error:"; - if (!request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) - { - errors.Add( - new ValidationIssue - { - Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, - Severity = ValidationIssueSeverity.Error, - Description = $"{errorBaseMessage} The request must include a Content-Disposition header" - } - ); - - return (false, errors); - } - - var maxSize = (long?)dataType?.MaxSize * 1024 * 1024; - if (maxSize != null && request.ContentLength > maxSize) - { - errors.Add( - new ValidationIssue - { - Code = ValidationIssueCodes.DataElementCodes.DataElementTooLarge, - Severity = ValidationIssueSeverity.Error, - Description = $"{errorBaseMessage} Binary attachment exceeds limit of {maxSize}" - } - ); - - return (false, errors); - } - - string? filename = GetFileNameFromHeader(headerValues); - - if (string.IsNullOrEmpty(filename)) - { - errors.Add( - new ValidationIssue - { - Code = ValidationIssueCodes.DataElementCodes.MissingFileName, - Severity = ValidationIssueSeverity.Error, - Description = $"{errorBaseMessage} The Content-Disposition header must contain a filename" - } - ); - - return (false, errors); - } - - string[] splitFilename = filename.Split('.'); - - if (splitFilename.Length < 2) - { - errors.Add( - new ValidationIssue - { - Code = ValidationIssueCodes.DataElementCodes.InvalidFileNameFormat, - Severity = ValidationIssueSeverity.Error, - Description = - $"{errorBaseMessage} Invalid format for filename: {filename}. Filename is expected to end with '.{{filetype}}'." - } - ); - - return (false, errors); - } - - if (dataType?.AllowedContentTypes == null || dataType.AllowedContentTypes.Count == 0) - { - return (true, errors); - } - - string filetype = splitFilename[splitFilename.Length - 1]; - var mimeType = MimeTypeMap.GetMimeType(filetype); - - if (!request.Headers.TryGetValue("Content-Type", out StringValues contentType)) - { - errors.Add( - new ValidationIssue - { - Code = ValidationIssueCodes.DataElementCodes.InvalidFileNameFormat, - Severity = ValidationIssueSeverity.Error, - Description = $"{errorBaseMessage} Content-Type header must be included in request." - } - ); - - return (false, errors); - } - - // Verify that file mime type matches content type in request - if (!contentType.Equals("application/octet-stream") && !mimeType.Equals(contentType)) - { - errors.Add( - new ValidationIssue - { - Code = ValidationIssueCodes.DataElementCodes.InvalidFileNameFormat, - Severity = ValidationIssueSeverity.Error, - Description = - $"{errorBaseMessage} Content type header {contentType} does not match mime type {mimeType} for uploaded file. Please fix header or upload another file." - } - ); - - return (false, errors); - } - - // Verify that file mime type is an allowed content-type - if ( - !dataType.AllowedContentTypes.Contains( - contentType.ToString(), - StringComparer.InvariantCultureIgnoreCase - ) && !dataType.AllowedContentTypes.Contains("application/octet-stream") - ) - { - errors.Add( - new ValidationIssue - { - Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, - Severity = ValidationIssueSeverity.Error, - Description = - $"{errorBaseMessage} Invalid content type: {mimeType}. Please try another file. Permitted content types include: {string.Join(", ", dataType.AllowedContentTypes)}" - } - ); - - return (false, errors); - } + errors.Add( + new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Severity = ValidationIssueSeverity.Error, + Description = $"{errorBaseMessage} The request must include a Content-Disposition header" + } + ); + + return (false, errors); + } - return (true, errors); + var maxSize = (long?)dataType?.MaxSize * 1024 * 1024; + if (maxSize != null && request.ContentLength > maxSize) + { + errors.Add( + new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.DataElementTooLarge, + Severity = ValidationIssueSeverity.Error, + Description = $"{errorBaseMessage} Binary attachment exceeds limit of {maxSize}" + } + ); + + return (false, errors); + } + + string? filename = GetFileNameFromHeader(headerValues); + + if (string.IsNullOrEmpty(filename)) + { + errors.Add( + new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.MissingFileName, + Severity = ValidationIssueSeverity.Error, + Description = $"{errorBaseMessage} The Content-Disposition header must contain a filename" + } + ); + + return (false, errors); } - /// - /// Uses the provided header to extract the filename - /// - public static string? GetFileNameFromHeader(StringValues headerValues) + string[] splitFilename = filename.Split('.'); + + if (splitFilename.Length < 2) { - string? headerValue = headerValues; - ArgumentNullException.ThrowIfNull(headerValue, nameof(headerValues)); + errors.Add( + new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.InvalidFileNameFormat, + Severity = ValidationIssueSeverity.Error, + Description = + $"{errorBaseMessage} Invalid format for filename: {filename}. Filename is expected to end with '.{{filetype}}'." + } + ); + + return (false, errors); + } - ContentDispositionHeaderValue contentDisposition = ContentDispositionHeaderValue.Parse(headerValue); - string? filename = contentDisposition.FileNameStar ?? contentDisposition.FileName; + if (dataType?.AllowedContentTypes == null || dataType.AllowedContentTypes.Count == 0) + { + return (true, errors); + } - // We actively remove quotes because we don't want them replaced with '_'. - // Quotes around filename in Content-Disposition is valid, but not as part of the filename. - filename = filename?.Trim('\"').AsFileName(false); + string filetype = splitFilename[splitFilename.Length - 1]; + var mimeType = MimeTypeMap.GetMimeType(filetype); - return filename; + if (!request.Headers.TryGetValue("Content-Type", out StringValues contentType)) + { + errors.Add( + new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.InvalidFileNameFormat, + Severity = ValidationIssueSeverity.Error, + Description = $"{errorBaseMessage} Content-Type header must be included in request." + } + ); + + return (false, errors); } + + // Verify that file mime type matches content type in request + if (!contentType.Equals("application/octet-stream") && !mimeType.Equals(contentType)) + { + errors.Add( + new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.InvalidFileNameFormat, + Severity = ValidationIssueSeverity.Error, + Description = + $"{errorBaseMessage} Content type header {contentType} does not match mime type {mimeType} for uploaded file. Please fix header or upload another file." + } + ); + + return (false, errors); + } + + // Verify that file mime type is an allowed content-type + if ( + !dataType.AllowedContentTypes.Contains(contentType.ToString(), StringComparer.InvariantCultureIgnoreCase) + && !dataType.AllowedContentTypes.Contains("application/octet-stream") + ) + { + errors.Add( + new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Severity = ValidationIssueSeverity.Error, + Description = + $"{errorBaseMessage} Invalid content type: {mimeType}. Please try another file. Permitted content types include: {string.Join(", ", dataType.AllowedContentTypes)}" + } + ); + + return (false, errors); + } + + return (true, errors); + } + + /// + /// Uses the provided header to extract the filename + /// + public static string? GetFileNameFromHeader(StringValues headerValues) + { + string? headerValue = headerValues; + ArgumentNullException.ThrowIfNull(headerValue, nameof(headerValues)); + + ContentDispositionHeaderValue contentDisposition = ContentDispositionHeaderValue.Parse(headerValue); + string? filename = contentDisposition.FileNameStar ?? contentDisposition.FileName; + + // We actively remove quotes because we don't want them replaced with '_'. + // Quotes around filename in Content-Disposition is valid, but not as part of the filename. + filename = filename?.Trim('\"').AsFileName(false); + + return filename; } } diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs b/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs index 462a35ada..a65a81bfa 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs @@ -2,138 +2,135 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Net.Http.Headers; -namespace Altinn.App.Api.Helpers.RequestHandling +namespace Altinn.App.Api.Helpers.RequestHandling; + +/// +/// Represents a reader that can read a multipart http request and split it in data elements. +/// +public class MultipartRequestReader { + private readonly HttpRequest _request; + /// - /// Represents a reader that can read a multipart http request and split it in data elements. + /// Initializes a new instance of the class with a . /// - public class MultipartRequestReader + /// The to be read. + public MultipartRequestReader(HttpRequest request) { - private readonly HttpRequest request; + _request = request; + Parts = new List(); + Errors = new List(); + } - /// - /// Initializes a new instance of the class with a . - /// - /// The to be read. - public MultipartRequestReader(HttpRequest request) + /// + /// Gets a value indicating whether the request has multiple parts using the request content type. + /// + public bool IsMultipart + { + get { - this.request = request; - Parts = new List(); - Errors = new List(); + return !string.IsNullOrEmpty(_request.ContentType) + && _request.ContentType.Contains("multipart/", StringComparison.OrdinalIgnoreCase); } + } - /// - /// Gets a value indicating whether the request has multiple parts using the request content type. - /// - public bool IsMultipart - { - get - { - return !string.IsNullOrEmpty(request.ContentType) - && request.ContentType.Contains("multipart/", StringComparison.OrdinalIgnoreCase); - } - } + /// + /// Gets a list of all parts. + /// + public List Parts { get; } - /// - /// Gets a list of all parts. - /// - public List Parts { get; } - - /// - /// Gets a list of errors. - /// - public List Errors { get; } - - /// - /// Process the request and generate parts. - /// - /// A representing the result of the asynchronous operation. - public async Task Read() + /// + /// Gets a list of errors. + /// + public List Errors { get; } + + /// + /// Process the request and generate parts. + /// + /// A representing the result of the asynchronous operation. + public async Task Read() + { + if (IsMultipart) { - if (IsMultipart) + int partCounter = 0; + try { - int partCounter = 0; - try + MultipartReader reader = new MultipartReader(GetBoundary(), _request.Body); + + MultipartSection? section; + while ((section = await reader.ReadNextSectionAsync()) != null) { - MultipartReader reader = new MultipartReader(GetBoundary(), request.Body); + partCounter++; - MultipartSection? section; - while ((section = await reader.ReadNextSectionAsync()) != null) + if ( + !ContentDispositionHeaderValue.TryParse( + section.ContentDisposition, + out ContentDispositionHeaderValue? contentDisposition + ) + ) { - partCounter++; + Errors.Add(string.Format("Part number {0} doesn't have a content disposition", partCounter)); + continue; + } - if ( - !ContentDispositionHeaderValue.TryParse( - section.ContentDisposition, - out ContentDispositionHeaderValue? contentDisposition - ) - ) - { - Errors.Add( - string.Format("Part number {0} doesn't have a content disposition", partCounter) - ); - continue; - } + if (section.ContentType == null) + { + Errors.Add(string.Format("Part number {0} doesn't have a content type", partCounter)); + continue; + } - if (section.ContentType == null) - { - Errors.Add(string.Format("Part number {0} doesn't have a content type", partCounter)); - continue; - } + string? sectionName = contentDisposition.Name.Value; + string? contentFileName = null; + if (contentDisposition.FileNameStar.HasValue) + { + contentFileName = contentDisposition.FileNameStar.Value; + } + else if (contentDisposition.FileName.HasValue) + { + contentFileName = contentDisposition.FileName.Value; + } - string? sectionName = contentDisposition.Name.Value; - string? contentFileName = null; - if (contentDisposition.FileNameStar.HasValue) - { - contentFileName = contentDisposition.FileNameStar.Value; - } - else if (contentDisposition.FileName.HasValue) + // We actively remove quotes because we don't want them replaced with '_'. + // Quotes around filename in Content-Disposition is valid, but not as part of the filename. + contentFileName = contentFileName?.Trim('\"').AsFileName(false); + + long fileSize = contentDisposition.Size ?? 0; + + MemoryStream memoryStream = new MemoryStream(); + await section.Body.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + Parts.Add( + new RequestPart() { - contentFileName = contentDisposition.FileName.Value; + ContentType = section.ContentType, + Name = sectionName, + Stream = memoryStream, + FileName = contentFileName, + FileSize = fileSize, } - - // We actively remove quotes because we don't want them replaced with '_'. - // Quotes around filename in Content-Disposition is valid, but not as part of the filename. - contentFileName = contentFileName?.Trim('\"').AsFileName(false); - - long fileSize = contentDisposition.Size ?? 0; - - MemoryStream memoryStream = new MemoryStream(); - await section.Body.CopyToAsync(memoryStream); - memoryStream.Position = 0; - - Parts.Add( - new RequestPart() - { - ContentType = section.ContentType, - Name = sectionName, - Stream = memoryStream, - FileName = contentFileName, - FileSize = fileSize, - } - ); - } - } - catch (IOException ioex) - { - Errors.Add("IOException while reading a section of the request: " + ioex.Message); + ); } } - else + catch (IOException ioex) { - // create part of content - if (request.ContentType != null) - { - Parts.Add(new RequestPart() { ContentType = request.ContentType, Stream = request.Body }); - } + Errors.Add("IOException while reading a section of the request: " + ioex.Message); } } - - private string GetBoundary() + else { - MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(request.ContentType); - return mediaType.Boundary.Value?.Trim('"') - ?? throw new Exception("Could not retrieve boundary value from Content-Type header"); + // create part of content + if (_request.ContentType != null) + { + Parts.Add(new RequestPart() { ContentType = _request.ContentType, Stream = _request.Body }); + } } } + + private string GetBoundary() + { + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(_request.ContentType); + return mediaType.Boundary.Value?.Trim('"') + ?? throw new Exception("Could not retrieve boundary value from Content-Type header"); + } } diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs index b0b37213c..876d580da 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs @@ -1,39 +1,38 @@ -namespace Altinn.App.Api.Helpers.RequestHandling +namespace Altinn.App.Api.Helpers.RequestHandling; + +/// +/// A helper to organise the parts in a multipart +/// +public class RequestPart { /// - /// A helper to organise the parts in a multipart + /// The stream to access this part. /// - public class RequestPart - { - /// - /// The stream to access this part. - /// #nullable disable - public Stream Stream { get; set; } + public Stream Stream { get; set; } #nullable restore - /// - /// The file name as given in content description. - /// - public string? FileName { get; set; } + /// + /// The file name as given in content description. + /// + public string? FileName { get; set; } - /// - /// The parts name. - /// - public string? Name { get; set; } + /// + /// The parts name. + /// + public string? Name { get; set; } - /// - /// The content type of the part. - /// + /// + /// The content type of the part. + /// #nullable disable - public string ContentType { get; set; } + public string ContentType { get; set; } #nullable restore - /// - /// The file size of the part, 0 if not given. - /// - public long FileSize { get; set; } - } + /// + /// The file size of the part, 0 if not given. + /// + public long FileSize { get; set; } } diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs index c03ea00a8..c52b919dd 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs @@ -1,115 +1,114 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Api.Helpers.RequestHandling +namespace Altinn.App.Api.Helpers.RequestHandling; + +/// +/// Represents a validator of a single with the help of app metadata +/// +public class RequestPartValidator { + private readonly Application _appInfo; + /// - /// Represents a validator of a single with the help of app metadata + /// Initialises a new instance of the class with the given application info. /// - public class RequestPartValidator + /// The application metadata to use when validating a . + public RequestPartValidator(Application appInfo) { - private readonly Application appInfo; + _appInfo = appInfo; + } - /// - /// Initialises a new instance of the class with the given application info. - /// - /// The application metadata to use when validating a . - public RequestPartValidator(Application appInfo) + /// + /// Operation that can validate a using the internal . + /// + /// The request part to be validated. + /// null if no errors where found. Otherwise an error message. + public string? ValidatePart(RequestPart part) + { + if (part.Name == "instance") { - this.appInfo = appInfo; - } + if (!part.ContentType.StartsWith("application/json")) + { + return $"Unexpected Content-Type '{part.ContentType}' of embedded instance template. Expecting 'application/json'"; + } - /// - /// Operation that can validate a using the internal . - /// - /// The request part to be validated. - /// null if no errors where found. Otherwise an error message. - public string? ValidatePart(RequestPart part) + //// TODO: Validate that the element can be read as an instance? + } + else { - if (part.Name == "instance") + DataType? dataType = _appInfo.DataTypes.Find(e => e.Id == part.Name); + if (dataType == null) { - if (!part.ContentType.StartsWith("application/json")) - { - return $"Unexpected Content-Type '{part.ContentType}' of embedded instance template. Expecting 'application/json'"; - } + return $"Multipart section named, '{part.Name}' does not correspond to an element type in application metadata"; + } - //// TODO: Validate that the element can be read as an instance? + if (part.ContentType == null) + { + return $"The multipart section named {part.Name} is missing Content-Type."; } else { - DataType? dataType = appInfo.DataTypes.Find(e => e.Id == part.Name); - if (dataType == null) - { - return $"Multipart section named, '{part.Name}' does not correspond to an element type in application metadata"; - } - - if (part.ContentType == null) - { - return $"The multipart section named {part.Name} is missing Content-Type."; - } - else - { - string contentTypeWithoutEncoding = part.ContentType.Split(";")[0]; - - // restrict content type if allowedContentTypes is specified - if ( - dataType.AllowedContentTypes != null - && dataType.AllowedContentTypes.Count > 0 - && !dataType.AllowedContentTypes.Contains(contentTypeWithoutEncoding) - ) - { - return $"The multipart section named {part.Name} has a Content-Type '{part.ContentType}' which is invalid for element type '{dataType.Id}'"; - } - } - - long contentSize = part.FileSize != 0 ? part.FileSize : part.Stream.Length; - - if (contentSize == 0) - { - return $"The multipart section named {part.Name} has no data. Cannot process empty part."; - } + string contentTypeWithoutEncoding = part.ContentType.Split(";")[0]; + // restrict content type if allowedContentTypes is specified if ( - dataType.MaxSize.HasValue - && dataType.MaxSize > 0 - && contentSize > (long)dataType.MaxSize.Value * 1024 * 1024 + dataType.AllowedContentTypes != null + && dataType.AllowedContentTypes.Count > 0 + && !dataType.AllowedContentTypes.Contains(contentTypeWithoutEncoding) ) { - return $"The multipart section named {part.Name} exceeds the size limit of element type '{dataType.Id}'"; + return $"The multipart section named {part.Name} has a Content-Type '{part.ContentType}' which is invalid for element type '{dataType.Id}'"; } } - return null; + long contentSize = part.FileSize != 0 ? part.FileSize : part.Stream.Length; + + if (contentSize == 0) + { + return $"The multipart section named {part.Name} has no data. Cannot process empty part."; + } + + if ( + dataType.MaxSize.HasValue + && dataType.MaxSize > 0 + && contentSize > (long)dataType.MaxSize.Value * 1024 * 1024 + ) + { + return $"The multipart section named {part.Name} exceeds the size limit of element type '{dataType.Id}'"; + } } - /// - /// Operation that can validate a list of elements using the internal . - /// - /// The list of request parts to be validated. - /// null if no errors where found. Otherwise an error message. - public string? ValidateParts(List parts) + return null; + } + + /// + /// Operation that can validate a list of elements using the internal . + /// + /// The list of request parts to be validated. + /// null if no errors where found. Otherwise an error message. + public string? ValidateParts(List parts) + { + foreach (RequestPart part in parts) { - foreach (RequestPart part in parts) + string? partError = ValidatePart(part); + if (partError != null) { - string? partError = ValidatePart(part); - if (partError != null) - { - return partError; - } + return partError; } + } - foreach (DataType dataType in appInfo.DataTypes) + foreach (DataType dataType in _appInfo.DataTypes) + { + if (dataType.MaxCount > 0) { - if (dataType.MaxCount > 0) + int partCount = parts.Count(p => p.Name == dataType.Id); + if (dataType.MaxCount < partCount) { - int partCount = parts.Count(p => p.Name == dataType.Id); - if (dataType.MaxCount < partCount) - { - return $"The list of parts contains more elements of type '{dataType.Id}' than the element type allows."; - } + return $"The list of parts contains more elements of type '{dataType.Id}' than the element type allows."; } } - - return null; } + + return null; } } diff --git a/src/Altinn.App.Api/Infrastructure/Filters/AutoValidateAntiforgeryTokenIfAuthCookieAttribute.cs b/src/Altinn.App.Api/Infrastructure/Filters/AutoValidateAntiforgeryTokenIfAuthCookieAttribute.cs index 262aaaf81..5be90f3a6 100644 --- a/src/Altinn.App.Api/Infrastructure/Filters/AutoValidateAntiforgeryTokenIfAuthCookieAttribute.cs +++ b/src/Altinn.App.Api/Infrastructure/Filters/AutoValidateAntiforgeryTokenIfAuthCookieAttribute.cs @@ -1,39 +1,38 @@ #nullable disable using Microsoft.AspNetCore.Mvc.Filters; -namespace Altinn.App.Api.Infrastructure.Filters +namespace Altinn.App.Api.Infrastructure.Filters; + +/// +/// This attribute is part of the anti request forgery system. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public class AutoValidateAntiforgeryTokenIfAuthCookieAttribute : Attribute, IFilterFactory, IOrderedFilter { /// - /// This attribute is part of the anti request forgery system. + /// Gets the order value for determining the order of execution of filters. Filters execute in + /// ascending numeric value of the property. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public class AutoValidateAntiforgeryTokenIfAuthCookieAttribute : Attribute, IFilterFactory, IOrderedFilter - { - /// - /// Gets the order value for determining the order of execution of filters. Filters execute in - /// ascending numeric value of the property. - /// - /// - /// - /// Filters are executed in a sequence determined by an ascending sort of the property. - /// - /// - /// The default Order for this attribute is 1000 because it must run after any filter which does authentication - /// or login in order to allow them to behave as expected (ie Unauthenticated or Redirect instead of 400). - /// - /// - /// Look at for more detailed info. - /// - /// - public int Order { get; set; } = 1000; + /// + /// + /// Filters are executed in a sequence determined by an ascending sort of the property. + /// + /// + /// The default Order for this attribute is 1000 because it must run after any filter which does authentication + /// or login in order to allow them to behave as expected (ie Unauthenticated or Redirect instead of 400). + /// + /// + /// Look at for more detailed info. + /// + /// + public int Order { get; set; } = 1000; - /// - public bool IsReusable => true; + /// + public bool IsReusable => true; - /// - public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) - { - return serviceProvider.GetRequiredService(); - } + /// + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + return serviceProvider.GetRequiredService(); } } diff --git a/src/Altinn.App.Api/Infrastructure/Filters/ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter.cs b/src/Altinn.App.Api/Infrastructure/Filters/ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter.cs index 6f305dc93..d7622978a 100644 --- a/src/Altinn.App.Api/Infrastructure/Filters/ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter.cs +++ b/src/Altinn.App.Api/Infrastructure/Filters/ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter.cs @@ -7,86 +7,85 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.Options; -namespace Altinn.App.Api.Infrastructure.Filters +namespace Altinn.App.Api.Infrastructure.Filters; + +/// +/// Represents a class that can perform request forgery checking. +/// +public class ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy { + private readonly IAntiforgery _antiforgery; + private readonly AppSettings _settings; + /// - /// Represents a class that can perform request forgery checking. + /// Initialize a new instance of the class. /// - public class ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy + /// An accessor to the antiforgery system. + /// A reference to the current app settings. + public ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter( + IAntiforgery antiforgery, + IOptionsMonitor settings + ) { - private readonly IAntiforgery _antiforgery; - private readonly AppSettings _settings; + ArgumentNullException.ThrowIfNull(antiforgery); - /// - /// Initialize a new instance of the class. - /// - /// An accessor to the antiforgery system. - /// A reference to the current app settings. - public ValidateAntiforgeryTokenIfAuthCookieAuthorizationFilter( - IAntiforgery antiforgery, - IOptionsMonitor settings - ) - { - ArgumentNullException.ThrowIfNull(antiforgery); + _antiforgery = antiforgery; + _settings = settings.CurrentValue; + } - _antiforgery = antiforgery; - _settings = settings.CurrentValue; + /// + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + if (!context.IsEffectivePolicy(this)) + { + return; } - /// - public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + if (ShouldValidate(context)) { - if (!context.IsEffectivePolicy(this)) + try { - return; + await _antiforgery.ValidateRequestAsync(context.HttpContext); } - - if (ShouldValidate(context)) + catch (AntiforgeryValidationException) { - try - { - await _antiforgery.ValidateRequestAsync(context.HttpContext); - } - catch (AntiforgeryValidationException) - { - context.Result = new AntiforgeryValidationFailedResult(); - } + context.Result = new AntiforgeryValidationFailedResult(); } } + } - /// - /// Method that evaluate if validation is required. - /// - /// The . - /// True if validation is needed. - protected virtual bool ShouldValidate(AuthorizationFilterContext context) - { - ArgumentNullException.ThrowIfNull(context); - - string method = context.HttpContext.Request.Method; - if ( - string.Equals("GET", method, StringComparison.OrdinalIgnoreCase) - || string.Equals("HEAD", method, StringComparison.OrdinalIgnoreCase) - || string.Equals("TRACE", method, StringComparison.OrdinalIgnoreCase) - || string.Equals("OPTIONS", method, StringComparison.OrdinalIgnoreCase) - ) - { - return false; - } + /// + /// Method that evaluate if validation is required. + /// + /// The . + /// True if validation is needed. + protected virtual bool ShouldValidate(AuthorizationFilterContext context) + { + ArgumentNullException.ThrowIfNull(context); - string authCookie = context.HttpContext.Request.Cookies[General.RuntimeCookieName]; - if (authCookie == null) - { - return false; - } + string method = context.HttpContext.Request.Method; + if ( + string.Equals("GET", method, StringComparison.OrdinalIgnoreCase) + || string.Equals("HEAD", method, StringComparison.OrdinalIgnoreCase) + || string.Equals("TRACE", method, StringComparison.OrdinalIgnoreCase) + || string.Equals("OPTIONS", method, StringComparison.OrdinalIgnoreCase) + ) + { + return false; + } - if (_settings.DisableCsrfCheck) - { - return false; - } + string authCookie = context.HttpContext.Request.Cookies[General.RuntimeCookieName]; + if (authCookie == null) + { + return false; + } - // Anything else requires a token. - return true; + if (_settings.DisableCsrfCheck) + { + return false; } + + // Anything else requires a token. + return true; } } diff --git a/src/Altinn.App.Api/Infrastructure/Health/HealthCheck.cs b/src/Altinn.App.Api/Infrastructure/Health/HealthCheck.cs index a34a0d4c7..5b54715d1 100644 --- a/src/Altinn.App.Api/Infrastructure/Health/HealthCheck.cs +++ b/src/Altinn.App.Api/Infrastructure/Health/HealthCheck.cs @@ -1,25 +1,24 @@ #nullable disable using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Altinn.App.Api.Infrastructure.Health +namespace Altinn.App.Api.Infrastructure.Health; + +/// +/// Health check service configured in startup https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks +/// +public class HealthCheck : IHealthCheck { /// - /// Health check service configured in startup https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks + /// Verifies the health status /// - public class HealthCheck : IHealthCheck + /// The healtcheck context + /// The cancellationtoken + /// The health check result + public Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default + ) { - /// - /// Verifies the health status - /// - /// The healtcheck context - /// The cancellationtoken - /// The health check result - public Task CheckHealthAsync( - HealthCheckContext context, - CancellationToken cancellationToken = default - ) - { - return Task.FromResult(HealthCheckResult.Healthy("A healthy result.")); - } + return Task.FromResult(HealthCheckResult.Healthy("A healthy result.")); } } diff --git a/src/Altinn.App.Api/Infrastructure/Middleware/SecurityHeadersMiddleware.cs b/src/Altinn.App.Api/Infrastructure/Middleware/SecurityHeadersMiddleware.cs index ad2d2f6c1..4810579bd 100644 --- a/src/Altinn.App.Api/Infrastructure/Middleware/SecurityHeadersMiddleware.cs +++ b/src/Altinn.App.Api/Infrastructure/Middleware/SecurityHeadersMiddleware.cs @@ -1,42 +1,41 @@ #nullable disable -namespace Altinn.App.Api.Infrastructure.Middleware +namespace Altinn.App.Api.Infrastructure.Middleware; + +/// +/// Middleware for sending security headers in response. +/// +/// The following headers will be set: +/// X-Frame-Options +/// X-Content-Type-Options +/// X-XSS-Protection +/// Referer-Policy +/// +public class SecurityHeadersMiddleware { + private readonly RequestDelegate _next; + /// - /// Middleware for sending security headers in response. - /// - /// The following headers will be set: - /// X-Frame-Options - /// X-Content-Type-Options - /// X-XSS-Protection - /// Referer-Policy + /// Default constructor for ASPNET Core Middleware. /// - public class SecurityHeadersMiddleware + /// The next middleware + public SecurityHeadersMiddleware(RequestDelegate next) { - private readonly RequestDelegate _next; - - /// - /// Default constructor for ASPNET Core Middleware. - /// - /// The next middleware - public SecurityHeadersMiddleware(RequestDelegate next) - { - _next = next; - } + _next = next; + } - /// - /// Executes the middleware. Expects the next middleware to be executed. - /// - /// The current HttpContext - /// - public Task Invoke(HttpContext context) - { - context.Response.Headers.Append("X-Frame-Options", "deny"); - context.Response.Headers.Append("X-Content-Type-Options", "nosniff"); - context.Response.Headers.Append("X-XSS-Protection", "0"); - context.Response.Headers.Append("Referer-Policy", "no-referer"); + /// + /// Executes the middleware. Expects the next middleware to be executed. + /// + /// The current HttpContext + /// + public Task Invoke(HttpContext context) + { + context.Response.Headers.Append("X-Frame-Options", "deny"); + context.Response.Headers.Append("X-Content-Type-Options", "nosniff"); + context.Response.Headers.Append("X-XSS-Protection", "0"); + context.Response.Headers.Append("Referer-Policy", "no-referer"); - return _next(context); - } + return _next(context); } } diff --git a/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs b/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs index 01edcfb01..e5c551ac6 100644 --- a/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs +++ b/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs @@ -11,7 +11,7 @@ internal sealed class TelemetryEnrichingMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; - private static readonly FrozenDictionary> ClaimActions; + private static readonly FrozenDictionary> _claimActions; static TelemetryEnrichingMiddleware() { @@ -56,7 +56,7 @@ static TelemetryEnrichingMiddleware() { AltinnCoreClaimTypes.OrgNumber, static (claim, activity) => activity.SetOrganisationNumber(claim.Value) }, }; - ClaimActions = actions.ToFrozenDictionary(); + _claimActions = actions.ToFrozenDictionary(); } /// @@ -87,7 +87,7 @@ public async Task InvokeAsync(HttpContext context) { foreach (var claim in context.User.Claims) { - if (ClaimActions.TryGetValue(claim.Type, out var action)) + if (_claimActions.TryGetValue(claim.Type, out var action)) { action(claim, activity); } diff --git a/src/Altinn.App.Api/Infrastructure/Telemetry/CustomTelemetryInitializer.cs b/src/Altinn.App.Api/Infrastructure/Telemetry/CustomTelemetryInitializer.cs index 0c65904b1..905f8cfa2 100644 --- a/src/Altinn.App.Api/Infrastructure/Telemetry/CustomTelemetryInitializer.cs +++ b/src/Altinn.App.Api/Infrastructure/Telemetry/CustomTelemetryInitializer.cs @@ -2,24 +2,23 @@ using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.Extensibility; -namespace Altinn.App.Api.Infrastructure.Telemetry +namespace Altinn.App.Api.Infrastructure.Telemetry; + +/// +/// Set up custom telemetry for Application Insights +/// +public class CustomTelemetryInitializer : ITelemetryInitializer { /// - /// Set up custom telemetry for Application Insights + /// Initializes properties of the specified object. /// - public class CustomTelemetryInitializer : ITelemetryInitializer + /// The object to initialize. + public void Initialize(ITelemetry telemetry) { - /// - /// Initializes properties of the specified object. - /// - /// The object to initialize. - public void Initialize(ITelemetry telemetry) + if (string.IsNullOrEmpty(telemetry.Context.Cloud.RoleName)) { - if (string.IsNullOrEmpty(telemetry.Context.Cloud.RoleName)) - { - string roleName = Environment.GetEnvironmentVariable("AppSettings__AppInsightsRoleName"); - telemetry.Context.Cloud.RoleName = roleName; - } + string roleName = Environment.GetEnvironmentVariable("AppSettings__AppInsightsRoleName"); + telemetry.Context.Cloud.RoleName = roleName; } } } diff --git a/src/Altinn.App.Api/Infrastructure/Telemetry/HealthTelemetryFilter.cs b/src/Altinn.App.Api/Infrastructure/Telemetry/HealthTelemetryFilter.cs index aa63dc50a..5b87dd537 100644 --- a/src/Altinn.App.Api/Infrastructure/Telemetry/HealthTelemetryFilter.cs +++ b/src/Altinn.App.Api/Infrastructure/Telemetry/HealthTelemetryFilter.cs @@ -4,45 +4,44 @@ using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; -namespace Altinn.App.Api.Infrastructure.Telemetry +namespace Altinn.App.Api.Infrastructure.Telemetry; + +/// +/// Filter to exclude health check request from Application Insights +/// +[ExcludeFromCodeCoverage] +public class HealthTelemetryFilter : ITelemetryProcessor { + private ITelemetryProcessor _next { get; set; } + /// - /// Filter to exclude health check request from Application Insights + /// Initializes a new instance of the class. /// - [ExcludeFromCodeCoverage] - public class HealthTelemetryFilter : ITelemetryProcessor + public HealthTelemetryFilter(ITelemetryProcessor next) { - private ITelemetryProcessor Next { get; set; } + _next = next; + } - /// - /// Initializes a new instance of the class. - /// - public HealthTelemetryFilter(ITelemetryProcessor next) + /// + public void Process(ITelemetry item) + { + if (ExcludeItemTelemetry(item)) { - Next = next; + return; } - /// - public void Process(ITelemetry item) - { - if (ExcludeItemTelemetry(item)) - { - return; - } + _next.Process(item); + } - Next.Process(item); - } + private static bool ExcludeItemTelemetry(ITelemetry item) + { + RequestTelemetry request = item as RequestTelemetry; - private static bool ExcludeItemTelemetry(ITelemetry item) + if (request != null && request.Url.ToString().EndsWith("/health")) { - RequestTelemetry request = item as RequestTelemetry; - - if (request != null && request.Url.ToString().EndsWith("/health")) - { - return true; - } - - return false; + return true; } + + return false; } } diff --git a/src/Altinn.App.Api/Infrastructure/Telemetry/IdentityTelemetryFilter.cs b/src/Altinn.App.Api/Infrastructure/Telemetry/IdentityTelemetryFilter.cs index 16475b8c5..f0eb983dc 100644 --- a/src/Altinn.App.Api/Infrastructure/Telemetry/IdentityTelemetryFilter.cs +++ b/src/Altinn.App.Api/Infrastructure/Telemetry/IdentityTelemetryFilter.cs @@ -4,61 +4,59 @@ using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.AspNetCore.Http; -namespace Altinn.App.Api.Infrastructure.Telemetry +namespace Altinn.App.Api.Infrastructure.Telemetry; + +/// +/// Filter to enrich request telemetry with identity information +/// +[ExcludeFromCodeCoverage] +public class IdentityTelemetryFilter : ITelemetryProcessor { + private ITelemetryProcessor _next { get; set; } + + private readonly IHttpContextAccessor _httpContextAccessor; + /// - /// Filter to enrich request telemetry with identity information + /// Initializes a new instance of the class. /// - [ExcludeFromCodeCoverage] - public class IdentityTelemetryFilter : ITelemetryProcessor + public IdentityTelemetryFilter(ITelemetryProcessor next, IHttpContextAccessor httpContextAccessor) { - private ITelemetryProcessor Next { get; set; } - - private readonly IHttpContextAccessor _httpContextAccessor; + _next = next; + _httpContextAccessor = httpContextAccessor; + } - /// - /// Initializes a new instance of the class. - /// - public IdentityTelemetryFilter(ITelemetryProcessor next, IHttpContextAccessor httpContextAccessor) - { - Next = next; - _httpContextAccessor = httpContextAccessor; - } + /// + public void Process(ITelemetry item) + { + RequestTelemetry request = item as RequestTelemetry; - /// - public void Process(ITelemetry item) + if (request != null) { - RequestTelemetry request = item as RequestTelemetry; + HttpContext ctx = _httpContextAccessor.HttpContext; - if (request != null) + if (ctx?.User != null) { - HttpContext ctx = _httpContextAccessor.HttpContext; + int? orgNumber = ctx.User.GetOrgNumber(); + int? userId = ctx.User.GetUserIdAsInt(); + int? partyId = ctx.User.GetPartyIdAsInt(); + int authLevel = ctx.User.GetAuthenticationLevel(); - if (ctx?.User != null) - { - int? orgNumber = ctx.User.GetOrgNumber(); - int? userId = ctx.User.GetUserIdAsInt(); - int? partyId = ctx.User.GetPartyIdAsInt(); - int authLevel = ctx.User.GetAuthenticationLevel(); - - request.Properties.Add("partyId", partyId.ToString()); - request.Properties.Add("authLevel", authLevel.ToString()); + request.Properties.Add("partyId", partyId.ToString()); + request.Properties.Add("authLevel", authLevel.ToString()); - if (userId != null) - { - request.Properties.Add("userId", userId.ToString()); - } + if (userId != null) + { + request.Properties.Add("userId", userId.ToString()); + } - if (orgNumber != null) - { - request.Properties.Add("orgNumber", orgNumber.ToString()); - } + if (orgNumber != null) + { + request.Properties.Add("orgNumber", orgNumber.ToString()); } } - - Next.Process(item); } + + _next.Process(item); } } diff --git a/src/Altinn.App.Api/Mappers/SimpleInstanceMapper.cs b/src/Altinn.App.Api/Mappers/SimpleInstanceMapper.cs index 631464570..5655f8a25 100644 --- a/src/Altinn.App.Api/Mappers/SimpleInstanceMapper.cs +++ b/src/Altinn.App.Api/Mappers/SimpleInstanceMapper.cs @@ -1,49 +1,48 @@ using Altinn.App.Api.Models; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Api.Mappers +namespace Altinn.App.Api.Mappers; + +/// +/// Mapper for simple instances. +/// +public static class SimpleInstanceMapper { /// - /// Mapper for simple instances. + /// Maps an instance to a simple instance object /// - public static class SimpleInstanceMapper + /// The instance to map + /// The full name of the entity to last change the instance + public static SimpleInstance MapInstanceToSimpleInstance(Instance instance, string lastChangedByName) { - /// - /// Maps an instance to a simple instance object - /// - /// The instance to map - /// The full name of the entity to last change the instance - public static SimpleInstance MapInstanceToSimpleInstance(Instance instance, string lastChangedByName) + return new SimpleInstance { - return new SimpleInstance - { - Id = instance.Id, - DueBefore = instance.DueBefore, - PresentationTexts = instance.PresentationTexts, - LastChanged = instance.LastChanged, - LastChangedBy = lastChangedByName - }; - } + Id = instance.Id, + DueBefore = instance.DueBefore, + PresentationTexts = instance.PresentationTexts, + LastChanged = instance.LastChanged, + LastChangedBy = lastChangedByName + }; + } - /// - /// Maps a list of instances to a list of simple instances - /// - /// The list of instances to map - /// A dictionary for looking up full name of the entity to last change the instance based on instance.LastChangedBy. - /// A list of simple instances. - public static List MapInstanceListToSimpleInstanceList( - List instances, - Dictionary userDictionary - ) + /// + /// Maps a list of instances to a list of simple instances + /// + /// The list of instances to map + /// A dictionary for looking up full name of the entity to last change the instance based on instance.LastChangedBy. + /// A list of simple instances. + public static List MapInstanceListToSimpleInstanceList( + List instances, + Dictionary userDictionary + ) + { + List simpleInstances = new List(); + foreach (Instance instance in instances) { - List simpleInstances = new List(); - foreach (Instance instance in instances) - { - userDictionary.TryGetValue(instance.LastChangedBy ?? string.Empty, out string? lastChangedByName); - simpleInstances.Add(MapInstanceToSimpleInstance(instance, lastChangedByName ?? string.Empty)); - } - - return simpleInstances; + userDictionary.TryGetValue(instance.LastChangedBy ?? string.Empty, out string? lastChangedByName); + simpleInstances.Add(MapInstanceToSimpleInstance(instance, lastChangedByName ?? string.Empty)); } + + return simpleInstances; } } diff --git a/src/Altinn.App.Api/Models/DataElementFileScanResult.cs b/src/Altinn.App.Api/Models/DataElementFileScanResult.cs index 78e9b539a..0b6db12ae 100644 --- a/src/Altinn.App.Api/Models/DataElementFileScanResult.cs +++ b/src/Altinn.App.Api/Models/DataElementFileScanResult.cs @@ -2,24 +2,23 @@ using System.Text.Json.Serialization; using Altinn.Platform.Storage.Interface.Enums; -namespace Altinn.App.Api.Models +namespace Altinn.App.Api.Models; + +/// +/// File scan result for an individual data element. +/// +public class DataElementFileScanResult { /// - /// File scan result for an individual data element. + /// Gets or sets the data element id /// - public class DataElementFileScanResult - { - /// - /// Gets or sets the data element id - /// - [JsonPropertyName("id")] - public string Id { get; set; } + [JsonPropertyName("id")] + public string Id { get; set; } - /// - /// Gets or sets the file scan result for the data element. - /// - [JsonPropertyName("fileScanResult")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public FileScanResult FileScanResult { get; set; } - } + /// + /// Gets or sets the file scan result for the data element. + /// + [JsonPropertyName("fileScanResult")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public FileScanResult FileScanResult { get; set; } } diff --git a/src/Altinn.App.Api/Models/InstanceFileScanResult.cs b/src/Altinn.App.Api/Models/InstanceFileScanResult.cs index 00259137f..2145e16db 100644 --- a/src/Altinn.App.Api/Models/InstanceFileScanResult.cs +++ b/src/Altinn.App.Api/Models/InstanceFileScanResult.cs @@ -3,78 +3,77 @@ using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Enums; -namespace Altinn.App.Api.Models +namespace Altinn.App.Api.Models; + +/// +/// Light weight model representing an instance and it's file scan result status. +/// +public class InstanceFileScanResult { + private readonly InstanceIdentifier _instanceIdentifier; + private readonly List _dataElements; + /// - /// Light weight model representing an instance and it's file scan result status. + /// Initializes a new instance of the class. /// - public class InstanceFileScanResult + public InstanceFileScanResult(InstanceIdentifier instanceIdentifier) { - private readonly InstanceIdentifier _instanceIdentifier; - private readonly List _dataElements; - - /// - /// Initializes a new instance of the class. - /// - public InstanceFileScanResult(InstanceIdentifier instanceIdentifier) - { - _instanceIdentifier = instanceIdentifier; - _dataElements = new List(); - } + _instanceIdentifier = instanceIdentifier; + _dataElements = new List(); + } - /// - /// Instance id - /// - [JsonPropertyName("id")] - public string Id - { - get { return _instanceIdentifier.ToString(); } - } + /// + /// Instance id + /// + [JsonPropertyName("id")] + public string Id + { + get { return _instanceIdentifier.ToString(); } + } - /// - /// Contains the aggregated file scan result for an instance. - /// Infected = If any data elements has a status of Infected - /// Clean = If all data elements has status Clean - /// Pending = If all or some are Pending and the rest are Clean - /// - [JsonPropertyName("fileScanResult")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public FileScanResult FileScanResult { get; private set; } + /// + /// Contains the aggregated file scan result for an instance. + /// Infected = If any data elements has a status of Infected + /// Clean = If all data elements has status Clean + /// Pending = If all or some are Pending and the rest are Clean + /// + [JsonPropertyName("fileScanResult")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public FileScanResult FileScanResult { get; private set; } - /// - /// File scan result for individual data elements. - /// - [JsonPropertyName("data")] - public IReadOnlyList DataElements => _dataElements.AsReadOnly(); + /// + /// File scan result for individual data elements. + /// + [JsonPropertyName("data")] + public IReadOnlyList DataElements => _dataElements.AsReadOnly(); - /// - /// Adds a individual data element file scan result and updates the aggregated file scan result status - /// - public void AddFileScanResult(DataElementFileScanResult dataElementFileScanResult) + /// + /// Adds a individual data element file scan result and updates the aggregated file scan result status + /// + public void AddFileScanResult(DataElementFileScanResult dataElementFileScanResult) + { + if (dataElementFileScanResult.FileScanResult != FileScanResult.NotApplicable) { - if (dataElementFileScanResult.FileScanResult != FileScanResult.NotApplicable) - { - _dataElements.Add(dataElementFileScanResult); + _dataElements.Add(dataElementFileScanResult); - RecalculateAggregatedStatus(); - } + RecalculateAggregatedStatus(); } + } - private void RecalculateAggregatedStatus() - { - FileScanResult = FileScanResult.Clean; + private void RecalculateAggregatedStatus() + { + FileScanResult = FileScanResult.Clean; - if (_dataElements.Any(dataElement => dataElement.FileScanResult == FileScanResult.Infected)) - { - FileScanResult = FileScanResult.Infected; - } - // This implicitly states that there are no infected files and that they - // either have to be clean or pending - so any pending would result in Pending status - // and if there are no Pending and no Infected they are all Clean. - else if (_dataElements.Any(dataElement => dataElement.FileScanResult == FileScanResult.Pending)) - { - FileScanResult = FileScanResult.Pending; - } + if (_dataElements.Any(dataElement => dataElement.FileScanResult == FileScanResult.Infected)) + { + FileScanResult = FileScanResult.Infected; + } + // This implicitly states that there are no infected files and that they + // either have to be clean or pending - so any pending would result in Pending status + // and if there are no Pending and no Infected they are all Clean. + else if (_dataElements.Any(dataElement => dataElement.FileScanResult == FileScanResult.Pending)) + { + FileScanResult = FileScanResult.Pending; } } } diff --git a/src/Altinn.App.Api/Models/InstansiationInstance.cs b/src/Altinn.App.Api/Models/InstansiationInstance.cs index 72f5dac4c..12ff981fa 100644 --- a/src/Altinn.App.Api/Models/InstansiationInstance.cs +++ b/src/Altinn.App.Api/Models/InstansiationInstance.cs @@ -2,42 +2,41 @@ using System.Text.Json; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Api.Models +namespace Altinn.App.Api.Models; + +/// +/// Specialized model for instansiation of instances +/// +public class InstansiationInstance { /// - /// Specialized model for instansiation of instances + /// Gets or sets the instance owner information. /// - public class InstansiationInstance - { - /// - /// Gets or sets the instance owner information. - /// - public InstanceOwner InstanceOwner { get; set; } + public InstanceOwner InstanceOwner { get; set; } - /// - /// Gets or sets the due date to submit the instance to application owner. - /// - public DateTime? DueBefore { get; set; } + /// + /// Gets or sets the due date to submit the instance to application owner. + /// + public DateTime? DueBefore { get; set; } - /// - /// Gets or sets date and time for when the instance should first become visible for the instance owner. - /// - public DateTime? VisibleAfter { get; set; } + /// + /// Gets or sets date and time for when the instance should first become visible for the instance owner. + /// + public DateTime? VisibleAfter { get; set; } - /// - /// Gets or sets the prefill values for the instance. - /// - public Dictionary Prefill { get; set; } + /// + /// Gets or sets the prefill values for the instance. + /// + public Dictionary Prefill { get; set; } - /// - /// Gets or sets the id of the instance to use as a source for the new instance. - /// - public string SourceInstanceId { get; set; } + /// + /// Gets or sets the id of the instance to use as a source for the new instance. + /// + public string SourceInstanceId { get; set; } - /// - public override string ToString() - { - return JsonSerializer.Serialize(this); - } + /// + public override string ToString() + { + return JsonSerializer.Serialize(this); } } diff --git a/src/Altinn.App.Api/Models/TagsList.cs b/src/Altinn.App.Api/Models/TagsList.cs index 1df8d8582..6141cd902 100644 --- a/src/Altinn.App.Api/Models/TagsList.cs +++ b/src/Altinn.App.Api/Models/TagsList.cs @@ -1,17 +1,16 @@ #nullable disable using System.Text.Json.Serialization; -namespace Altinn.App.Api.Models +namespace Altinn.App.Api.Models; + +/// +/// Represents the response from an API endpoint providing a list of tags. +/// +public class TagsList { /// - /// Represents the response from an API endpoint providing a list of tags. + /// A list of tags represented as sting values. /// - public class TagsList - { - /// - /// A list of tags represented as sting values. - /// - [JsonPropertyName("tags")] - public List Tags { get; set; } = new List(); - } + [JsonPropertyName("tags")] + public List Tags { get; set; } = new List(); } diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 565d61ad0..aed4577ae 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -1,8 +1,10 @@ net8.0 - enable - enable + Altinn;Studio;App;Core + + This class library holds all the core features used by a standard Altinn 3 App. + @@ -30,4 +32,4 @@ - + \ No newline at end of file diff --git a/src/Altinn.App.Core/Configuration/AppSettings.cs b/src/Altinn.App.Core/Configuration/AppSettings.cs index 5f98b5783..59c70b419 100644 --- a/src/Altinn.App.Core/Configuration/AppSettings.cs +++ b/src/Altinn.App.Core/Configuration/AppSettings.cs @@ -1,225 +1,221 @@ -using Altinn.App.Core.Models; -using Newtonsoft.Json; +namespace Altinn.App.Core.Configuration; -namespace Altinn.App.Core.Configuration +/// +/// Class that represents the ServiceRepositorySettings +/// +// TODO: IOptions validation so that we know which of these properties are required +public class AppSettings { /// - /// Class that represents the ServiceRepositorySettings - /// - // TODO: IOptions validation so that we know which of these properties are required - public class AppSettings - { - /// - /// Constant for the location of json schema file - /// - public const string JSON_SCHEMA_FILENAME = "schema.json"; - - /// - /// Constant for the location of validation configuration file - /// - public const string VALIDATION_CONFIG_FILENAME = "validation.json"; - - /// - /// The app configuration baseUrl where files are stored in the container - /// - public string AppBasePath { get; set; } = string.Empty; - - /// - /// The app configuration baseUrl where files are stored in the container - /// - public string ConfigurationFolder { get; set; } = "config/"; - - /// - /// The app options base folder where files are stored in the container - /// - public string OptionsFolder { get; set; } = "options/"; - - /// - /// The ui configuration baseUrl where files are stored in the container - /// - public string UiFolder { get; set; } = "ui/"; - - /// - /// The models folder - /// - public string ModelsFolder { get; set; } = "models/"; - - /// - /// The text folder - /// - public string TextFolder { get; set; } = "texts/"; - - /// - /// The process folder - /// - public string ProcessFolder { get; set; } = "process/"; - - /// - /// The authorization folder - /// - public string AuthorizationFolder { get; set; } = "authorization/"; - - /// - /// Gets or sets the BaseResourceFolderContainer that identifies where in the docker container the runtime can find files needed - /// - // TODO: can this be removed? - // Env var being set is ServiceRepositorySettings__BaseResourceFolderContainer, but this prop is not used anywhere + /// Constant for the location of json schema file + /// + public const string JSON_SCHEMA_FILENAME = "schema.json"; + + /// + /// Constant for the location of validation configuration file + /// + public const string VALIDATION_CONFIG_FILENAME = "validation.json"; + + /// + /// The app configuration baseUrl where files are stored in the container + /// + public string AppBasePath { get; set; } = string.Empty; + + /// + /// The app configuration baseUrl where files are stored in the container + /// + public string ConfigurationFolder { get; set; } = "config/"; + + /// + /// The app options base folder where files are stored in the container + /// + public string OptionsFolder { get; set; } = "options/"; + + /// + /// The ui configuration baseUrl where files are stored in the container + /// + public string UiFolder { get; set; } = "ui/"; + + /// + /// The models folder + /// + public string ModelsFolder { get; set; } = "models/"; + + /// + /// The text folder + /// + public string TextFolder { get; set; } = "texts/"; + + /// + /// The process folder + /// + public string ProcessFolder { get; set; } = "process/"; + + /// + /// The authorization folder + /// + public string AuthorizationFolder { get; set; } = "authorization/"; + + /// + /// Gets or sets the BaseResourceFolderContainer that identifies where in the docker container the runtime can find files needed + /// + // TODO: can this be removed? + // Env var being set is ServiceRepositorySettings__BaseResourceFolderContainer, but this prop is not used anywhere #nullable disable - [Obsolete("This is not used, and will be removed in the next major version")] - public string BaseResourceFolderContainer { get; set; } + [Obsolete("This is not used, and will be removed in the next major version")] + public string BaseResourceFolderContainer { get; set; } #nullable restore - /// - /// Gets or sets The name of the FormLayout json file Name - /// - public string FormLayoutJSONFileName { get; set; } = "FormLayout.json"; - - /// - /// Gets or sets the name of the layout setting file name - /// - public string FormLayoutSettingsFileName { get; set; } = "Settings.json"; - - /// - /// Gets or sets the name of the layoutsets file name - /// - public string LayoutSetsFileName { get; set; } = "layout-sets.json"; - - /// - /// Gets or sets the name of the layout setting file name - /// - public string FooterFileName { get; set; } = "footer.json"; - - /// - /// Gets or sets The name of the rule configuration json file Name - /// - public string RuleConfigurationJSONFileName { get; set; } = "RuleConfiguration.json"; - - /// - /// Gets or sets The JSON schema file name - /// - public string JsonSchemaFileName { get; set; } = JSON_SCHEMA_FILENAME; - - /// - /// Gets or sets The JSON schema file name - /// - public string ValidationConfigurationFileName { get; set; } = VALIDATION_CONFIG_FILENAME; - - /// - /// Gets or sets the filename for application meta data - /// - public string ApplicationMetadataFileName { get; set; } = "applicationmetadata.json"; - - /// - /// Gets the location for the XACML Policy file - /// - public string ApplicationXACMLPolicyFileName { get; init; } = "policy.xml"; - - /// - /// Gets or sets the filename for process file - /// - public string ProcessFileName { get; set; } = "process.bpmn"; - - /// - /// Gets or sets React file name - /// - public string RuntimeAppFileName { get; set; } = "runtime.js"; - - /// - /// Gets or sets React CSS file name - /// - public string RuntimeCssFileName { get; set; } = "runtime.css"; - - /// - /// Gets or sets styles config file name for the app. - /// - public string ServiceStylesConfigFileName { get; set; } = "Styles.json"; - - /// - /// Gets or sets default Bootstrap url - /// - public string DefaultBootstrapUrl { get; set; } = - "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"; - - /// - /// Gets or sets the filename for the instantiation handler - /// - public string RuleHandlerFileName { get; set; } = "RuleHandler.js"; - - /// - /// Open Id Connect Well known endpoint - /// + /// + /// Gets or sets The name of the FormLayout json file Name + /// + public string FormLayoutJSONFileName { get; set; } = "FormLayout.json"; + + /// + /// Gets or sets the name of the layout setting file name + /// + public string FormLayoutSettingsFileName { get; set; } = "Settings.json"; + + /// + /// Gets or sets the name of the layoutsets file name + /// + public string LayoutSetsFileName { get; set; } = "layout-sets.json"; + + /// + /// Gets or sets the name of the layout setting file name + /// + public string FooterFileName { get; set; } = "footer.json"; + + /// + /// Gets or sets The name of the rule configuration json file Name + /// + public string RuleConfigurationJSONFileName { get; set; } = "RuleConfiguration.json"; + + /// + /// Gets or sets The JSON schema file name + /// + public string JsonSchemaFileName { get; set; } = JSON_SCHEMA_FILENAME; + + /// + /// Gets or sets The JSON schema file name + /// + public string ValidationConfigurationFileName { get; set; } = VALIDATION_CONFIG_FILENAME; + + /// + /// Gets or sets the filename for application meta data + /// + public string ApplicationMetadataFileName { get; set; } = "applicationmetadata.json"; + + /// + /// Gets the location for the XACML Policy file + /// + public string ApplicationXACMLPolicyFileName { get; init; } = "policy.xml"; + + /// + /// Gets or sets the filename for process file + /// + public string ProcessFileName { get; set; } = "process.bpmn"; + + /// + /// Gets or sets React file name + /// + public string RuntimeAppFileName { get; set; } = "runtime.js"; + + /// + /// Gets or sets React CSS file name + /// + public string RuntimeCssFileName { get; set; } = "runtime.css"; + + /// + /// Gets or sets styles config file name for the app. + /// + public string ServiceStylesConfigFileName { get; set; } = "Styles.json"; + + /// + /// Gets or sets default Bootstrap url + /// + public string DefaultBootstrapUrl { get; set; } = + "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"; + + /// + /// Gets or sets the filename for the instantiation handler + /// + public string RuleHandlerFileName { get; set; } = "RuleHandler.js"; + + /// + /// Open Id Connect Well known endpoint + /// #nullable disable - public string OpenIdWellKnownEndpoint { get; set; } + public string OpenIdWellKnownEndpoint { get; set; } - /// - /// App OIDC provider for application that overrides the default OIDC provider in platform - /// - public string AppOidcProvider { get; set; } + /// + /// App OIDC provider for application that overrides the default OIDC provider in platform + /// + public string AppOidcProvider { get; set; } - /// - /// Name of the cookie for runtime - /// - public string RuntimeCookieName { get; set; } + /// + /// Name of the cookie for runtime + /// + public string RuntimeCookieName { get; set; } #nullable restore - /// - /// Option to disable csrf check - /// - public bool DisableCsrfCheck { get; set; } - - /// - /// Cache lifetime for app resources - /// - public int CacheResourceLifeTimeInSeconds { get; set; } = 3600; - - /// - /// Gets or sets a value indicating whether the app should send events to the Events component. - /// - public bool RegisterEventsWithEventsComponent { get; set; } = false; - - /// - /// Gets or sets a value indicating whether the eFormidlingIntegration should be enabled. - /// - public bool EnableEFormidling { get; set; } = false; - - /// - /// Gets or sets the sender of the eFormidling shipment. - /// - /// - /// If overriding for testing purposes, ensure to only update appsettings.Development. - /// Integration will not work if value is overrided in staging or prodution. - /// - public string EFormidlingSender { get; set; } = "910075918"; - - /// - /// Gets or sets the version of the application. - /// + /// + /// Option to disable csrf check + /// + public bool DisableCsrfCheck { get; set; } + + /// + /// Cache lifetime for app resources + /// + public int CacheResourceLifeTimeInSeconds { get; set; } = 3600; + + /// + /// Gets or sets a value indicating whether the app should send events to the Events component. + /// + public bool RegisterEventsWithEventsComponent { get; set; } = false; + + /// + /// Gets or sets a value indicating whether the eFormidlingIntegration should be enabled. + /// + public bool EnableEFormidling { get; set; } = false; + + /// + /// Gets or sets the sender of the eFormidling shipment. + /// + /// + /// If overriding for testing purposes, ensure to only update appsettings.Development. + /// Integration will not work if value is overrided in staging or prodution. + /// + public string EFormidlingSender { get; set; } = "910075918"; + + /// + /// Gets or sets the version of the application. + /// #nullable disable - public string AppVersion { get; set; } + public string AppVersion { get; set; } #nullable restore - /// - /// Enable the functionality to load layout in backend and remove data from hidden components before task completion - /// - public bool RemoveHiddenData { get; set; } = false; - - /// - /// Enable the functionality to load layout in backend and validate required fields as defined in the layout - /// - public bool RequiredValidation { get; set; } = false; - - /// - /// Enable the functionality to run expression validation in backend - /// - public bool ExpressionValidation { get; set; } = false; - - /// - /// Enables OpenTelemetry as a substitute for Application Insights SDK - /// Improves instrumentation throughout the Altinn app libraries. - /// - public bool UseOpenTelemetry { get; set; } - } + /// + /// Enable the functionality to load layout in backend and remove data from hidden components before task completion + /// + public bool RemoveHiddenData { get; set; } = false; + + /// + /// Enable the functionality to load layout in backend and validate required fields as defined in the layout + /// + public bool RequiredValidation { get; set; } = false; + + /// + /// Enable the functionality to run expression validation in backend + /// + public bool ExpressionValidation { get; set; } = false; + + /// + /// Enables OpenTelemetry as a substitute for Application Insights SDK + /// Improves instrumentation throughout the Altinn app libraries. + /// + public bool UseOpenTelemetry { get; set; } } diff --git a/src/Altinn.App.Core/Configuration/CacheSettings.cs b/src/Altinn.App.Core/Configuration/CacheSettings.cs index 599d37d15..2cbc06faf 100644 --- a/src/Altinn.App.Core/Configuration/CacheSettings.cs +++ b/src/Altinn.App.Core/Configuration/CacheSettings.cs @@ -1,13 +1,12 @@ -namespace Altinn.App.Core.Configuration +namespace Altinn.App.Core.Configuration; + +/// +/// Represents caching settings used by the platform services +/// +public class CacheSettings { /// - /// Represents caching settings used by the platform services + /// The number of seconds the user profile will be kept in the cache /// - public class CacheSettings - { - /// - /// The number of seconds the user profile will be kept in the cache - /// - public int ProfileCacheLifetimeSeconds { get; set; } = 540; - } + public int ProfileCacheLifetimeSeconds { get; set; } = 540; } diff --git a/src/Altinn.App.Core/Configuration/FrontEndSettings.cs b/src/Altinn.App.Core/Configuration/FrontEndSettings.cs index a7bcfff36..d7ce52893 100644 --- a/src/Altinn.App.Core/Configuration/FrontEndSettings.cs +++ b/src/Altinn.App.Core/Configuration/FrontEndSettings.cs @@ -1,8 +1,7 @@ -namespace Altinn.App.Core.Configuration -{ - /// - /// Represents settings used by the front end react application. These settings are separate because they are - /// exposed to the end user. They should never contain secrets. - /// - public class FrontEndSettings : Dictionary { } -} +namespace Altinn.App.Core.Configuration; + +/// +/// Represents settings used by the front end react application. These settings are separate because they are +/// exposed to the end user. They should never contain secrets. +/// +public class FrontEndSettings : Dictionary { } diff --git a/src/Altinn.App.Core/Configuration/GeneralSettings.cs b/src/Altinn.App.Core/Configuration/GeneralSettings.cs index b4e20fc24..f2c2fcbb3 100644 --- a/src/Altinn.App.Core/Configuration/GeneralSettings.cs +++ b/src/Altinn.App.Core/Configuration/GeneralSettings.cs @@ -1,78 +1,77 @@ using System.Text; using Altinn.App.Core.Models; -namespace Altinn.App.Core.Configuration +namespace Altinn.App.Core.Configuration; + +/// +/// General configuration settings +/// +public class GeneralSettings { /// - /// General configuration settings + /// Gets or sets the soft validation prefix. /// - public class GeneralSettings - { - /// - /// Gets or sets the soft validation prefix. - /// - public string SoftValidationPrefix { get; set; } = "*WARNING*"; + public string SoftValidationPrefix { get; set; } = "*WARNING*"; - /// - /// Gets or sets the fixed validation prefix. - /// - public string FixedValidationPrefix { get; set; } = "*FIXED*"; + /// + /// Gets or sets the fixed validation prefix. + /// + public string FixedValidationPrefix { get; set; } = "*FIXED*"; - /// - /// Gets or sets the info validation prefix. - /// - public string InfoValidationPrefix { get; set; } = "*INFO*"; + /// + /// Gets or sets the info validation prefix. + /// + public string InfoValidationPrefix { get; set; } = "*INFO*"; - /// - /// Gets or sets the success validation prefix. - /// - public string SuccessValidationPrefix { get; set; } = "*SUCCESS*"; + /// + /// Gets or sets the success validation prefix. + /// + public string SuccessValidationPrefix { get; set; } = "*SUCCESS*"; - /// - /// Gets or sets the host name. This is used for cookes, - /// and might not be the full url you can access the app on. - /// - public string HostName { get; set; } = "local.altinn.cloud"; + /// + /// Gets or sets the host name. This is used for cookes, + /// and might not be the full url you can access the app on. + /// + public string HostName { get; set; } = "local.altinn.cloud"; - /// - /// The externally accesible base url for the app with trailing / - /// - /// - /// This setting offers the following replacemnts - ///
- /// {hostName}: GeneralSettings::Hostname
- /// {org}: Org from applicationmetadata.json
- /// {app}: App from applicationmetadata.json
- ///
- public string ExternalAppBaseUrl { get; set; } = "http://{hostName}/{org}/{app}/"; + /// + /// The externally accesible base url for the app with trailing / + /// + /// + /// This setting offers the following replacemnts + ///
+ /// {hostName}: GeneralSettings::Hostname
+ /// {org}: Org from applicationmetadata.json
+ /// {app}: App from applicationmetadata.json
+ ///
+ public string ExternalAppBaseUrl { get; set; } = "http://{hostName}/{org}/{app}/"; - /// - /// Convenience method to get with segments replaced and trailing / - /// - public string FormattedExternalAppBaseUrl(AppIdentifier app) - { - var sb = new StringBuilder(ExternalAppBaseUrl.ToLowerInvariant()); - sb.Replace("{hostname}", HostName); - sb.Replace("{org}", app.Org); - sb.Replace("{app}", app.App); - return sb.ToString(); - } + /// + /// Convenience method to get with segments replaced and trailing / + /// + public string FormattedExternalAppBaseUrl(AppIdentifier app) + { + var sb = new StringBuilder(ExternalAppBaseUrl.ToLowerInvariant()); + sb.Replace("{hostname}", HostName); + sb.Replace("{org}", app.Org); + sb.Replace("{app}", app.App); + return sb.ToString(); + } - /// - /// Gets or sets the AltinnParty cookie name. - /// - public string AltinnPartyCookieName { get; set; } = "AltinnPartyId"; + /// + /// Gets or sets the AltinnParty cookie name. + /// + public string AltinnPartyCookieName { get; set; } = "AltinnPartyId"; - /// - /// Gets the altinn party cookie from kubernetes environment variables or appSettings if environment variable is missing. - /// - public string GetAltinnPartyCookieName + /// + /// Gets the altinn party cookie from kubernetes environment variables or appSettings if environment variable is missing. + /// + public string GetAltinnPartyCookieName + { + get { - get - { - return Environment.GetEnvironmentVariable("GeneralSettings__AltinnPartyCookieName") - ?? AltinnPartyCookieName; - } + return Environment.GetEnvironmentVariable("GeneralSettings__AltinnPartyCookieName") + ?? AltinnPartyCookieName; } } } diff --git a/src/Altinn.App.Core/Configuration/PlatformSettings.cs b/src/Altinn.App.Core/Configuration/PlatformSettings.cs index ad8f3b1cc..0cc5e033f 100644 --- a/src/Altinn.App.Core/Configuration/PlatformSettings.cs +++ b/src/Altinn.App.Core/Configuration/PlatformSettings.cs @@ -1,58 +1,57 @@ -namespace Altinn.App.Core.Configuration +namespace Altinn.App.Core.Configuration; + +/// +/// Represents a set of configuration options when communicating with the platform API. +/// Instances of this class is initialised with values from app settings. Some values can be overridden by environment variables. +/// +public class PlatformSettings { /// - /// Represents a set of configuration options when communicating with the platform API. - /// Instances of this class is initialised with values from app settings. Some values can be overridden by environment variables. - /// - public class PlatformSettings - { - /// - /// Gets or sets the url for the Storage API endpoint. - /// - public string ApiStorageEndpoint { get; set; } = "http://localhost:5101/storage/api/v1/"; - - /// - /// Gets or sets the url for the Register API endpoint. - /// - public string ApiRegisterEndpoint { get; set; } = "http://localhost:5101/register/api/v1/"; - - /// - /// Gets or sets the url for the Profile API endpoint. - /// - public string ApiProfileEndpoint { get; set; } = "http://localhost:5101/profile/api/v1/"; - - /// - /// Gets or sets the url for the Authentication API endpoint. - /// - public string ApiAuthenticationEndpoint { get; set; } = "http://localhost:5101/authentication/api/v1/"; - - /// - /// Gets or sets the url for the Authorization API endpoint. - /// - public string ApiAuthorizationEndpoint { get; set; } = "http://localhost:5101/authorization/api/v1/"; - - /// - /// Gets or sets the url for the Events API endpoint. - /// - public string ApiEventsEndpoint { get; set; } = "http://localhost:5101/events/api/v1/"; - - /// - /// Gets or sets tue url for the new browser based PDF API endpoint. - /// - public string ApiPdf2Endpoint { get; set; } = "http://localhost:5300/pdf"; - - /// - /// Gets or sets the url for the Notification API endpoint. - /// - public string ApiNotificationEndpoint { get; set; } = "http://localhost:5101/notifications/api/v1/"; - - /// - /// Gets or sets the subscription key value to use in requests against the platform. - /// A new subscription key is generated automatically every time an app is deployed to an environment. The new key is then automatically - /// added to the environment for the app code during deploy. This will override the value stored in app settings. - /// + /// Gets or sets the url for the Storage API endpoint. + /// + public string ApiStorageEndpoint { get; set; } = "http://localhost:5101/storage/api/v1/"; + + /// + /// Gets or sets the url for the Register API endpoint. + /// + public string ApiRegisterEndpoint { get; set; } = "http://localhost:5101/register/api/v1/"; + + /// + /// Gets or sets the url for the Profile API endpoint. + /// + public string ApiProfileEndpoint { get; set; } = "http://localhost:5101/profile/api/v1/"; + + /// + /// Gets or sets the url for the Authentication API endpoint. + /// + public string ApiAuthenticationEndpoint { get; set; } = "http://localhost:5101/authentication/api/v1/"; + + /// + /// Gets or sets the url for the Authorization API endpoint. + /// + public string ApiAuthorizationEndpoint { get; set; } = "http://localhost:5101/authorization/api/v1/"; + + /// + /// Gets or sets the url for the Events API endpoint. + /// + public string ApiEventsEndpoint { get; set; } = "http://localhost:5101/events/api/v1/"; + + /// + /// Gets or sets tue url for the new browser based PDF API endpoint. + /// + public string ApiPdf2Endpoint { get; set; } = "http://localhost:5300/pdf"; + + /// + /// Gets or sets the url for the Notification API endpoint. + /// + public string ApiNotificationEndpoint { get; set; } = "http://localhost:5101/notifications/api/v1/"; + + /// + /// Gets or sets the subscription key value to use in requests against the platform. + /// A new subscription key is generated automatically every time an app is deployed to an environment. The new key is then automatically + /// added to the environment for the app code during deploy. This will override the value stored in app settings. + /// #nullable disable - public string SubscriptionKey { get; set; } + public string SubscriptionKey { get; set; } #nullable restore - } } diff --git a/src/Altinn.App.Core/Constants/AuthzConstants.cs b/src/Altinn.App.Core/Constants/AuthzConstants.cs index 65ec641be..97517042d 100644 --- a/src/Altinn.App.Core/Constants/AuthzConstants.cs +++ b/src/Altinn.App.Core/Constants/AuthzConstants.cs @@ -1,28 +1,27 @@ -namespace Altinn.App.Core.Constants +namespace Altinn.App.Core.Constants; + +/// +/// Constands related to authorization. +/// +public static class AuthzConstants { /// - /// Constands related to authorization. + /// Policy tag for reading an instance. /// - public static class AuthzConstants - { - /// - /// Policy tag for reading an instance. - /// - public const string POLICY_INSTANCE_WRITE = "InstanceWrite"; + public const string POLICY_INSTANCE_WRITE = "InstanceWrite"; - /// - /// Policy tag for writing on instance. - /// - public const string POLICY_INSTANCE_READ = "InstanceRead"; + /// + /// Policy tag for writing on instance. + /// + public const string POLICY_INSTANCE_READ = "InstanceRead"; - /// - /// Policy tag for writing on instance. - /// - public const string POLICY_INSTANCE_DELETE = "InstanceDelete"; + /// + /// Policy tag for writing on instance. + /// + public const string POLICY_INSTANCE_DELETE = "InstanceDelete"; - /// - /// Policy tag for authorizing client scope. - /// - public const string POLICY_INSTANCE_COMPLETE = "InstanceComplete"; - } + /// + /// Policy tag for authorizing client scope. + /// + public const string POLICY_INSTANCE_COMPLETE = "InstanceComplete"; } diff --git a/src/Altinn.App.Core/Constants/General.cs b/src/Altinn.App.Core/Constants/General.cs index 64261b21f..40f871807 100644 --- a/src/Altinn.App.Core/Constants/General.cs +++ b/src/Altinn.App.Core/Constants/General.cs @@ -1,38 +1,37 @@ -namespace Altinn.App.Core.Constants +namespace Altinn.App.Core.Constants; + +/// +/// app token +/// +public static class General { /// - /// app token + /// app token name /// - public static class General - { - /// - /// app token name - /// - public const string AppTokenName = "AltinnToken"; + public const string AppTokenName = "AltinnToken"; - /// - /// The name of the authorization token - /// - public const string AuthorizationTokenHeaderName = "Authorization"; + /// + /// The name of the authorization token + /// + public const string AuthorizationTokenHeaderName = "Authorization"; - /// - /// The name of the cookie used for asp authentication in runtime application - /// - public const string RuntimeCookieName = "AltinnStudioRuntime"; + /// + /// The name of the cookie used for asp authentication in runtime application + /// + public const string RuntimeCookieName = "AltinnStudioRuntime"; - /// - /// The name of the cookie used for asp authentication in designer application - /// - public const string DesignerCookieName = "AltinnStudioDesigner"; + /// + /// The name of the cookie used for asp authentication in designer application + /// + public const string DesignerCookieName = "AltinnStudioDesigner"; - /// - /// Header key for API management subscription key - /// - public const string SubscriptionKeyHeaderName = "Ocp-Apim-Subscription-Key"; + /// + /// Header key for API management subscription key + /// + public const string SubscriptionKeyHeaderName = "Ocp-Apim-Subscription-Key"; - /// - /// Header key for access token for eFormidling Integration Point - /// - public const string EFormidlingAccessTokenHeaderName = "AltinnIntegrationPointToken"; - } + /// + /// Header key for access token for eFormidling Integration Point + /// + public const string EFormidlingAccessTokenHeaderName = "AltinnIntegrationPointToken"; } diff --git a/src/Altinn.App.Core/EFormidling/EformidlingConstants.cs b/src/Altinn.App.Core/EFormidling/EformidlingConstants.cs index b04955249..840090ebc 100644 --- a/src/Altinn.App.Core/EFormidling/EformidlingConstants.cs +++ b/src/Altinn.App.Core/EFormidling/EformidlingConstants.cs @@ -1,14 +1,13 @@ -namespace Altinn.App.Core.EFormidling +namespace Altinn.App.Core.EFormidling; + +/// +/// Shared constants within the Eformidling area. +/// +public static class EformidlingConstants { /// - /// Shared constants within the Eformidling area. + /// Name of event type for publishing and subscribing to be remined about instances sent + /// and that needs status checking. /// - public static class EformidlingConstants - { - /// - /// Name of event type for publishing and subscribing to be remined about instances sent - /// and that needs status checking. - /// - public const string CheckInstanceStatusEventType = "app.eformidling.reminder.checkinstancestatus"; - } + public const string CheckInstanceStatusEventType = "app.eformidling.reminder.checkinstancestatus"; } diff --git a/src/Altinn.App.Core/EFormidling/EformidlingStartup.cs b/src/Altinn.App.Core/EFormidling/EformidlingStartup.cs index d3533caba..9337136a6 100644 --- a/src/Altinn.App.Core/EFormidling/EformidlingStartup.cs +++ b/src/Altinn.App.Core/EFormidling/EformidlingStartup.cs @@ -4,67 +4,66 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Altinn.App.Core.EFormidling +namespace Altinn.App.Core.EFormidling; + +/// +/// Hosted service to set up prequisites for Eformidling integration. +/// +public class EformidlingStartup : IHostedService { + private readonly AppIdentifier _appIdentifier; + private readonly IEventsSubscription _eventsSubscriptionClient; + private readonly ILogger _logger; + /// - /// Hosted service to set up prequisites for Eformidling integration. + /// Initializes a new instance of the class. /// - public class EformidlingStartup : IHostedService + public EformidlingStartup( + AppIdentifier appId, + IEventsSubscription eventsSubscriptionClient, + ILogger logger + ) { - private readonly AppIdentifier _appIdentifier; - private readonly IEventsSubscription _eventsSubscriptionClient; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - public EformidlingStartup( - AppIdentifier appId, - IEventsSubscription eventsSubscriptionClient, - ILogger logger - ) - { - _appIdentifier = appId; - _eventsSubscriptionClient = eventsSubscriptionClient; - _logger = logger; - } + _appIdentifier = appId; + _eventsSubscriptionClient = eventsSubscriptionClient; + _logger = logger; + } - /// - public async Task StartAsync(CancellationToken cancellationToken) + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + var eventType = EformidlingConstants.CheckInstanceStatusEventType; + try { - var eventType = EformidlingConstants.CheckInstanceStatusEventType; - try - { - Subscription subscription = await _eventsSubscriptionClient.AddSubscription( - _appIdentifier.Org, - _appIdentifier.App, - eventType - ); + Subscription subscription = await _eventsSubscriptionClient.AddSubscription( + _appIdentifier.Org, + _appIdentifier.App, + eventType + ); - _logger.LogInformation( - "Successfully subscribed to event {eventType} for app {appIdentifier}. Subscription {subscriptionId} is being used.", - eventType, - _appIdentifier, - subscription.Id - ); - } - catch (Exception ex) - { - _logger.LogError( - "Unable to subscribe to event {eventType} for app {appIdentifier}. Received exception {exceptionMessage} with {stackTrace}", - eventType, - _appIdentifier, - ex.Message, - ex.StackTrace - ); - throw; - } + _logger.LogInformation( + "Successfully subscribed to event {eventType} for app {appIdentifier}. Subscription {subscriptionId} is being used.", + eventType, + _appIdentifier, + subscription.Id + ); } - - /// - public Task StopAsync(CancellationToken cancellationToken) + catch (Exception ex) { - return Task.CompletedTask; + _logger.LogError( + "Unable to subscribe to event {eventType} for app {appIdentifier}. Received exception {exceptionMessage} with {stackTrace}", + eventType, + _appIdentifier, + ex.Message, + ex.StackTrace + ); + throw; } } + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } } diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs index 7ed19ca88..3591b88f5 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs @@ -1,20 +1,19 @@ -namespace Altinn.App.Core.EFormidling.Implementation +namespace Altinn.App.Core.EFormidling.Implementation; + +/// +/// Exception thrown when Eformidling is unable to process the message delivered to +/// the integration point. +/// +public class EformidlingDeliveryException : Exception { - /// - /// Exception thrown when Eformidling is unable to process the message delivered to - /// the integration point. - /// - public class EformidlingDeliveryException : Exception - { - /// - public EformidlingDeliveryException() { } + /// + public EformidlingDeliveryException() { } - /// - public EformidlingDeliveryException(string message) - : base(message) { } + /// + public EformidlingDeliveryException(string message) + : base(message) { } - /// - public EformidlingDeliveryException(string message, Exception inner) - : base(message, inner) { } - } + /// + public EformidlingDeliveryException(string message, Exception inner) + : base(message, inner) { } } diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs index 69b18852c..bd7abe987 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs @@ -19,243 +19,241 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; -namespace Altinn.App.Core.EFormidling.Implementation +namespace Altinn.App.Core.EFormidling.Implementation; + +/// +/// Handles status checking of messages sent through the Eformidling integration point. +/// +[Obsolete("UseEformidlingStatusCheckEventHandler2 instead. This class will be removed in V9.")] +public class EformidlingStatusCheckEventHandler : IEventHandler { + private readonly IEFormidlingClient _eFormidlingClient; + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IMaskinportenService _maskinportenService; + private readonly MaskinportenSettings _maskinportenSettings; + private readonly IX509CertificateProvider _x509CertificateProvider; + private readonly PlatformSettings _platformSettings; + private readonly GeneralSettings _generalSettings; + /// - /// Handles status checking of messages sent through the Eformidling integration point. + /// Initializes a new instance of the class. /// - [Obsolete("UseEformidlingStatusCheckEventHandler2 instead. This class will be removed in V9.")] - public class EformidlingStatusCheckEventHandler : IEventHandler + public EformidlingStatusCheckEventHandler( + IEFormidlingClient eFormidlingClient, + IHttpClientFactory httpClientFactory, + ILogger logger, + IMaskinportenService maskinportenService, + IOptions maskinportenSettings, + IX509CertificateProvider x509CertificateProvider, + IOptions platformSettings, + IOptions generalSettings + ) { - private readonly IEFormidlingClient _eFormidlingClient; - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IMaskinportenService _maskinportenService; - private readonly MaskinportenSettings _maskinportenSettings; - private readonly IX509CertificateProvider _x509CertificateProvider; - private readonly PlatformSettings _platformSettings; - private readonly GeneralSettings _generalSettings; - - /// - /// Initializes a new instance of the class. - /// - public EformidlingStatusCheckEventHandler( - IEFormidlingClient eFormidlingClient, - IHttpClientFactory httpClientFactory, - ILogger logger, - IMaskinportenService maskinportenService, - IOptions maskinportenSettings, - IX509CertificateProvider x509CertificateProvider, - IOptions platformSettings, - IOptions generalSettings - ) - { - _eFormidlingClient = eFormidlingClient; - _logger = logger; - _httpClientFactory = httpClientFactory; - _maskinportenService = maskinportenService; - _maskinportenSettings = maskinportenSettings.Value; - _x509CertificateProvider = x509CertificateProvider; - _platformSettings = platformSettings.Value; - _generalSettings = generalSettings.Value; - } + _eFormidlingClient = eFormidlingClient; + _logger = logger; + _httpClientFactory = httpClientFactory; + _maskinportenService = maskinportenService; + _maskinportenSettings = maskinportenSettings.Value; + _x509CertificateProvider = x509CertificateProvider; + _platformSettings = platformSettings.Value; + _generalSettings = generalSettings.Value; + } - /// - public string EventType { get; internal set; } = EformidlingConstants.CheckInstanceStatusEventType; + /// + public string EventType { get; internal set; } = EformidlingConstants.CheckInstanceStatusEventType; - /// - public async Task ProcessEvent(CloudEvent cloudEvent) - { - var subject = cloudEvent.Subject; - - _logger.LogInformation("Received reminder for subject {subject}", subject); - - AppIdentifier appIdentifier = AppIdentifier.CreateFromUrl(cloudEvent.Source.ToString()); - InstanceIdentifier instanceIdentifier = InstanceIdentifier.CreateFromUrl(cloudEvent.Source.ToString()); - - // Instance GUID is used as shipment identifier - string id = instanceIdentifier.InstanceGuid.ToString(); - Statuses statusesForShipment = await GetStatusesForShipment(id); - if (MessageDeliveredToKS(statusesForShipment)) - { - // Update status on instance if message is confirmed delivered to KS. - // The instance should wait in feedback step. This enforces a feedback step in the process in current version. - // Moving forward sending to Eformidling should considered as a ServiceTask with auto advance in the process - // when the message is confirmed. - - await ProcessMoveNext(appIdentifier, instanceIdentifier); - _ = await AddCompleteConfirmation(instanceIdentifier); - - return true; - } - else if (MessageMalformed(statusesForShipment, out string errorMalformed)) - { - throw new EformidlingDeliveryException( - $"The message with id {id} was not delivered by Eformidling to KS. Error from Eformidling: {errorMalformed}." - ); - } - else if (MessageTimedOutToKS(statusesForShipment, out string errorTimeout)) - { - throw new EformidlingDeliveryException( - $"The message with id {id} was not delivered by Eformidling to KS. The message lifetime has expired. Error from Eformidling: {errorTimeout}" - ); - } - else - { - // The message isn't processed yet. - // We will try again later. - return false; - } - - // We don't know if this is the last reminder from the Event system. If the - // Event system gives up (after 48 hours) it will end up in the dead letter queue, - // and be handled by the Platform team manually. - } + /// + public async Task ProcessEvent(CloudEvent cloudEvent) + { + var subject = cloudEvent.Subject; + + _logger.LogInformation("Received reminder for subject {subject}", subject); - private async Task ProcessMoveNext(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier) + AppIdentifier appIdentifier = AppIdentifier.CreateFromUrl(cloudEvent.Source.ToString()); + InstanceIdentifier instanceIdentifier = InstanceIdentifier.CreateFromUrl(cloudEvent.Source.ToString()); + + // Instance GUID is used as shipment identifier + string id = instanceIdentifier.InstanceGuid.ToString(); + Statuses statusesForShipment = await GetStatusesForShipment(id); + if (MessageDeliveredToKS(statusesForShipment)) { - string baseUrl = _generalSettings.FormattedExternalAppBaseUrl(appIdentifier); - string url = $"{baseUrl}instances/{instanceIdentifier}/process/next"; + // Update status on instance if message is confirmed delivered to KS. + // The instance should wait in feedback step. This enforces a feedback step in the process in current version. + // Moving forward sending to Eformidling should considered as a ServiceTask with auto advance in the process + // when the message is confirmed. - TokenResponse altinnToken = await GetOrganizationToken(); - HttpClient httpClient = _httpClientFactory.CreateClient(); + await ProcessMoveNext(appIdentifier, instanceIdentifier); + _ = await AddCompleteConfirmation(instanceIdentifier); - HttpResponseMessage response = await httpClient.PutAsync( - altinnToken.AccessToken, - url, - new StringContent(string.Empty) + return true; + } + else if (MessageMalformed(statusesForShipment, out string errorMalformed)) + { + throw new EformidlingDeliveryException( + $"The message with id {id} was not delivered by Eformidling to KS. Error from Eformidling: {errorMalformed}." ); - - if (response.IsSuccessStatusCode) - { - _logger.LogInformation("Moved instance {instanceId} to next step.", instanceIdentifier); - } - else - { - _logger.LogError( - "Failed moving instance {instanceId} to next step. Received error: {errorCode}. Received content: {content}", - instanceIdentifier, - response.StatusCode, - await response.Content.ReadAsStringAsync() - ); - } } - - /// This is basically a duplicate of the method in - /// Duplication is done since the original method requires an http context - /// with a logged on user/org, while we would like to authenticate against maskinporten - /// here and now and avoid calling out of the app and back into the app on the matching - /// endpoint in InstanceController. This method should be removed once we have a better - /// alernative for authenticating the app/org without having a http request context with - /// a logged on user/org. - private async Task AddCompleteConfirmation(InstanceIdentifier instanceIdentifier) + else if (MessageTimedOutToKS(statusesForShipment, out string errorTimeout)) { - string url = - $"instances/{instanceIdentifier.InstanceOwnerPartyId}/{instanceIdentifier.InstanceGuid}/complete"; + throw new EformidlingDeliveryException( + $"The message with id {id} was not delivered by Eformidling to KS. The message lifetime has expired. Error from Eformidling: {errorTimeout}" + ); + } + else + { + // The message isn't processed yet. + // We will try again later. + return false; + } - TokenResponse altinnToken = await GetOrganizationToken(); + // We don't know if this is the last reminder from the Event system. If the + // Event system gives up (after 48 hours) it will end up in the dead letter queue, + // and be handled by the Platform team manually. + } - HttpClient httpClient = _httpClientFactory.CreateClient(); - httpClient.BaseAddress = new Uri(_platformSettings.ApiStorageEndpoint); - httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + private async Task ProcessMoveNext(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier) + { + string baseUrl = _generalSettings.FormattedExternalAppBaseUrl(appIdentifier); + string url = $"{baseUrl}instances/{instanceIdentifier}/process/next"; - HttpResponseMessage response = await httpClient.PostAsync( - altinnToken.AccessToken, - url, - new StringContent(string.Empty) - ); + TokenResponse altinnToken = await GetOrganizationToken(); + HttpClient httpClient = _httpClientFactory.CreateClient(); - if (response.StatusCode == HttpStatusCode.OK) - { - string instanceData = await response.Content.ReadAsStringAsync(); - Instance? instance = JsonConvert.DeserializeObject(instanceData); - return instance; - } + HttpResponseMessage response = await httpClient.PutAsync( + altinnToken.AccessToken, + url, + new StringContent(string.Empty) + ); - throw await PlatformHttpException.CreateAsync(response); + if (response.IsSuccessStatusCode) + { + _logger.LogInformation("Moved instance {instanceId} to next step.", instanceIdentifier); } - - private async Task GetOrganizationToken() + else { - X509Certificate2 x509cert = await _x509CertificateProvider.GetCertificate(); - var maskinportenToken = await _maskinportenService.GetToken( - x509cert, - _maskinportenSettings.Environment, - _maskinportenSettings.ClientId, - "altinn:serviceowner/instances.read altinn:serviceowner/instances.write", - string.Empty + _logger.LogError( + "Failed moving instance {instanceId} to next step. Received error: {errorCode}. Received content: {content}", + instanceIdentifier, + response.StatusCode, + await response.Content.ReadAsStringAsync() ); - var altinnToken = await _maskinportenService.ExchangeToAltinnToken( - maskinportenToken, - _maskinportenSettings.Environment - ); - - return altinnToken; } + } + + /// This is basically a duplicate of the method in + /// Duplication is done since the original method requires an http context + /// with a logged on user/org, while we would like to authenticate against maskinporten + /// here and now and avoid calling out of the app and back into the app on the matching + /// endpoint in InstanceController. This method should be removed once we have a better + /// alernative for authenticating the app/org without having a http request context with + /// a logged on user/org. + private async Task AddCompleteConfirmation(InstanceIdentifier instanceIdentifier) + { + string url = $"instances/{instanceIdentifier.InstanceOwnerPartyId}/{instanceIdentifier.InstanceGuid}/complete"; - private async Task GetStatusesForShipment(string shipmentId) + TokenResponse altinnToken = await GetOrganizationToken(); + + HttpClient httpClient = _httpClientFactory.CreateClient(); + httpClient.BaseAddress = new Uri(_platformSettings.ApiStorageEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + HttpResponseMessage response = await httpClient.PostAsync( + altinnToken.AccessToken, + url, + new StringContent(string.Empty) + ); + + if (response.StatusCode == HttpStatusCode.OK) { - var requestHeaders = new Dictionary - { - { General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey } - }; - - Statuses statuses = await _eFormidlingClient.GetMessageStatusById(shipmentId, requestHeaders); - - if (statuses != null && statuses.Content != null) - { - _logger.LogInformation( - "Received the following {count} statuses: {statusValues}.", - statuses.Content.Count, - string.Join(",", statuses.Content.Select(s => s.Status).ToArray()) - ); - } - else - { - _logger.LogWarning("Did not receive any statuses for shipment id {shipmentId}", shipmentId); - statuses ??= new Statuses(); - statuses.Content = new List(); - } - - return statuses; + string instanceData = await response.Content.ReadAsStringAsync(); + Instance? instance = JsonConvert.DeserializeObject(instanceData); + return instance; } - private static bool MessageDeliveredToKS(Statuses statuses) + throw await PlatformHttpException.CreateAsync(response); + } + + private async Task GetOrganizationToken() + { + X509Certificate2 x509cert = await _x509CertificateProvider.GetCertificate(); + var maskinportenToken = await _maskinportenService.GetToken( + x509cert, + _maskinportenSettings.Environment, + _maskinportenSettings.ClientId, + "altinn:serviceowner/instances.read altinn:serviceowner/instances.write", + string.Empty + ); + var altinnToken = await _maskinportenService.ExchangeToAltinnToken( + maskinportenToken, + _maskinportenSettings.Environment + ); + + return altinnToken; + } + + private async Task GetStatusesForShipment(string shipmentId) + { + var requestHeaders = new Dictionary { - return statuses.Content.FirstOrDefault(s => - s.Status.Equals("levert", StringComparison.OrdinalIgnoreCase) - || s.Status.Equals("lest", StringComparison.OrdinalIgnoreCase) - ) != null; - } + { General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey } + }; + + Statuses statuses = await _eFormidlingClient.GetMessageStatusById(shipmentId, requestHeaders); - private static bool MessageTimedOutToKS(Statuses statuses, out string errorMessage) + if (statuses != null && statuses.Content != null) { - (bool error, errorMessage) = CheckErrorStatus(statuses, "levetid_utlopt"); - return error; + _logger.LogInformation( + "Received the following {count} statuses: {statusValues}.", + statuses.Content.Count, + string.Join(",", statuses.Content.Select(s => s.Status).ToArray()) + ); } - - private static bool MessageMalformed(Statuses statuses, out string errorMessage) + else { - (bool error, errorMessage) = CheckErrorStatus(statuses, "feil"); - return error; + _logger.LogWarning("Did not receive any statuses for shipment id {shipmentId}", shipmentId); + statuses ??= new Statuses(); + statuses.Content = new List(); } - private static (bool Error, string ErrorMessage) CheckErrorStatus(Statuses statuses, string errorStatus) - { - bool isError = false; - string errorMessage = string.Empty; + return statuses; + } - var status = statuses.Content.FirstOrDefault(s => - s.Status.Equals(errorStatus, StringComparison.OrdinalIgnoreCase) - ); - if (status != null) - { - isError = true; - errorMessage = status.Description; - } + private static bool MessageDeliveredToKS(Statuses statuses) + { + return statuses.Content.FirstOrDefault(s => + s.Status.Equals("levert", StringComparison.OrdinalIgnoreCase) + || s.Status.Equals("lest", StringComparison.OrdinalIgnoreCase) + ) != null; + } + + private static bool MessageTimedOutToKS(Statuses statuses, out string errorMessage) + { + (bool error, errorMessage) = CheckErrorStatus(statuses, "levetid_utlopt"); + return error; + } - return (isError, errorMessage); + private static bool MessageMalformed(Statuses statuses, out string errorMessage) + { + (bool error, errorMessage) = CheckErrorStatus(statuses, "feil"); + return error; + } + + private static (bool Error, string ErrorMessage) CheckErrorStatus(Statuses statuses, string errorStatus) + { + bool isError = false; + string errorMessage = string.Empty; + + var status = statuses.Content.FirstOrDefault(s => + s.Status.Equals(errorStatus, StringComparison.OrdinalIgnoreCase) + ); + if (status != null) + { + isError = true; + errorMessage = status.Description; } + + return (isError, errorMessage); } } diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs index dae04b275..687e47079 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler2.cs @@ -15,221 +15,215 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; -namespace Altinn.App.Core.EFormidling.Implementation +namespace Altinn.App.Core.EFormidling.Implementation; + +/// +/// Handles status checking of messages sent through the Eformidling integration point. +/// +public class EformidlingStatusCheckEventHandler2 : IEventHandler { + private readonly IEFormidlingClient _eFormidlingClient; + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IMaskinportenTokenProvider _maskinportenTokenProvider; + private readonly PlatformSettings _platformSettings; + private readonly GeneralSettings _generalSettings; + /// - /// Handles status checking of messages sent through the Eformidling integration point. + /// Initializes a new instance of the class. /// - public class EformidlingStatusCheckEventHandler2 : IEventHandler + public EformidlingStatusCheckEventHandler2( + IEFormidlingClient eFormidlingClient, + IHttpClientFactory httpClientFactory, + ILogger logger, + IMaskinportenTokenProvider maskinportenTokenProvider, + IOptions platformSettings, + IOptions generalSettings + ) { - private readonly IEFormidlingClient _eFormidlingClient; - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IMaskinportenTokenProvider _maskinportenTokenProvider; - private readonly PlatformSettings _platformSettings; - private readonly GeneralSettings _generalSettings; - - /// - /// Initializes a new instance of the class. - /// - public EformidlingStatusCheckEventHandler2( - IEFormidlingClient eFormidlingClient, - IHttpClientFactory httpClientFactory, - ILogger logger, - IMaskinportenTokenProvider maskinportenTokenProvider, - IOptions platformSettings, - IOptions generalSettings - ) + _eFormidlingClient = eFormidlingClient; + _logger = logger; + _httpClientFactory = httpClientFactory; + _maskinportenTokenProvider = maskinportenTokenProvider; + _platformSettings = platformSettings.Value; + _generalSettings = generalSettings.Value; + } + + /// + public string EventType { get; internal set; } = EformidlingConstants.CheckInstanceStatusEventType; + + /// + public async Task ProcessEvent(CloudEvent cloudEvent) + { + var subject = cloudEvent.Subject; + + _logger.LogInformation("Received reminder for subject {subject}", subject); + + AppIdentifier appIdentifier = AppIdentifier.CreateFromUrl(cloudEvent.Source.ToString()); + InstanceIdentifier instanceIdentifier = InstanceIdentifier.CreateFromUrl(cloudEvent.Source.ToString()); + + // Instance GUID is used as shipment identifier + string id = instanceIdentifier.InstanceGuid.ToString(); + Statuses statusesForShipment = await GetStatusesForShipment(id); + if (MessageDeliveredToKS(statusesForShipment)) { - _eFormidlingClient = eFormidlingClient; - _logger = logger; - _httpClientFactory = httpClientFactory; - _maskinportenTokenProvider = maskinportenTokenProvider; - _platformSettings = platformSettings.Value; - _generalSettings = generalSettings.Value; - } + // Update status on instance if message is confirmed delivered to KS. + // The instance should wait in feedback step. This enforces a feedback step in the process in current version. + // Moving forward sending to Eformidling should considered as a ServiceTask with auto advance in the process + // when the message is confirmed. - /// - public string EventType { get; internal set; } = EformidlingConstants.CheckInstanceStatusEventType; + await ProcessMoveNext(appIdentifier, instanceIdentifier); + _ = await AddCompleteConfirmation(instanceIdentifier); - /// - public async Task ProcessEvent(CloudEvent cloudEvent) + return true; + } + else if (MessageMalformed(statusesForShipment, out string errorMalformed)) { - var subject = cloudEvent.Subject; - - _logger.LogInformation("Received reminder for subject {subject}", subject); - - AppIdentifier appIdentifier = AppIdentifier.CreateFromUrl(cloudEvent.Source.ToString()); - InstanceIdentifier instanceIdentifier = InstanceIdentifier.CreateFromUrl(cloudEvent.Source.ToString()); - - // Instance GUID is used as shipment identifier - string id = instanceIdentifier.InstanceGuid.ToString(); - Statuses statusesForShipment = await GetStatusesForShipment(id); - if (MessageDeliveredToKS(statusesForShipment)) - { - // Update status on instance if message is confirmed delivered to KS. - // The instance should wait in feedback step. This enforces a feedback step in the process in current version. - // Moving forward sending to Eformidling should considered as a ServiceTask with auto advance in the process - // when the message is confirmed. - - await ProcessMoveNext(appIdentifier, instanceIdentifier); - _ = await AddCompleteConfirmation(instanceIdentifier); - - return true; - } - else if (MessageMalformed(statusesForShipment, out string errorMalformed)) - { - throw new EformidlingDeliveryException( - $"The message with id {id} was not delivered by Eformidling to KS. Error from Eformidling: {errorMalformed}." - ); - } - else if (MessageTimedOutToKS(statusesForShipment, out string errorTimeout)) - { - throw new EformidlingDeliveryException( - $"The message with id {id} was not delivered by Eformidling to KS. The message lifetime has expired. Error from Eformidling: {errorTimeout}" - ); - } - else - { - // The message isn't processed yet. - // We will try again later. - return false; - } - - // We don't know if this is the last reminder from the Event system. If the - // Event system gives up (after 48 hours) it will end up in the dead letter queue, - // and be handled by the Platform team manually. + throw new EformidlingDeliveryException( + $"The message with id {id} was not delivered by Eformidling to KS. Error from Eformidling: {errorMalformed}." + ); } - - private async Task ProcessMoveNext(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier) + else if (MessageTimedOutToKS(statusesForShipment, out string errorTimeout)) { - string baseUrl = _generalSettings.FormattedExternalAppBaseUrl(appIdentifier); - string url = $"{baseUrl}instances/{instanceIdentifier}/process/next"; - - string altinnToken = await GetOrganizationToken(); - HttpClient httpClient = _httpClientFactory.CreateClient(); - - HttpResponseMessage response = await httpClient.PutAsync(altinnToken, url, new StringContent(string.Empty)); - - if (response.IsSuccessStatusCode) - { - _logger.LogInformation("Moved instance {instanceId} to next step.", instanceIdentifier); - } - else - { - _logger.LogError( - "Failed moving instance {instanceId} to next step. Received error: {errorCode}. Received content: {content}", - instanceIdentifier, - response.StatusCode, - await response.Content.ReadAsStringAsync() - ); - } + throw new EformidlingDeliveryException( + $"The message with id {id} was not delivered by Eformidling to KS. The message lifetime has expired. Error from Eformidling: {errorTimeout}" + ); } - - /// This is basically a duplicate of the method in - /// Duplication is done since the original method requires an http context - /// with a logged on user/org, while we would like to authenticate against maskinporten - /// here and now and avoid calling out of the app and back into the app on the matching - /// endpoint in InstanceController. This method should be removed once we have a better - /// alernative for authenticating the app/org without having a http request context with - /// a logged on user/org. - private async Task AddCompleteConfirmation(InstanceIdentifier instanceIdentifier) + else { - string url = - $"instances/{instanceIdentifier.InstanceOwnerPartyId}/{instanceIdentifier.InstanceGuid}/complete"; + // The message isn't processed yet. + // We will try again later. + return false; + } - string altinnToken = await GetOrganizationToken(); + // We don't know if this is the last reminder from the Event system. If the + // Event system gives up (after 48 hours) it will end up in the dead letter queue, + // and be handled by the Platform team manually. + } - HttpClient httpClient = _httpClientFactory.CreateClient(); - httpClient.BaseAddress = new Uri(_platformSettings.ApiStorageEndpoint); - httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + private async Task ProcessMoveNext(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier) + { + string baseUrl = _generalSettings.FormattedExternalAppBaseUrl(appIdentifier); + string url = $"{baseUrl}instances/{instanceIdentifier}/process/next"; - HttpResponseMessage response = await httpClient.PostAsync( - altinnToken, - url, - new StringContent(string.Empty) - ); + string altinnToken = await GetOrganizationToken(); + HttpClient httpClient = _httpClientFactory.CreateClient(); - if (response.StatusCode == HttpStatusCode.OK) - { - string instanceData = await response.Content.ReadAsStringAsync(); - Instance? instance = JsonConvert.DeserializeObject(instanceData); - return instance; - } + HttpResponseMessage response = await httpClient.PutAsync(altinnToken, url, new StringContent(string.Empty)); - throw await PlatformHttpException.CreateAsync(response); + if (response.IsSuccessStatusCode) + { + _logger.LogInformation("Moved instance {instanceId} to next step.", instanceIdentifier); } - - private async Task GetOrganizationToken() + else { - string scopes = "altinn:serviceowner/instances.read altinn:serviceowner/instances.write"; - - return await _maskinportenTokenProvider.GetAltinnExchangedToken(scopes); + _logger.LogError( + "Failed moving instance {instanceId} to next step. Received error: {errorCode}. Received content: {content}", + instanceIdentifier, + response.StatusCode, + await response.Content.ReadAsStringAsync() + ); } + } + + /// This is basically a duplicate of the method in + /// Duplication is done since the original method requires an http context + /// with a logged on user/org, while we would like to authenticate against maskinporten + /// here and now and avoid calling out of the app and back into the app on the matching + /// endpoint in InstanceController. This method should be removed once we have a better + /// alernative for authenticating the app/org without having a http request context with + /// a logged on user/org. + private async Task AddCompleteConfirmation(InstanceIdentifier instanceIdentifier) + { + string url = $"instances/{instanceIdentifier.InstanceOwnerPartyId}/{instanceIdentifier.InstanceGuid}/complete"; + + string altinnToken = await GetOrganizationToken(); + + HttpClient httpClient = _httpClientFactory.CreateClient(); + httpClient.BaseAddress = new Uri(_platformSettings.ApiStorageEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - private async Task GetStatusesForShipment(string shipmentId) + HttpResponseMessage response = await httpClient.PostAsync(altinnToken, url, new StringContent(string.Empty)); + + if (response.StatusCode == HttpStatusCode.OK) { - var requestHeaders = new Dictionary - { - { General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey } - }; - - Statuses statuses = await _eFormidlingClient.GetMessageStatusById(shipmentId, requestHeaders); - - if (statuses != null && statuses.Content != null) - { - _logger.LogInformation( - "Received the following {count} statuses: {statusValues}.", - statuses.Content.Count, - string.Join(",", statuses.Content.Select(s => s.Status).ToArray()) - ); - } - else - { - _logger.LogWarning("Did not receive any statuses for shipment id {shipmentId}", shipmentId); - statuses ??= new Statuses(); - statuses.Content = new List(); - } - - return statuses; + string instanceData = await response.Content.ReadAsStringAsync(); + Instance? instance = JsonConvert.DeserializeObject(instanceData); + return instance; } - private static bool MessageDeliveredToKS(Statuses statuses) + throw await PlatformHttpException.CreateAsync(response); + } + + private async Task GetOrganizationToken() + { + string scopes = "altinn:serviceowner/instances.read altinn:serviceowner/instances.write"; + + return await _maskinportenTokenProvider.GetAltinnExchangedToken(scopes); + } + + private async Task GetStatusesForShipment(string shipmentId) + { + var requestHeaders = new Dictionary { - return statuses.Content.FirstOrDefault(s => - s.Status.Equals("levert", StringComparison.OrdinalIgnoreCase) - || s.Status.Equals("lest", StringComparison.OrdinalIgnoreCase) - ) != null; - } + { General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey } + }; - private static bool MessageTimedOutToKS(Statuses statuses, out string errorMessage) + Statuses statuses = await _eFormidlingClient.GetMessageStatusById(shipmentId, requestHeaders); + + if (statuses != null && statuses.Content != null) { - (bool error, errorMessage) = CheckErrorStatus(statuses, "levetid_utlopt"); - return error; + _logger.LogInformation( + "Received the following {count} statuses: {statusValues}.", + statuses.Content.Count, + string.Join(",", statuses.Content.Select(s => s.Status).ToArray()) + ); } - - private static bool MessageMalformed(Statuses statuses, out string errorMessage) + else { - (bool error, errorMessage) = CheckErrorStatus(statuses, "feil"); - return error; + _logger.LogWarning("Did not receive any statuses for shipment id {shipmentId}", shipmentId); + statuses ??= new Statuses(); + statuses.Content = new List(); } - private static (bool Error, string ErrorMessage) CheckErrorStatus(Statuses statuses, string errorStatus) - { - bool isError = false; - string errorMessage = string.Empty; + return statuses; + } - var status = statuses.Content.FirstOrDefault(s => - s.Status.Equals(errorStatus, StringComparison.OrdinalIgnoreCase) - ); - if (status != null) - { - isError = true; - errorMessage = status.Description; - } + private static bool MessageDeliveredToKS(Statuses statuses) + { + return statuses.Content.FirstOrDefault(s => + s.Status.Equals("levert", StringComparison.OrdinalIgnoreCase) + || s.Status.Equals("lest", StringComparison.OrdinalIgnoreCase) + ) != null; + } - return (isError, errorMessage); + private static bool MessageTimedOutToKS(Statuses statuses, out string errorMessage) + { + (bool error, errorMessage) = CheckErrorStatus(statuses, "levetid_utlopt"); + return error; + } + + private static bool MessageMalformed(Statuses statuses, out string errorMessage) + { + (bool error, errorMessage) = CheckErrorStatus(statuses, "feil"); + return error; + } + + private static (bool Error, string ErrorMessage) CheckErrorStatus(Statuses statuses, string errorStatus) + { + bool isError = false; + string errorMessage = string.Empty; + + var status = statuses.Content.FirstOrDefault(s => + s.Status.Equals(errorStatus, StringComparison.OrdinalIgnoreCase) + ); + if (status != null) + { + isError = true; + errorMessage = status.Description; } + + return (isError, errorMessage); } } diff --git a/src/Altinn.App.Core/Extensions/ClaimsPrincipalExtensions.cs b/src/Altinn.App.Core/Extensions/ClaimsPrincipalExtensions.cs index c950eb189..c1fd384cb 100644 --- a/src/Altinn.App.Core/Extensions/ClaimsPrincipalExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ClaimsPrincipalExtensions.cs @@ -1,66 +1,65 @@ using System.Security.Claims; using AltinnCore.Authentication.Constants; -namespace Altinn.App.Core.Extensions +namespace Altinn.App.Core.Extensions; + +/// +/// This class holds a collection of extension methods for the class. +/// +public static class ClaimsPrincipalExtensions { /// - /// This class holds a collection of extension methods for the class. + /// Gets the userId or the orgNumber or null if neither claims are present. /// - public static class ClaimsPrincipalExtensions + public static string? GetUserOrOrgId(this ClaimsPrincipal user) { - /// - /// Gets the userId or the orgNumber or null if neither claims are present. - /// - public static string? GetUserOrOrgId(this ClaimsPrincipal user) + int? userId = GetUserIdAsInt(user); + if (userId.HasValue) { - int? userId = GetUserIdAsInt(user); - if (userId.HasValue) - { - return userId.Value.ToString(); - } - - int? orgId = GetOrgNumber(user); - if (orgId.HasValue) - { - return orgId.Value.ToString(); - } + return userId.Value.ToString(); + } - return null; + int? orgId = GetOrgNumber(user); + if (orgId.HasValue) + { + return orgId.Value.ToString(); } - /// - /// Get the org identifier string or null if it is not an org. - /// - public static string? GetOrg(this ClaimsPrincipal user) => user.GetFirstOfType(AltinnCoreClaimTypes.Org); + return null; + } - /// - /// Returns the organisation number of an org user or null if claim does not exist. - /// - public static int? GetOrgNumber(this ClaimsPrincipal? user) => - user.GetFirstOfTypeAsInt(AltinnCoreClaimTypes.OrgNumber); + /// + /// Get the org identifier string or null if it is not an org. + /// + public static string? GetOrg(this ClaimsPrincipal user) => user.GetFirstOfType(AltinnCoreClaimTypes.Org); - /// - /// Return the userId as an int or null if UserId claim is not set - /// - public static int? GetUserIdAsInt(this ClaimsPrincipal? user) => - user.GetFirstOfTypeAsInt(AltinnCoreClaimTypes.UserId); + /// + /// Returns the organisation number of an org user or null if claim does not exist. + /// + public static int? GetOrgNumber(this ClaimsPrincipal? user) => + user.GetFirstOfTypeAsInt(AltinnCoreClaimTypes.OrgNumber); - /// - /// Returns the authentication level of the user. - /// - public static int GetAuthenticationLevel(this ClaimsPrincipal user) => - user.GetFirstOfTypeAsInt(AltinnCoreClaimTypes.AuthenticationLevel) ?? 0; + /// + /// Return the userId as an int or null if UserId claim is not set + /// + public static int? GetUserIdAsInt(this ClaimsPrincipal? user) => + user.GetFirstOfTypeAsInt(AltinnCoreClaimTypes.UserId); + + /// + /// Returns the authentication level of the user. + /// + public static int GetAuthenticationLevel(this ClaimsPrincipal user) => + user.GetFirstOfTypeAsInt(AltinnCoreClaimTypes.AuthenticationLevel) ?? 0; - /// - /// Return the partyId as an int or null if PartyId claim is not set - /// - public static int? GetPartyIdAsInt(this ClaimsPrincipal user) => - user.GetFirstOfTypeAsInt(AltinnCoreClaimTypes.PartyID); + /// + /// Return the partyId as an int or null if PartyId claim is not set + /// + public static int? GetPartyIdAsInt(this ClaimsPrincipal user) => + user.GetFirstOfTypeAsInt(AltinnCoreClaimTypes.PartyID); - private static string? GetFirstOfType(this ClaimsPrincipal? user, string type) => - user?.FindFirst(c => c.Type == type)?.Value; + private static string? GetFirstOfType(this ClaimsPrincipal? user, string type) => + user?.FindFirst(c => c.Type == type)?.Value; - private static int? GetFirstOfTypeAsInt(this ClaimsPrincipal? user, string type) => - int.TryParse(user.GetFirstOfType(type), out var v) ? v : null; - } + private static int? GetFirstOfTypeAsInt(this ClaimsPrincipal? user, string type) => + int.TryParse(user.GetFirstOfType(type), out var v) ? v : null; } diff --git a/src/Altinn.App.Core/Extensions/ConfigurationBuilderExtensions.cs b/src/Altinn.App.Core/Extensions/ConfigurationBuilderExtensions.cs index 9d0301c85..6588fc3b0 100644 --- a/src/Altinn.App.Core/Extensions/ConfigurationBuilderExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ConfigurationBuilderExtensions.cs @@ -1,40 +1,39 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.FileProviders; -namespace Altinn.App.Core.Extensions +namespace Altinn.App.Core.Extensions; + +/// +/// This class holds a collection of extension methods for . +/// +public static class ConfigurationBuilderExtensions { /// - /// This class holds a collection of extension methods for . + /// Load all known configuration sources known to be needed by an app. /// - public static class ConfigurationBuilderExtensions + /// The + /// The original command line arguments + public static void LoadAppConfig(this IConfigurationBuilder builder, string[]? args = null) { - /// - /// Load all known configuration sources known to be needed by an app. - /// - /// The - /// The original command line arguments - public static void LoadAppConfig(this IConfigurationBuilder builder, string[]? args = null) + try { - try - { - builder.AddJsonFile( - new PhysicalFileProvider("/altinn-appsettings-secret"), - @"altinn-appsettings-secret.json", - true, - true - ); - } - catch (DirectoryNotFoundException) - { - // Extra secrets configuration file is optional. The directory does not exist in dev environments, but - // is otherwise mounted to a folder directly on the filesystem root. We could init the file provider - // with the root folder (and not have to catch this exception), but that would cause - // 'reloadOnChange: true' to recurse through the entire file system to monitor for changes. - } - - // Add values from environment and command line arguments last, to override values from other sources. - builder.AddEnvironmentVariables(); - builder.AddCommandLine(args ?? []); + builder.AddJsonFile( + new PhysicalFileProvider("/altinn-appsettings-secret"), + @"altinn-appsettings-secret.json", + true, + true + ); } + catch (DirectoryNotFoundException) + { + // Extra secrets configuration file is optional. The directory does not exist in dev environments, but + // is otherwise mounted to a folder directly on the filesystem root. We could init the file provider + // with the root folder (and not have to catch this exception), but that would cause + // 'reloadOnChange: true' to recurse through the entire file system to monitor for changes. + } + + // Add values from environment and command line arguments last, to override values from other sources. + builder.AddEnvironmentVariables(); + builder.AddCommandLine(args ?? []); } } diff --git a/src/Altinn.App.Core/Extensions/DataProtectionConfiguration.cs b/src/Altinn.App.Core/Extensions/DataProtectionConfiguration.cs index 9d5bed631..ff06fdc23 100644 --- a/src/Altinn.App.Core/Extensions/DataProtectionConfiguration.cs +++ b/src/Altinn.App.Core/Extensions/DataProtectionConfiguration.cs @@ -2,43 +2,40 @@ using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.Extensions.DependencyInjection; -namespace Altinn.App.Core.Extensions +namespace Altinn.App.Core.Extensions; + +/// +/// Configuration for DataProtection +/// +public static class DataProtectionConfiguration { /// - /// Configuration for DataProtection + /// Configure data protection on the services collection. /// - public static class DataProtectionConfiguration + /// The service collections + public static void ConfigureDataProtection(this IServiceCollection services) { - /// - /// Configure data protection on the services collection. - /// - /// The service collections - public static void ConfigureDataProtection(this IServiceCollection services) + var dir = GetKeysDirectory(); + if (dir is null) { - var dir = GetKeysDirectory(); - if (dir is null) - { - throw new DirectoryNotFoundException( - "Could not find a suitable directory for storing DataProtection keys" - ); - } - services.AddDataProtection().PersistKeysToFileSystem(dir); + throw new DirectoryNotFoundException("Could not find a suitable directory for storing DataProtection keys"); } + services.AddDataProtection().PersistKeysToFileSystem(dir); + } - /// - /// Return a directory based on the running operating system. It is possible to override the directory based on the ALTINN_KEYS_DIRECTORY environment variable. - /// - /// - private static DirectoryInfo? GetKeysDirectory() + /// + /// Return a directory based on the running operating system. It is possible to override the directory based on the ALTINN_KEYS_DIRECTORY environment variable. + /// + /// + private static DirectoryInfo? GetKeysDirectory() + { + var environmentVariable = System.Environment.GetEnvironmentVariable("ALTINN_KEYS_DIRECTORY"); + if (!string.IsNullOrWhiteSpace(environmentVariable)) { - var environmentVariable = System.Environment.GetEnvironmentVariable("ALTINN_KEYS_DIRECTORY"); - if (!string.IsNullOrWhiteSpace(environmentVariable)) - { - return new DirectoryInfo(environmentVariable); - } - - // Return a key directory based on the current operating system - return FileSystemXmlRepository.DefaultKeyStorageDirectory; + return new DirectoryInfo(environmentVariable); } + + // Return a key directory based on the current operating system + return FileSystemXmlRepository.DefaultKeyStorageDirectory; } } diff --git a/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs b/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs index 8e5f6e90d..1872d539d 100644 --- a/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs +++ b/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs @@ -1,37 +1,36 @@ using System.Net; using System.Text; -namespace Altinn.App.Core.Extensions +namespace Altinn.App.Core.Extensions; + +/// +/// Extension methods for +/// +public static class DictionaryExtensions { /// - /// Extension methods for + /// Converts a dictionary to a name value string on the form key1=value1,key2=value2 url encoding both key and value. /// - public static class DictionaryExtensions + public static string ToUrlEncodedNameValueString(this Dictionary? parameters, char separator) { - /// - /// Converts a dictionary to a name value string on the form key1=value1,key2=value2 url encoding both key and value. - /// - public static string ToUrlEncodedNameValueString(this Dictionary? parameters, char separator) + if (parameters == null) { - if (parameters == null) - { - return string.Empty; - } + return string.Empty; + } - StringBuilder builder = new(); - foreach (var param in parameters) + StringBuilder builder = new(); + foreach (var param in parameters) + { + if (builder.Length > 0) { - if (builder.Length > 0) - { - builder.Append(separator); - } - - builder.Append(WebUtility.UrlEncode(param.Key)); - builder.Append('='); - builder.Append(WebUtility.UrlEncode(param.Value)); + builder.Append(separator); } - return builder.ToString(); + builder.Append(WebUtility.UrlEncode(param.Key)); + builder.Append('='); + builder.Append(WebUtility.UrlEncode(param.Value)); } + + return builder.ToString(); } } diff --git a/src/Altinn.App.Core/Extensions/HttpClientExtension.cs b/src/Altinn.App.Core/Extensions/HttpClientExtension.cs index 6ce0f29e8..bb5ca1719 100644 --- a/src/Altinn.App.Core/Extensions/HttpClientExtension.cs +++ b/src/Altinn.App.Core/Extensions/HttpClientExtension.cs @@ -1,114 +1,113 @@ -namespace Altinn.App.Core.Extensions +namespace Altinn.App.Core.Extensions; + +/// +/// This extension is created to make it easy to add a bearer token to a HttpRequests. +/// +public static class HttpClientExtension { /// - /// This extension is created to make it easy to add a bearer token to a HttpRequests. + /// Extension that add authorization header to request /// - public static class HttpClientExtension + /// The HttpClient + /// the authorization token (jwt) + /// The request Uri + /// The http content + /// The platformAccess tokens + /// A HttpResponseMessage + public static Task PostAsync( + this HttpClient httpClient, + string authorizationToken, + string requestUri, + HttpContent? content, + string? platformAccessToken = null + ) { - /// - /// Extension that add authorization header to request - /// - /// The HttpClient - /// the authorization token (jwt) - /// The request Uri - /// The http content - /// The platformAccess tokens - /// A HttpResponseMessage - public static Task PostAsync( - this HttpClient httpClient, - string authorizationToken, - string requestUri, - HttpContent? content, - string? platformAccessToken = null - ) + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Add("Authorization", "Bearer " + authorizationToken); + request.Content = content; + if (!string.IsNullOrEmpty(platformAccessToken)) { - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Add("Authorization", "Bearer " + authorizationToken); - request.Content = content; - if (!string.IsNullOrEmpty(platformAccessToken)) - { - request.Headers.Add("PlatformAccessToken", platformAccessToken); - } - - return httpClient.SendAsync(request, CancellationToken.None); + request.Headers.Add("PlatformAccessToken", platformAccessToken); } - /// - /// Extension that add authorization header to request - /// - /// The HttpClient - /// the authorization token (jwt) - /// The request Uri - /// The http content - /// The platformAccess tokens - /// A HttpResponseMessage - public static Task PutAsync( - this HttpClient httpClient, - string authorizationToken, - string requestUri, - HttpContent? content, - string? platformAccessToken = null - ) - { - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, requestUri); - request.Headers.Add("Authorization", "Bearer " + authorizationToken); - request.Content = content; - if (!string.IsNullOrEmpty(platformAccessToken)) - { - request.Headers.Add("PlatformAccessToken", platformAccessToken); - } + return httpClient.SendAsync(request, CancellationToken.None); + } - return httpClient.SendAsync(request, CancellationToken.None); + /// + /// Extension that add authorization header to request + /// + /// The HttpClient + /// the authorization token (jwt) + /// The request Uri + /// The http content + /// The platformAccess tokens + /// A HttpResponseMessage + public static Task PutAsync( + this HttpClient httpClient, + string authorizationToken, + string requestUri, + HttpContent? content, + string? platformAccessToken = null + ) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, requestUri); + request.Headers.Add("Authorization", "Bearer " + authorizationToken); + request.Content = content; + if (!string.IsNullOrEmpty(platformAccessToken)) + { + request.Headers.Add("PlatformAccessToken", platformAccessToken); } - /// - /// Extension that add authorization header to request - /// - /// The HttpClient - /// the authorization token (jwt) - /// The request Uri - /// The platformAccess tokens - /// A HttpResponseMessage - public static Task GetAsync( - this HttpClient httpClient, - string authorizationToken, - string requestUri, - string? platformAccessToken = null - ) - { - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Add("Authorization", "Bearer " + authorizationToken); - if (!string.IsNullOrEmpty(platformAccessToken)) - { - request.Headers.Add("PlatformAccessToken", platformAccessToken); - } + return httpClient.SendAsync(request, CancellationToken.None); + } - return httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, CancellationToken.None); + /// + /// Extension that add authorization header to request + /// + /// The HttpClient + /// the authorization token (jwt) + /// The request Uri + /// The platformAccess tokens + /// A HttpResponseMessage + public static Task GetAsync( + this HttpClient httpClient, + string authorizationToken, + string requestUri, + string? platformAccessToken = null + ) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("Authorization", "Bearer " + authorizationToken); + if (!string.IsNullOrEmpty(platformAccessToken)) + { + request.Headers.Add("PlatformAccessToken", platformAccessToken); } - /// - /// Extension that add authorization header to request - /// - /// The HttpClient - /// the authorization token (jwt) - /// The request Uri - /// The platformAccess tokens - /// A HttpResponseMessage - public static Task DeleteAsync( - this HttpClient httpClient, - string authorizationToken, - string requestUri, - string? platformAccessToken = null - ) - { - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, requestUri); - request.Headers.Add("Authorization", "Bearer " + authorizationToken); - if (!string.IsNullOrEmpty(platformAccessToken)) - { - request.Headers.Add("PlatformAccessToken", platformAccessToken); - } + return httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, CancellationToken.None); + } - return httpClient.SendAsync(request, CancellationToken.None); + /// + /// Extension that add authorization header to request + /// + /// The HttpClient + /// the authorization token (jwt) + /// The request Uri + /// The platformAccess tokens + /// A HttpResponseMessage + public static Task DeleteAsync( + this HttpClient httpClient, + string authorizationToken, + string requestUri, + string? platformAccessToken = null + ) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, requestUri); + request.Headers.Add("Authorization", "Bearer " + authorizationToken); + if (!string.IsNullOrEmpty(platformAccessToken)) + { + request.Headers.Add("PlatformAccessToken", platformAccessToken); } + + return httpClient.SendAsync(request, CancellationToken.None); } } diff --git a/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs index ce049a48e..59f303f8a 100644 --- a/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs +++ b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs @@ -2,28 +2,27 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Altinn.App.Core.Extensions +namespace Altinn.App.Core.Extensions; + +/// +/// Extension methods for +/// +public static class HttpContextExtensions { /// - /// Extension methods for + /// Reads the request body and returns it as a /// - public static class HttpContextExtensions + public static StreamContent CreateContentStream(this HttpRequest request) { - /// - /// Reads the request body and returns it as a - /// - public static StreamContent CreateContentStream(this HttpRequest request) - { - StreamContent content = new StreamContent(request.Body); - ArgumentNullException.ThrowIfNull(request.ContentType); - content.Headers.ContentType = MediaTypeHeaderValue.Parse(request.ContentType); - - if (request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) - { - content.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse(headerValues.ToString()); - } + StreamContent content = new StreamContent(request.Body); + ArgumentNullException.ThrowIfNull(request.ContentType); + content.Headers.ContentType = MediaTypeHeaderValue.Parse(request.ContentType); - return content; + if (request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) + { + content.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse(headerValues.ToString()); } + + return content; } } diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index d2bca1616..3848fdd0f 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -63,291 +63,290 @@ using ProcessEngine = Altinn.App.Core.Internal.Process.ProcessEngine; using ProcessReader = Altinn.App.Core.Internal.Process.ProcessReader; -namespace Altinn.App.Core.Extensions +namespace Altinn.App.Core.Extensions; + +/// +/// This class holds a collection of extension methods for the interface. +/// +public static class ServiceCollectionExtensions { /// - /// This class holds a collection of extension methods for the interface. + /// Adds all http clients for platform functionality. /// - public static class ServiceCollectionExtensions + /// The being built. + /// A reference to the current object. + /// A reference to the current object. + public static void AddPlatformServices( + this IServiceCollection services, + IConfiguration configuration, + IWebHostEnvironment env + ) { - /// - /// Adds all http clients for platform functionality. - /// - /// The being built. - /// A reference to the current object. - /// A reference to the current object. - public static void AddPlatformServices( - this IServiceCollection services, - IConfiguration configuration, - IWebHostEnvironment env - ) - { - services.Configure(configuration.GetSection("AppSettings")); - services.Configure(configuration.GetSection("GeneralSettings")); - services.Configure(configuration.GetSection("PlatformSettings")); - services.Configure(configuration.GetSection("CacheSettings")); - - AddApplicationIdentifier(services); - - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.Decorate(); - services.AddHttpClient(); + services.Configure(configuration.GetSection("AppSettings")); + services.Configure(configuration.GetSection("GeneralSettings")); + services.Configure(configuration.GetSection("PlatformSettings")); + services.Configure(configuration.GetSection("CacheSettings")); + + AddApplicationIdentifier(services); + + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.Decorate(); + services.AddHttpClient(); #pragma warning disable CS0618 // Type or member is obsolete - services.AddHttpClient(); + services.AddHttpClient(); #pragma warning restore CS0618 // Type or member is obsolete - services.AddHttpClient(); - services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); - } - - private static void AddApplicationIdentifier(IServiceCollection services) - { - services.AddSingleton(sp => - { - var appIdentifier = GetApplicationId(); - return new AppIdentifier(appIdentifier); - }); - } + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + } - private static string GetApplicationId() + private static void AddApplicationIdentifier(IServiceCollection services) + { + services.AddSingleton(sp => { - string appMetaDataString = File.ReadAllText("config/applicationmetadata.json"); - JObject appMetadataJObject = JObject.Parse(appMetaDataString); + var appIdentifier = GetApplicationId(); + return new AppIdentifier(appIdentifier); + }); + } - var id = appMetadataJObject?.SelectToken("id")?.Value(); + private static string GetApplicationId() + { + string appMetaDataString = File.ReadAllText("config/applicationmetadata.json"); + JObject appMetadataJObject = JObject.Parse(appMetaDataString); - if (id == null) - { - throw new KeyNotFoundException( - "Could not find id in applicationmetadata.json. Please ensure applicationmeta.json is well formed and contains a key for id." - ); - } + var id = appMetadataJObject?.SelectToken("id")?.Value(); - return id; + if (id == null) + { + throw new KeyNotFoundException( + "Could not find id in applicationmetadata.json. Please ensure applicationmeta.json is well formed and contains a key for id." + ); } - /// - /// Adds all the app services. - /// - /// The being built. - /// A reference to the current object. - /// A reference to the current object. - public static void AddAppServices( - this IServiceCollection services, - IConfiguration configuration, - IWebHostEnvironment env - ) - { - // Services for Altinn App - services.TryAddTransient(); - AddValidationServices(services, configuration); - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddTransient(); + return id; + } + + /// + /// Adds all the app services. + /// + /// The being built. + /// A reference to the current object. + /// A reference to the current object. + public static void AddAppServices( + this IServiceCollection services, + IConfiguration configuration, + IWebHostEnvironment env + ) + { + // Services for Altinn App + services.TryAddTransient(); + AddValidationServices(services, configuration); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddTransient(); #pragma warning disable CS0618, CS0612 // Type or member is obsolete - services.TryAddTransient(); + services.TryAddTransient(); #pragma warning restore CS0618, CS0612 // Type or member is obsolete - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); - services.AddTransient(); - services.Configure(configuration.GetSection("PEPSettings")); - services.Configure(configuration.GetSection("PlatformSettings")); - services.Configure(configuration.GetSection("AccessTokenSettings")); - services.Configure(configuration.GetSection(nameof(FrontEndSettings))); - services.Configure(configuration.GetSection(nameof(PdfGeneratorSettings))); - - AddAppOptions(services); - AddActionServices(services); - AddPdfServices(services); - AddNetsPaymentServices(services, configuration); - AddSignatureServices(services); - AddEventServices(services); - AddNotificationServices(services); - AddProcessServices(services); - AddFileAnalyserServices(services); - AddFileValidatorServices(services); - - if (!env.IsDevelopment()) - { - services.TryAddSingleton(); - services.Configure(configuration.GetSection("kvSetting")); - } - else - { - services.TryAddSingleton(); - } + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.AddTransient(); + services.Configure(configuration.GetSection("PEPSettings")); + services.Configure(configuration.GetSection("PlatformSettings")); + services.Configure(configuration.GetSection("AccessTokenSettings")); + services.Configure(configuration.GetSection(nameof(FrontEndSettings))); + services.Configure(configuration.GetSection(nameof(PdfGeneratorSettings))); + + AddAppOptions(services); + AddActionServices(services); + AddPdfServices(services); + AddNetsPaymentServices(services, configuration); + AddSignatureServices(services); + AddEventServices(services); + AddNotificationServices(services); + AddProcessServices(services); + AddFileAnalyserServices(services); + AddFileValidatorServices(services); + + if (!env.IsDevelopment()) + { + services.TryAddSingleton(); + services.Configure(configuration.GetSection("kvSetting")); } - - private static void AddValidationServices(IServiceCollection services, IConfiguration configuration) + else { - services.AddTransient(); - services.TryAddTransient(); - if (configuration.GetSection("AppSettings").Get()?.RequiredValidation == true) - { - services.AddTransient(); - } - - if (configuration.GetSection("AppSettings").Get()?.ExpressionValidation == true) - { - services.AddTransient(); - } - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.TryAddSingleton(); } + } - /// - /// Checks if a service is already added to the collection. - /// - /// true if the services allready exists in the collection, otherwise false - public static bool IsAdded(this IServiceCollection services, Type serviceType) + private static void AddValidationServices(IServiceCollection services, IConfiguration configuration) + { + services.AddTransient(); + services.TryAddTransient(); + if (configuration.GetSection("AppSettings").Get()?.RequiredValidation == true) { - if (services.Any(x => x.ServiceType == serviceType)) - { - return true; - } - - return false; + services.AddTransient(); } - private static void AddEventServices(IServiceCollection services) + if (configuration.GetSection("AppSettings").Get()?.ExpressionValidation == true) { - services.AddTransient(); - services.AddTransient(); - services.TryAddSingleton(); - - // The event subscription client depends uppon a maskinporten messagehandler beeing - // added to the client during setup. As of now this needs to be done in the apps - // if subscription is to be added. This registration is to prevent the DI container - // from failing for the apps not using event subscription. If you try to use - // event subscription with this client you will get a 401 Unauthorized. - if (!services.IsAdded(typeof(IEventsSubscription))) - { - services.AddHttpClient(); - } + services.AddTransient(); } + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } - private static void AddNotificationServices(IServiceCollection services) + /// + /// Checks if a service is already added to the collection. + /// + /// true if the services allready exists in the collection, otherwise false + public static bool IsAdded(this IServiceCollection services, Type serviceType) + { + if (services.Any(x => x.ServiceType == serviceType)) { - services.AddHttpClient(); - services.AddHttpClient(); + return true; } - private static void AddPdfServices(IServiceCollection services) + return false; + } + + private static void AddEventServices(IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.TryAddSingleton(); + + // The event subscription client depends uppon a maskinporten messagehandler beeing + // added to the client during setup. As of now this needs to be done in the apps + // if subscription is to be added. This registration is to prevent the DI container + // from failing for the apps not using event subscription. If you try to use + // event subscription with this client you will get a 401 Unauthorized. + if (!services.IsAdded(typeof(IEventsSubscription))) { - services.TryAddTransient(); - services.TryAddTransient(); + services.AddHttpClient(); + } + } + + private static void AddNotificationServices(IServiceCollection services) + { + services.AddHttpClient(); + services.AddHttpClient(); + } + + private static void AddPdfServices(IServiceCollection services) + { + services.TryAddTransient(); + services.TryAddTransient(); #pragma warning disable CS0618 // Type or member is obsolete - services.TryAddTransient(); + services.TryAddTransient(); #pragma warning restore CS0618 // Type or member is obsolete - } + } - private static void AddNetsPaymentServices(this IServiceCollection services, IConfiguration configuration) - { - IConfigurationSection configurationSection = configuration.GetSection("NetsPaymentSettings"); - - if (configurationSection.Exists()) - { - services.Configure(configurationSection); - services.AddHttpClient(); - services.AddTransient(); - } - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - } + private static void AddNetsPaymentServices(this IServiceCollection services, IConfiguration configuration) + { + IConfigurationSection configurationSection = configuration.GetSection("NetsPaymentSettings"); - private static void AddSignatureServices(IServiceCollection services) + if (configurationSection.Exists()) { - services.AddHttpClient(); + services.Configure(configurationSection); + services.AddHttpClient(); + services.AddTransient(); } - private static void AddAppOptions(IServiceCollection services) - { - // Main service for interacting with options - services.TryAddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } - // Services related to application options - services.TryAddTransient(); - services.AddTransient(); - services.TryAddTransient(); + private static void AddSignatureServices(IServiceCollection services) + { + services.AddHttpClient(); + } - // Services related to instance aware and secure app options - services.TryAddTransient(); - } + private static void AddAppOptions(IServiceCollection services) + { + // Main service for interacting with options + services.TryAddTransient(); - private static void AddProcessServices(IServiceCollection services) - { - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddTransient(); - services.AddTransient(); - services.TryAddTransient(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - //PROCESS TASKS - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - //SERVICE TASKS - services.AddTransient(); - services.AddTransient(); - } + // Services related to application options + services.TryAddTransient(); + services.AddTransient(); + services.TryAddTransient(); - private static void AddActionServices(IServiceCollection services) - { - services.TryAddTransient(); - services.AddTransient(); - services.AddTransientUserActionAuthorizerForActionInAllTasks("sign"); - } + // Services related to instance aware and secure app options + services.TryAddTransient(); + } - private static void AddFileAnalyserServices(IServiceCollection services) - { - services.TryAddTransient(); - services.TryAddTransient(); - } + private static void AddProcessServices(IServiceCollection services) + { + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddTransient(); + services.AddTransient(); + services.TryAddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + //PROCESS TASKS + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + //SERVICE TASKS + services.AddTransient(); + services.AddTransient(); + } - private static void AddFileValidatorServices(IServiceCollection services) - { - services.TryAddTransient(); - services.TryAddTransient(); - } + private static void AddActionServices(IServiceCollection services) + { + services.TryAddTransient(); + services.AddTransient(); + services.AddTransientUserActionAuthorizerForActionInAllTasks("sign"); + } + + private static void AddFileAnalyserServices(IServiceCollection services) + { + services.TryAddTransient(); + services.TryAddTransient(); + } + + private static void AddFileValidatorServices(IServiceCollection services) + { + services.TryAddTransient(); + services.TryAddTransient(); } } diff --git a/src/Altinn.App.Core/Extensions/StringExtensions.cs b/src/Altinn.App.Core/Extensions/StringExtensions.cs index eb96b240a..c738d065b 100644 --- a/src/Altinn.App.Core/Extensions/StringExtensions.cs +++ b/src/Altinn.App.Core/Extensions/StringExtensions.cs @@ -1,50 +1,49 @@ -namespace Altinn.App.Core.Extensions +namespace Altinn.App.Core.Extensions; + +/// +/// Extension methods for string +/// +public static class StringExtensions { /// - /// Extension methods for string + /// Checks if a given character is contained more than once within a string. /// - public static class StringExtensions + /// true if the string has more than one occurences of the provided char, otherwise false + public static bool ContainsMoreThanOne(this string s, char ch) { - /// - /// Checks if a given character is contained more than once within a string. - /// - /// true if the string has more than one occurences of the provided char, otherwise false - public static bool ContainsMoreThanOne(this string s, char ch) + if (s.IndexOf(ch) != s.LastIndexOf(ch)) { - if (s.IndexOf(ch) != s.LastIndexOf(ch)) - { - return true; - } - - return false; + return true; } - /// - /// Checks if a given character doesn't exists within a string - /// - /// true if the character does not contain the character, otherwise false - public static bool DoesNotContain(this string s, char ch) + return false; + } + + /// + /// Checks if a given character doesn't exists within a string + /// + /// true if the character does not contain the character, otherwise false + public static bool DoesNotContain(this string s, char ch) + { + return !s.Contains(ch); + } + + /// + /// + /// + /// true if it contains exactly one, false if it contains zero or more than one + public static bool ContainsExactlyOne(this string s, char ch) + { + if (!s.Contains(ch)) { - return !s.Contains(ch); + return false; } - /// - /// - /// - /// true if it contains exactly one, false if it contains zero or more than one - public static bool ContainsExactlyOne(this string s, char ch) + if (s.IndexOf(ch) == s.LastIndexOf(ch)) { - if (!s.Contains(ch)) - { - return false; - } - - if (s.IndexOf(ch) == s.LastIndexOf(ch)) - { - return true; - } - - return false; + return true; } + + return false; } } diff --git a/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs b/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs index e534e4353..ceb741cfd 100644 --- a/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs +++ b/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs @@ -1,282 +1,278 @@ using System.Linq.Expressions; using System.Xml.Linq; -namespace Altinn.App.Core.Extensions +namespace Altinn.App.Core.Extensions; + +/// +/// XML to LINQ helper extensions +/// +public static class XmlToLinqExtensions { + #region Public Methods and Operators + /// - /// XML to LINQ helper extensions + /// Adds a new XAttribute to the current XElement, and returns the current XElement (Fluent) /// - public static class XmlToLinqExtensions + /// + /// This XElement + /// + /// + /// New Attribute Name + /// + /// + /// Attribute value + /// + /// + /// This updated XML Element + /// + public static XElement AddAttribute(this XElement element, string attributeName, object value) { - #region Public Methods and Operators + var valueStr = + value.ToString() + ?? throw new ArgumentException("String representation of parameter 'value' is null", nameof(value)); + element.Add(new XAttribute(attributeName, valueStr)); + return element; + } - /// - /// Adds a new XAttribute to the current XElement, and returns the current XElement (Fluent) - /// - /// - /// This XElement - /// - /// - /// New Attribute Name - /// - /// - /// Attribute value - /// - /// - /// This updated XML Element - /// - public static XElement AddAttribute(this XElement element, string attributeName, object value) - { - var valueStr = - value.ToString() - ?? throw new ArgumentException("String representation of parameter 'value' is null", nameof(value)); - element.Add(new XAttribute(attributeName, valueStr)); - return element; - } + /// + /// Adds a new XElement to the current XElement, and returns the new XElement + /// + /// + /// This XElement + /// + /// + /// New element name + /// + /// + /// Add element, will be used as string via ToString() + /// + /// + /// The added XElement instance + /// + public static XElement AddElement(this XElement element, string elementName, object elementValue) + { + element.Add(new XElement(elementName, elementValue.ToString())); + return element; + } - /// - /// Adds a new XElement to the current XElement, and returns the new XElement - /// - /// - /// This XElement - /// - /// - /// New element name - /// - /// - /// Add element, will be used as string via ToString() - /// - /// - /// The added XElement instance - /// - public static XElement AddElement(this XElement element, string elementName, object elementValue) + /// + /// Adds the properties as X element. + /// + /// + /// Type of object + /// + /// + /// The element. + /// + /// + /// The expressions. + /// + /// + /// XML element + /// + public static XElement AddPropertiesAsXElement(this XElement element, params Expression>[] expressions) + { + foreach (Expression> expression in expressions) { - element.Add(new XElement(elementName, elementValue.ToString())); - return element; + element.AddPropertiesAsXElement(expression); } - /// - /// Adds the properties as X element. - /// - /// - /// Type of object - /// - /// - /// The element. - /// - /// - /// The expressions. - /// - /// - /// XML element - /// - public static XElement AddPropertiesAsXElement( - this XElement element, - params Expression>[] expressions - ) + return element; + } + + /// + /// Adds the property as X element. + /// + /// + /// The element. + /// + /// + /// The expression. + /// + /// + /// XML element + /// + public static XElement AddPropertyAsXElement(this XElement element, Expression> exp) + { + string name = string.Empty; + MemberExpression? body = exp.Body as MemberExpression; + if (body == null) { - foreach (Expression> expression in expressions) + UnaryExpression ubody = (UnaryExpression)exp.Body; + body = ubody.Operand as MemberExpression; + if (body != null) { - element.AddPropertiesAsXElement(expression); + name = body.Member.Name; } + } + else + { + name = body.Member.Name; + } - return element; + object value = exp.Compile().Invoke(); + if (value != null) + { + element.Add(new XElement(name, value)); } - /// - /// Adds the property as X element. - /// - /// - /// The element. - /// - /// - /// The expression. - /// - /// - /// XML element - /// - public static XElement AddPropertyAsXElement(this XElement element, Expression> exp) + return element; + } + + /// + /// Adds the rule as X element. + /// + /// + /// The element. + /// + /// + /// The expression. + /// + /// + /// XML element + /// + public static XElement AddRuleAsXElement(this XElement element, Expression> exp) + { + string name = string.Empty; + MemberExpression? body = exp.Body as MemberExpression; + if (body == null) { - string name = string.Empty; - MemberExpression? body = exp.Body as MemberExpression; - if (body == null) - { - UnaryExpression ubody = (UnaryExpression)exp.Body; - body = ubody.Operand as MemberExpression; - if (body != null) - { - name = body.Member.Name; - } - } - else + UnaryExpression ubody = (UnaryExpression)exp.Body; + body = ubody.Operand as MemberExpression; + if (body != null) { name = body.Member.Name; } - - object value = exp.Compile().Invoke(); - if (value != null) - { - element.Add(new XElement(name, value)); - } - - return element; + } + else + { + name = body.Member.Name; } - /// - /// Adds the rule as X element. - /// - /// - /// The element. - /// - /// - /// The expression. - /// - /// - /// XML element - /// - public static XElement AddRuleAsXElement(this XElement element, Expression> exp) + object value = exp.Compile().Invoke(); + if (value != null) { - string name = string.Empty; - MemberExpression? body = exp.Body as MemberExpression; - if (body == null) - { - UnaryExpression ubody = (UnaryExpression)exp.Body; - body = ubody.Operand as MemberExpression; - if (body != null) - { - name = body.Member.Name; - } - } - else - { - name = body.Member.Name; - } + element.Add(new XElement("rule", new XAttribute("type", name), new XAttribute("value", value))); + } - object value = exp.Compile().Invoke(); - if (value != null) - { - element.Add(new XElement("rule", new XAttribute("type", name), new XAttribute("value", value))); - } + return element; + } - return element; + /// + /// Get the Attribute value. + /// + /// + /// The element. + /// + /// + /// Name of the x. + /// + /// + /// attribute value + /// + public static string AttributeValue(this XElement element, XName xName) + { + if (element == null) + { + return string.Empty; } - /// - /// Get the Attribute value. - /// - /// - /// The element. - /// - /// - /// Name of the x. - /// - /// - /// attribute value - /// - public static string AttributeValue(this XElement element, XName xName) - { - if (element == null) - { - return string.Empty; - } + XAttribute? attribute = element.Attribute(xName); + return attribute == null ? string.Empty : attribute.Value; + } - XAttribute? attribute = element.Attribute(xName); - return attribute == null ? string.Empty : attribute.Value; - } + /// + /// Attributes the value. + /// + /// + /// The element. + /// + /// + /// Name of the attribute. + /// + /// + /// attribute value + /// + public static string AttributeValue(this XElement element, string attributeName) + { + return element.AttributeValue(XName.Get(attributeName)); + } - /// - /// Attributes the value. - /// - /// - /// The element. - /// - /// - /// Name of the attribute. - /// - /// - /// attribute value - /// - public static string AttributeValue(this XElement element, string attributeName) + /// + /// Gets the element value. + /// + /// + /// The element. + /// + /// + /// Name of the x. + /// + /// + /// element value + /// + public static string ElementValue(this XElement element, XName xName) + { + if (element == null) { - return element.AttributeValue(XName.Get(attributeName)); + return string.Empty; } - /// - /// Gets the element value. - /// - /// - /// The element. - /// - /// - /// Name of the x. - /// - /// - /// element value - /// - public static string ElementValue(this XElement element, XName xName) - { - if (element == null) - { - return string.Empty; - } + XElement? item = element.Element(xName); + return item == null ? string.Empty : item.Value; + } - XElement? item = element.Element(xName); - return item == null ? string.Empty : item.Value; - } + /// + /// Gets the restriction value. + /// + /// + /// The restriction element. + /// + /// + /// The search element. + /// + /// + /// The value. + /// + /// + /// restriction value + /// + public static bool GetRestrictionValue(this XElement restrictionElement, XName searchElement, out int value) + { + XElement? element = restrictionElement.Descendants(searchElement).FirstOrDefault(); + string stringValue = element != null ? element.AttributeValue("value") : string.Empty; + return int.TryParse(stringValue, out value); + } - /// - /// Gets the restriction value. - /// - /// - /// The restriction element. - /// - /// - /// The search element. - /// - /// - /// The value. - /// - /// - /// restriction value - /// - public static bool GetRestrictionValue(this XElement restrictionElement, XName searchElement, out int value) + /// + /// Determines whether [is same as attribute value] [the specified element]. + /// + /// + /// The element. + /// + /// + /// The source attribute. + /// + /// + /// The attribute value. + /// + /// + /// true if [is same as attribute value] [the specified element]; otherwise, false. + /// + public static bool IsSameAsAttributeValue(this XElement element, string sourceAttribute, string attributeValue) + { + if (element == null || sourceAttribute == null || attributeValue == null) { - XElement? element = restrictionElement.Descendants(searchElement).FirstOrDefault(); - string stringValue = element != null ? element.AttributeValue("value") : string.Empty; - return int.TryParse(stringValue, out value); + return false; } - /// - /// Determines whether [is same as attribute value] [the specified element]. - /// - /// - /// The element. - /// - /// - /// The source attribute. - /// - /// - /// The attribute value. - /// - /// - /// true if [is same as attribute value] [the specified element]; otherwise, false. - /// - public static bool IsSameAsAttributeValue(this XElement element, string sourceAttribute, string attributeValue) + XAttribute? thisAttributeValue = element.Attribute(sourceAttribute); + if (thisAttributeValue == null) { - if (element == null || sourceAttribute == null || attributeValue == null) - { - return false; - } - - XAttribute? thisAttributeValue = element.Attribute(sourceAttribute); - if (thisAttributeValue == null) - { - return false; - } - - return thisAttributeValue.Value == attributeValue; + return false; } - #endregion + return thisAttributeValue.Value == attributeValue; } + + #endregion } diff --git a/src/Altinn.App.Core/Features/Action/PaymentUserAction.cs b/src/Altinn.App.Core/Features/Action/PaymentUserAction.cs index 58d46240a..d9b77d322 100644 --- a/src/Altinn.App.Core/Features/Action/PaymentUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/PaymentUserAction.cs @@ -8,83 +8,79 @@ using Altinn.App.Core.Models.UserAction; using Microsoft.Extensions.Logging; -namespace Altinn.App.Core.Features.Action +namespace Altinn.App.Core.Features.Action; + +/// +/// User action for payment +/// +internal class PaymentUserAction : IUserAction { + private readonly IProcessReader _processReader; + private readonly ILogger _logger; + private readonly IPaymentService _paymentService; + /// - /// User action for payment + /// Initializes a new instance of the class /// - internal class PaymentUserAction : IUserAction + public PaymentUserAction( + IProcessReader processReader, + IPaymentService paymentService, + ILogger logger + ) { - private readonly IProcessReader _processReader; - private readonly ILogger _logger; - private readonly IPaymentService _paymentService; + _processReader = processReader; + _paymentService = paymentService; + _logger = logger; + } + + /// + public string Id => "pay"; - /// - /// Initializes a new instance of the class - /// - public PaymentUserAction( - IProcessReader processReader, - IPaymentService paymentService, - ILogger logger + /// + public async Task HandleAction(UserActionContext context) + { + if ( + _processReader.GetFlowElement(context.Instance.Process.CurrentTask.ElementId) is not ProcessTask currentTask ) { - _processReader = processReader; - _paymentService = paymentService; - _logger = logger; + return UserActionResult.FailureResult( + new ActionError() { Code = "NoProcessTask", Message = "Current task is not a process task." } + ); } - /// - public string Id => "pay"; + _logger.LogInformation( + "Payment action handler invoked for instance {Id}. In task: {CurrentTaskId}", + context.Instance.Id, + currentTask.Id + ); - /// - public async Task HandleAction(UserActionContext context) + AltinnPaymentConfiguration? paymentConfiguration = currentTask + .ExtensionElements + ?.TaskExtension + ?.PaymentConfiguration; + if (paymentConfiguration == null) { - if ( - _processReader.GetFlowElement(context.Instance.Process.CurrentTask.ElementId) - is not ProcessTask currentTask - ) - { - return UserActionResult.FailureResult( - new ActionError() { Code = "NoProcessTask", Message = "Current task is not a process task." } - ); - } - - _logger.LogInformation( - "Payment action handler invoked for instance {Id}. In task: {CurrentTaskId}", - context.Instance.Id, - currentTask.Id - ); + throw new ApplicationConfigException("PaymentConfig is missing in the payment process task configuration."); + } - AltinnPaymentConfiguration? paymentConfiguration = currentTask - .ExtensionElements - ?.TaskExtension - ?.PaymentConfiguration; - if (paymentConfiguration == null) - { - throw new ApplicationConfigException( - "PaymentConfig is missing in the payment process task configuration." - ); - } + (PaymentInformation paymentInformation, bool alreadyPaid) = await _paymentService.StartPayment( + context.Instance, + paymentConfiguration.Validate(), + context.Language + ); - (PaymentInformation paymentInformation, bool alreadyPaid) = await _paymentService.StartPayment( - context.Instance, - paymentConfiguration.Validate(), - context.Language + if (alreadyPaid) + { + return UserActionResult.FailureResult( + error: new ActionError { Code = "PaymentAlreadyCompleted", Message = "Payment already completed." }, + errorType: ProcessErrorType.Conflict ); + } - if (alreadyPaid) - { - return UserActionResult.FailureResult( - error: new ActionError { Code = "PaymentAlreadyCompleted", Message = "Payment already completed." }, - errorType: ProcessErrorType.Conflict - ); - } - - string? paymentDetailsRedirectUrl = paymentInformation.PaymentDetails?.RedirectUrl; + string? paymentDetailsRedirectUrl = paymentInformation.PaymentDetails?.RedirectUrl; - return paymentDetailsRedirectUrl == null - ? UserActionResult.SuccessResult() - : UserActionResult.RedirectResult(new Uri(paymentDetailsRedirectUrl)); - } + return paymentDetailsRedirectUrl == null + ? UserActionResult.SuccessResult() + : UserActionResult.RedirectResult(new Uri(paymentDetailsRedirectUrl)); } } diff --git a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs index f1c95b63c..13865d0ed 100644 --- a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; diff --git a/src/Altinn.App.Core/Features/Action/UserActionService.cs b/src/Altinn.App.Core/Features/Action/UserActionService.cs index 4e6f2add2..306e3a676 100644 --- a/src/Altinn.App.Core/Features/Action/UserActionService.cs +++ b/src/Altinn.App.Core/Features/Action/UserActionService.cs @@ -1,5 +1,3 @@ -using Altinn.App.Core.Internal; - namespace Altinn.App.Core.Features.Action; /// diff --git a/src/Altinn.App.Core/Features/DataLists/DataListsFactory.cs b/src/Altinn.App.Core/Features/DataLists/DataListsFactory.cs index e10f17a66..5e1a3cf6c 100644 --- a/src/Altinn.App.Core/Features/DataLists/DataListsFactory.cs +++ b/src/Altinn.App.Core/Features/DataLists/DataListsFactory.cs @@ -1,43 +1,36 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Altinn.App.Core.Features.DataLists; -namespace Altinn.App.Core.Features.DataLists +/// +/// Factory class for resolving implementations +/// based on the name/id of the data lists requested. +/// +public class DataListsFactory { /// - /// Factory class for resolving implementations - /// based on the name/id of the data lists requested. + /// Initializes a new instance of the class. /// - public class DataListsFactory + public DataListsFactory(IEnumerable dataListProviders) { - /// - /// Initializes a new instance of the class. - /// - public DataListsFactory(IEnumerable dataListProviders) - { - DataListProviders = dataListProviders; - } + _dataListProviders = dataListProviders; + } - private IEnumerable DataListProviders { get; } + private IEnumerable _dataListProviders { get; } - /// - /// Finds the implementation of IDataListsProvider based on the options id - /// provided. - /// - /// Id matching the options requested. - public IDataListProvider GetDataListProvider(string listId) + /// + /// Finds the implementation of IDataListsProvider based on the options id + /// provided. + /// + /// Id matching the options requested. + public IDataListProvider GetDataListProvider(string listId) + { + foreach (var dataListProvider in _dataListProviders) { - foreach (var dataListProvider in DataListProviders) + if (dataListProvider.Id.ToLower().Equals(listId.ToLower())) { - if (dataListProvider.Id.ToLower().Equals(listId.ToLower())) - { - return dataListProvider; - } + return dataListProvider; } - - return new NullDataListProvider(); } + + return new NullDataListProvider(); } } diff --git a/src/Altinn.App.Core/Features/DataLists/DataListsService.cs b/src/Altinn.App.Core/Features/DataLists/DataListsService.cs index a6c94d754..588de4e69 100644 --- a/src/Altinn.App.Core/Features/DataLists/DataListsService.cs +++ b/src/Altinn.App.Core/Features/DataLists/DataListsService.cs @@ -1,53 +1,52 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features.DataLists +namespace Altinn.App.Core.Features.DataLists; + +/// +/// Service for handling datalists. +/// +public class DataListsService : IDataListsService { + private readonly DataListsFactory _dataListsFactory; + private readonly InstanceDataListsFactory _instanceDataListsFactory; + private readonly Telemetry? _telemetry; + /// - /// Service for handling datalists. + /// Initializes a new instance of the class. /// - public class DataListsService : IDataListsService + public DataListsService( + DataListsFactory dataListsFactory, + InstanceDataListsFactory instanceDataListsFactory, + Telemetry? telemetry = null + ) { - private readonly DataListsFactory _dataListsFactory; - private readonly InstanceDataListsFactory _instanceDataListsFactory; - private readonly Telemetry? _telemetry; - - /// - /// Initializes a new instance of the class. - /// - public DataListsService( - DataListsFactory dataListsFactory, - InstanceDataListsFactory instanceDataListsFactory, - Telemetry? telemetry = null - ) - { - _dataListsFactory = dataListsFactory; - _instanceDataListsFactory = instanceDataListsFactory; - _telemetry = telemetry; - } + _dataListsFactory = dataListsFactory; + _instanceDataListsFactory = instanceDataListsFactory; + _telemetry = telemetry; + } - /// - public async Task GetDataListAsync( - string dataListId, - string? language, - Dictionary keyValuePairs - ) - { - using var activity = _telemetry?.StartDataListActivity(); - return await _dataListsFactory.GetDataListProvider(dataListId).GetDataListAsync(language, keyValuePairs); - } + /// + public async Task GetDataListAsync( + string dataListId, + string? language, + Dictionary keyValuePairs + ) + { + using var activity = _telemetry?.StartDataListActivity(); + return await _dataListsFactory.GetDataListProvider(dataListId).GetDataListAsync(language, keyValuePairs); + } - /// - public async Task GetDataListAsync( - InstanceIdentifier instanceIdentifier, - string dataListId, - string? language, - Dictionary keyValuePairs - ) - { - using var activity = _telemetry?.StartDataListActivity(instanceIdentifier); - return await _instanceDataListsFactory - .GetDataListProvider(dataListId) - .GetInstanceDataListAsync(instanceIdentifier, language, keyValuePairs); - } + /// + public async Task GetDataListAsync( + InstanceIdentifier instanceIdentifier, + string dataListId, + string? language, + Dictionary keyValuePairs + ) + { + using var activity = _telemetry?.StartDataListActivity(instanceIdentifier); + return await _instanceDataListsFactory + .GetDataListProvider(dataListId) + .GetInstanceDataListAsync(instanceIdentifier, language, keyValuePairs); } } diff --git a/src/Altinn.App.Core/Features/DataLists/IDataListsService.cs b/src/Altinn.App.Core/Features/DataLists/IDataListsService.cs index a9738443d..8838a4213 100644 --- a/src/Altinn.App.Core/Features/DataLists/IDataListsService.cs +++ b/src/Altinn.App.Core/Features/DataLists/IDataListsService.cs @@ -1,41 +1,35 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features.DataLists +namespace Altinn.App.Core.Features.DataLists; + +/// +/// Interface for working with +/// +public interface IDataListsService { /// - /// Interface for working with + /// Get the list of options for a specific options list by its id and key/value pairs. /// - public interface IDataListsService - { - /// - /// Get the list of options for a specific options list by its id and key/value pairs. - /// - /// The id of the options list to retrieve - /// The language code requested. - /// Optional list of key/value pairs to use for filtering and further lookup. - /// The list of options - Task GetDataListAsync(string dataListId, string? language, Dictionary keyValuePairs); + /// The id of the options list to retrieve + /// The language code requested. + /// Optional list of key/value pairs to use for filtering and further lookup. + /// The list of options + Task GetDataListAsync(string dataListId, string? language, Dictionary keyValuePairs); - /// - /// Get the list of instance specific datalist for a specific data list based on the - /// and key/value pairs. The values returned from this implementation could be specific to the instance and/or - /// instance owner and should not be cached without careful thinking around caching strategy. - /// - /// Class identifying the instance by instance owner party id and instance guid. - /// The id of the options list to retrieve - /// The language code requested. - /// Optional list of key/value pairs to use for filtering and further lookup. - /// The list of options - Task GetDataListAsync( - InstanceIdentifier instanceIdentifier, - string dataListId, - string? language, - Dictionary keyValuePairs - ); - } + /// + /// Get the list of instance specific datalist for a specific data list based on the + /// and key/value pairs. The values returned from this implementation could be specific to the instance and/or + /// instance owner and should not be cached without careful thinking around caching strategy. + /// + /// Class identifying the instance by instance owner party id and instance guid. + /// The id of the options list to retrieve + /// The language code requested. + /// Optional list of key/value pairs to use for filtering and further lookup. + /// The list of options + Task GetDataListAsync( + InstanceIdentifier instanceIdentifier, + string dataListId, + string? language, + Dictionary keyValuePairs + ); } diff --git a/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs b/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs index 5378df447..eb24f5ded 100644 --- a/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs +++ b/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs @@ -1,43 +1,36 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Altinn.App.Core.Features.DataLists; -namespace Altinn.App.Core.Features.DataLists +/// +/// Factory class for resolving implementations +/// based on the name/id of the data lists requested. +/// +public class InstanceDataListsFactory { /// - /// Factory class for resolving implementations - /// based on the name/id of the data lists requested. + /// Initializes a new instance of the class. /// - public class InstanceDataListsFactory + public InstanceDataListsFactory(IEnumerable instanceDataListProvider) { - /// - /// Initializes a new instance of the class. - /// - public InstanceDataListsFactory(IEnumerable instanceDataListProvider) - { - InstanceDataListProviders = instanceDataListProvider; - } + _instanceDataListProviders = instanceDataListProvider; + } - private IEnumerable InstanceDataListProviders { get; } + private IEnumerable _instanceDataListProviders { get; } - /// - /// Finds the implementation of IDataListsProvider based on the options id - /// provided. - /// - /// Id matching the options requested. - public IInstanceDataListProvider GetDataListProvider(string listId) + /// + /// Finds the implementation of IDataListsProvider based on the options id + /// provided. + /// + /// Id matching the options requested. + public IInstanceDataListProvider GetDataListProvider(string listId) + { + foreach (var instanceDataListProvider in _instanceDataListProviders) { - foreach (var instanceDataListProvider in InstanceDataListProviders) + if (instanceDataListProvider.Id.ToLower().Equals(listId.ToLower())) { - if (instanceDataListProvider.Id.ToLower().Equals(listId.ToLower())) - { - return instanceDataListProvider; - } + return instanceDataListProvider; } - - return new NullInstanceDataListProvider(); } + + return new NullInstanceDataListProvider(); } } diff --git a/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs b/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs index 3b484dc4e..b59ad5277 100644 --- a/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs +++ b/src/Altinn.App.Core/Features/DataLists/NullDataListProvider.cs @@ -1,28 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features.DataLists +namespace Altinn.App.Core.Features.DataLists; + +/// +/// Nullobject for cases where there is no match on the requested +/// ListIttems is set to null and not an empty list for the controller to be able to differensiate +/// between option provider found, but with no values and no option provider found ie. returns 404. +/// +public class NullDataListProvider : IDataListProvider { - /// - /// Nullobject for cases where there is no match on the requested - /// ListIttems is set to null and not an empty list for the controller to be able to differensiate - /// between option provider found, but with no values and no option provider found ie. returns 404. - /// - public class NullDataListProvider : IDataListProvider - { - /// - public string Id => string.Empty; + /// + public string Id => string.Empty; - /// - public Task GetDataListAsync(string? language, Dictionary keyValuePairs) - { + /// + public Task GetDataListAsync(string? language, Dictionary keyValuePairs) + { #nullable disable - return Task.FromResult(new DataList() { ListItems = null }); + return Task.FromResult(new DataList() { ListItems = null }); #nullable restore - } } } diff --git a/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs b/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs index fc928389f..53097be3c 100644 --- a/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs +++ b/src/Altinn.App.Core/Features/DataLists/NullInstanceDataListProvider.cs @@ -1,32 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features.DataLists +namespace Altinn.App.Core.Features.DataLists; + +/// +/// Nullobject for cases where there is no match on the requested +/// ListIttems is set to null and not an empty list for the controller to be able to differensiate +/// between option provider found, but with no values and no option provider found ie. returns 404. +/// +public class NullInstanceDataListProvider : IInstanceDataListProvider { - /// - /// Nullobject for cases where there is no match on the requested - /// ListIttems is set to null and not an empty list for the controller to be able to differensiate - /// between option provider found, but with no values and no option provider found ie. returns 404. - /// - public class NullInstanceDataListProvider : IInstanceDataListProvider - { - /// - public string Id => string.Empty; + /// + public string Id => string.Empty; - /// - public Task GetInstanceDataListAsync( - InstanceIdentifier instanceIdentifier, - string? language, - Dictionary keyValuePairs - ) - { + /// + public Task GetInstanceDataListAsync( + InstanceIdentifier instanceIdentifier, + string? language, + Dictionary keyValuePairs + ) + { #nullable disable - return Task.FromResult(new DataList() { ListItems = null }); + return Task.FromResult(new DataList() { ListItems = null }); #nullable restore - } } } diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs index 6b84b8b13..fde4ee2a8 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs @@ -1,29 +1,28 @@ using Altinn.App.Core.Features.FileAnalysis; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Features.FileAnalyzis +namespace Altinn.App.Core.Features.FileAnalyzis; + +/// +/// Factory class that resolves the correct file analysers to run on against a . +/// +public class FileAnalyserFactory : IFileAnalyserFactory { + private readonly IEnumerable _fileAnalysers; + /// - /// Factory class that resolves the correct file analysers to run on against a . + /// Initializes a new instance of the class. /// - public class FileAnalyserFactory : IFileAnalyserFactory + public FileAnalyserFactory(IEnumerable fileAnalysers) { - private readonly IEnumerable _fileAnalysers; - - /// - /// Initializes a new instance of the class. - /// - public FileAnalyserFactory(IEnumerable fileAnalysers) - { - _fileAnalysers = fileAnalysers; - } + _fileAnalysers = fileAnalysers; + } - /// - /// Finds the specified file analyser implementations based on the specified analyser id's. - /// - public IEnumerable GetFileAnalysers(IEnumerable analyserIds) - { - return _fileAnalysers.Where(x => analyserIds.Contains(x.Id)).ToList(); - } + /// + /// Finds the specified file analyser implementations based on the specified analyser id's. + /// + public IEnumerable GetFileAnalysers(IEnumerable analyserIds) + { + return _fileAnalysers.Where(x => analyserIds.Contains(x.Id)).ToList(); } } diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs index 16942a550..415eaaaa3 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs @@ -1,44 +1,43 @@ using Altinn.App.Core.Features.FileAnalyzis; -namespace Altinn.App.Core.Features.FileAnalysis +namespace Altinn.App.Core.Features.FileAnalysis; + +/// +/// Results from a file analysis done based the content of the file, ie. the binary data. +/// +public class FileAnalysisResult { /// - /// Results from a file analysis done based the content of the file, ie. the binary data. + /// Initializes a new instance of the class. /// - public class FileAnalysisResult + public FileAnalysisResult(string analyserId) { - /// - /// Initializes a new instance of the class. - /// - public FileAnalysisResult(string analyserId) - { - AnalyserId = analyserId; - } + AnalyserId = analyserId; + } - /// - /// The id of the analyser generating the result. - /// - public string AnalyserId { get; internal set; } + /// + /// The id of the analyser generating the result. + /// + public string AnalyserId { get; internal set; } - /// - /// The name of the analysed file. - /// - public string? Filename { get; set; } + /// + /// The name of the analysed file. + /// + public string? Filename { get; set; } - /// - /// The file extension(s) without the . i.e. pdf | png | docx - /// Some mime types might have multiple extensions registered for ecample image/jpeg has both jpg and jpeg. - /// - public List Extensions { get; set; } = new List(); + /// + /// The file extension(s) without the . i.e. pdf | png | docx + /// Some mime types might have multiple extensions registered for ecample image/jpeg has both jpg and jpeg. + /// + public List Extensions { get; set; } = new List(); - /// - /// The mime type - /// - public string? MimeType { get; set; } + /// + /// The mime type + /// + public string? MimeType { get; set; } - /// - /// Key/Value pairs containing findings from the analysis. - /// - public IDictionary Metadata { get; private set; } = new Dictionary(); - } + /// + /// Key/Value pairs containing findings from the analysis. + /// + public IDictionary Metadata { get; private set; } = new Dictionary(); } diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs index 6ba5999a6..81846b363 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs @@ -1,53 +1,52 @@ using Altinn.App.Core.Features.FileAnalysis; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Features.FileAnalyzis +namespace Altinn.App.Core.Features.FileAnalyzis; + +/// +/// Analyses a file using the registred analysers on the +/// +public class FileAnalysisService : IFileAnalysisService { + private readonly IFileAnalyserFactory _fileAnalyserFactory; + private readonly Telemetry? _telemetry; + /// - /// Analyses a file using the registred analysers on the + /// Initializes a new instance of the class. /// - public class FileAnalysisService : IFileAnalysisService + public FileAnalysisService(IFileAnalyserFactory fileAnalyserFactory, Telemetry? telemetry = null) { - private readonly IFileAnalyserFactory _fileAnalyserFactory; - private readonly Telemetry? _telemetry; + _fileAnalyserFactory = fileAnalyserFactory; + _telemetry = telemetry; + } - /// - /// Initializes a new instance of the class. - /// - public FileAnalysisService(IFileAnalyserFactory fileAnalyserFactory, Telemetry? telemetry = null) - { - _fileAnalyserFactory = fileAnalyserFactory; - _telemetry = telemetry; - } + /// + /// Runs the specified file analysers against the stream provided. + /// + public async Task> Analyse( + DataType dataType, + Stream fileStream, + string? filename = null + ) + { + using var activity = _telemetry?.StartAnalyseActivity(); + List fileAnalysers = _fileAnalyserFactory + .GetFileAnalysers(dataType.EnabledFileAnalysers) + .ToList(); - /// - /// Runs the specified file analysers against the stream provided. - /// - public async Task> Analyse( - DataType dataType, - Stream fileStream, - string? filename = null - ) + List fileAnalysisResults = new(); + foreach (var analyser in fileAnalysers) { - using var activity = _telemetry?.StartAnalyseActivity(); - List fileAnalysers = _fileAnalyserFactory - .GetFileAnalysers(dataType.EnabledFileAnalysers) - .ToList(); - - List fileAnalysisResults = new(); - foreach (var analyser in fileAnalysers) + if (fileStream.CanSeek) { - if (fileStream.CanSeek) - { - fileStream.Position = fileStream.Seek(0, SeekOrigin.Begin); - } - var result = await analyser.Analyse(fileStream, filename); - result.AnalyserId = analyser.Id; - result.Filename = filename; - fileAnalysisResults.Add(result); + fileStream.Position = fileStream.Seek(0, SeekOrigin.Begin); } - - return fileAnalysisResults; + var result = await analyser.Analyse(fileStream, filename); + result.AnalyserId = analyser.Id; + result.Filename = filename; + fileAnalysisResults.Add(result); } + + return fileAnalysisResults; } } diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs index 4b88a2abf..9f60bcb56 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs @@ -1,20 +1,19 @@ -namespace Altinn.App.Core.Features.FileAnalysis +namespace Altinn.App.Core.Features.FileAnalysis; + +/// +/// Interface for doing extended binary file analysing. +/// +public interface IFileAnalyser { /// - /// Interface for doing extended binary file analysing. + /// The id of the analyser to be used when enabling it from config. /// - public interface IFileAnalyser - { - /// - /// The id of the analyser to be used when enabling it from config. - /// - public string Id { get; } + public string Id { get; } - /// - /// Analyses a stream with the intent to extract metadata. - /// - /// The stream to analyse. One stream = one file. - /// Filename. Optional parameter if the implementation needs the name of the file, relative or absolute path. - public Task Analyse(Stream stream, string? filename = null); - } + /// + /// Analyses a stream with the intent to extract metadata. + /// + /// The stream to analyse. One stream = one file. + /// Filename. Optional parameter if the implementation needs the name of the file, relative or absolute path. + public Task Analyse(Stream stream, string? filename = null); } diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs index e67430de6..fef086b83 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs @@ -1,16 +1,15 @@ using Altinn.App.Core.Features.FileAnalysis; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Features.FileAnalyzis +namespace Altinn.App.Core.Features.FileAnalyzis; + +/// +/// Interface responsible for resolving the correct file analysers to run on against a . +/// +public interface IFileAnalyserFactory { /// - /// Interface responsible for resolving the correct file analysers to run on against a . + /// Finds analyser implementations based on the specified id's provided. /// - public interface IFileAnalyserFactory - { - /// - /// Finds analyser implementations based on the specified id's provided. - /// - IEnumerable GetFileAnalysers(IEnumerable analyserIds); - } + IEnumerable GetFileAnalysers(IEnumerable analyserIds); } diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs index 2dabbc806..ee8108299 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs @@ -1,19 +1,18 @@ using Altinn.App.Core.Features.FileAnalysis; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Features.FileAnalyzis +namespace Altinn.App.Core.Features.FileAnalyzis; + +/// +/// Interface for running all file analysers registered on a data type. +/// +public interface IFileAnalysisService { /// - /// Interface for running all file analysers registered on a data type. + /// Analyses the the specified file stream. /// - public interface IFileAnalysisService - { - /// - /// Analyses the the specified file stream. - /// - /// The where the anlysers are registered. - /// The file stream to analyse - /// The name of the file - Task> Analyse(DataType dataType, Stream fileStream, string? filename = null); - } + /// The where the anlysers are registered. + /// The file stream to analyse + /// The name of the file + Task> Analyse(DataType dataType, Stream fileStream, string? filename = null); } diff --git a/src/Altinn.App.Core/Features/IAppOptionsProvider.cs b/src/Altinn.App.Core/Features/IAppOptionsProvider.cs index 78c3eea5e..d1745d385 100644 --- a/src/Altinn.App.Core/Features/IAppOptionsProvider.cs +++ b/src/Altinn.App.Core/Features/IAppOptionsProvider.cs @@ -1,25 +1,24 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features +namespace Altinn.App.Core.Features; + +/// +/// Interface for providing +/// +public interface IAppOptionsProvider { /// - /// Interface for providing + /// The id/name that is used in the optionsId parameter in the SelectionComponents (Checkboxes, RadioButtons, Dropdown ...) + /// You can have as many providers as you like, but you should have only one per id. /// - public interface IAppOptionsProvider - { - /// - /// The id/name that is used in the optionsId parameter in the SelectionComponents (Checkboxes, RadioButtons, Dropdown ...) - /// You can have as many providers as you like, but you should have only one per id. - /// - string Id { get; } + string Id { get; } - /// - /// Gets the based on the provided options id and key value pairs. - /// - /// Language code - /// Key/value pairs to control what options to get. - /// When called from the options controller this will be the querystring key/value pairs. - /// A representing the result of the asynchronous operation. - Task GetAppOptionsAsync(string? language, Dictionary keyValuePairs); - } + /// + /// Gets the based on the provided options id and key value pairs. + /// + /// Language code + /// Key/value pairs to control what options to get. + /// When called from the options controller this will be the querystring key/value pairs. + /// A representing the result of the asynchronous operation. + Task GetAppOptionsAsync(string? language, Dictionary keyValuePairs); } diff --git a/src/Altinn.App.Core/Features/IDataListProvider.cs b/src/Altinn.App.Core/Features/IDataListProvider.cs index 32f9626ac..5287ada7b 100644 --- a/src/Altinn.App.Core/Features/IDataListProvider.cs +++ b/src/Altinn.App.Core/Features/IDataListProvider.cs @@ -1,30 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features +namespace Altinn.App.Core.Features; + +/// +/// Interface for providing +/// +public interface IDataListProvider { /// - /// Interface for providing + /// The id/name of the options this provider supports ie. land, fylker, kommuner. + /// You can have as many providers as you like, but you should have only one per id. /// - public interface IDataListProvider - { - /// - /// The id/name of the options this provider supports ie. land, fylker, kommuner. - /// You can have as many providers as you like, but you should have only one per id. - /// - string Id { get; } + string Id { get; } - /// - /// Gets the based on the provided options id and key value pairs. - /// - /// Language code - /// Key/value pairs to control what options to get. - /// When called from the data lists controller this will be the querystring key/value pairs. - /// A representing the result of the asynchronous operation. - Task GetDataListAsync(string? language, Dictionary keyValuePairs); - } + /// + /// Gets the based on the provided options id and key value pairs. + /// + /// Language code + /// Key/value pairs to control what options to get. + /// When called from the data lists controller this will be the querystring key/value pairs. + /// A representing the result of the asynchronous operation. + Task GetDataListAsync(string? language, Dictionary keyValuePairs); } diff --git a/src/Altinn.App.Core/Features/IEventHandler.cs b/src/Altinn.App.Core/Features/IEventHandler.cs index d5b565c94..e1be51030 100644 --- a/src/Altinn.App.Core/Features/IEventHandler.cs +++ b/src/Altinn.App.Core/Features/IEventHandler.cs @@ -1,20 +1,19 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features +namespace Altinn.App.Core.Features; + +/// +/// Class handling a dedicated type of event from the Event system ie. an external event. +/// +public interface IEventHandler { /// - /// Class handling a dedicated type of event from the Event system ie. an external event. + /// The type of event as specified in ref. https://github.com/cloudevents/spec/blob/v1.0/spec.md. /// - public interface IEventHandler - { - /// - /// The type of event as specified in ref. https://github.com/cloudevents/spec/blob/v1.0/spec.md. - /// - string EventType { get; } + string EventType { get; } - /// - /// Implementation of what should happen when the event is received in the application. - /// - Task ProcessEvent(CloudEvent cloudEvent); - } + /// + /// Implementation of what should happen when the event is received in the application. + /// + Task ProcessEvent(CloudEvent cloudEvent); } diff --git a/src/Altinn.App.Core/Features/IInstanceAppOptionsProvider.cs b/src/Altinn.App.Core/Features/IInstanceAppOptionsProvider.cs index a5b8caaf9..b169f7ada 100644 --- a/src/Altinn.App.Core/Features/IInstanceAppOptionsProvider.cs +++ b/src/Altinn.App.Core/Features/IInstanceAppOptionsProvider.cs @@ -1,30 +1,29 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features +namespace Altinn.App.Core.Features; + +/// +/// Interface for providing related to an instance/instance owner. +/// +public interface IInstanceAppOptionsProvider { /// - /// Interface for providing related to an instance/instance owner. + /// The id/name of the options this provider supports ie. land, fylker, kommuner. + /// You can have as many providers as you like, but you should have only one per id. /// - public interface IInstanceAppOptionsProvider - { - /// - /// The id/name of the options this provider supports ie. land, fylker, kommuner. - /// You can have as many providers as you like, but you should have only one per id. - /// - string Id { get; } + string Id { get; } - /// - /// Gets the based on the provided options id and key value pairs. - /// - /// Class identifying the instance. - /// Language code - /// Key/value pairs to control what options to get. - /// When called from the options controller this will be the querystring key/value pairs. - /// A representing the result of the asynchronous operation. - Task GetInstanceAppOptionsAsync( - InstanceIdentifier instanceIdentifier, - string? language, - Dictionary keyValuePairs - ); - } + /// + /// Gets the based on the provided options id and key value pairs. + /// + /// Class identifying the instance. + /// Language code + /// Key/value pairs to control what options to get. + /// When called from the options controller this will be the querystring key/value pairs. + /// A representing the result of the asynchronous operation. + Task GetInstanceAppOptionsAsync( + InstanceIdentifier instanceIdentifier, + string? language, + Dictionary keyValuePairs + ); } diff --git a/src/Altinn.App.Core/Features/IInstanceDataListProvider.cs b/src/Altinn.App.Core/Features/IInstanceDataListProvider.cs index 79d6600c7..9721d6491 100644 --- a/src/Altinn.App.Core/Features/IInstanceDataListProvider.cs +++ b/src/Altinn.App.Core/Features/IInstanceDataListProvider.cs @@ -1,35 +1,29 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features +namespace Altinn.App.Core.Features; + +/// +/// Interface for providing related to an instance/instance owner. +/// +public interface IInstanceDataListProvider { /// - /// Interface for providing related to an instance/instance owner. + /// The id/name of the options this provider supports ie. land, fylker, kommuner. + /// You can have as many providers as you like, but you should have only one per id. /// - public interface IInstanceDataListProvider - { - /// - /// The id/name of the options this provider supports ie. land, fylker, kommuner. - /// You can have as many providers as you like, but you should have only one per id. - /// - string Id { get; } + string Id { get; } - /// - /// Gets the based on the provided options id and key value pairs. - /// - /// Class identifying the instance. - /// Language code - /// Key/value pairs to control what options to get. - /// When called from the data lists controller this will be the querystring key/value pairs. - /// A representing the result of the asynchronous operation. - Task GetInstanceDataListAsync( - InstanceIdentifier instanceIdentifier, - string? language, - Dictionary keyValuePairs - ); - } + /// + /// Gets the based on the provided options id and key value pairs. + /// + /// Class identifying the instance. + /// Language code + /// Key/value pairs to control what options to get. + /// When called from the data lists controller this will be the querystring key/value pairs. + /// A representing the result of the asynchronous operation. + Task GetInstanceDataListAsync( + InstanceIdentifier instanceIdentifier, + string? language, + Dictionary keyValuePairs + ); } diff --git a/src/Altinn.App.Core/Features/IInstanceValidator.cs b/src/Altinn.App.Core/Features/IInstanceValidator.cs index 3ec6255ea..6cd452bc4 100644 --- a/src/Altinn.App.Core/Features/IInstanceValidator.cs +++ b/src/Altinn.App.Core/Features/IInstanceValidator.cs @@ -1,4 +1,3 @@ -using Altinn.App.Core.Features.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Mvc.ModelBinding; diff --git a/src/Altinn.App.Core/Features/IPageOrder.cs b/src/Altinn.App.Core/Features/IPageOrder.cs index 02fad3e64..5f62f1522 100644 --- a/src/Altinn.App.Core/Features/IPageOrder.cs +++ b/src/Altinn.App.Core/Features/IPageOrder.cs @@ -1,30 +1,29 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features +namespace Altinn.App.Core.Features; + +/// +/// Interface for page order handling in stateful apps +/// +[Obsolete("IPageOrder does not work with frontend version 4")] +public interface IPageOrder { /// - /// Interface for page order handling in stateful apps + /// Gets the current page order of the app /// - [Obsolete("IPageOrder does not work with frontend version 4")] - public interface IPageOrder - { - /// - /// Gets the current page order of the app - /// - /// Object identifying the app - /// Object identifying the instance - /// The layout set id - /// The current page of the instance. - /// The data type id of the current layout. - /// The form data. - /// The pages in sorted order. - Task> GetPageOrder( - AppIdentifier appIdentifier, - InstanceIdentifier instanceIdentifier, - string layoutSetId, - string currentPage, - string dataTypeId, - object formData - ); - } + /// Object identifying the app + /// Object identifying the instance + /// The layout set id + /// The current page of the instance. + /// The data type id of the current layout. + /// The form data. + /// The pages in sorted order. + Task> GetPageOrder( + AppIdentifier appIdentifier, + InstanceIdentifier instanceIdentifier, + string layoutSetId, + string currentPage, + string dataTypeId, + object formData + ); } diff --git a/src/Altinn.App.Core/Features/Notifications/Email/EmailNotificationClient.cs b/src/Altinn.App.Core/Features/Notifications/Email/EmailNotificationClient.cs index 9e8ee46e7..79d0dea38 100644 --- a/src/Altinn.App.Core/Features/Notifications/Email/EmailNotificationClient.cs +++ b/src/Altinn.App.Core/Features/Notifications/Email/EmailNotificationClient.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Net.Http.Headers; using System.Text.Json; using Altinn.App.Core.Configuration; diff --git a/src/Altinn.App.Core/Features/Notifications/Sms/SmsNotificationClient.cs b/src/Altinn.App.Core/Features/Notifications/Sms/SmsNotificationClient.cs index 5d7d4b862..e0fa35fb2 100644 --- a/src/Altinn.App.Core/Features/Notifications/Sms/SmsNotificationClient.cs +++ b/src/Altinn.App.Core/Features/Notifications/Sms/SmsNotificationClient.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Net.Http.Headers; using System.Text.Json; using Altinn.App.Core.Configuration; diff --git a/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2CodeListProvider.cs b/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2CodeListProvider.cs index 5a253fce1..8c3cdae1c 100644 --- a/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2CodeListProvider.cs +++ b/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2CodeListProvider.cs @@ -1,107 +1,106 @@ using Altinn.App.Core.Models; using Microsoft.Extensions.Caching.Memory; -namespace Altinn.App.Core.Features.Options.Altinn2Provider +namespace Altinn.App.Core.Features.Options.Altinn2Provider; + +/// +/// Implementation of a IAppOptionsProviders for the old altinn2 apis +/// +public class Altinn2CodeListProvider : IAppOptionsProvider { /// - /// Implementation of a IAppOptionsProviders for the old altinn2 apis + /// Mapping function to get from the altinn2 model to altinn 3 option /// - public class Altinn2CodeListProvider : IAppOptionsProvider - { - /// - /// Mapping function to get from the altinn2 model to altinn 3 option - /// - private readonly Func _transform; + private readonly Func _transform; - /// - /// Filter function in case you only want a subset of the altinn2 codelist - /// - private readonly Func? _filter; + /// + /// Filter function in case you only want a subset of the altinn2 codelist + /// + private readonly Func? _filter; - /// - /// id for use in altinn2 api - /// - private readonly string _metadataApiId; + /// + /// id for use in altinn2 api + /// + private readonly string _metadataApiId; - /// - /// version of the code list in the altinn2 metadata api - /// - private readonly int? _codeListVersion; + /// + /// version of the code list in the altinn2 metadata api + /// + private readonly int? _codeListVersion; - /// - /// Altinn2MetadataApiClient for requesting - /// - private readonly Altinn2MetadataApiClient _client; + /// + /// Altinn2MetadataApiClient for requesting + /// + private readonly Altinn2MetadataApiClient _client; - /// - /// Cache for options as altinn2 options are static - /// - private readonly IMemoryCache _cache; + /// + /// Cache for options as altinn2 options are static + /// + private readonly IMemoryCache _cache; - /// - public string Id { get; private set; } + /// + public string Id { get; private set; } - /// - /// - /// - public Altinn2CodeListProvider( - IMemoryCache cache, - Altinn2MetadataApiClient client, - string id, - Func transform, - Func? filter, - string? metadataApiId = null, - int? codeListVersion = null - ) - { - _cache = cache; - _client = client; - Id = id; // id in layout definitions - _metadataApiId = metadataApiId ?? id; // codelist id in api (often the same as id, but if the same codelist is used with different filters, it has to be different) - _transform = transform; - _filter = filter; - _codeListVersion = codeListVersion; - } + /// + /// + /// + public Altinn2CodeListProvider( + IMemoryCache cache, + Altinn2MetadataApiClient client, + string id, + Func transform, + Func? filter, + string? metadataApiId = null, + int? codeListVersion = null + ) + { + _cache = cache; + _client = client; + Id = id; // id in layout definitions + _metadataApiId = metadataApiId ?? id; // codelist id in api (often the same as id, but if the same codelist is used with different filters, it has to be different) + _transform = transform; + _filter = filter; + _codeListVersion = codeListVersion; + } - /// - /// Utility method if you need the raw codelist for dataprocessinghandler - /// - public async Task GetRawAltinn2CodelistAsync(string? language) + /// + /// Utility method if you need the raw codelist for dataprocessinghandler + /// + public async Task GetRawAltinn2CodelistAsync(string? language) + { + var langCode = language switch { - var langCode = language switch - { - "nb" => "1044", - "nn" => "2068", - "en" => "1033", - _ => "1044", // default to norwegian bokmål - }; + "nb" => "1044", + "nn" => "2068", + "en" => "1033", + _ => "1044", // default to norwegian bokmål + }; - // ! TODO: address this is next major release, should never return null - return ( - await _cache.GetOrCreateAsync( - $"{_metadataApiId}{langCode}{_codeListVersion}", - async (entry) => - { - entry.Priority = CacheItemPriority.NeverRemove; - entry.AbsoluteExpiration = DateTimeOffset.MaxValue; - return await _client.GetAltinn2Codelist(_metadataApiId, langCode, _codeListVersion); - } - ) - )!; - } + // ! TODO: address this is next major release, should never return null + return ( + await _cache.GetOrCreateAsync( + $"{_metadataApiId}{langCode}{_codeListVersion}", + async (entry) => + { + entry.Priority = CacheItemPriority.NeverRemove; + entry.AbsoluteExpiration = DateTimeOffset.MaxValue; + return await _client.GetAltinn2Codelist(_metadataApiId, langCode, _codeListVersion); + } + ) + )!; + } - /// - public async Task GetAppOptionsAsync(string? language, Dictionary keyValuePairs) - { - var codelist = await GetRawAltinn2CodelistAsync(language); + /// + public async Task GetAppOptionsAsync(string? language, Dictionary keyValuePairs) + { + var codelist = await GetRawAltinn2CodelistAsync(language); - AppOptions options = - new() - { - Options = codelist.Codes.Where(_filter ?? (c => true)).Select(_transform).ToList(), - IsCacheable = true - }; - return options; - } + AppOptions options = + new() + { + Options = codelist.Codes.Where(_filter ?? (c => true)).Select(_transform).ToList(), + IsCacheable = true + }; + return options; } } diff --git a/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs b/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs index cc273b0d7..d2973c618 100644 --- a/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs +++ b/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs @@ -1,45 +1,44 @@ using Altinn.App.Core.Helpers; -namespace Altinn.App.Core.Features.Options.Altinn2Provider +namespace Altinn.App.Core.Features.Options.Altinn2Provider; + +/// +/// HttpClientWrapper for the altinn2 metadata/codelists api +/// +public class Altinn2MetadataApiClient { /// - /// HttpClientWrapper for the altinn2 metadata/codelists api + /// HttpClient /// - public class Altinn2MetadataApiClient - { - /// - /// HttpClient - /// - private readonly HttpClient _client; + private readonly HttpClient _client; - /// - /// Constructor - /// - public Altinn2MetadataApiClient(HttpClient client) - { - _client = client; - } + /// + /// Constructor + /// + public Altinn2MetadataApiClient(HttpClient client) + { + _client = client; + } - /// - /// Fetch the code list - /// - /// id of the code list - /// Language code per altinn2 definisions (nb=>1044, ...) - /// The version number for the list in the api - public async Task GetAltinn2Codelist(string id, string langCode, int? version = null) + /// + /// Fetch the code list + /// + /// id of the code list + /// Language code per altinn2 definisions (nb=>1044, ...) + /// The version number for the list in the api + public async Task GetAltinn2Codelist(string id, string langCode, int? version = null) + { + var response = await _client.GetAsync( + $"https://www.altinn.no/api/metadata/codelists/{id}/{version?.ToString() ?? string.Empty}?language={langCode}" + ); + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { - var response = await _client.GetAsync( - $"https://www.altinn.no/api/metadata/codelists/{id}/{version?.ToString() ?? string.Empty}?language={langCode}" + response = await _client.GetAsync( + $"https://www.altinn.no/api/metadata/codelists/{id}/{version?.ToString() ?? string.Empty}" ); - if (response.StatusCode == System.Net.HttpStatusCode.NotFound) - { - response = await _client.GetAsync( - $"https://www.altinn.no/api/metadata/codelists/{id}/{version?.ToString() ?? string.Empty}" - ); - } - response.EnsureSuccessStatusCode(); - var codelist = await JsonSerializerPermissive.DeserializeAsync(response.Content); - return codelist; } + response.EnsureSuccessStatusCode(); + var codelist = await JsonSerializerPermissive.DeserializeAsync(response.Content); + return codelist; } } diff --git a/src/Altinn.App.Core/Features/Options/Altinn2Provider/MetadataCodelistResponse.cs b/src/Altinn.App.Core/Features/Options/Altinn2Provider/MetadataCodelistResponse.cs index 023957d51..d2d728c5b 100644 --- a/src/Altinn.App.Core/Features/Options/Altinn2Provider/MetadataCodelistResponse.cs +++ b/src/Altinn.App.Core/Features/Options/Altinn2Provider/MetadataCodelistResponse.cs @@ -1,59 +1,58 @@ #nullable disable -namespace Altinn.App.Core.Features.Options.Altinn2Provider +namespace Altinn.App.Core.Features.Options.Altinn2Provider; + +/// +/// Outer model for the https://www.altinn.no/api/metadata/codelists api +/// +public class MetadataCodelistResponse { /// - /// Outer model for the https://www.altinn.no/api/metadata/codelists api - /// - public class MetadataCodelistResponse - { - /// - /// Name of the code list - /// - public string Name { get; set; } - - /// - /// Language code of the code list - /// 1044 => "no", - /// 1044 => "nb", - /// 2068 => "nn", - /// 1033 => "en", - /// - public int Language { get; set; } - - /// - /// Version number from altinn 2 for the code list - /// - public int Version { get; set; } - - /// - /// List of the code in the code list - /// - public List Codes { get; set; } - } - - /// - /// Altinn 2 code list item - /// - public class MetadataCodeListCodes - { - /// - /// Coode for the entry that is shared between languages - /// - public string Code { get; set; } - - /// - /// Value 1 from the altinn2 metadata api - /// - public string Value1 { get; set; } - - /// - /// Value 2 from the altinn2 metadata api - /// - public string Value2 { get; set; } - - /// - /// Value 3 from the altinn2 metadata api - /// - public string Value3 { get; set; } - } + /// Name of the code list + /// + public string Name { get; set; } + + /// + /// Language code of the code list + /// 1044 => "no", + /// 1044 => "nb", + /// 2068 => "nn", + /// 1033 => "en", + /// + public int Language { get; set; } + + /// + /// Version number from altinn 2 for the code list + /// + public int Version { get; set; } + + /// + /// List of the code in the code list + /// + public List Codes { get; set; } +} + +/// +/// Altinn 2 code list item +/// +public class MetadataCodeListCodes +{ + /// + /// Coode for the entry that is shared between languages + /// + public string Code { get; set; } + + /// + /// Value 1 from the altinn2 metadata api + /// + public string Value1 { get; set; } + + /// + /// Value 2 from the altinn2 metadata api + /// + public string Value2 { get; set; } + + /// + /// Value 3 from the altinn2 metadata api + /// + public string Value3 { get; set; } } diff --git a/src/Altinn.App.Core/Features/Options/AppOptionsFactory.cs b/src/Altinn.App.Core/Features/Options/AppOptionsFactory.cs index 107ff7cea..f91fd8b95 100644 --- a/src/Altinn.App.Core/Features/Options/AppOptionsFactory.cs +++ b/src/Altinn.App.Core/Features/Options/AppOptionsFactory.cs @@ -1,56 +1,55 @@ -namespace Altinn.App.Core.Features.Options +namespace Altinn.App.Core.Features.Options; + +/// +/// Factory class for resolving implementations +/// based on the name/id of the app options requested. +/// +public class AppOptionsFactory { + private const string DEFAULT_PROVIDER_NAME = "default"; + /// - /// Factory class for resolving implementations - /// based on the name/id of the app options requested. + /// Initializes a new instance of the class. /// - public class AppOptionsFactory + public AppOptionsFactory(IEnumerable appOptionsProviders) { - private const string DEFAULT_PROVIDER_NAME = "default"; + _appOptionsProviders = appOptionsProviders; + } - /// - /// Initializes a new instance of the class. - /// - public AppOptionsFactory(IEnumerable appOptionsProviders) - { - AppOptionsProviders = appOptionsProviders; - } + private IEnumerable _appOptionsProviders { get; } - private IEnumerable AppOptionsProviders { get; } + /// + /// Finds the implementation of IAppOptionsProvider based on the options id + /// provided. + /// + /// Id matching the options requested. + public IAppOptionsProvider GetOptionsProvider(string optionsId) + { + bool isDefault = optionsId == DEFAULT_PROVIDER_NAME; - /// - /// Finds the implementation of IAppOptionsProvider based on the options id - /// provided. - /// - /// Id matching the options requested. - public IAppOptionsProvider GetOptionsProvider(string optionsId) + foreach (var appOptionProvider in _appOptionsProviders) { - bool isDefault = optionsId == DEFAULT_PROVIDER_NAME; - - foreach (var appOptionProvider in AppOptionsProviders) + if (!appOptionProvider.Id.Equals(optionsId, StringComparison.OrdinalIgnoreCase)) { - if (!appOptionProvider.Id.Equals(optionsId, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - return appOptionProvider; - } - - if (isDefault) - { - throw new KeyNotFoundException( - "No default app options provider found in the configures services. Please check your services configuration." - ); + continue; } - // In the case of no providers registred specifically for the requested id, - // we use the default provider as base. Hence we set the requested id as this is - // the key for finding the options file. - var defaultAppOptions = (DefaultAppOptionsProvider)GetOptionsProvider(DEFAULT_PROVIDER_NAME); - var clonedAppOptions = defaultAppOptions.CloneDefaultTo(optionsId); + return appOptionProvider; + } - return clonedAppOptions; + if (isDefault) + { + throw new KeyNotFoundException( + "No default app options provider found in the configures services. Please check your services configuration." + ); } + + // In the case of no providers registred specifically for the requested id, + // we use the default provider as base. Hence we set the requested id as this is + // the key for finding the options file. + var defaultAppOptions = (DefaultAppOptionsProvider)GetOptionsProvider(DEFAULT_PROVIDER_NAME); + var clonedAppOptions = defaultAppOptions.CloneDefaultTo(optionsId); + + return clonedAppOptions; } } diff --git a/src/Altinn.App.Core/Features/Options/AppOptionsFileHandler.cs b/src/Altinn.App.Core/Features/Options/AppOptionsFileHandler.cs index 66026c9bb..6e96e4c1d 100644 --- a/src/Altinn.App.Core/Features/Options/AppOptionsFileHandler.cs +++ b/src/Altinn.App.Core/Features/Options/AppOptionsFileHandler.cs @@ -5,46 +5,38 @@ using Altinn.App.Core.Models; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Features.Options +namespace Altinn.App.Core.Features.Options; + +/// +public class AppOptionsFileHandler : IAppOptionsFileHandler { - /// - public class AppOptionsFileHandler : IAppOptionsFileHandler + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new(JsonSerializerDefaults.Web) { ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }; + + private readonly AppSettings _settings; + + /// + /// Initializes a new instance of the class. + /// + public AppOptionsFileHandler(IOptions settings) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new(JsonSerializerDefaults.Web) - { - ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true, - }; + _settings = settings.Value; + } - private readonly AppSettings _settings; + /// + public async Task?> ReadOptionsFromFileAsync(string optionId) + { + string legalPath = Path.Join(_settings.AppBasePath, _settings.OptionsFolder); + string filename = legalPath + optionId + ".json"; + PathHelper.EnsureLegalPath(legalPath, filename); - /// - /// Initializes a new instance of the class. - /// - public AppOptionsFileHandler(IOptions settings) + if (File.Exists(filename)) { - _settings = settings.Value; + string fileData = await File.ReadAllTextAsync(filename, Encoding.UTF8); + List? options = JsonSerializer.Deserialize>(fileData, _jsonSerializerOptions); + return options; } - /// - public async Task?> ReadOptionsFromFileAsync(string optionId) - { - string legalPath = Path.Join(_settings.AppBasePath, _settings.OptionsFolder); - string filename = legalPath + optionId + ".json"; - PathHelper.EnsureLegalPath(legalPath, filename); - - if (File.Exists(filename)) - { - string fileData = await File.ReadAllTextAsync(filename, Encoding.UTF8); - List? options = JsonSerializer.Deserialize>( - fileData, - _jsonSerializerOptions - ); - return options; - } - - return null; - } + return null; } } diff --git a/src/Altinn.App.Core/Features/Options/AppOptionsService.cs b/src/Altinn.App.Core/Features/Options/AppOptionsService.cs index 20b20f084..c723c5e97 100644 --- a/src/Altinn.App.Core/Features/Options/AppOptionsService.cs +++ b/src/Altinn.App.Core/Features/Options/AppOptionsService.cs @@ -1,53 +1,52 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features.Options +namespace Altinn.App.Core.Features.Options; + +/// +/// Service for handling app options aka code lists. +/// +public class AppOptionsService : IAppOptionsService { + private readonly AppOptionsFactory _appOpptionsFactory; + private readonly InstanceAppOptionsFactory _instanceAppOptionsFactory; + private readonly Telemetry? _telemetry; + /// - /// Service for handling app options aka code lists. + /// Initializes a new instance of the class. /// - public class AppOptionsService : IAppOptionsService + public AppOptionsService( + AppOptionsFactory appOptionsFactory, + InstanceAppOptionsFactory instanceAppOptionsFactory, + Telemetry? telemetry = null + ) { - private readonly AppOptionsFactory _appOpptionsFactory; - private readonly InstanceAppOptionsFactory _instanceAppOptionsFactory; - private readonly Telemetry? _telemetry; - - /// - /// Initializes a new instance of the class. - /// - public AppOptionsService( - AppOptionsFactory appOptionsFactory, - InstanceAppOptionsFactory instanceAppOptionsFactory, - Telemetry? telemetry = null - ) - { - _appOpptionsFactory = appOptionsFactory; - _instanceAppOptionsFactory = instanceAppOptionsFactory; - _telemetry = telemetry; - } + _appOpptionsFactory = appOptionsFactory; + _instanceAppOptionsFactory = instanceAppOptionsFactory; + _telemetry = telemetry; + } - /// - public async Task GetOptionsAsync( - string optionId, - string? language, - Dictionary keyValuePairs - ) - { - using var activity = _telemetry?.StartGetOptionsActivity(); - return await _appOpptionsFactory.GetOptionsProvider(optionId).GetAppOptionsAsync(language, keyValuePairs); - } + /// + public async Task GetOptionsAsync( + string optionId, + string? language, + Dictionary keyValuePairs + ) + { + using var activity = _telemetry?.StartGetOptionsActivity(); + return await _appOpptionsFactory.GetOptionsProvider(optionId).GetAppOptionsAsync(language, keyValuePairs); + } - /// - public async Task GetOptionsAsync( - InstanceIdentifier instanceIdentifier, - string optionId, - string? language, - Dictionary keyValuePairs - ) - { - using var activity = _telemetry?.StartGetOptionsActivity(instanceIdentifier); - return await _instanceAppOptionsFactory - .GetOptionsProvider(optionId) - .GetInstanceAppOptionsAsync(instanceIdentifier, language, keyValuePairs); - } + /// + public async Task GetOptionsAsync( + InstanceIdentifier instanceIdentifier, + string optionId, + string? language, + Dictionary keyValuePairs + ) + { + using var activity = _telemetry?.StartGetOptionsActivity(instanceIdentifier); + return await _instanceAppOptionsFactory + .GetOptionsProvider(optionId) + .GetInstanceAppOptionsAsync(instanceIdentifier, language, keyValuePairs); } } diff --git a/src/Altinn.App.Core/Features/Options/CommonOptionProviderServiceCollectionExtensions.cs b/src/Altinn.App.Core/Features/Options/CommonOptionProviderServiceCollectionExtensions.cs index 8a8516d59..d8d034c52 100644 --- a/src/Altinn.App.Core/Features/Options/CommonOptionProviderServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Features/Options/CommonOptionProviderServiceCollectionExtensions.cs @@ -3,63 +3,62 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; -namespace Altinn.App.Core.Features.Options +namespace Altinn.App.Core.Features.Options; + +/// +/// class to hold the Extention method for +/// IServiceCollection to add the AddAltinn2CodeList() method +/// +public static class CommonOptionProviderServiceCollectionExtensions { /// - /// class to hold the Extention method for - /// IServiceCollection to add the AddAltinn2CodeList() method + /// Extention method for IServiceCollection to add the AddAltinn2CodeList() method + /// + /// services.AddAltinn2CodeList( + /// id: "ASF_Land", + /// transform: (code) => new (){ Value = code.Code, Label=code.Value1 }, + /// // filter: (code) => int.Parse(code.Value3) > 100, + /// codeListVersion: 2758, + /// metadataApiId: "ASF_land" + /// }); + /// /// - public static class CommonOptionProviderServiceCollectionExtensions + /// The service collection to add altinn 2 codelists to + /// + /// The id/name that is used in the optionsId parameter in the SelectionComponents (Checkboxes, RadioButtons, Dropdown ...) + /// If is null, this is also used for altinn2 code list name + /// + /// Mapping function to get from the altinn2 model to altinn 3 option + /// Filter function in case you only want a subset of the altinn2 codelist + /// id for use in altinn2 api (will use , if this is null) + /// version of the code list in the altinn2 metadata api + public static IServiceCollection AddAltinn2CodeList( + this IServiceCollection serviceCollection, + string id, + Func transform, + Func? filter = null, + string? metadataApiId = null, + int? codeListVersion = null + ) { - /// - /// Extention method for IServiceCollection to add the AddAltinn2CodeList() method - /// - /// services.AddAltinn2CodeList( - /// id: "ASF_Land", - /// transform: (code) => new (){ Value = code.Code, Label=code.Value1 }, - /// // filter: (code) => int.Parse(code.Value3) > 100, - /// codeListVersion: 2758, - /// metadataApiId: "ASF_land" - /// }); - /// - /// - /// The service collection to add altinn 2 codelists to - /// - /// The id/name that is used in the optionsId parameter in the SelectionComponents (Checkboxes, RadioButtons, Dropdown ...) - /// If is null, this is also used for altinn2 code list name - /// - /// Mapping function to get from the altinn2 model to altinn 3 option - /// Filter function in case you only want a subset of the altinn2 codelist - /// id for use in altinn2 api (will use , if this is null) - /// version of the code list in the altinn2 metadata api - public static IServiceCollection AddAltinn2CodeList( - this IServiceCollection serviceCollection, - string id, - Func transform, - Func? filter = null, - string? metadataApiId = null, - int? codeListVersion = null + if ( + !serviceCollection.Any(serviceDescriptor => + serviceDescriptor.ServiceType == typeof(Altinn2MetadataApiClient) + ) ) { - if ( - !serviceCollection.Any(serviceDescriptor => - serviceDescriptor.ServiceType == typeof(Altinn2MetadataApiClient) - ) - ) - { - serviceCollection.AddHttpClient(); - } - - serviceCollection.AddTransient(sp => new Altinn2CodeListProvider( - sp.GetRequiredService(), - sp.GetRequiredService(), - id, - transform, - filter, - metadataApiId, - codeListVersion - )); - return serviceCollection; + serviceCollection.AddHttpClient(); } + + serviceCollection.AddTransient(sp => new Altinn2CodeListProvider( + sp.GetRequiredService(), + sp.GetRequiredService(), + id, + transform, + filter, + metadataApiId, + codeListVersion + )); + return serviceCollection; } } diff --git a/src/Altinn.App.Core/Features/Options/DefaultAppOptionsProvider.cs b/src/Altinn.App.Core/Features/Options/DefaultAppOptionsProvider.cs index d81dc1eb0..92b97f064 100644 --- a/src/Altinn.App.Core/Features/Options/DefaultAppOptionsProvider.cs +++ b/src/Altinn.App.Core/Features/Options/DefaultAppOptionsProvider.cs @@ -1,47 +1,46 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features.Options +namespace Altinn.App.Core.Features.Options; + +/// +public class DefaultAppOptionsProvider : IAppOptionsProvider { - /// - public class DefaultAppOptionsProvider : IAppOptionsProvider - { - private readonly IAppOptionsFileHandler _appOptionsFileHandler; + private readonly IAppOptionsFileHandler _appOptionsFileHandler; - /// - /// Initializes a new instance of the class. - /// - public DefaultAppOptionsProvider(IAppOptionsFileHandler appOptionsFileHandler) - { - _appOptionsFileHandler = appOptionsFileHandler; - } + /// + /// Initializes a new instance of the class. + /// + public DefaultAppOptionsProvider(IAppOptionsFileHandler appOptionsFileHandler) + { + _appOptionsFileHandler = appOptionsFileHandler; + } - /// - /// This is the default app options implementation and will resolve static - /// json files in the options folder of the app. As the id is used to resolve - /// the file name, this particular Id=Default will be replaced run-time by - /// the when providing the class. - /// - public string Id { get; internal set; } = "default"; + /// + /// This is the default app options implementation and will resolve static + /// json files in the options folder of the app. As the id is used to resolve + /// the file name, this particular Id=Default will be replaced run-time by + /// the when providing the class. + /// + public string Id { get; internal set; } = "default"; - /// - public async Task GetAppOptionsAsync(string? language, Dictionary keyValuePairs) - { - // This will get static options if it exists - var appOptions = new AppOptions { Options = await _appOptionsFileHandler.ReadOptionsFromFileAsync(Id) }; + /// + public async Task GetAppOptionsAsync(string? language, Dictionary keyValuePairs) + { + // This will get static options if it exists + var appOptions = new AppOptions { Options = await _appOptionsFileHandler.ReadOptionsFromFileAsync(Id) }; - return appOptions; - } + return appOptions; + } - /// - /// Internal method for cloning the default implementation and setting the id - /// as the implementation will use the id when finding the configured option files. - /// - /// The actual option id to use. - /// - internal IAppOptionsProvider CloneDefaultTo(string cloneToOptionId) - { - var clone = new DefaultAppOptionsProvider(_appOptionsFileHandler) { Id = cloneToOptionId }; - return clone; - } + /// + /// Internal method for cloning the default implementation and setting the id + /// as the implementation will use the id when finding the configured option files. + /// + /// The actual option id to use. + /// + internal IAppOptionsProvider CloneDefaultTo(string cloneToOptionId) + { + var clone = new DefaultAppOptionsProvider(_appOptionsFileHandler) { Id = cloneToOptionId }; + return clone; } } diff --git a/src/Altinn.App.Core/Features/Options/IAppOptionsFileHandler.cs b/src/Altinn.App.Core/Features/Options/IAppOptionsFileHandler.cs index b589f8b6e..1ebc866c9 100644 --- a/src/Altinn.App.Core/Features/Options/IAppOptionsFileHandler.cs +++ b/src/Altinn.App.Core/Features/Options/IAppOptionsFileHandler.cs @@ -1,17 +1,16 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features.Options +namespace Altinn.App.Core.Features.Options; + +/// +/// Interface for handling option files on disk +/// +public interface IAppOptionsFileHandler { /// - /// Interface for handling option files on disk + /// Reads the app options from file /// - public interface IAppOptionsFileHandler - { - /// - /// Reads the app options from file - /// - /// The option id that should be loaded. Should equal the filename without the .json extension. - /// A containing the option from the json file on disk. If no file is found null is returned. - Task?> ReadOptionsFromFileAsync(string optionId); - } + /// The option id that should be loaded. Should equal the filename without the .json extension. + /// A containing the option from the json file on disk. If no file is found null is returned. + Task?> ReadOptionsFromFileAsync(string optionId); } diff --git a/src/Altinn.App.Core/Features/Options/IAppOptionsService.cs b/src/Altinn.App.Core/Features/Options/IAppOptionsService.cs index 93e264415..ca0970c2e 100644 --- a/src/Altinn.App.Core/Features/Options/IAppOptionsService.cs +++ b/src/Altinn.App.Core/Features/Options/IAppOptionsService.cs @@ -1,36 +1,35 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features.Options +namespace Altinn.App.Core.Features.Options; + +/// +/// Interface for working with +/// +public interface IAppOptionsService { /// - /// Interface for working with + /// Get the list of options for a specific options list by its id and key/value pairs. /// - public interface IAppOptionsService - { - /// - /// Get the list of options for a specific options list by its id and key/value pairs. - /// - /// The id of the options list to retrieve - /// The language code requested. - /// Optional list of key/value pairs to use for filtering and further lookup. - /// The list of options - Task GetOptionsAsync(string optionId, string? language, Dictionary keyValuePairs); + /// The id of the options list to retrieve + /// The language code requested. + /// Optional list of key/value pairs to use for filtering and further lookup. + /// The list of options + Task GetOptionsAsync(string optionId, string? language, Dictionary keyValuePairs); - /// - /// Get the list of instance specific options for a specific options list based on the - /// and key/value pairs. The values returned from this implementation could be specific to the instance and/or - /// instance owner and should not be cached without careful thinking around caching strategy. - /// - /// Class identifying the instance by instance owner party id and instance guid. - /// The id of the options list to retrieve - /// The language code requested. - /// Optional list of key/value pairs to use for filtering and further lookup. - /// The list of options - Task GetOptionsAsync( - InstanceIdentifier instanceIdentifier, - string optionId, - string? language, - Dictionary keyValuePairs - ); - } + /// + /// Get the list of instance specific options for a specific options list based on the + /// and key/value pairs. The values returned from this implementation could be specific to the instance and/or + /// instance owner and should not be cached without careful thinking around caching strategy. + /// + /// Class identifying the instance by instance owner party id and instance guid. + /// The id of the options list to retrieve + /// The language code requested. + /// Optional list of key/value pairs to use for filtering and further lookup. + /// The list of options + Task GetOptionsAsync( + InstanceIdentifier instanceIdentifier, + string optionId, + string? language, + Dictionary keyValuePairs + ); } diff --git a/src/Altinn.App.Core/Features/Options/InstanceAppOptionsFactory.cs b/src/Altinn.App.Core/Features/Options/InstanceAppOptionsFactory.cs index a859f8ed0..196f7f217 100644 --- a/src/Altinn.App.Core/Features/Options/InstanceAppOptionsFactory.cs +++ b/src/Altinn.App.Core/Features/Options/InstanceAppOptionsFactory.cs @@ -1,39 +1,38 @@ -namespace Altinn.App.Core.Features.Options +namespace Altinn.App.Core.Features.Options; + +/// +/// Factory class for resolving implementations +/// based on the name/id of the app options requested. +/// +public class InstanceAppOptionsFactory { /// - /// Factory class for resolving implementations - /// based on the name/id of the app options requested. + /// Initializes a new instance of the class. /// - public class InstanceAppOptionsFactory + public InstanceAppOptionsFactory(IEnumerable instanceAppOptionsProviders) { - /// - /// Initializes a new instance of the class. - /// - public InstanceAppOptionsFactory(IEnumerable instanceAppOptionsProviders) - { - InstanceAppOptionsProviders = instanceAppOptionsProviders; - } + _instanceAppOptionsProviders = instanceAppOptionsProviders; + } - private IEnumerable InstanceAppOptionsProviders { get; } + private IEnumerable _instanceAppOptionsProviders { get; } - /// - /// Finds the implementation of IInstanceAppOptionsProvider based on the options id - /// provided. - /// - /// Id matching the options requested. - public IInstanceAppOptionsProvider GetOptionsProvider(string optionsId) + /// + /// Finds the implementation of IInstanceAppOptionsProvider based on the options id + /// provided. + /// + /// Id matching the options requested. + public IInstanceAppOptionsProvider GetOptionsProvider(string optionsId) + { + foreach (var instanceAppOptionProvider in _instanceAppOptionsProviders) { - foreach (var instanceAppOptionProvider in InstanceAppOptionsProviders) + if (!instanceAppOptionProvider.Id.Equals(optionsId, StringComparison.OrdinalIgnoreCase)) { - if (!instanceAppOptionProvider.Id.Equals(optionsId, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - return instanceAppOptionProvider; + continue; } - return new NullInstanceAppOptionsProvider(); + return instanceAppOptionProvider; } + + return new NullInstanceAppOptionsProvider(); } } diff --git a/src/Altinn.App.Core/Features/Options/NullInstanceAppOptionsProvider.cs b/src/Altinn.App.Core/Features/Options/NullInstanceAppOptionsProvider.cs index da906a3e3..b26b61fa8 100644 --- a/src/Altinn.App.Core/Features/Options/NullInstanceAppOptionsProvider.cs +++ b/src/Altinn.App.Core/Features/Options/NullInstanceAppOptionsProvider.cs @@ -1,25 +1,24 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features.Options +namespace Altinn.App.Core.Features.Options; + +/// +/// Nullobject for cases where there is no match on the requested +/// Options is set to null and not an empty list for the controller to be able to differensiate +/// between option provider found, but with no values and no option provider found ie. returns 404. +/// +public class NullInstanceAppOptionsProvider : IInstanceAppOptionsProvider { - /// - /// Nullobject for cases where there is no match on the requested - /// Options is set to null and not an empty list for the controller to be able to differensiate - /// between option provider found, but with no values and no option provider found ie. returns 404. - /// - public class NullInstanceAppOptionsProvider : IInstanceAppOptionsProvider - { - /// - public string Id => string.Empty; + /// + public string Id => string.Empty; - /// - public Task GetInstanceAppOptionsAsync( - InstanceIdentifier instanceIdentifier, - string? language, - Dictionary keyValuePairs - ) - { - return Task.FromResult(new AppOptions() { IsCacheable = false, Options = null }); - } + /// + public Task GetInstanceAppOptionsAsync( + InstanceIdentifier instanceIdentifier, + string? language, + Dictionary keyValuePairs + ) + { + return Task.FromResult(new AppOptions() { IsCacheable = false, Options = null }); } } diff --git a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs index 16f401b44..66a2285ad 100644 --- a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs +++ b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs @@ -1,48 +1,47 @@ using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features.PageOrder +namespace Altinn.App.Core.Features.PageOrder; + +/// +/// Interface for page order handling in stateless apps +/// +[Obsolete("IPageOrder does not work with frontend version 4")] +public class DefaultPageOrder : IPageOrder { + private readonly IAppResources _resources; + /// - /// Interface for page order handling in stateless apps + /// Default implementation for page order /// - [Obsolete("IPageOrder does not work with frontend version 4")] - public class DefaultPageOrder : IPageOrder + /// IAppResources service + public DefaultPageOrder(IAppResources resources) + { + _resources = resources; + } + + /// + public Task> GetPageOrder( + AppIdentifier appIdentifier, + InstanceIdentifier instanceIdentifier, + string layoutSetId, + string currentPage, + string dataTypeId, + object formData + ) { - private readonly IAppResources _resources; + LayoutSettings? layoutSettings; - /// - /// Default implementation for page order - /// - /// IAppResources service - public DefaultPageOrder(IAppResources resources) + if (string.IsNullOrEmpty(layoutSetId)) { - _resources = resources; + layoutSettings = _resources.GetLayoutSettings(); } - - /// - public Task> GetPageOrder( - AppIdentifier appIdentifier, - InstanceIdentifier instanceIdentifier, - string layoutSetId, - string currentPage, - string dataTypeId, - object formData - ) + else { - LayoutSettings? layoutSettings; - - if (string.IsNullOrEmpty(layoutSetId)) - { - layoutSettings = _resources.GetLayoutSettings(); - } - else - { - layoutSettings = _resources.GetLayoutSettingsForSet(layoutSetId); - } + layoutSettings = _resources.GetLayoutSettingsForSet(layoutSetId); + } #nullable disable - return Task.FromResult(layoutSettings.Pages.Order); + return Task.FromResult(layoutSettings.Pages.Order); #nullable restore - } } } diff --git a/src/Altinn.App.Core/Features/Payment/Exceptions/PaymentException.cs b/src/Altinn.App.Core/Features/Payment/Exceptions/PaymentException.cs index 2f383f11a..bbe5657c1 100644 --- a/src/Altinn.App.Core/Features/Payment/Exceptions/PaymentException.cs +++ b/src/Altinn.App.Core/Features/Payment/Exceptions/PaymentException.cs @@ -1,15 +1,14 @@ -namespace Altinn.App.Core.Features.Payment.Exceptions +namespace Altinn.App.Core.Features.Payment.Exceptions; + +/// +/// Represents an exception that is thrown when an error occurs during payment processing. +/// +public class PaymentException : Exception { /// - /// Represents an exception that is thrown when an error occurs during payment processing. + /// Initializes a new instance of the class. /// - public class PaymentException : Exception - { - /// - /// Initializes a new instance of the class. - /// - /// - public PaymentException(string message) - : base(message) { } - } + /// + public PaymentException(string message) + : base(message) { } } diff --git a/src/Altinn.App.Core/Features/Payment/Models/PaymentInformation.cs b/src/Altinn.App.Core/Features/Payment/Models/PaymentInformation.cs index 35024d503..4bcca01a1 100644 --- a/src/Altinn.App.Core/Features/Payment/Models/PaymentInformation.cs +++ b/src/Altinn.App.Core/Features/Payment/Models/PaymentInformation.cs @@ -1,28 +1,27 @@ -namespace Altinn.App.Core.Features.Payment.Models +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// The payment information for a transaction. +/// +public class PaymentInformation { /// - /// The payment information for a transaction. + /// The taskId of the payment task this payment information is associated with. /// - public class PaymentInformation - { - /// - /// The taskId of the payment task this payment information is associated with. - /// - public required string TaskId { get; set; } + public required string TaskId { get; set; } - /// - /// The status of the payment. - /// - public required PaymentStatus Status { get; set; } + /// + /// The status of the payment. + /// + public required PaymentStatus Status { get; set; } - /// - /// The order details for the transaction. - /// - public required OrderDetails OrderDetails { get; set; } + /// + /// The order details for the transaction. + /// + public required OrderDetails OrderDetails { get; set; } - /// - /// Contains details about the payment, set by the payment processor implementation. - /// - public PaymentDetails? PaymentDetails { get; set; } - } + /// + /// Contains details about the payment, set by the payment processor implementation. + /// + public PaymentDetails? PaymentDetails { get; set; } } diff --git a/src/Altinn.App.Core/Features/Payment/Models/PaymentStatus.cs b/src/Altinn.App.Core/Features/Payment/Models/PaymentStatus.cs index d537c42cd..fddd03b38 100644 --- a/src/Altinn.App.Core/Features/Payment/Models/PaymentStatus.cs +++ b/src/Altinn.App.Core/Features/Payment/Models/PaymentStatus.cs @@ -1,41 +1,40 @@ using System.Text.Json.Serialization; -namespace Altinn.App.Core.Features.Payment.Models +namespace Altinn.App.Core.Features.Payment.Models; + +/// +/// The status of a payment. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum PaymentStatus { /// - /// The status of a payment. + /// The payment is not initialized. We have not contacted the payment processor yet. /// - [JsonConverter(typeof(JsonStringEnumConverter))] - public enum PaymentStatus - { - /// - /// The payment is not initialized. We have not contacted the payment processor yet. - /// - Uninitialized, + Uninitialized, - /// - /// The payment request has been created and sent to payment processor. - /// - Created, + /// + /// The payment request has been created and sent to payment processor. + /// + Created, - /// - /// The payment has been paid. - /// - Paid, + /// + /// The payment has been paid. + /// + Paid, - /// - /// Something went wrong and the payment is considered failed. - /// - Failed, + /// + /// Something went wrong and the payment is considered failed. + /// + Failed, - /// - /// The payment has been cancelled. - /// - Cancelled, + /// + /// The payment has been cancelled. + /// + Cancelled, - /// - /// The payment was skipped, likely because the sum of the order was zero. - /// - Skipped, - } + /// + /// The payment was skipped, likely because the sum of the order was zero. + /// + Skipped, } diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/INetsClient.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/INetsClient.cs index ec92fbd12..90862d98d 100644 --- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/INetsClient.cs +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/INetsClient.cs @@ -1,31 +1,30 @@ using Altinn.App.Core.Features.Payment.Processors.Nets.Models; -namespace Altinn.App.Core.Features.Payment.Processors.Nets +namespace Altinn.App.Core.Features.Payment.Processors.Nets; + +/// +/// Represents a client for interacting with the Nets payment provider. +/// +internal interface INetsClient { /// - /// Represents a client for interacting with the Nets payment provider. + /// Creates a payment using the Nets payment provider. /// - internal interface INetsClient - { - /// - /// Creates a payment using the Nets payment provider. - /// - /// The payment details. - /// - Task> CreatePayment(NetsCreatePayment payment); + /// The payment details. + /// + Task> CreatePayment(NetsCreatePayment payment); - /// - /// Retrieve existing payment. - /// - /// - /// - Task> RetrievePayment(string paymentId); + /// + /// Retrieve existing payment. + /// + /// + /// + Task> RetrievePayment(string paymentId); - /// - /// Terminate a payment that has not been captured. - /// - /// - /// - Task TerminatePayment(string paymentId); - } + /// + /// Terminate a payment that has not been captured. + /// + /// + /// + Task TerminatePayment(string paymentId); } diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/HttpApiResult.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/HttpApiResult.cs index 69e5b9c56..3039f4c36 100644 --- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/HttpApiResult.cs +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/HttpApiResult.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http.Json; -using System.Reflection; using System.Text.Json; namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; @@ -9,7 +8,7 @@ namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; internal class HttpApiResult { // ReSharper disable once StaticMemberInGenericType - private static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions + private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, @@ -45,7 +44,7 @@ public static async Task> FromHttpResponse(HttpResponseMessage { Status = response.StatusCode, Result = - await response.Content.ReadFromJsonAsync(JSON_OPTIONS) + await response.Content.ReadFromJsonAsync(_jsonSerializerOptions) ?? throw new JsonException("Could not deserialize response"), }; } diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePayment.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePayment.cs index 8185d231d..e3495b46c 100644 --- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePayment.cs +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsCreatePayment.cs @@ -1,5 +1,3 @@ -using Altinn.App.Core.Features.Payment.Models; - namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; /// diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrderItem.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrderItem.cs index 36f9e692f..74e54c506 100644 --- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrderItem.cs +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsOrderItem.cs @@ -1,60 +1,59 @@ -namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +internal class NetsOrderItem { - internal class NetsOrderItem - { - /// - /// A reference to recognize the product, usually the SKU (stock keeping unit) of the product. For convenience in the case of refunds or modifications of placed orders, the reference should be unique for each variation of a product item (size, color, etc). - /// Length: 0-128 - /// The following special characters are not supported: <,>,\\ - /// - public required string Reference { get; set; } - - /// - /// The name of the product. - /// Length: 0-128 - /// The following special characters are not supported: <,>,\\ - /// - public required string Name { get; set; } - - /// - /// The quantity of the product. - /// Allowed: >=0 - /// - public required double Quantity { get; set; } - - /// - /// The defined unit of measurement for the product, for example pcs, liters, or kg. - /// Length: 0-128 - /// The following special characters are not supported: <,>,\,’,”,&,\\ - /// - public required string Unit { get; set; } - - /// - /// The price per unit excluding VAT. - /// Note: The amount can be negative. - /// - public required int UnitPrice { get; set; } - - /// - /// The tax/VAT rate (in percentage times 100). For examlpe, the value 2500 corresponds to 25%. Defaults to 0 if not provided. - /// - public int? TaxRate { get; set; } = 0; - - /// - /// The tax/VAT amount (unitPrice * quantity * taxRate / 10000). Defaults to 0 if not provided. taxAmount should include the total tax amount for the entire order item. - /// - public int? TaxAmount { get; set; } = 0; - - /// - /// The total amount including VAT (netTotalAmount + taxAmount). - /// Note: The amount can be negative. - /// - public required int GrossTotalAmount { get; set; } - - /// - /// The total amount excluding VAT (unitPrice * quantity). - /// Note: The amount can be negative. - /// - public required int NetTotalAmount { get; set; } - } + /// + /// A reference to recognize the product, usually the SKU (stock keeping unit) of the product. For convenience in the case of refunds or modifications of placed orders, the reference should be unique for each variation of a product item (size, color, etc). + /// Length: 0-128 + /// The following special characters are not supported: <,>,\\ + /// + public required string Reference { get; set; } + + /// + /// The name of the product. + /// Length: 0-128 + /// The following special characters are not supported: <,>,\\ + /// + public required string Name { get; set; } + + /// + /// The quantity of the product. + /// Allowed: >=0 + /// + public required double Quantity { get; set; } + + /// + /// The defined unit of measurement for the product, for example pcs, liters, or kg. + /// Length: 0-128 + /// The following special characters are not supported: <,>,\,’,”,&,\\ + /// + public required string Unit { get; set; } + + /// + /// The price per unit excluding VAT. + /// Note: The amount can be negative. + /// + public required int UnitPrice { get; set; } + + /// + /// The tax/VAT rate (in percentage times 100). For examlpe, the value 2500 corresponds to 25%. Defaults to 0 if not provided. + /// + public int? TaxRate { get; set; } = 0; + + /// + /// The tax/VAT amount (unitPrice * quantity * taxRate / 10000). Defaults to 0 if not provided. taxAmount should include the total tax amount for the entire order item. + /// + public int? TaxAmount { get; set; } = 0; + + /// + /// The total amount including VAT (netTotalAmount + taxAmount). + /// Note: The amount can be negative. + /// + public required int GrossTotalAmount { get; set; } + + /// + /// The total amount excluding VAT (unitPrice * quantity). + /// Note: The amount can be negative. + /// + public required int NetTotalAmount { get; set; } } diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethod.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethod.cs index 55e4c6a38..93cf5fdc3 100644 --- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethod.cs +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethod.cs @@ -1,16 +1,15 @@ -namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +internal class NetsPaymentMethod { - internal class NetsPaymentMethod - { - /// - /// The name of the payment method. - /// Possible value currently is: 'easy-invoice'. - /// - public string? Name { get; set; } + /// + /// The name of the payment method. + /// Possible value currently is: 'easy-invoice'. + /// + public string? Name { get; set; } - /// - /// Represents a line of a customer order. An order item refers to a product that the customer has bought. A product can be anything from a physical product to an online subscription or shipping. - /// - public NetsOrderItem? Fee { get; set; } - } + /// + /// Represents a line of a customer order. An order item refers to a product that the customer has bought. A product can be anything from a physical product to an online subscription or shipping. + /// + public NetsOrderItem? Fee { get; set; } } diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethodConfiguration.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethodConfiguration.cs index 57e28492e..9288aac21 100644 --- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethodConfiguration.cs +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/Models/NetsPaymentMethodConfiguration.cs @@ -1,22 +1,21 @@ -namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models +namespace Altinn.App.Core.Features.Payment.Processors.Nets.Models; + +/// +/// Specifies payment methods configuration to be used for this payment, ignored if empty or null. +/// +internal class NetsPaymentMethodConfiguration { /// - /// Specifies payment methods configuration to be used for this payment, ignored if empty or null. + /// The name of the payment method or payment type to be configured, if the specified payment method is not configured correctly in the merchant configurations then this won't take effect. + /// Payment type cannot be specified alongside payment methods that belong to it, if it happens the request will fail with an error. + /// Possible payment methods values: "Visa", "MasterCard", "Dankort", "AmericanExpress", "PayPal", "Vipps", "MobilePay", "Swish", "Arvato", "EasyInvoice", "EasyInstallment", "EasyCampaign", "RatePayInvoice", "RatePayInstallment", "RatePaySepa", "Sofort", "Trustly". + /// Possible payment types values: "Card", "Invoice", "Installment", "A2A", "Wallet". /// - internal class NetsPaymentMethodConfiguration - { - /// - /// The name of the payment method or payment type to be configured, if the specified payment method is not configured correctly in the merchant configurations then this won't take effect. - /// Payment type cannot be specified alongside payment methods that belong to it, if it happens the request will fail with an error. - /// Possible payment methods values: "Visa", "MasterCard", "Dankort", "AmericanExpress", "PayPal", "Vipps", "MobilePay", "Swish", "Arvato", "EasyInvoice", "EasyInstallment", "EasyCampaign", "RatePayInvoice", "RatePayInstallment", "RatePaySepa", "Sofort", "Trustly". - /// Possible payment types values: "Card", "Invoice", "Installment", "A2A", "Wallet". - /// - public required string Name { get; set; } + public required string Name { get; set; } - /// - /// Indicates that the specified payment method/type is allowed to be used for this payment, defaults to true. - /// If one or more payment method/type is configured in the parent array then this value will be considered false for any other payment method that the parent array doesn't cover. - /// - public bool Enabled { get; set; } = true; - } + /// + /// Indicates that the specified payment method/type is allowed to be used for this payment, defaults to true. + /// If one or more payment method/type is configured in the parent array then this value will be considered false for any other payment method that the parent array doesn't cover. + /// + public bool Enabled { get; set; } = true; } diff --git a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs index 3503e9812..15cd36915 100644 --- a/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs +++ b/src/Altinn.App.Core/Features/Payment/Processors/Nets/NetsPaymentProcessor.cs @@ -1,4 +1,3 @@ -using System.Web; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features.Payment.Exceptions; using Altinn.App.Core.Features.Payment.Models; diff --git a/src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs b/src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs index da5219fe2..4ac5528ec 100644 --- a/src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs +++ b/src/Altinn.App.Core/Features/Payment/Services/IPaymentService.cs @@ -2,39 +2,38 @@ using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Features.Payment.Services +namespace Altinn.App.Core.Features.Payment.Services; + +/// +/// Service for handling payment. +/// +internal interface IPaymentService { /// - /// Service for handling payment. + /// Start payment for an instance. Will clean up any existing non-completed payment before starting a new payment. /// - internal interface IPaymentService - { - /// - /// Start payment for an instance. Will clean up any existing non-completed payment before starting a new payment. - /// - Task<(PaymentInformation paymentInformation, bool alreadyPaid)> StartPayment( - Instance instance, - ValidAltinnPaymentConfiguration paymentConfiguration, - string? language - ); + Task<(PaymentInformation paymentInformation, bool alreadyPaid)> StartPayment( + Instance instance, + ValidAltinnPaymentConfiguration paymentConfiguration, + string? language + ); - /// - /// Check updated payment information from payment provider and store the updated data. - /// - Task CheckAndStorePaymentStatus( - Instance instance, - ValidAltinnPaymentConfiguration paymentConfiguration, - string? language - ); + /// + /// Check updated payment information from payment provider and store the updated data. + /// + Task CheckAndStorePaymentStatus( + Instance instance, + ValidAltinnPaymentConfiguration paymentConfiguration, + string? language + ); - /// - /// Check our internal state to see if payment is complete. - /// - Task IsPaymentCompleted(Instance instance, ValidAltinnPaymentConfiguration paymentConfiguration); + /// + /// Check our internal state to see if payment is complete. + /// + Task IsPaymentCompleted(Instance instance, ValidAltinnPaymentConfiguration paymentConfiguration); - /// - /// Cancel payment with payment processor and delete internal payment information. - /// - Task CancelAndDeleteAnyExistingPayment(Instance instance, ValidAltinnPaymentConfiguration paymentConfiguration); - } + /// + /// Cancel payment with payment processor and delete internal payment information. + /// + Task CancelAndDeleteAnyExistingPayment(Instance instance, ValidAltinnPaymentConfiguration paymentConfiguration); } diff --git a/src/Altinn.App.Core/Features/Pdf/NullPdfFormatter.cs b/src/Altinn.App.Core/Features/Pdf/NullPdfFormatter.cs index 8eba2ab8e..137f24d83 100644 --- a/src/Altinn.App.Core/Features/Pdf/NullPdfFormatter.cs +++ b/src/Altinn.App.Core/Features/Pdf/NullPdfFormatter.cs @@ -1,19 +1,18 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Features.Pdf +namespace Altinn.App.Core.Features.Pdf; + +/// +/// Null object for representing a custom PDF formatter. +/// +[Obsolete( + "This class was used for the old PDF generator, and is used for backwards compatibility in the new one. Create a custom pdf layout instead if you need to customize the PDF layout." +)] +public class NullPdfFormatter : IPdfFormatter { - /// - /// Null object for representing a custom PDF formatter. - /// - [Obsolete( - "This class was used for the old PDF generator, and is used for backwards compatibility in the new one. Create a custom pdf layout instead if you need to customize the PDF layout." - )] - public class NullPdfFormatter : IPdfFormatter + /// + public Task FormatPdf(LayoutSettings layoutSettings, object data) { - /// - public Task FormatPdf(LayoutSettings layoutSettings, object data) - { - return Task.FromResult(layoutSettings); - } + return Task.FromResult(layoutSettings); } } diff --git a/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Client.cs b/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Client.cs index 94e0c9ef4..80774c4a2 100644 --- a/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Client.cs +++ b/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Client.cs @@ -5,16 +5,16 @@ namespace Altinn.App.Core.Features; partial class Telemetry { - internal Activity? StartGetApplicationMetadataActivity() => ActivitySource.StartActivity($"{_prefix}.Get"); + internal Activity? StartGetApplicationMetadataActivity() => ActivitySource.StartActivity($"{Prefix}.Get"); internal Activity? StartGetApplicationXACMLPolicyActivity() => - ActivitySource.StartActivity($"{_prefix}.GetXACMLPolicy"); + ActivitySource.StartActivity($"{Prefix}.GetXACMLPolicy"); internal Activity? StartGetApplicationBPMNProcessActivity() => - ActivitySource.StartActivity($"{_prefix}.GetBPMNProcess"); + ActivitySource.StartActivity($"{Prefix}.GetBPMNProcess"); internal static class ApplicationMetadataClient { - internal const string _prefix = "ApplicationMetadata.Client"; + internal const string Prefix = "ApplicationMetadata.Client"; } } diff --git a/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Service.cs b/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Service.cs index aa03c1ed5..399cb752f 100644 --- a/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Service.cs +++ b/src/Altinn.App.Core/Features/Telemetry.ApplicationMetadata.Service.cs @@ -5,62 +5,62 @@ namespace Altinn.App.Core.Features; partial class Telemetry { - internal Activity? StartGetTextActivity() => ActivitySource.StartActivity($"{_prefix}.GetText"); + internal Activity? StartGetTextActivity() => ActivitySource.StartActivity($"{Prefix}.GetText"); - internal Activity? StartGetApplicationActivity() => ActivitySource.StartActivity($"{_prefix}.GetApplication"); + internal Activity? StartGetApplicationActivity() => ActivitySource.StartActivity($"{Prefix}.GetApplication"); internal Activity? StartGetModelJsonSchemaActivity() => - ActivitySource.StartActivity($"{_prefix}.GetModelJsonSchema"); + ActivitySource.StartActivity($"{Prefix}.GetModelJsonSchema"); - internal Activity? StartGetPrefillJsonActivity() => ActivitySource.StartActivity($"{_prefix}.GetPrefillJson"); + internal Activity? StartGetPrefillJsonActivity() => ActivitySource.StartActivity($"{Prefix}.GetPrefillJson"); - internal Activity? StartGetLayoutsActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayouts"); + internal Activity? StartGetLayoutsActivity() => ActivitySource.StartActivity($"{Prefix}.GetLayouts"); - internal Activity? StartGetLayoutSetActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayoutSet"); + internal Activity? StartGetLayoutSetActivity() => ActivitySource.StartActivity($"{Prefix}.GetLayoutSet"); - internal Activity? StartGetLayoutSetsActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayoutSets"); + internal Activity? StartGetLayoutSetsActivity() => ActivitySource.StartActivity($"{Prefix}.GetLayoutSets"); - internal Activity? StartGetLayoutsForSetActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayoutsForSet"); + internal Activity? StartGetLayoutsForSetActivity() => ActivitySource.StartActivity($"{Prefix}.GetLayoutsForSet"); internal Activity? StartGetLayoutSetsForTaskActivity() => - ActivitySource.StartActivity($"{_prefix}.GetLayoutSetsForTask"); + ActivitySource.StartActivity($"{Prefix}.GetLayoutSetsForTask"); - internal Activity? StartGetLayoutSettingsActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayoutSettings"); + internal Activity? StartGetLayoutSettingsActivity() => ActivitySource.StartActivity($"{Prefix}.GetLayoutSettings"); internal Activity? StartGetLayoutSettingsStringActivity() => - ActivitySource.StartActivity($"{_prefix}.GetLayoutSettingsString"); + ActivitySource.StartActivity($"{Prefix}.GetLayoutSettingsString"); internal Activity? StartGetLayoutSettingsForSetActivity() => - ActivitySource.StartActivity($"{_prefix}.GetLayoutSettingsForSet"); + ActivitySource.StartActivity($"{Prefix}.GetLayoutSettingsForSet"); internal Activity? StartGetLayoutSettingsStringForSetActivity() => - ActivitySource.StartActivity($"{_prefix}.GetLayoutSettingsStringForSet"); + ActivitySource.StartActivity($"{Prefix}.GetLayoutSettingsStringForSet"); - internal Activity? StartGetTextsActivity() => ActivitySource.StartActivity($"{_prefix}.GetTexts"); + internal Activity? StartGetTextsActivity() => ActivitySource.StartActivity($"{Prefix}.GetTexts"); internal Activity? StartGetRuleConfigurationForSetActivity() => - ActivitySource.StartActivity($"{_prefix}.GetRuleConfigurationForSet"); + ActivitySource.StartActivity($"{Prefix}.GetRuleConfigurationForSet"); internal Activity? StartGetRuleHandlerForSetActivity() => - ActivitySource.StartActivity($"{_prefix}.GetRuleHandlerForSet"); + ActivitySource.StartActivity($"{Prefix}.GetRuleHandlerForSet"); - internal Activity? StartGetFooterActivity() => ActivitySource.StartActivity($"{_prefix}.GetFooter"); + internal Activity? StartGetFooterActivity() => ActivitySource.StartActivity($"{Prefix}.GetFooter"); internal Activity? StartGetValidationConfigurationActivity() => - ActivitySource.StartActivity($"{_prefix}.GetValidationConfiguration"); + ActivitySource.StartActivity($"{Prefix}.GetValidationConfiguration"); - internal Activity? StartGetLayoutModelActivity() => ActivitySource.StartActivity($"{_prefix}.GetLayoutModel"); + internal Activity? StartGetLayoutModelActivity() => ActivitySource.StartActivity($"{Prefix}.GetLayoutModel"); - internal Activity? StartGetClassRefActivity() => ActivitySource.StartActivity($"{_prefix}.GetClassRef"); + internal Activity? StartGetClassRefActivity() => ActivitySource.StartActivity($"{Prefix}.GetClassRef"); internal Activity? StartClientGetApplicationXACMLPolicyActivity() => - ActivitySource.StartActivity($"{_prefix}.GetXACMLPolicy"); + ActivitySource.StartActivity($"{Prefix}.GetXACMLPolicy"); internal Activity? StartClientGetApplicationBPMNProcessActivity() => - ActivitySource.StartActivity($"{_prefix}.GetBPMNProcess"); + ActivitySource.StartActivity($"{Prefix}.GetBPMNProcess"); internal static class ApplicationMetadataService { - internal const string _prefix = "ApplicationMetadata.Service"; + internal const string Prefix = "ApplicationMetadata.Service"; } } diff --git a/src/Altinn.App.Core/Features/Telemetry.Authorization.Client.cs b/src/Altinn.App.Core/Features/Telemetry.Authorization.Client.cs index 2abfd9e3d..0abd5c909 100644 --- a/src/Altinn.App.Core/Features/Telemetry.Authorization.Client.cs +++ b/src/Altinn.App.Core/Features/Telemetry.Authorization.Client.cs @@ -9,7 +9,7 @@ partial class Telemetry { internal Activity? StartClientGetPartyListActivity(int userId) { - var activity = ActivitySource.StartActivity($"{_prefix}.GetPartyList"); + var activity = ActivitySource.StartActivity($"{Prefix}.GetPartyList"); activity?.SetUserId(userId); return activity; @@ -17,7 +17,7 @@ partial class Telemetry internal Activity? StartClientValidateSelectedPartyActivity(int userId, int partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.ValidateSelectedParty"); + var activity = ActivitySource.StartActivity($"{Prefix}.ValidateSelectedParty"); { activity?.SetUserId(userId); activity?.SetInstanceOwnerPartyId(partyId); @@ -31,7 +31,7 @@ partial class Telemetry string? taskId = null ) { - var activity = ActivitySource.StartActivity($"{_prefix}.AuthorizeAction"); + var activity = ActivitySource.StartActivity($"{Prefix}.AuthorizeAction"); activity?.SetInstanceId(instanceIdentifier.InstanceGuid); activity?.SetInstanceOwnerPartyId(instanceIdentifier.InstanceOwnerPartyId); @@ -43,7 +43,7 @@ partial class Telemetry internal Activity? StartClientAuthorizeActionsActivity(Platform.Storage.Interface.Models.Instance instance) { - var activity = ActivitySource.StartActivity($"{_prefix}.AuthorizeActions"); + var activity = ActivitySource.StartActivity($"{Prefix}.AuthorizeActions"); activity?.SetInstanceId(instance); @@ -56,7 +56,7 @@ partial class Telemetry string action ) { - var activity = ActivitySource.StartActivity($"{_prefix}.IsAuthorizerForTaskAndAction"); + var activity = ActivitySource.StartActivity($"{Prefix}.IsAuthorizerForTaskAndAction"); if (activity is not null) { activity.SetTaskId(taskId); @@ -71,6 +71,6 @@ string action internal static class AuthorizationClient { - internal const string _prefix = "Authorization.Client"; + internal const string Prefix = "Authorization.Client"; } } diff --git a/src/Altinn.App.Core/Features/Telemetry.Authorization.Service.cs b/src/Altinn.App.Core/Features/Telemetry.Authorization.Service.cs index eae417d33..2df669bf9 100644 --- a/src/Altinn.App.Core/Features/Telemetry.Authorization.Service.cs +++ b/src/Altinn.App.Core/Features/Telemetry.Authorization.Service.cs @@ -10,14 +10,14 @@ partial class Telemetry { internal Activity? StartGetPartyListActivity(int userId) { - var activity = ActivitySource.StartActivity($"{_prefix}.GetPartyList"); + var activity = ActivitySource.StartActivity($"{Prefix}.GetPartyList"); activity?.SetUserId(userId); return activity; } internal Activity? StartValidateSelectedPartyActivity(int userId, int partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.ValidateSelectedParty"); + var activity = ActivitySource.StartActivity($"{Prefix}.ValidateSelectedParty"); activity?.SetUserId(userId); activity?.SetInstanceOwnerPartyId(partyId); return activity; @@ -29,7 +29,7 @@ partial class Telemetry string? taskId = null ) { - var activity = ActivitySource.StartActivity($"{_prefix}.AuthorizeAction"); + var activity = ActivitySource.StartActivity($"{Prefix}.AuthorizeAction"); activity?.SetInstanceId(instanceIdentifier.InstanceGuid); activity?.SetInstanceOwnerPartyId(instanceIdentifier.InstanceOwnerPartyId); @@ -43,7 +43,7 @@ partial class Telemetry List actions ) { - var activity = ActivitySource.StartActivity($"{_prefix}.AuthorizeActions"); + var activity = ActivitySource.StartActivity($"{Prefix}.AuthorizeActions"); if (activity is not null) { activity.SetInstanceId(instance); @@ -67,7 +67,7 @@ List actions string action ) { - var activity = ActivitySource.StartActivity($"{_prefix}.IsAuthorizerForTaskAndAction"); + var activity = ActivitySource.StartActivity($"{Prefix}.IsAuthorizerForTaskAndAction"); if (activity is not null) { activity.SetTaskId(taskId); @@ -82,6 +82,6 @@ string action internal static class AuthorizationService { - internal const string _prefix = "Authorization.Service"; + internal const string Prefix = "Authorization.Service"; } } diff --git a/src/Altinn.App.Core/Features/Telemetry.Data.cs b/src/Altinn.App.Core/Features/Telemetry.Data.cs index 06219c636..a049d74e2 100644 --- a/src/Altinn.App.Core/Features/Telemetry.Data.cs +++ b/src/Altinn.App.Core/Features/Telemetry.Data.cs @@ -27,14 +27,14 @@ internal void DataPatched(PatchResult result) => internal Activity? StartDataPatchActivity(Instance instance) { - var activity = ActivitySource.StartActivity($"{_prefix}.Patch"); + var activity = ActivitySource.StartActivity($"{Prefix}.Patch"); activity?.SetInstanceId(instance); return activity; } internal static class Data { - internal const string _prefix = "Data"; + internal const string Prefix = "Data"; internal static readonly string MetricNameDataPatched = Metrics.CreateLibName("data_patched"); [EnumExtensions] diff --git a/src/Altinn.App.Core/Features/Telemetry.DataClient.cs b/src/Altinn.App.Core/Features/Telemetry.DataClient.cs index a827d6c5e..21a3cc89e 100644 --- a/src/Altinn.App.Core/Features/Telemetry.DataClient.cs +++ b/src/Altinn.App.Core/Features/Telemetry.DataClient.cs @@ -8,14 +8,14 @@ partial class Telemetry { internal Activity? StartInsertFormDataActivity(Instance? instance) { - var activity = ActivitySource.StartActivity($"{_prefix}.InsertFormData"); + var activity = ActivitySource.StartActivity($"{Prefix}.InsertFormData"); activity?.SetInstanceId(instance); return activity; } internal Activity? StartInsertFormDataActivity(Guid? instanceId, int? partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.InsertFormData"); + var activity = ActivitySource.StartActivity($"{Prefix}.InsertFormData"); activity?.SetInstanceId(instanceId); activity?.SetInstanceOwnerPartyId(partyId); return activity; @@ -23,7 +23,7 @@ partial class Telemetry internal Activity? StartUpdateDataActivity(Guid? instanceId, int? partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.UpdateData"); + var activity = ActivitySource.StartActivity($"{Prefix}.UpdateData"); activity?.SetInstanceId(instanceId); activity?.SetInstanceOwnerPartyId(partyId); return activity; @@ -31,7 +31,7 @@ partial class Telemetry internal Activity? StartGetBinaryDataActivity(Guid? instanceId, int? partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.GetBinaryData"); + var activity = ActivitySource.StartActivity($"{Prefix}.GetBinaryData"); activity?.SetInstanceId(instanceId); activity?.SetInstanceOwnerPartyId(partyId); return activity; @@ -39,7 +39,7 @@ partial class Telemetry internal Activity? StartGetBinaryDataListActivity(Guid? instanceId, int? partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.GetBinaryDataList"); + var activity = ActivitySource.StartActivity($"{Prefix}.GetBinaryDataList"); activity?.SetInstanceId(instanceId); activity?.SetInstanceOwnerPartyId(partyId); return activity; @@ -47,7 +47,7 @@ partial class Telemetry internal Activity? StartDeleteBinaryDataActivity(Guid? instanceId, int? partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.DeleteBinaryData"); + var activity = ActivitySource.StartActivity($"{Prefix}.DeleteBinaryData"); activity?.SetInstanceId(instanceId); activity?.SetInstanceOwnerPartyId(partyId); return activity; @@ -55,7 +55,7 @@ partial class Telemetry internal Activity? StartInsertBinaryDataActivity(Guid? instanceId, int? partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.InsertBinaryData"); + var activity = ActivitySource.StartActivity($"{Prefix}.InsertBinaryData"); activity?.SetInstanceId(instanceId); activity?.SetInstanceOwnerPartyId(partyId); return activity; @@ -63,14 +63,14 @@ partial class Telemetry internal Activity? StartInsertBinaryDataActivity(string? instanceId) { - var activity = ActivitySource.StartActivity($"{_prefix}.InsertBinaryData"); + var activity = ActivitySource.StartActivity($"{Prefix}.InsertBinaryData"); activity?.SetInstanceId(instanceId); return activity; } internal Activity? StartUpdateBinaryDataActivity(Guid? instanceId, int? partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.UpdateBinaryData"); + var activity = ActivitySource.StartActivity($"{Prefix}.UpdateBinaryData"); activity?.SetInstanceId(instanceId); activity?.SetInstanceOwnerPartyId(partyId); return activity; @@ -78,21 +78,21 @@ partial class Telemetry internal Activity? StartUpdateBinaryDataActivity(string? instanceId) { - var activity = ActivitySource.StartActivity($"{_prefix}.UpdateBinaryData"); + var activity = ActivitySource.StartActivity($"{Prefix}.UpdateBinaryData"); activity?.SetInstanceId(instanceId); return activity; } internal Activity? StartUpdateDataActivity(Instance? instance) { - var activity = ActivitySource.StartActivity($"{_prefix}.UpdateData"); + var activity = ActivitySource.StartActivity($"{Prefix}.UpdateData"); activity?.SetInstanceId(instance); return activity; } internal Activity? StartDeleteDataActivity(Guid? instanceId, int? partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.DeleteData"); + var activity = ActivitySource.StartActivity($"{Prefix}.DeleteData"); activity?.SetInstanceId(instanceId); activity?.SetInstanceOwnerPartyId(partyId); return activity; @@ -100,7 +100,7 @@ partial class Telemetry internal Activity? StartGetFormDataActivity(Guid? instanceId, int? partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.GetFormData"); + var activity = ActivitySource.StartActivity($"{Prefix}.GetFormData"); activity?.SetInstanceId(instanceId); activity?.SetInstanceOwnerPartyId(partyId); return activity; @@ -108,7 +108,7 @@ partial class Telemetry internal Activity? StartLockDataElementActivity(string? instanceId, Guid? dataGuid) { - var activity = ActivitySource.StartActivity($"{_prefix}.LockDataElement"); + var activity = ActivitySource.StartActivity($"{Prefix}.LockDataElement"); activity?.SetInstanceId(instanceId); activity?.SetDataElementId(dataGuid); return activity; @@ -116,7 +116,7 @@ partial class Telemetry internal Activity? StartUnlockDataElementActivity(string? instanceId, Guid? dataGuid) { - var activity = ActivitySource.StartActivity($"{_prefix}.UnlockDataElement"); + var activity = ActivitySource.StartActivity($"{Prefix}.UnlockDataElement"); activity?.SetInstanceId(instanceId); activity?.SetDataElementId(dataGuid); return activity; @@ -124,6 +124,6 @@ partial class Telemetry internal static class DataClient { - internal const string _prefix = "DataClient"; + internal const string Prefix = "DataClient"; } } diff --git a/src/Altinn.App.Core/Features/Telemetry.Instances.cs b/src/Altinn.App.Core/Features/Telemetry.Instances.cs index 757b93fb3..7bb4dc2cc 100644 --- a/src/Altinn.App.Core/Features/Telemetry.Instances.cs +++ b/src/Altinn.App.Core/Features/Telemetry.Instances.cs @@ -41,36 +41,36 @@ internal void InstanceDeleted(Instance instance) internal Activity? StartGetInstanceByGuidActivity(Guid? instanceGuid = null) { - var activity = ActivitySource.StartActivity($"{_prefix}.GetInstanceByGuid"); + var activity = ActivitySource.StartActivity($"{Prefix}.GetInstanceByGuid"); activity?.SetInstanceId(instanceGuid); return activity; } internal Activity? StartGetInstanceByInstanceActivity(Guid? instanceGuid = null) { - var activity = ActivitySource.StartActivity($"{_prefix}.GetInstanceByInstance"); + var activity = ActivitySource.StartActivity($"{Prefix}.GetInstanceByInstance"); activity?.SetInstanceId(instanceGuid); return activity; } internal Activity? StartGetInstancesActivity(Guid? instanceGuid = null) { - var activity = ActivitySource.StartActivity($"{_prefix}.GetInstances"); + var activity = ActivitySource.StartActivity($"{Prefix}.GetInstances"); activity?.SetInstanceId(instanceGuid); return activity; } - internal Activity? StartQueryInstancesActivity() => ActivitySource.StartActivity($"{_prefix}.Query"); + internal Activity? StartQueryInstancesActivity() => ActivitySource.StartActivity($"{Prefix}.Query"); internal Activity? StartCreateInstanceActivity() { - var activity = ActivitySource.StartActivity($"{_prefix}.Create"); + var activity = ActivitySource.StartActivity($"{Prefix}.Create"); return activity; } internal Activity? StartDeleteInstanceActivity(Guid instanceGuid, int instanceOwnerPartyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.Delete"); + var activity = ActivitySource.StartActivity($"{Prefix}.Delete"); activity?.SetInstanceId(instanceGuid); activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); return activity; @@ -78,14 +78,14 @@ internal void InstanceDeleted(Instance instance) internal Activity? StartUpdateProcessActivity(Instance instance) { - var activity = ActivitySource.StartActivity($"{_prefix}.UpdateProcess"); + var activity = ActivitySource.StartActivity($"{Prefix}.UpdateProcess"); activity?.SetInstanceId(instance); return activity; } internal Activity? StartCompleteConfirmationActivity(Guid instanceGuid, int instanceOwnerPartyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.CompleteConfirmation"); + var activity = ActivitySource.StartActivity($"{Prefix}.CompleteConfirmation"); activity?.SetInstanceId(instanceGuid); activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); return activity; @@ -93,7 +93,7 @@ internal void InstanceDeleted(Instance instance) internal Activity? StartUpdateReadStatusActivity(Guid instanceGuid, int instanceOwnerPartyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.UpdateReadStatus"); + var activity = ActivitySource.StartActivity($"{Prefix}.UpdateReadStatus"); activity?.SetInstanceId(instanceGuid); activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); return activity; @@ -101,7 +101,7 @@ internal void InstanceDeleted(Instance instance) internal Activity? StartUpdateSubStatusActivity(Guid instanceGuid, int instanceOwnerPartyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.UpdateSubStatus"); + var activity = ActivitySource.StartActivity($"{Prefix}.UpdateSubStatus"); activity?.SetInstanceId(instanceGuid); activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); return activity; @@ -109,7 +109,7 @@ internal void InstanceDeleted(Instance instance) internal Activity? StartUpdatePresentationTextActivity(Guid instanceGuid, int instanceOwnerPartyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.UpdatePresentationText"); + var activity = ActivitySource.StartActivity($"{Prefix}.UpdatePresentationText"); activity?.SetInstanceId(instanceGuid); activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); return activity; @@ -117,7 +117,7 @@ internal void InstanceDeleted(Instance instance) internal Activity? StartUpdateDataValuesActivity(Guid instanceGuid, int instanceOwnerPartyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.UpdateDataValues"); + var activity = ActivitySource.StartActivity($"{Prefix}.UpdateDataValues"); activity?.SetInstanceId(instanceGuid); activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId); return activity; @@ -125,7 +125,7 @@ internal void InstanceDeleted(Instance instance) internal static class Instances { - internal const string _prefix = "Instance"; + internal const string Prefix = "Instance"; internal static readonly string MetricNameInstancesCreated = Metrics.CreateLibName("instances_created"); internal static readonly string MetricNameInstancesCompleted = Metrics.CreateLibName("instances_completed"); diff --git a/src/Altinn.App.Core/Features/Telemetry.Prefill.Service.cs b/src/Altinn.App.Core/Features/Telemetry.Prefill.Service.cs index f2f3b00ec..99c73827a 100644 --- a/src/Altinn.App.Core/Features/Telemetry.Prefill.Service.cs +++ b/src/Altinn.App.Core/Features/Telemetry.Prefill.Service.cs @@ -5,17 +5,17 @@ namespace Altinn.App.Core.Features; partial class Telemetry { - internal Activity? StartPrefillDataModelActivity() => ActivitySource.StartActivity($"{_prefix}.PrefillDataModel"); + internal Activity? StartPrefillDataModelActivity() => ActivitySource.StartActivity($"{Prefix}.PrefillDataModel"); internal Activity? StartPrefillDataModelActivity(string? partyId) { - var activity = ActivitySource.StartActivity($"{_prefix}.PrefillDataModelWithId"); + var activity = ActivitySource.StartActivity($"{Prefix}.PrefillDataModelWithId"); activity?.SetInstanceOwnerPartyId(partyId); return activity; } internal static class PrefillService { - internal const string _prefix = "PrefillService"; + internal const string Prefix = "PrefillService"; } } diff --git a/src/Altinn.App.Core/Features/Telemetry.Processes.cs b/src/Altinn.App.Core/Features/Telemetry.Processes.cs index a27b0bd3a..0b24625f1 100644 --- a/src/Altinn.App.Core/Features/Telemetry.Processes.cs +++ b/src/Altinn.App.Core/Features/Telemetry.Processes.cs @@ -35,28 +35,28 @@ internal void ProcessEnded(ProcessStateChange processChange) internal Activity? StartProcessStartActivity(Instance instance) { - var activity = ActivitySource.StartActivity($"{_prefix}.Start"); + var activity = ActivitySource.StartActivity($"{Prefix}.Start"); activity?.SetInstanceId(instance); return activity; } internal Activity? StartProcessNextActivity(Instance instance) { - var activity = ActivitySource.StartActivity($"{_prefix}.Next"); + var activity = ActivitySource.StartActivity($"{Prefix}.Next"); activity?.SetInstanceId(instance); return activity; } internal Activity? StartProcessEndActivity(Instance instance) { - var activity = ActivitySource.StartActivity($"{_prefix}.End"); + var activity = ActivitySource.StartActivity($"{Prefix}.End"); activity?.SetInstanceId(instance); return activity; } internal static class Processes { - internal const string _prefix = "Process"; + internal const string Prefix = "Process"; internal static readonly string MetricNameProcessesStarted = Metrics.CreateLibName("processes_started"); internal static readonly string MetricNameProcessesEnded = Metrics.CreateLibName("processes_ended"); diff --git a/src/Altinn.App.Core/Features/Telemetry.Validation.cs b/src/Altinn.App.Core/Features/Telemetry.Validation.cs index 173728d26..9ef4c4763 100644 --- a/src/Altinn.App.Core/Features/Telemetry.Validation.cs +++ b/src/Altinn.App.Core/Features/Telemetry.Validation.cs @@ -12,7 +12,7 @@ private void InitValidation(InitContext context) { } { ArgumentException.ThrowIfNullOrWhiteSpace(taskId); - var activity = ActivitySource.StartActivity($"{_prefix}.ValidateInstanceAtTask"); + var activity = ActivitySource.StartActivity($"{Prefix}.ValidateInstanceAtTask"); activity?.SetTaskId(taskId); activity?.SetInstanceId(instance); return activity; @@ -20,7 +20,7 @@ private void InitValidation(InitContext context) { } internal Activity? StartRunTaskValidatorActivity(ITaskValidator validator) { - var activity = ActivitySource.StartActivity($"{_prefix}.RunTaskValidator"); + var activity = ActivitySource.StartActivity($"{Prefix}.RunTaskValidator"); activity?.SetTag(InternalLabels.ValidatorType, validator.GetType().Name); activity?.SetTag(InternalLabels.ValidatorSource, validator.ValidationSource); @@ -30,7 +30,7 @@ private void InitValidation(InitContext context) { } internal Activity? StartValidateDataElementActivity(Instance instance, DataElement dataElement) { - var activity = ActivitySource.StartActivity($"{_prefix}.ValidateDataElement"); + var activity = ActivitySource.StartActivity($"{Prefix}.ValidateDataElement"); activity?.SetInstanceId(instance); activity?.SetDataElementId(dataElement); return activity; @@ -38,7 +38,7 @@ private void InitValidation(InitContext context) { } internal Activity? StartRunDataElementValidatorActivity(IDataElementValidator validator) { - var activity = ActivitySource.StartActivity($"{_prefix}.RunDataElementValidator"); + var activity = ActivitySource.StartActivity($"{Prefix}.RunDataElementValidator"); activity?.SetTag(InternalLabels.ValidatorType, validator.GetType().Name); activity?.SetTag(InternalLabels.ValidatorSource, validator.ValidationSource); @@ -48,7 +48,7 @@ private void InitValidation(InitContext context) { } internal Activity? StartValidateFormDataActivity(Instance instance, DataElement dataElement) { - var activity = ActivitySource.StartActivity($"{_prefix}.ValidateFormData"); + var activity = ActivitySource.StartActivity($"{Prefix}.ValidateFormData"); activity?.SetInstanceId(instance); activity?.SetDataElementId(dataElement); @@ -57,7 +57,7 @@ private void InitValidation(InitContext context) { } internal Activity? StartRunFormDataValidatorActivity(IFormDataValidator validator) { - var activity = ActivitySource.StartActivity($"{_prefix}.RunFormDataValidator"); + var activity = ActivitySource.StartActivity($"{Prefix}.RunFormDataValidator"); activity?.SetTag(InternalLabels.ValidatorType, validator.GetType().Name); activity?.SetTag(InternalLabels.ValidatorSource, validator.ValidationSource); @@ -67,6 +67,6 @@ private void InitValidation(InitContext context) { } internal static class Validation { - internal const string _prefix = "Validation"; + internal const string Prefix = "Validation"; } } diff --git a/src/Altinn.App.Core/Features/Validation/Default/DefaultTaskValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/DefaultTaskValidator.cs index 2f86141b6..0d31dd24e 100644 --- a/src/Altinn.App.Core/Features/Validation/Default/DefaultTaskValidator.cs +++ b/src/Altinn.App.Core/Features/Validation/Default/DefaultTaskValidator.cs @@ -1,8 +1,6 @@ using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; -using Microsoft.Extensions.DependencyInjection; namespace Altinn.App.Core.Features.Validation.Default; diff --git a/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs index 246fb7ef7..e1c48343e 100644 --- a/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs +++ b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs @@ -5,7 +5,6 @@ using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Altinn.App.Core.Features.Validation.Default; diff --git a/src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs b/src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs index 59601a833..672b3aee4 100644 --- a/src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs +++ b/src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs @@ -25,7 +25,7 @@ protected GenericFormDataValidator(string dataType) public string DataType { get; private init; } // ReSharper disable once StaticMemberInGenericType - private static readonly AsyncLocal> ValidationIssues = new(); + private static readonly AsyncLocal> _validationIssues = new(); /// /// Default implementation that respects the runFor prefixes. @@ -67,7 +67,7 @@ protected void CreateValidationIssue( List? customTextParams = null ) { - Debug.Assert(ValidationIssues.Value is not null); + Debug.Assert(_validationIssues.Value is not null); AddValidationIssue( new ValidationIssue { @@ -86,8 +86,8 @@ protected void CreateValidationIssue( /// protected void AddValidationIssue(ValidationIssue issue) { - Debug.Assert(ValidationIssues.Value is not null); - ValidationIssues.Value.Add(issue); + Debug.Assert(_validationIssues.Value is not null); + _validationIssues.Value.Add(issue); } /// @@ -106,9 +106,9 @@ public async Task> ValidateFormData( throw new ArgumentException($"Data is not of type {typeof(TModel)}"); } - ValidationIssues.Value = new List(); + _validationIssues.Value = new List(); await ValidateFormData(instance, dataElement, model, language); - return ValidationIssues.Value; + return _validationIssues.Value; } /// diff --git a/src/Altinn.App.Core/Features/Validation/IFileValidator.cs b/src/Altinn.App.Core/Features/Validation/IFileValidator.cs index 14576443a..1b9b8bf44 100644 --- a/src/Altinn.App.Core/Features/Validation/IFileValidator.cs +++ b/src/Altinn.App.Core/Features/Validation/IFileValidator.cs @@ -2,24 +2,23 @@ using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Features.Validation +namespace Altinn.App.Core.Features.Validation; + +/// +/// Interface for handling validation of files added to an instance. +/// +public interface IFileValidator { /// - /// Interface for handling validation of files added to an instance. + /// The id of the validator to be used when enabling it from config. /// - public interface IFileValidator - { - /// - /// The id of the validator to be used when enabling it from config. - /// - public string Id { get; } + public string Id { get; } - /// - /// Validating a file based on analysis results. - /// - Task<(bool Success, IEnumerable Errors)> Validate( - DataType dataType, - IEnumerable fileAnalysisResults - ); - } + /// + /// Validating a file based on analysis results. + /// + Task<(bool Success, IEnumerable Errors)> Validate( + DataType dataType, + IEnumerable fileAnalysisResults + ); } diff --git a/src/Altinn.App.Core/Helpers/AppTextHelper.cs b/src/Altinn.App.Core/Helpers/AppTextHelper.cs index 8d8642846..d441fc2ee 100644 --- a/src/Altinn.App.Core/Helpers/AppTextHelper.cs +++ b/src/Altinn.App.Core/Helpers/AppTextHelper.cs @@ -1,47 +1,46 @@ -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// Helper for service Text +/// +public static class AppTextHelper { /// - /// Helper for service Text + /// Get a given app Text /// - public static class AppTextHelper + /// The key + /// List of serviceText + /// Parameters for text + /// The languageId + /// The given text + public static string GetAppText( + string key, + Dictionary> serviceText, + List? textParams, + string languageId + ) { - /// - /// Get a given app Text - /// - /// The key - /// List of serviceText - /// Parameters for text - /// The languageId - /// The given text - public static string GetAppText( - string key, - Dictionary> serviceText, - List? textParams, - string languageId - ) + string text = key; + if (serviceText != null && serviceText.TryGetValue(key, out var serviceTextMap)) { - string text = key; - if (serviceText != null && serviceText.TryGetValue(key, out var serviceTextMap)) + if (serviceTextMap.TryGetValue(languageId, out var lookupText)) { - if (serviceTextMap.TryGetValue(languageId, out var lookupText)) - { - text = lookupText; - } - - if (textParams != null && textParams.Count > 0) - { - object[] stringList = new object[textParams.Count]; + text = lookupText; + } - for (int i = 0; i < textParams.Count; i++) - { - stringList[i] = textParams[i]; - } + if (textParams != null && textParams.Count > 0) + { + object[] stringList = new object[textParams.Count]; - text = string.Format(text, stringList); + for (int i = 0; i < textParams.Count; i++) + { + stringList[i] = textParams[i]; } - } - return text; + text = string.Format(text, stringList); + } } + + return text; } } diff --git a/src/Altinn.App.Core/Helpers/AuthenticationHelper.cs b/src/Altinn.App.Core/Helpers/AuthenticationHelper.cs index bc1f0ccbb..8d7a7cc34 100644 --- a/src/Altinn.App.Core/Helpers/AuthenticationHelper.cs +++ b/src/Altinn.App.Core/Helpers/AuthenticationHelper.cs @@ -2,34 +2,33 @@ using AltinnCore.Authentication.Constants; using Microsoft.AspNetCore.Http; -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// helper class for authentication +/// +public static class AuthenticationHelper { /// - /// helper class for authentication + /// Gets the users id /// - public static class AuthenticationHelper + /// the http context + /// the logged in users id + public static int GetUserId(HttpContext context) { - /// - /// Gets the users id - /// - /// the http context - /// the logged in users id - public static int GetUserId(HttpContext context) - { - int userId = 0; + int userId = 0; - if (context.User != null) + if (context.User != null) + { + foreach (Claim claim in context.User.Claims) { - foreach (Claim claim in context.User.Claims) + if (claim.Type.Equals(AltinnCoreClaimTypes.UserId)) { - if (claim.Type.Equals(AltinnCoreClaimTypes.UserId)) - { - userId = Convert.ToInt32(claim.Value); - } + userId = Convert.ToInt32(claim.Value); } } - - return userId; } + + return userId; } } diff --git a/src/Altinn.App.Core/Helpers/CamelCaseExceptDictionaryResolver.cs b/src/Altinn.App.Core/Helpers/CamelCaseExceptDictionaryResolver.cs index 93ac99cc8..8340512c3 100644 --- a/src/Altinn.App.Core/Helpers/CamelCaseExceptDictionaryResolver.cs +++ b/src/Altinn.App.Core/Helpers/CamelCaseExceptDictionaryResolver.cs @@ -1,16 +1,15 @@ using Newtonsoft.Json.Serialization; -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// Camel case property name resolver but does not camel case dictionary keys +/// +public class CamelCaseExceptDictionaryResolver : CamelCasePropertyNamesContractResolver { - /// - /// Camel case property name resolver but does not camel case dictionary keys - /// - public class CamelCaseExceptDictionaryResolver : CamelCasePropertyNamesContractResolver + /// + protected override string ResolveDictionaryKey(string dictionaryKey) { - /// - protected override string ResolveDictionaryKey(string dictionaryKey) - { - return dictionaryKey; - } + return dictionaryKey; } } diff --git a/src/Altinn.App.Core/Helpers/DataHelper.cs b/src/Altinn.App.Core/Helpers/DataHelper.cs index 916e4c88a..0b13240a4 100644 --- a/src/Altinn.App.Core/Helpers/DataHelper.cs +++ b/src/Altinn.App.Core/Helpers/DataHelper.cs @@ -1,186 +1,185 @@ using System.Reflection; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// Helper class for handling data +/// +public static class DataHelper { /// - /// Helper class for handling data + /// Identifies updated data values texts by extracting data fields from data object and comparing to dictionary of current values. /// - public static class DataHelper + /// The data fields to monitor + /// The current dictionary of data values + /// The type of the updated data objects + /// The updated data object + /// A dictionary with the new or changed data values + public static Dictionary GetUpdatedDataValues( + List? dataFields, + Dictionary currentDataValues, + string dataType, + object updatedData + ) { - /// - /// Identifies updated data values texts by extracting data fields from data object and comparing to dictionary of current values. - /// - /// The data fields to monitor - /// The current dictionary of data values - /// The type of the updated data objects - /// The updated data object - /// A dictionary with the new or changed data values - public static Dictionary GetUpdatedDataValues( - List? dataFields, - Dictionary currentDataValues, - string dataType, - object updatedData - ) - { - Dictionary dataFieldValues = GetDataFieldValues(dataFields, dataType, updatedData); - return CompareDictionaries(currentDataValues, dataFieldValues); - } + Dictionary dataFieldValues = GetDataFieldValues(dataFields, dataType, updatedData); + return CompareDictionaries(currentDataValues, dataFieldValues); + } - /// - /// Re-sets the listed data fields to their default value in the data object. - /// - /// The data fields to monitor - /// The data object - public static void ResetDataFields(List dataFields, object data) + /// + /// Re-sets the listed data fields to their default value in the data object. + /// + /// The data fields to monitor + /// The data object + public static void ResetDataFields(List dataFields, object data) + { + foreach (string dataField in dataFields) { - foreach (string dataField in dataFields) - { - string fixedPath = dataField.Replace("-", string.Empty); - string[] keys = fixedPath.Split("."); - ResetDataField(keys, data); - } + string fixedPath = dataField.Replace("-", string.Empty); + string[] keys = fixedPath.Split("."); + ResetDataField(keys, data); } + } + + private static void ResetDataField(string[] keys, object data, int index = 0) + { + string key = keys[index]; + Type current = data.GetType(); + bool isLastKey = (keys.Length - 1) == index; - private static void ResetDataField(string[] keys, object data, int index = 0) + PropertyInfo? property = current.GetProperty( + key, + BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance + ); + + if (property == null) { - string key = keys[index]; - Type current = data.GetType(); - bool isLastKey = (keys.Length - 1) == index; + return; + } - PropertyInfo? property = current.GetProperty( - key, - BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance - ); + object? propertyValue = property.GetValue(data, null); - if (property == null) - { - return; - } + if (propertyValue == null) + { + return; + } - object? propertyValue = property.GetValue(data, null); + if (isLastKey) + { + object? defaultValue = property.PropertyType.IsValueType + ? Activator.CreateInstance(property.PropertyType) + : null; + property.SetValue(data, defaultValue); + return; + } - if (propertyValue == null) - { - return; - } + ResetDataField(keys, propertyValue, index + 1); + } - if (isLastKey) - { - object? defaultValue = property.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; - property.SetValue(data, defaultValue); - return; - } + /// + /// Retrieves data values from a data object based on a list of data fields. + /// + private static Dictionary GetDataFieldValues( + List? dataFields, + string dataType, + object data + ) + { + Dictionary dataFieldValues = new Dictionary(); - ResetDataField(keys, propertyValue, index + 1); + if (dataFields == null || !dataFields.Any(pf => pf.DataTypeId == dataType)) + { + return dataFieldValues; } - /// - /// Retrieves data values from a data object based on a list of data fields. - /// - private static Dictionary GetDataFieldValues( - List? dataFields, - string dataType, - object data - ) + foreach (DataField field in dataFields) { - Dictionary dataFieldValues = new Dictionary(); - - if (dataFields == null || !dataFields.Any(pf => pf.DataTypeId == dataType)) + if (dataType != field.DataTypeId) { - return dataFieldValues; + break; } - foreach (DataField field in dataFields) - { - if (dataType != field.DataTypeId) - { - break; - } + string fixedPath = field.Path.Replace("-", string.Empty); + string[] keys = fixedPath.Split("."); - string fixedPath = field.Path.Replace("-", string.Empty); - string[] keys = fixedPath.Split("."); + string? value = GetValueFromDatamodel(keys, data); + dataFieldValues.Add(field.Id, value); + } - string? value = GetValueFromDatamodel(keys, data); - dataFieldValues.Add(field.Id, value); - } + return dataFieldValues; + } - return dataFieldValues; + /// + /// Compares entries in the new dictionary with the original dictionary. + /// + /// The original dictionary + /// The updated dictionary + /// A dictionary containing changed and new entries not represented in the original dictionary. + private static Dictionary CompareDictionaries( + Dictionary? originalDictionary, + Dictionary newDictionary + ) + { + Dictionary updatedValues = new Dictionary(); + + if (originalDictionary == null) + { + return newDictionary; } - /// - /// Compares entries in the new dictionary with the original dictionary. - /// - /// The original dictionary - /// The updated dictionary - /// A dictionary containing changed and new entries not represented in the original dictionary. - private static Dictionary CompareDictionaries( - Dictionary? originalDictionary, - Dictionary newDictionary - ) + foreach (KeyValuePair entry in newDictionary) { - Dictionary updatedValues = new Dictionary(); + string key = entry.Key; + string? value = entry.Value; - if (originalDictionary == null) + if (originalDictionary.TryGetValue(key, out string? originalValue) && originalValue != value) { - return newDictionary; + updatedValues.Add(key, value); } - - foreach (KeyValuePair entry in newDictionary) + else if (!originalDictionary.ContainsKey(key)) { - string key = entry.Key; - string? value = entry.Value; - - if (originalDictionary.TryGetValue(key, out string? originalValue) && originalValue != value) - { - updatedValues.Add(key, value); - } - else if (!originalDictionary.ContainsKey(key)) - { - updatedValues.Add(key, value); - } + updatedValues.Add(key, value); } - - return updatedValues; } - private static string? GetValueFromDatamodel(string[] keys, object data, int index = 0) - { - string key = keys[index]; - bool isLastKey = (keys.Length - 1) == index; - Type current = data.GetType(); + return updatedValues; + } + + private static string? GetValueFromDatamodel(string[] keys, object data, int index = 0) + { + string key = keys[index]; + bool isLastKey = (keys.Length - 1) == index; + Type current = data.GetType(); - PropertyInfo? property = current.GetProperty( - key, - BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance - ); + PropertyInfo? property = current.GetProperty( + key, + BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance + ); - if (property == null) + if (property == null) + { + string errorMessage = + $"Could not find the field {string.Join(".", keys)}, property {key} is not defined in the data model."; + throw new IndexOutOfRangeException(errorMessage); + } + else + { + object? propertyValue = property.GetValue(data, null); + if (isLastKey) { - string errorMessage = - $"Could not find the field {string.Join(".", keys)}, property {key} is not defined in the data model."; - throw new IndexOutOfRangeException(errorMessage); + return propertyValue == null ? null : propertyValue.ToString(); } else { - object? propertyValue = property.GetValue(data, null); - if (isLastKey) - { - return propertyValue == null ? null : propertyValue.ToString(); - } - else + // no need to look further down, it is not defined yet. + if (propertyValue == null) { - // no need to look further down, it is not defined yet. - if (propertyValue == null) - { - return null; - } - - // recurivly assign values - return GetValueFromDatamodel(keys, propertyValue, index + 1); + return null; } + + // recurivly assign values + return GetValueFromDatamodel(keys, propertyValue, index + 1); } } } diff --git a/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs b/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs index 6dff9697b..ae7b5a0d6 100644 --- a/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs +++ b/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs @@ -119,7 +119,7 @@ internal static string JoinFieldKeyParts(string? currentKey, string? key) return currentKey + "." + key; } - private static readonly Regex RowIndexRegex = new Regex( + private static readonly Regex _rowIndexRegex = new Regex( @"^([^[\]]+(\[(\d+)])?)+$", RegexOptions.None, TimeSpan.FromSeconds(1) @@ -130,7 +130,7 @@ internal static string JoinFieldKeyParts(string? currentKey, string? key) /// public static int[]? GetRowIndices(string key) { - var match = RowIndexRegex.Match(key); + var match = _rowIndexRegex.Match(key); var rowIndices = match.Groups[3].Captures.Select(c => c.Value).Select(int.Parse).ToArray(); return rowIndices.Length == 0 ? null : rowIndices; } @@ -211,7 +211,7 @@ private string[] GetResolvedKeysRecursive( return null; } - private static readonly Regex KeyPartRegex = new Regex(@"^([^\s\[\]\.]+)\[(\d+)\]?$"); + private static readonly Regex _keyPartRegex = new Regex(@"^([^\s\[\]\.]+)\[(\d+)\]?$"); internal static (string key, int? index) ParseKeyPart(string keypart) { @@ -223,7 +223,7 @@ internal static (string key, int? index) ParseKeyPart(string keypart) { return (keypart, null); } - var match = KeyPartRegex.Match(keypart); + var match = _keyPartRegex.Match(keypart); return (match.Groups[1].Value, int.Parse(match.Groups[2].Value)); } diff --git a/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs b/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs index 9992dbacf..1507dc5f6 100644 --- a/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs +++ b/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs @@ -1,5 +1,3 @@ -using System.Runtime.Serialization; - namespace Altinn.App.Core.Helpers.DataModel; /// diff --git a/src/Altinn.App.Core/Helpers/Extensions/StringExtensions.cs b/src/Altinn.App.Core/Helpers/Extensions/StringExtensions.cs index 225db105d..4e6fd038e 100644 --- a/src/Altinn.App.Core/Helpers/Extensions/StringExtensions.cs +++ b/src/Altinn.App.Core/Helpers/Extensions/StringExtensions.cs @@ -1,35 +1,34 @@ -namespace Altinn.App.Core.Helpers.Extensions +namespace Altinn.App.Core.Helpers.Extensions; + +/// +/// Extensions to facilitate sanitization of string values +/// +public static class StringExtensions { /// - /// Extensions to facilitate sanitization of string values + /// Sanitize the input as a file name. /// - public static class StringExtensions + /// The input variable to be sanitized. + /// Throw exception instead of replacing invalid characters with '_'. + /// A filename cleaned of any impurities. + public static string AsFileName(this string input, bool throwExceptionOnInvalidCharacters = true) { - /// - /// Sanitize the input as a file name. - /// - /// The input variable to be sanitized. - /// Throw exception instead of replacing invalid characters with '_'. - /// A filename cleaned of any impurities. - public static string AsFileName(this string input, bool throwExceptionOnInvalidCharacters = true) + if (string.IsNullOrWhiteSpace(input)) { - if (string.IsNullOrWhiteSpace(input)) - { - return input; - } + return input; + } - char[] illegalFileNameCharacters = System.IO.Path.GetInvalidFileNameChars(); - if (throwExceptionOnInvalidCharacters) + char[] illegalFileNameCharacters = System.IO.Path.GetInvalidFileNameChars(); + if (throwExceptionOnInvalidCharacters) + { + if (illegalFileNameCharacters.Any(ic => input.Any(i => ic == i))) { - if (illegalFileNameCharacters.Any(ic => input.Any(i => ic == i))) - { - throw new ArgumentOutOfRangeException(nameof(input)); - } - - return input; + throw new ArgumentOutOfRangeException(nameof(input)); } - return illegalFileNameCharacters.Aggregate(input, (current, c) => current.Replace(c, '_')); + return input; } + + return illegalFileNameCharacters.Aggregate(input, (current, c) => current.Replace(c, '_')); } } diff --git a/src/Altinn.App.Core/Helpers/Extensions/Utf8JsonReaderExtentions.cs b/src/Altinn.App.Core/Helpers/Extensions/Utf8JsonReaderExtentions.cs index 5ffc5b5ae..250ec0659 100644 --- a/src/Altinn.App.Core/Helpers/Extensions/Utf8JsonReaderExtentions.cs +++ b/src/Altinn.App.Core/Helpers/Extensions/Utf8JsonReaderExtentions.cs @@ -4,12 +4,12 @@ namespace Altinn.App.Core.Helpers.Extensions; internal static class Utf8JsonReaderExtensions { - private static readonly JsonWriterOptions OPTIONS = new() { Indented = true, }; + private static readonly JsonWriterOptions _options = new() { Indented = true, }; internal static string SkipReturnString(this ref Utf8JsonReader reader) { using var stream = new System.IO.MemoryStream(); - using var writer = new Utf8JsonWriter(stream, OPTIONS); + using var writer = new Utf8JsonWriter(stream, _options); Copy(ref reader, writer); writer.Flush(); diff --git a/src/Altinn.App.Core/Helpers/IDataModel.cs b/src/Altinn.App.Core/Helpers/IDataModel.cs index b4b3bca3e..1d364a536 100644 --- a/src/Altinn.App.Core/Helpers/IDataModel.cs +++ b/src/Altinn.App.Core/Helpers/IDataModel.cs @@ -1,8 +1,3 @@ -using System.Linq; -using System.Reflection; -using System.Text.Json; -using System.Text.RegularExpressions; - namespace Altinn.App.Core.Helpers; /// diff --git a/src/Altinn.App.Core/Helpers/InstantiationHelper.cs b/src/Altinn.App.Core/Helpers/InstantiationHelper.cs index ba6734f87..f0b31da71 100644 --- a/src/Altinn.App.Core/Helpers/InstantiationHelper.cs +++ b/src/Altinn.App.Core/Helpers/InstantiationHelper.cs @@ -2,211 +2,210 @@ using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// Various helper methods for instantiation +/// +public static class InstantiationHelper { + private const string BANKRUPTCY_CODE = "KBO"; + private const string SUB_UNIT_CODE = "BEDR"; + private const string SUB_UNIT_CODE_AAFY = "AAFY"; + /// - /// Various helper methods for instantiation + /// Filters a list of parties based on an applications allowed party types. /// - public static class InstantiationHelper + /// The list of parties to be filtered + /// The allowed party types + /// A list with the filtered parties + public static List FilterPartiesByAllowedPartyTypes( + List? parties, + PartyTypesAllowed? partyTypesAllowed + ) { - private const string BANKRUPTCY_CODE = "KBO"; - private const string SUB_UNIT_CODE = "BEDR"; - private const string SUB_UNIT_CODE_AAFY = "AAFY"; - - /// - /// Filters a list of parties based on an applications allowed party types. - /// - /// The list of parties to be filtered - /// The allowed party types - /// A list with the filtered parties - public static List FilterPartiesByAllowedPartyTypes( - List? parties, - PartyTypesAllowed? partyTypesAllowed - ) + List allowed = new List(); + if (parties == null || partyTypesAllowed == null) { - List allowed = new List(); - if (parties == null || partyTypesAllowed == null) - { - return allowed; - } + return allowed; + } - parties.ForEach(party => + parties.ForEach(party => + { + bool canPartyInstantiate = IsPartyAllowedToInstantiate(party, partyTypesAllowed); + bool isChildPartyAllowed = false; + List? allowedChildParties = null; + if (party.ChildParties != null) { - bool canPartyInstantiate = IsPartyAllowedToInstantiate(party, partyTypesAllowed); - bool isChildPartyAllowed = false; - List? allowedChildParties = null; - if (party.ChildParties != null) + allowedChildParties = new List(); + foreach (Party childParty in party.ChildParties) { - allowedChildParties = new List(); - foreach (Party childParty in party.ChildParties) + if (IsPartyAllowedToInstantiate(childParty, partyTypesAllowed)) { - if (IsPartyAllowedToInstantiate(childParty, partyTypesAllowed)) - { - allowedChildParties.Add(childParty); - isChildPartyAllowed = true; - } + allowedChildParties.Add(childParty); + isChildPartyAllowed = true; } } + } - if (canPartyInstantiate && isChildPartyAllowed) - { - party.ChildParties = new List(); - if (allowedChildParties is null) - { - throw new Exception("List of allowed child parties unexpectedly null"); - } - party.ChildParties.AddRange(allowedChildParties); - allowed.Add(party); - } - else if (!canPartyInstantiate && isChildPartyAllowed) + if (canPartyInstantiate && isChildPartyAllowed) + { + party.ChildParties = new List(); + if (allowedChildParties is null) { - party.ChildParties = new List(); - party.OnlyHierarchyElementWithNoAccess = true; - if (allowedChildParties is null) - { - throw new Exception("List of allowed child parties unexpectedly null"); - } - party.ChildParties.AddRange(allowedChildParties); - allowed.Add(party); + throw new Exception("List of allowed child parties unexpectedly null"); } - else if (canPartyInstantiate) + party.ChildParties.AddRange(allowedChildParties); + allowed.Add(party); + } + else if (!canPartyInstantiate && isChildPartyAllowed) + { + party.ChildParties = new List(); + party.OnlyHierarchyElementWithNoAccess = true; + if (allowedChildParties is null) { - party.ChildParties = new List(); - allowed.Add(party); + throw new Exception("List of allowed child parties unexpectedly null"); } - }); - return allowed; - } - - /// - /// Checks if a party is allowed to initiate an application based on the applications AllowedPartyTypes - /// - /// The party to check - /// The allowed party types - /// True or false - public static bool IsPartyAllowedToInstantiate(Party? party, PartyTypesAllowed? partyTypesAllowed) - { - if (party == null) + party.ChildParties.AddRange(allowedChildParties); + allowed.Add(party); + } + else if (canPartyInstantiate) { - return false; + party.ChildParties = new List(); + allowed.Add(party); } + }); + return allowed; + } - if ( - partyTypesAllowed == null - || ( - !partyTypesAllowed.BankruptcyEstate - && !partyTypesAllowed.Organisation - && !partyTypesAllowed.Person - && !partyTypesAllowed.SubUnit - ) + /// + /// Checks if a party is allowed to initiate an application based on the applications AllowedPartyTypes + /// + /// The party to check + /// The allowed party types + /// True or false + public static bool IsPartyAllowedToInstantiate(Party? party, PartyTypesAllowed? partyTypesAllowed) + { + if (party == null) + { + return false; + } + + if ( + partyTypesAllowed == null + || ( + !partyTypesAllowed.BankruptcyEstate + && !partyTypesAllowed.Organisation + && !partyTypesAllowed.Person + && !partyTypesAllowed.SubUnit ) - { - // if party types not set, all parties are allowed to initiate - return true; - } + ) + { + // if party types not set, all parties are allowed to initiate + return true; + } - PartyType partyType = party.PartyTypeName; - bool isAllowed = false; + PartyType partyType = party.PartyTypeName; + bool isAllowed = false; - bool isSubUnit = - party.UnitType != null - && (SUB_UNIT_CODE.Equals(party.UnitType.Trim()) || SUB_UNIT_CODE_AAFY.Equals(party.UnitType.Trim())); - bool isMainUnit = !isSubUnit; - bool isKbo = party.UnitType != null && BANKRUPTCY_CODE.Equals(party.UnitType.Trim()); + bool isSubUnit = + party.UnitType != null + && (SUB_UNIT_CODE.Equals(party.UnitType.Trim()) || SUB_UNIT_CODE_AAFY.Equals(party.UnitType.Trim())); + bool isMainUnit = !isSubUnit; + bool isKbo = party.UnitType != null && BANKRUPTCY_CODE.Equals(party.UnitType.Trim()); - switch (partyType) - { - case PartyType.Person: - if (partyTypesAllowed.Person) - { - isAllowed = true; - } + switch (partyType) + { + case PartyType.Person: + if (partyTypesAllowed.Person) + { + isAllowed = true; + } - break; - case PartyType.Organisation: + break; + case PartyType.Organisation: - if (isMainUnit && partyTypesAllowed.Organisation) - { - isAllowed = true; - } - else if (isSubUnit && partyTypesAllowed.SubUnit) - { - isAllowed = true; - } - else if (isKbo && partyTypesAllowed.BankruptcyEstate) - { - isAllowed = true; - } + if (isMainUnit && partyTypesAllowed.Organisation) + { + isAllowed = true; + } + else if (isSubUnit && partyTypesAllowed.SubUnit) + { + isAllowed = true; + } + else if (isKbo && partyTypesAllowed.BankruptcyEstate) + { + isAllowed = true; + } - break; - case PartyType.SelfIdentified: - if (partyTypesAllowed.Person) - { - isAllowed = true; - } + break; + case PartyType.SelfIdentified: + if (partyTypesAllowed.Person) + { + isAllowed = true; + } - break; - } + break; + } + + return isAllowed; + } - return isAllowed; + /// + /// Finds a party based on partyId + /// + /// The party list + /// The party id + /// party from the party list + public static Party? GetPartyByPartyId(List? partyList, int partyId) + { + if (partyList == null) + { + return null; } - /// - /// Finds a party based on partyId - /// - /// The party list - /// The party id - /// party from the party list - public static Party? GetPartyByPartyId(List? partyList, int partyId) + Party? validParty = null; + + foreach (Party party in partyList) { - if (partyList == null) + if (party.PartyId == partyId) { - return null; + validParty = party; } - - Party? validParty = null; - - foreach (Party party in partyList) + else if (party.ChildParties != null && party.ChildParties.Count > 0) { - if (party.PartyId == partyId) + Party? validChildParty = party.ChildParties.Find(cp => cp.PartyId == partyId); + if (validChildParty != null) { - validParty = party; - } - else if (party.ChildParties != null && party.ChildParties.Count > 0) - { - Party? validChildParty = party.ChildParties.Find(cp => cp.PartyId == partyId); - if (validChildParty != null) - { - validParty = validChildParty; - } + validParty = validChildParty; } } - - return validParty; } - /// - /// Get the correct object from the object of the entity that should own the instance - /// - public static InstanceOwner PartyToInstanceOwner(Party party) + return validParty; + } + + /// + /// Get the correct object from the object of the entity that should own the instance + /// + public static InstanceOwner PartyToInstanceOwner(Party party) + { + if (!string.IsNullOrEmpty(party.SSN)) { - if (!string.IsNullOrEmpty(party.SSN)) - { - return new() { PartyId = party.PartyId.ToString(), PersonNumber = party.SSN, }; - } - else if (!string.IsNullOrEmpty(party.OrgNumber)) - { - return new() { PartyId = party.PartyId.ToString(), OrganisationNumber = party.OrgNumber, }; - } - else if (party.PartyTypeName.Equals(PartyType.SelfIdentified)) - { - return new() { PartyId = party.PartyId.ToString(), Username = party.Name, }; - } - return new() - { - PartyId = party.PartyId.ToString(), - // instanceOwnerPartyType == "unknown" - }; + return new() { PartyId = party.PartyId.ToString(), PersonNumber = party.SSN, }; } + else if (!string.IsNullOrEmpty(party.OrgNumber)) + { + return new() { PartyId = party.PartyId.ToString(), OrganisationNumber = party.OrgNumber, }; + } + else if (party.PartyTypeName.Equals(PartyType.SelfIdentified)) + { + return new() { PartyId = party.PartyId.ToString(), Username = party.Name, }; + } + return new() + { + PartyId = party.PartyId.ToString(), + // instanceOwnerPartyType == "unknown" + }; } } diff --git a/src/Altinn.App.Core/Helpers/JsonHelper.cs b/src/Altinn.App.Core/Helpers/JsonHelper.cs index 3d369c652..92fa5775e 100644 --- a/src/Altinn.App.Core/Helpers/JsonHelper.cs +++ b/src/Altinn.App.Core/Helpers/JsonHelper.cs @@ -4,223 +4,222 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// Helper class for processing JSON objects +/// +public static class JsonHelper { /// - /// Helper class for processing JSON objects + /// Run DataProcessWrite returning the dictionary of the changed fields. /// - public static class JsonHelper + public static async Task?> ProcessDataWriteWithDiff( + Instance instance, + Guid dataGuid, + object serviceModel, + string? language, + IEnumerable dataProcessors, + ILogger logger + ) { - /// - /// Run DataProcessWrite returning the dictionary of the changed fields. - /// - public static async Task?> ProcessDataWriteWithDiff( - Instance instance, - Guid dataGuid, - object serviceModel, - string? language, - IEnumerable dataProcessors, - ILogger logger - ) + if (!dataProcessors.Any()) { - if (!dataProcessors.Any()) - { - return null; - } - - string serviceModelJsonString = System.Text.Json.JsonSerializer.Serialize(serviceModel); - foreach (var dataProcessor in dataProcessors) - { - logger.LogInformation( - "ProcessDataRead for {modelType} using {dataProcesor}", - serviceModel.GetType().Name, - dataProcessor.GetType().Name - ); - await dataProcessor.ProcessDataWrite(instance, dataGuid, serviceModel, null, language); - } - - string updatedServiceModelString = System.Text.Json.JsonSerializer.Serialize(serviceModel); - try - { - var changed = FindChangedFields(serviceModelJsonString, updatedServiceModelString); - return changed.Count == 0 ? null : changed; - } - catch (Exception e) - { - logger.LogError(e, "Unable to determine changed fields"); - return null; - } + return null; } - /// - /// Find changed fields between old and new json objects - /// - /// The old JSON object - /// The new JSON object - /// Key-value pairs of the changed fields - public static Dictionary FindChangedFields(string oldJson, string currentJson) + string serviceModelJsonString = System.Text.Json.JsonSerializer.Serialize(serviceModel); + foreach (var dataProcessor in dataProcessors) { - JToken old = JToken.Parse(oldJson); - JToken current = JToken.Parse(currentJson); - Dictionary dict = new Dictionary(); - FindDiff(dict, old, current, string.Empty); - return dict; + logger.LogInformation( + "ProcessDataRead for {modelType} using {dataProcesor}", + serviceModel.GetType().Name, + dataProcessor.GetType().Name + ); + await dataProcessor.ProcessDataWrite(instance, dataGuid, serviceModel, null, language); } - private static void FindDiff(Dictionary dict, JToken? old, JToken? current, string prefix) + string updatedServiceModelString = System.Text.Json.JsonSerializer.Serialize(serviceModel); + try + { + var changed = FindChangedFields(serviceModelJsonString, updatedServiceModelString); + return changed.Count == 0 ? null : changed; + } + catch (Exception e) { - if (JToken.DeepEquals(old, current)) - { - return; - } + logger.LogError(e, "Unable to determine changed fields"); + return null; + } + } - int index = 0; - JArray? oldArray = old as JArray; - JObject? currentObj = current as JObject; - JObject? oldObj = old as JObject; + /// + /// Find changed fields between old and new json objects + /// + /// The old JSON object + /// The new JSON object + /// Key-value pairs of the changed fields + public static Dictionary FindChangedFields(string oldJson, string currentJson) + { + JToken old = JToken.Parse(oldJson); + JToken current = JToken.Parse(currentJson); + Dictionary dict = new Dictionary(); + FindDiff(dict, old, current, string.Empty); + return dict; + } - switch (current?.Type) - { - case JTokenType.Object: + private static void FindDiff(Dictionary dict, JToken? old, JToken? current, string prefix) + { + if (JToken.DeepEquals(old, current)) + { + return; + } + + int index = 0; + JArray? oldArray = old as JArray; + JObject? currentObj = current as JObject; + JObject? oldObj = old as JObject; - if (oldArray != null) + switch (current?.Type) + { + case JTokenType.Object: + + if (oldArray != null) + { + for (index = 0; index < oldArray.Count; index++) { - for (index = 0; index < oldArray.Count; index++) - { - dict.Add($"{prefix}[{index}]", null); - } + dict.Add($"{prefix}[{index}]", null); } - else if (old?.Type != JTokenType.Object) + } + else if (old?.Type != JTokenType.Object) + { + // Scalar values would use the plain prefix, but object create deeper prefixes. If a scalar + // value is replaced by an object, we need to unset the scalar value as well. + dict.Add(prefix, null); + } + + if (oldObj == null && currentObj != null) + { + foreach (string key in currentObj.Properties().Select(c => c.Name)) { - // Scalar values would use the plain prefix, but object create deeper prefixes. If a scalar - // value is replaced by an object, we need to unset the scalar value as well. - dict.Add(prefix, null); + FindDiff(dict, JValue.CreateNull(), currentObj[key], Join(prefix, key)); } - if (oldObj == null && currentObj != null) + break; + } + + if (oldObj != null && currentObj != null) + { + IEnumerable addedKeys = currentObj + .Properties() + .Select(c => c.Name) + .Except(oldObj.Properties().Select(c => c.Name)); + IEnumerable removedKeys = oldObj + .Properties() + .Select(c => c.Name) + .Except(currentObj.Properties().Select(c => c.Name)); + IEnumerable unchangedKeys = currentObj + .Properties() + .Where(c => JToken.DeepEquals(c.Value, oldObj[c.Name])) + .Select(c => c.Name); + foreach (string key in addedKeys) { - foreach (string key in currentObj.Properties().Select(c => c.Name)) - { - FindDiff(dict, JValue.CreateNull(), currentObj[key], Join(prefix, key)); - } - - break; + FindDiff(dict, JValue.CreateNull(), currentObj[key], Join(prefix, key)); } - if (oldObj != null && currentObj != null) + foreach (string key in removedKeys) { - IEnumerable addedKeys = currentObj - .Properties() - .Select(c => c.Name) - .Except(oldObj.Properties().Select(c => c.Name)); - IEnumerable removedKeys = oldObj - .Properties() - .Select(c => c.Name) - .Except(currentObj.Properties().Select(c => c.Name)); - IEnumerable unchangedKeys = currentObj - .Properties() - .Where(c => JToken.DeepEquals(c.Value, oldObj[c.Name])) - .Select(c => c.Name); - foreach (string key in addedKeys) - { - FindDiff(dict, JValue.CreateNull(), currentObj[key], Join(prefix, key)); - } - - foreach (string key in removedKeys) - { - FindDiff(dict, oldObj[key], JValue.CreateNull(), Join(prefix, key)); - } - - var potentiallyModifiedKeys = currentObj - .Properties() - .Select(c => c.Name) - .Except(addedKeys) - .Except(unchangedKeys); - foreach (var key in potentiallyModifiedKeys) - { - FindDiff(dict, oldObj[key], currentObj[key], Join(prefix, key)); - } + FindDiff(dict, oldObj[key], JValue.CreateNull(), Join(prefix, key)); } - break; - - case JTokenType.Array: - if (oldArray != null) + var potentiallyModifiedKeys = currentObj + .Properties() + .Select(c => c.Name) + .Except(addedKeys) + .Except(unchangedKeys); + foreach (var key in potentiallyModifiedKeys) { - foreach (var value in current.Children()) - { - FindDiff( - dict, - oldArray?.Count - 1 >= index ? oldArray?[index] : new JObject(), - value, - $"{prefix}[{index}]" - ); - - index++; - } - - while (index < oldArray?.Count) - { - FindDiff(dict, oldArray[index], JValue.CreateNull(), $"{prefix}[{index}]"); - index++; - } + FindDiff(dict, oldObj[key], currentObj[key], Join(prefix, key)); } - else + } + + break; + + case JTokenType.Array: + if (oldArray != null) + { + foreach (var value in current.Children()) { - if (old?.Type == JTokenType.Object) - { - FindDiff(dict, old, JValue.CreateNull(), prefix); - } - - foreach (JToken value in current.Children()) - { - FindDiff(dict, JValue.CreateNull(), value, $"{prefix}[{index}]"); - index++; - } + FindDiff( + dict, + oldArray?.Count - 1 >= index ? oldArray?[index] : new JObject(), + value, + $"{prefix}[{index}]" + ); + + index++; } - break; - - case JTokenType.Null: - if (oldObj != null && old?.Type == JTokenType.Object) + while (index < oldArray?.Count) { - foreach (string key in oldObj.Properties().Select(c => c.Name)) - { - FindDiff(dict, oldObj[key], JValue.CreateNull(), Join(prefix, key)); - } + FindDiff(dict, oldArray[index], JValue.CreateNull(), $"{prefix}[{index}]"); + index++; } - else if (oldArray != null && old?.Type == JTokenType.Array) + } + else + { + if (old?.Type == JTokenType.Object) { - for (index = 0; index < oldArray.Count; index++) - { - FindDiff(dict, oldArray[index], JValue.CreateNull(), $"{prefix}[{index}]"); - } + FindDiff(dict, old, JValue.CreateNull(), prefix); } - else + + foreach (JToken value in current.Children()) { - dict.Add(prefix, ((JValue)current).Value); + FindDiff(dict, JValue.CreateNull(), value, $"{prefix}[{index}]"); + index++; } + } - break; + break; - default: - var convertedValue = (current as JValue)?.Value switch + case JTokenType.Null: + if (oldObj != null && old?.Type == JTokenType.Object) + { + foreach (string key in oldObj.Properties().Select(c => c.Name)) { - // BigInteger is not supported in json, so try to reduce to decimal, if possible, or string if too big - BigInteger bigInt - => bigInt <= new BigInteger(decimal.MaxValue) - ? (decimal)bigInt - : bigInt.ToString(System.Globalization.NumberFormatInfo.InvariantInfo), - _ => (current as JValue)?.Value - }; - dict.Add(prefix, convertedValue); - break; - } + FindDiff(dict, oldObj[key], JValue.CreateNull(), Join(prefix, key)); + } + } + else if (oldArray != null && old?.Type == JTokenType.Array) + { + for (index = 0; index < oldArray.Count; index++) + { + FindDiff(dict, oldArray[index], JValue.CreateNull(), $"{prefix}[{index}]"); + } + } + else + { + dict.Add(prefix, ((JValue)current).Value); + } + + break; + + default: + var convertedValue = (current as JValue)?.Value switch + { + // BigInteger is not supported in json, so try to reduce to decimal, if possible, or string if too big + BigInteger bigInt + => bigInt <= new BigInteger(decimal.MaxValue) + ? (decimal)bigInt + : bigInt.ToString(System.Globalization.NumberFormatInfo.InvariantInfo), + _ => (current as JValue)?.Value + }; + dict.Add(prefix, convertedValue); + break; } + } - private static string Join(string prefix, string name) - { - return string.IsNullOrEmpty(prefix) ? name : prefix + "." + name; - } + private static string Join(string prefix, string name) + { + return string.IsNullOrEmpty(prefix) ? name : prefix + "." + name; } } diff --git a/src/Altinn.App.Core/Helpers/MimeTypeMap.cs b/src/Altinn.App.Core/Helpers/MimeTypeMap.cs index 10f572a69..58a68c7e0 100644 --- a/src/Altinn.App.Core/Helpers/MimeTypeMap.cs +++ b/src/Altinn.App.Core/Helpers/MimeTypeMap.cs @@ -1,646 +1,645 @@ using System.Collections.Frozen; using Altinn.App.Core.Models; -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// MimeTypeMap based on https://github.com/samuelneff/MimeTypeMap +/// +public static class MimeTypeMap { - /// - /// MimeTypeMap based on https://github.com/samuelneff/MimeTypeMap - /// - public static class MimeTypeMap - { - private static readonly Lazy> _mappings = new Lazy< - FrozenDictionary - >(BuildMappings); + private static readonly Lazy> _mappings = new Lazy< + FrozenDictionary + >(BuildMappings); - private static FrozenDictionary BuildMappings() + private static FrozenDictionary BuildMappings() + { + var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase) { - var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - #region Big freaking list of mime types - - { ".323", new("text/h323") }, - { ".3g2", new("video/3gpp2") }, - { ".3gp", new("video/3gpp", "audio/3gpp") }, - { ".3gp2", new("video/3gpp2") }, - { ".3gpp", new("video/3gpp") }, - { ".7z", new("application/x-7z-compressed") }, - { ".aa", new("audio/audible") }, - { ".AAC", new("audio/aac") }, - { ".aaf", new("application/octet-stream") }, - { ".aax", new("audio/vnd.audible.aax") }, - { ".ac3", new("audio/ac3") }, - { ".aca", new("application/octet-stream") }, - { ".accda", new("application/msaccess.addin") }, - { ".accdb", new("application/msaccess") }, - { ".accdc", new("application/msaccess.cab") }, - { ".accde", new("application/msaccess") }, - { ".accdr", new("application/msaccess.runtime") }, - { ".accdt", new("application/msaccess") }, - { ".accdw", new("application/msaccess.webapplication") }, - { ".accft", new("application/msaccess.ftemplate") }, - { ".acx", new("application/internet-property-stream") }, - { ".AddIn", new("text/xml") }, - { ".ade", new("application/msaccess") }, - { ".adobebridge", new("application/x-bridge-url") }, - { ".adp", new("application/msaccess") }, - { ".ADT", new("audio/vnd.dlna.adts") }, - { ".ADTS", new("audio/aac") }, - { ".afm", new("application/octet-stream") }, - { ".ai", new("application/postscript") }, - { ".aif", new("audio/aiff") }, - { ".aifc", new("audio/aiff") }, - { ".aiff", new("audio/aiff") }, - { ".air", new("application/vnd.adobe.air-application-installer-package+zip") }, - { ".amc", new("application/mpeg") }, - { ".anx", new("application/annodex") }, - { ".apk", new("application/vnd.android.package-archive") }, - { ".application", new("application/x-ms-application") }, - { ".art", new("image/x-jg") }, - { ".asa", new("application/xml") }, - { ".asax", new("application/xml") }, - { ".ascx", new("application/xml") }, - { ".asd", new("application/octet-stream") }, - { ".asf", new("video/x-ms-asf") }, - { ".ashx", new("application/xml") }, - { ".asi", new("application/octet-stream") }, - { ".asm", new("text/plain") }, - { ".asmx", new("application/xml") }, - { ".aspx", new("application/xml") }, - { ".asr", new("video/x-ms-asf") }, - { ".asx", new("video/x-ms-asf") }, - { ".atom", new("application/atom+xml") }, - { ".au", new("audio/basic") }, - { ".avi", new("video/x-msvideo") }, - { ".axa", new("audio/annodex") }, - { ".axs", new("application/olescript") }, - { ".axv", new("video/annodex") }, - { ".bas", new("text/plain") }, - { ".bcpio", new("application/x-bcpio") }, - { ".bin", new("application/octet-stream") }, - { ".bmp", new("image/bmp") }, - { ".c", new("text/plain") }, - { ".cab", new("application/octet-stream") }, - { ".caf", new("audio/x-caf") }, - { ".calx", new("application/vnd.ms-office.calx") }, - { ".cat", new("application/vnd.ms-pki.seccat") }, - { ".cc", new("text/plain") }, - { ".cd", new("text/plain") }, - { ".cdda", new("audio/aiff") }, - { ".cdf", new("application/x-cdf") }, - { ".cer", new("application/x-x509-ca-cert") }, - { ".cfg", new("text/plain") }, - { ".chm", new("application/octet-stream") }, - { ".class", new("application/x-java-applet") }, - { ".clp", new("application/x-msclip") }, - { ".cmd", new("text/plain") }, - { ".cmx", new("image/x-cmx") }, - { ".cnf", new("text/plain") }, - { ".cod", new("image/cis-cod") }, - { ".config", new("application/xml") }, - { ".contact", new("text/x-ms-contact") }, - { ".coverage", new("application/xml") }, - { ".cpio", new("application/x-cpio") }, - { ".cpp", new("text/plain") }, - { ".crd", new("application/x-mscardfile") }, - { ".crl", new("application/pkix-crl") }, - { ".crt", new("application/x-x509-ca-cert") }, - { ".cs", new("text/plain") }, - { ".csdproj", new("text/plain") }, - { ".csh", new("application/x-csh") }, - { ".csproj", new("text/plain") }, - { ".css", new("text/css") }, - { ".csv", new("text/csv") }, - { ".cur", new("application/octet-stream") }, - { ".cxx", new("text/plain") }, - { ".dat", new("application/octet-stream") }, - { ".datasource", new("application/xml") }, - { ".dbproj", new("text/plain") }, - { ".dcr", new("application/x-director") }, - { ".def", new("text/plain") }, - { ".deploy", new("application/octet-stream") }, - { ".der", new("application/x-x509-ca-cert") }, - { ".dgml", new("application/xml") }, - { ".dib", new("image/bmp") }, - { ".dif", new("video/x-dv") }, - { ".dir", new("application/x-director") }, - { ".disco", new("text/xml") }, - { ".divx", new("video/divx") }, - { ".dll", new("application/x-msdownload") }, - { ".dll.config", new("text/xml") }, - { ".dlm", new("text/dlm") }, - { ".doc", new("application/msword") }, - { ".docm", new("application/vnd.ms-word.document.macroEnabled.12") }, - { ".docx", new("application/vnd.openxmlformats-officedocument.wordprocessingml.document") }, - { ".dot", new("application/msword") }, - { ".dotm", new("application/vnd.ms-word.template.macroEnabled.12") }, - { ".dotx", new("application/vnd.openxmlformats-officedocument.wordprocessingml.template") }, - { ".dsp", new("application/octet-stream") }, - { ".dsw", new("text/plain") }, - { ".dtd", new("text/xml") }, - { ".dtsConfig", new("text/xml") }, - { ".dv", new("video/x-dv") }, - { ".dvi", new("application/x-dvi") }, - { ".dwf", new("drawing/x-dwf") }, - { ".dwp", new("application/octet-stream") }, - { ".dxr", new("application/x-director") }, - { ".eml", new("message/rfc822") }, - { ".emz", new("application/octet-stream") }, - { ".eot", new("application/vnd.ms-fontobject") }, - { ".eps", new("application/postscript") }, - { ".etl", new("application/etl") }, - { ".etx", new("text/x-setext") }, - { ".evy", new("application/envoy") }, - { ".exe", new("application/octet-stream") }, - { ".exe.config", new("text/xml") }, - { ".fdf", new("application/vnd.fdf") }, - { ".fif", new("application/fractals") }, - { ".filters", new("application/xml") }, - { ".fla", new("application/octet-stream") }, - { ".flac", new("audio/flac") }, - { ".flr", new("x-world/x-vrml") }, - { ".flv", new("video/x-flv") }, - { ".fsscript", new("application/fsharp-script") }, - { ".fsx", new("application/fsharp-script") }, - { ".generictest", new("application/xml") }, - { ".gif", new("image/gif") }, - { ".gml", new("application/gml+xml") }, - { ".gpx", new("application/gpx+xml") }, - { ".group", new("text/x-ms-group") }, - { ".gsm", new("audio/x-gsm") }, - { ".gtar", new("application/x-gtar") }, - { ".gz", new("application/x-gzip") }, - { ".h", new("text/plain") }, - { ".hdf", new("application/x-hdf") }, - { ".hdml", new("text/x-hdml") }, - { ".hhc", new("application/x-oleobject") }, - { ".hhk", new("application/octet-stream") }, - { ".hhp", new("application/octet-stream") }, - { ".hlp", new("application/winhlp") }, - { ".hpp", new("text/plain") }, - { ".hqx", new("application/mac-binhex40") }, - { ".hta", new("application/hta") }, - { ".htc", new("text/x-component") }, - { ".htm", new("text/html") }, - { ".html", new("text/html") }, - { ".htt", new("text/webviewhtml") }, - { ".hxa", new("application/xml") }, - { ".hxc", new("application/xml") }, - { ".hxd", new("application/octet-stream") }, - { ".hxe", new("application/xml") }, - { ".hxf", new("application/xml") }, - { ".hxh", new("application/octet-stream") }, - { ".hxi", new("application/octet-stream") }, - { ".hxk", new("application/xml") }, - { ".hxq", new("application/octet-stream") }, - { ".hxr", new("application/octet-stream") }, - { ".hxs", new("application/octet-stream") }, - { ".hxt", new("text/html") }, - { ".hxv", new("application/xml") }, - { ".hxw", new("application/octet-stream") }, - { ".hxx", new("text/plain") }, - { ".i", new("text/plain") }, - { ".ico", new("image/x-icon") }, - { ".ics", new("application/octet-stream") }, - { ".idl", new("text/plain") }, - { ".ief", new("image/ief") }, - { ".iii", new("application/x-iphone") }, - { ".inc", new("text/plain") }, - { ".inf", new("application/octet-stream") }, - { ".ini", new("text/plain") }, - { ".inl", new("text/plain") }, - { ".ins", new("application/x-internet-signup") }, - { ".ipa", new("application/x-itunes-ipa") }, - { ".ipg", new("application/x-itunes-ipg") }, - { ".ipproj", new("text/plain") }, - { ".ipsw", new("application/x-itunes-ipsw") }, - { ".iqy", new("text/x-ms-iqy") }, - { ".isp", new("application/x-internet-signup") }, - { ".ite", new("application/x-itunes-ite") }, - { ".itlp", new("application/x-itunes-itlp") }, - { ".itms", new("application/x-itunes-itms") }, - { ".itpc", new("application/x-itunes-itpc") }, - { ".IVF", new("video/x-ivf") }, - { ".jar", new("application/java-archive") }, - { ".java", new("application/octet-stream") }, - { ".jck", new("application/liquidmotion") }, - { ".jcz", new("application/liquidmotion") }, - { ".jfif", new("image/pjpeg") }, - { ".jnlp", new("application/x-java-jnlp-file") }, - { ".jpb", new("application/octet-stream") }, - { ".jpe", new("image/jpeg") }, - { ".jpeg", new("image/jpeg") }, - { ".jpg", new("image/jpeg") }, - { ".js", new("application/javascript") }, - { ".json", new("application/json") }, - { ".jsx", new("text/jscript") }, - { ".jsxbin", new("text/plain") }, - { ".latex", new("application/x-latex") }, - { ".library-ms", new("application/windows-library+xml") }, - { ".lit", new("application/x-ms-reader") }, - { ".loadtest", new("application/xml") }, - { ".lpk", new("application/octet-stream") }, - { ".lsf", new("video/x-la-asf") }, - { ".lst", new("text/plain") }, - { ".lsx", new("video/x-la-asf") }, - { ".lzh", new("application/octet-stream") }, - { ".m13", new("application/x-msmediaview") }, - { ".m14", new("application/x-msmediaview") }, - { ".m1v", new("video/mpeg") }, - { ".m2t", new("video/vnd.dlna.mpeg-tts") }, - { ".m2ts", new("video/vnd.dlna.mpeg-tts") }, - { ".m2v", new("video/mpeg") }, - { ".m3u", new("audio/x-mpegurl") }, - { ".m3u8", new("audio/x-mpegurl") }, - { ".m4a", new("audio/m4a") }, - { ".m4b", new("audio/m4b") }, - { ".m4p", new("audio/m4p") }, - { ".m4r", new("audio/x-m4r") }, - { ".m4v", new("video/x-m4v") }, - { ".mac", new("image/x-macpaint") }, - { ".mak", new("text/plain") }, - { ".man", new("application/x-troff-man") }, - { ".manifest", new("application/x-ms-manifest") }, - { ".map", new("text/plain") }, - { ".master", new("application/xml") }, - { ".mda", new("application/msaccess") }, - { ".mdb", new("application/x-msaccess") }, - { ".mde", new("application/msaccess") }, - { ".mdp", new("application/octet-stream") }, - { ".me", new("application/x-troff-me") }, - { ".mfp", new("application/x-shockwave-flash") }, - { ".mht", new("message/rfc822") }, - { ".mhtml", new("message/rfc822") }, - { ".mid", new("audio/mid") }, - { ".midi", new("audio/mid") }, - { ".mix", new("application/octet-stream") }, - { ".mk", new("text/plain") }, - { ".mmf", new("application/x-smaf") }, - { ".mno", new("text/xml") }, - { ".mny", new("application/x-msmoney") }, - { ".mod", new("video/mpeg") }, - { ".mov", new("video/quicktime") }, - { ".movie", new("video/x-sgi-movie") }, - { ".mp2", new("video/mpeg") }, - { ".mp2v", new("video/mpeg") }, - { ".mp3", new("audio/mpeg") }, - { ".mp4", new("video/mp4") }, - { ".mp4v", new("video/mp4") }, - { ".mpa", new("video/mpeg") }, - { ".mpe", new("video/mpeg") }, - { ".mpeg", new("video/mpeg") }, - { ".mpf", new("application/vnd.ms-mediapackage") }, - { ".mpg", new("video/mpeg") }, - { ".mpp", new("application/vnd.ms-project") }, - { ".mpv2", new("video/mpeg") }, - { ".mqv", new("video/quicktime") }, - { ".ms", new("application/x-troff-ms") }, - { ".msi", new("application/octet-stream") }, - { ".mso", new("application/octet-stream") }, - { ".mts", new("video/vnd.dlna.mpeg-tts") }, - { ".mtx", new("application/xml") }, - { ".mvb", new("application/x-msmediaview") }, - { ".mvc", new("application/x-miva-compiled") }, - { ".mxp", new("application/x-mmxp") }, - { ".nc", new("application/x-netcdf") }, - { ".nsc", new("video/x-ms-asf") }, - { ".nws", new("message/rfc822") }, - { ".ocx", new("application/octet-stream") }, - { ".oda", new("application/oda") }, - { ".odb", new("application/vnd.oasis.opendocument.database") }, - { ".odc", new("application/vnd.oasis.opendocument.chart") }, - { ".odf", new("application/vnd.oasis.opendocument.formula") }, - { ".odg", new("application/vnd.oasis.opendocument.graphics") }, - { ".odh", new("text/plain") }, - { ".odi", new("application/vnd.oasis.opendocument.image") }, - { ".odl", new("text/plain") }, - { ".odm", new("application/vnd.oasis.opendocument.text-master") }, - { ".odp", new("application/vnd.oasis.opendocument.presentation") }, - { ".ods", new("application/vnd.oasis.opendocument.spreadsheet") }, - { ".odt", new("application/vnd.oasis.opendocument.text") }, - { ".oga", new("audio/ogg") }, - { ".ogg", new("audio/ogg") }, - { ".ogv", new("video/ogg") }, - { ".ogx", new("application/ogg") }, - { ".one", new("application/onenote") }, - { ".onea", new("application/onenote") }, - { ".onepkg", new("application/onenote") }, - { ".onetmp", new("application/onenote") }, - { ".onetoc", new("application/onenote") }, - { ".onetoc2", new("application/onenote") }, - { ".opus", new("audio/ogg") }, - { ".orderedtest", new("application/xml") }, - { ".osdx", new("application/opensearchdescription+xml") }, - { ".otf", new("application/font-sfnt") }, - { ".otg", new("application/vnd.oasis.opendocument.graphics-template") }, - { ".oth", new("application/vnd.oasis.opendocument.text-web") }, - { ".otp", new("application/vnd.oasis.opendocument.presentation-template") }, - { ".ots", new("application/vnd.oasis.opendocument.spreadsheet-template") }, - { ".ott", new("application/vnd.oasis.opendocument.text-template") }, - { ".oxt", new("application/vnd.openofficeorg.extension") }, - { ".p10", new("application/pkcs10") }, - { ".p12", new("application/x-pkcs12") }, - { ".p7b", new("application/x-pkcs7-certificates") }, - { ".p7c", new("application/pkcs7-mime") }, - { ".p7m", new("application/pkcs7-mime") }, - { ".p7r", new("application/x-pkcs7-certreqresp") }, - { ".p7s", new("application/pkcs7-signature") }, - { ".pbm", new("image/x-portable-bitmap") }, - { ".pcast", new("application/x-podcast") }, - { ".pct", new("image/pict") }, - { ".pcx", new("application/octet-stream") }, - { ".pcz", new("application/octet-stream") }, - { ".pdf", new("application/pdf") }, - { ".pfb", new("application/octet-stream") }, - { ".pfm", new("application/octet-stream") }, - { ".pfx", new("application/x-pkcs12") }, - { ".pgm", new("image/x-portable-graymap") }, - { ".pic", new("image/pict") }, - { ".pict", new("image/pict") }, - { ".pkgdef", new("text/plain") }, - { ".pkgundef", new("text/plain") }, - { ".pko", new("application/vnd.ms-pki.pko") }, - { ".pls", new("audio/scpls") }, - { ".pma", new("application/x-perfmon") }, - { ".pmc", new("application/x-perfmon") }, - { ".pml", new("application/x-perfmon") }, - { ".pmr", new("application/x-perfmon") }, - { ".pmw", new("application/x-perfmon") }, - { ".png", new("image/png") }, - { ".pnm", new("image/x-portable-anymap") }, - { ".pnt", new("image/x-macpaint") }, - { ".pntg", new("image/x-macpaint") }, - { ".pnz", new("image/png") }, - { ".pot", new("application/vnd.ms-powerpoint") }, - { ".potm", new("application/vnd.ms-powerpoint.template.macroEnabled.12") }, - { ".potx", new("application/vnd.openxmlformats-officedocument.presentationml.template") }, - { ".ppa", new("application/vnd.ms-powerpoint") }, - { ".ppam", new("application/vnd.ms-powerpoint.addin.macroEnabled.12") }, - { ".ppm", new("image/x-portable-pixmap") }, - { ".pps", new("application/vnd.ms-powerpoint") }, - { ".ppsm", new("application/vnd.ms-powerpoint.slideshow.macroEnabled.12") }, - { ".ppsx", new("application/vnd.openxmlformats-officedocument.presentationml.slideshow") }, - { ".ppt", new("application/vnd.ms-powerpoint") }, - { ".pptm", new("application/vnd.ms-powerpoint.presentation.macroEnabled.12") }, - { ".pptx", new("application/vnd.openxmlformats-officedocument.presentationml.presentation") }, - { ".prf", new("application/pics-rules") }, - { ".prm", new("application/octet-stream") }, - { ".prx", new("application/octet-stream") }, - { ".ps", new("application/postscript") }, - { ".psc1", new("application/PowerShell") }, - { ".psd", new("application/octet-stream") }, - { ".psess", new("application/xml") }, - { ".psm", new("application/octet-stream") }, - { ".psp", new("application/octet-stream") }, - { ".pub", new("application/x-mspublisher") }, - { ".pwz", new("application/vnd.ms-powerpoint") }, - { ".qht", new("text/x-html-insertion") }, - { ".qhtm", new("text/x-html-insertion") }, - { ".qt", new("video/quicktime") }, - { ".qti", new("image/x-quicktime") }, - { ".qtif", new("image/x-quicktime") }, - { ".qtl", new("application/x-quicktimeplayer") }, - { ".qxd", new("application/octet-stream") }, - { ".ra", new("audio/x-pn-realaudio") }, - { ".ram", new("audio/x-pn-realaudio") }, - { ".rar", new("application/x-rar-compressed") }, - { ".ras", new("image/x-cmu-raster") }, - { ".rat", new("application/rat-file") }, - { ".rc", new("text/plain") }, - { ".rc2", new("text/plain") }, - { ".rct", new("text/plain") }, - { ".rdlc", new("application/xml") }, - { ".reg", new("text/plain") }, - { ".resx", new("application/xml") }, - { ".rf", new("image/vnd.rn-realflash") }, - { ".rgb", new("image/x-rgb") }, - { ".rgs", new("text/plain") }, - { ".rm", new("application/vnd.rn-realmedia") }, - { ".rmi", new("audio/mid") }, - { ".rmp", new("application/vnd.rn-rn_music_package") }, - { ".roff", new("application/x-troff") }, - { ".rpm", new("audio/x-pn-realaudio-plugin") }, - { ".rqy", new("text/x-ms-rqy") }, - { ".rtf", new("application/rtf") }, - { ".rtx", new("text/richtext") }, - { ".ruleset", new("application/xml") }, - { ".s", new("text/plain") }, - { ".safariextz", new("application/x-safari-safariextz") }, - { ".scd", new("application/x-msschedule") }, - { ".scr", new("text/plain") }, - { ".sct", new("text/scriptlet") }, - { ".sd2", new("audio/x-sd2") }, - { ".sdp", new("application/sdp") }, - { ".sea", new("application/octet-stream") }, - { ".searchConnector-ms", new("application/windows-search-connector+xml") }, - { ".setpay", new("application/set-payment-initiation") }, - { ".setreg", new("application/set-registration-initiation") }, - { ".settings", new("application/xml") }, - { ".sgimb", new("application/x-sgimb") }, - { ".sgml", new("text/sgml") }, - { ".sh", new("application/x-sh") }, - { ".shar", new("application/x-shar") }, - { ".shtml", new("text/html") }, - { ".sit", new("application/x-stuffit") }, - { ".sitemap", new("application/xml") }, - { ".skin", new("application/xml") }, - { ".sldm", new("application/vnd.ms-powerpoint.slide.macroEnabled.12") }, - { ".sldx", new("application/vnd.openxmlformats-officedocument.presentationml.slide") }, - { ".slk", new("application/vnd.ms-excel") }, - { ".sln", new("text/plain") }, - { ".slupkg-ms", new("application/x-ms-license") }, - { ".smd", new("audio/x-smd") }, - { ".smi", new("application/octet-stream") }, - { ".smx", new("audio/x-smd") }, - { ".smz", new("audio/x-smd") }, - { ".snd", new("audio/basic") }, - { ".snippet", new("application/xml") }, - { ".snp", new("application/octet-stream") }, - { ".sol", new("text/plain") }, - { ".sor", new("text/plain") }, - { ".sos", new("text/vnd.sosi") }, - { ".spc", new("application/x-pkcs7-certificates") }, - { ".spl", new("application/futuresplash") }, - { ".spx", new("audio/ogg") }, - { ".src", new("application/x-wais-source") }, - { ".srf", new("text/plain") }, - { ".SSISDeploymentManifest", new("text/xml") }, - { ".ssm", new("application/streamingmedia") }, - { ".sst", new("application/vnd.ms-pki.certstore") }, - { ".stl", new("application/vnd.ms-pki.stl") }, - { ".sv4cpio", new("application/x-sv4cpio") }, - { ".sv4crc", new("application/x-sv4crc") }, - { ".svc", new("application/xml") }, - { ".svg", new("image/svg+xml") }, - { ".swf", new("application/x-shockwave-flash") }, - { ".step", new("application/step") }, - { ".stp", new("application/step") }, - { ".t", new("application/x-troff") }, - { ".tar", new("application/x-tar") }, - { ".tcl", new("application/x-tcl") }, - { ".testrunconfig", new("application/xml") }, - { ".testsettings", new("application/xml") }, - { ".tex", new("application/x-tex") }, - { ".texi", new("application/x-texinfo") }, - { ".texinfo", new("application/x-texinfo") }, - { ".tgz", new("application/x-compressed") }, - { ".thmx", new("application/vnd.ms-officetheme") }, - { ".thn", new("application/octet-stream") }, - { ".tif", new("image/tiff") }, - { ".tiff", new("image/tiff") }, - { ".tlh", new("text/plain") }, - { ".tli", new("text/plain") }, - { ".toc", new("application/octet-stream") }, - { ".tr", new("application/x-troff") }, - { ".trm", new("application/x-msterminal") }, - { ".trx", new("application/xml") }, - { ".ts", new("video/vnd.dlna.mpeg-tts") }, - { ".tsv", new("text/tab-separated-values") }, - { ".ttf", new("application/font-sfnt") }, - { ".tts", new("video/vnd.dlna.mpeg-tts") }, - { ".txt", new("text/plain") }, - { ".u32", new("application/octet-stream") }, - { ".uls", new("text/iuls") }, - { ".user", new("text/plain") }, - { ".ustar", new("application/x-ustar") }, - { ".vb", new("text/plain") }, - { ".vbdproj", new("text/plain") }, - { ".vbk", new("video/mpeg") }, - { ".vbproj", new("text/plain") }, - { ".vbs", new("text/vbscript") }, - { ".vcf", new("text/x-vcard") }, - { ".vcproj", new("application/xml") }, - { ".vcs", new("text/plain") }, - { ".vcxproj", new("application/xml") }, - { ".vddproj", new("text/plain") }, - { ".vdp", new("text/plain") }, - { ".vdproj", new("text/plain") }, - { ".vdx", new("application/vnd.ms-visio.viewer") }, - { ".vml", new("text/xml") }, - { ".vscontent", new("application/xml") }, - { ".vsct", new("text/xml") }, - { ".vsd", new("application/vnd.visio") }, - { ".vsi", new("application/ms-vsi") }, - { ".vsix", new("application/vsix") }, - { ".vsixlangpack", new("text/xml") }, - { ".vsixmanifest", new("text/xml") }, - { ".vsmdi", new("application/xml") }, - { ".vspscc", new("text/plain") }, - { ".vss", new("application/vnd.visio") }, - { ".vsscc", new("text/plain") }, - { ".vssettings", new("text/xml") }, - { ".vssscc", new("text/plain") }, - { ".vst", new("application/vnd.visio") }, - { ".vstemplate", new("text/xml") }, - { ".vsto", new("application/x-ms-vsto") }, - { ".vsw", new("application/vnd.visio") }, - { ".vsx", new("application/vnd.visio") }, - { ".vtx", new("application/vnd.visio") }, - { ".wav", new("audio/wav") }, - { ".wave", new("audio/wav") }, - { ".wax", new("audio/x-ms-wax") }, - { ".wbk", new("application/msword") }, - { ".wbmp", new("image/vnd.wap.wbmp") }, - { ".wcm", new("application/vnd.ms-works") }, - { ".wdb", new("application/vnd.ms-works") }, - { ".wdp", new("image/vnd.ms-photo") }, - { ".webarchive", new("application/x-safari-webarchive") }, - { ".webm", new("video/webm") }, - { ".webp", new("image/webp") }, /* https://en.wikipedia.org/wiki/WebP */ - { ".webtest", new("application/xml") }, - { ".wiq", new("application/xml") }, - { ".wiz", new("application/msword") }, - { ".wks", new("application/vnd.ms-works") }, - { ".WLMP", new("application/wlmoviemaker") }, - { ".wlpginstall", new("application/x-wlpg-detect") }, - { ".wlpginstall3", new("application/x-wlpg3-detect") }, - { ".wm", new("video/x-ms-wm") }, - { ".wma", new("audio/x-ms-wma") }, - { ".wmd", new("application/x-ms-wmd") }, - { ".wmf", new("application/x-msmetafile") }, - { ".wml", new("text/vnd.wap.wml") }, - { ".wmlc", new("application/vnd.wap.wmlc") }, - { ".wmls", new("text/vnd.wap.wmlscript") }, - { ".wmlsc", new("application/vnd.wap.wmlscriptc") }, - { ".wmp", new("video/x-ms-wmp") }, - { ".wmv", new("video/x-ms-wmv") }, - { ".wmx", new("video/x-ms-wmx") }, - { ".wmz", new("application/x-ms-wmz") }, - { ".woff", new("application/font-woff") }, - { ".wpl", new("application/vnd.ms-wpl") }, - { ".wps", new("application/vnd.ms-works") }, - { ".wri", new("application/x-mswrite") }, - { ".wrl", new("x-world/x-vrml") }, - { ".wrz", new("x-world/x-vrml") }, - { ".wsc", new("text/scriptlet") }, - { ".wsdl", new("text/xml") }, - { ".wvx", new("video/x-ms-wvx") }, - { ".x", new("application/directx") }, - { ".xaf", new("x-world/x-vrml") }, - { ".xaml", new("application/xaml+xml") }, - { ".xap", new("application/x-silverlight-app") }, - { ".xbap", new("application/x-ms-xbap") }, - { ".xbm", new("image/x-xbitmap") }, - { ".xdr", new("text/plain") }, - { ".xht", new("application/xhtml+xml") }, - { ".xhtml", new("application/xhtml+xml") }, - { ".xla", new("application/vnd.ms-excel") }, - { ".xlam", new("application/vnd.ms-excel.addin.macroEnabled.12") }, - { ".xlc", new("application/vnd.ms-excel") }, - { ".xld", new("application/vnd.ms-excel") }, - { ".xlk", new("application/vnd.ms-excel") }, - { ".xll", new("application/vnd.ms-excel") }, - { ".xlm", new("application/vnd.ms-excel") }, - { ".xls", new("application/vnd.ms-excel") }, - { ".xlsb", new("application/vnd.ms-excel.sheet.binary.macroEnabled.12") }, - { ".xlsm", new("application/vnd.ms-excel.sheet.macroEnabled.12") }, - { ".xlsx", new("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") }, - { ".xlt", new("application/vnd.ms-excel") }, - { ".xltm", new("application/vnd.ms-excel.template.macroEnabled.12") }, - { ".xltx", new("application/vnd.openxmlformats-officedocument.spreadsheetml.template") }, - { ".xlw", new("application/vnd.ms-excel") }, - { ".xml", new("application/xml", "text/xml") }, - { ".xmta", new("application/xml") }, - { ".xof", new("x-world/x-vrml") }, - { ".XOML", new("text/plain") }, - { ".xpm", new("image/x-xpixmap") }, - { ".xps", new("application/vnd.ms-xpsdocument") }, - { ".xrm-ms", new("text/xml") }, - { ".xsc", new("application/xml") }, - { ".xsd", new("text/xml") }, - { ".xsf", new("text/xml") }, - { ".xsl", new("text/xml") }, - { ".xslt", new("text/xml") }, - { ".xsn", new("application/octet-stream") }, - { ".xss", new("application/xml") }, - { ".xspf", new("application/xspf+xml") }, - { ".xtp", new("application/octet-stream") }, - { ".xwd", new("image/x-xwindowdump") }, - { ".z", new("application/x-compress") }, - { ".zip", new("application/zip", "application/x-zip-compressed", "application/zip-compressed") } + #region Big freaking list of mime types - #endregion - }; + { ".323", new("text/h323") }, + { ".3g2", new("video/3gpp2") }, + { ".3gp", new("video/3gpp", "audio/3gpp") }, + { ".3gp2", new("video/3gpp2") }, + { ".3gpp", new("video/3gpp") }, + { ".7z", new("application/x-7z-compressed") }, + { ".aa", new("audio/audible") }, + { ".AAC", new("audio/aac") }, + { ".aaf", new("application/octet-stream") }, + { ".aax", new("audio/vnd.audible.aax") }, + { ".ac3", new("audio/ac3") }, + { ".aca", new("application/octet-stream") }, + { ".accda", new("application/msaccess.addin") }, + { ".accdb", new("application/msaccess") }, + { ".accdc", new("application/msaccess.cab") }, + { ".accde", new("application/msaccess") }, + { ".accdr", new("application/msaccess.runtime") }, + { ".accdt", new("application/msaccess") }, + { ".accdw", new("application/msaccess.webapplication") }, + { ".accft", new("application/msaccess.ftemplate") }, + { ".acx", new("application/internet-property-stream") }, + { ".AddIn", new("text/xml") }, + { ".ade", new("application/msaccess") }, + { ".adobebridge", new("application/x-bridge-url") }, + { ".adp", new("application/msaccess") }, + { ".ADT", new("audio/vnd.dlna.adts") }, + { ".ADTS", new("audio/aac") }, + { ".afm", new("application/octet-stream") }, + { ".ai", new("application/postscript") }, + { ".aif", new("audio/aiff") }, + { ".aifc", new("audio/aiff") }, + { ".aiff", new("audio/aiff") }, + { ".air", new("application/vnd.adobe.air-application-installer-package+zip") }, + { ".amc", new("application/mpeg") }, + { ".anx", new("application/annodex") }, + { ".apk", new("application/vnd.android.package-archive") }, + { ".application", new("application/x-ms-application") }, + { ".art", new("image/x-jg") }, + { ".asa", new("application/xml") }, + { ".asax", new("application/xml") }, + { ".ascx", new("application/xml") }, + { ".asd", new("application/octet-stream") }, + { ".asf", new("video/x-ms-asf") }, + { ".ashx", new("application/xml") }, + { ".asi", new("application/octet-stream") }, + { ".asm", new("text/plain") }, + { ".asmx", new("application/xml") }, + { ".aspx", new("application/xml") }, + { ".asr", new("video/x-ms-asf") }, + { ".asx", new("video/x-ms-asf") }, + { ".atom", new("application/atom+xml") }, + { ".au", new("audio/basic") }, + { ".avi", new("video/x-msvideo") }, + { ".axa", new("audio/annodex") }, + { ".axs", new("application/olescript") }, + { ".axv", new("video/annodex") }, + { ".bas", new("text/plain") }, + { ".bcpio", new("application/x-bcpio") }, + { ".bin", new("application/octet-stream") }, + { ".bmp", new("image/bmp") }, + { ".c", new("text/plain") }, + { ".cab", new("application/octet-stream") }, + { ".caf", new("audio/x-caf") }, + { ".calx", new("application/vnd.ms-office.calx") }, + { ".cat", new("application/vnd.ms-pki.seccat") }, + { ".cc", new("text/plain") }, + { ".cd", new("text/plain") }, + { ".cdda", new("audio/aiff") }, + { ".cdf", new("application/x-cdf") }, + { ".cer", new("application/x-x509-ca-cert") }, + { ".cfg", new("text/plain") }, + { ".chm", new("application/octet-stream") }, + { ".class", new("application/x-java-applet") }, + { ".clp", new("application/x-msclip") }, + { ".cmd", new("text/plain") }, + { ".cmx", new("image/x-cmx") }, + { ".cnf", new("text/plain") }, + { ".cod", new("image/cis-cod") }, + { ".config", new("application/xml") }, + { ".contact", new("text/x-ms-contact") }, + { ".coverage", new("application/xml") }, + { ".cpio", new("application/x-cpio") }, + { ".cpp", new("text/plain") }, + { ".crd", new("application/x-mscardfile") }, + { ".crl", new("application/pkix-crl") }, + { ".crt", new("application/x-x509-ca-cert") }, + { ".cs", new("text/plain") }, + { ".csdproj", new("text/plain") }, + { ".csh", new("application/x-csh") }, + { ".csproj", new("text/plain") }, + { ".css", new("text/css") }, + { ".csv", new("text/csv") }, + { ".cur", new("application/octet-stream") }, + { ".cxx", new("text/plain") }, + { ".dat", new("application/octet-stream") }, + { ".datasource", new("application/xml") }, + { ".dbproj", new("text/plain") }, + { ".dcr", new("application/x-director") }, + { ".def", new("text/plain") }, + { ".deploy", new("application/octet-stream") }, + { ".der", new("application/x-x509-ca-cert") }, + { ".dgml", new("application/xml") }, + { ".dib", new("image/bmp") }, + { ".dif", new("video/x-dv") }, + { ".dir", new("application/x-director") }, + { ".disco", new("text/xml") }, + { ".divx", new("video/divx") }, + { ".dll", new("application/x-msdownload") }, + { ".dll.config", new("text/xml") }, + { ".dlm", new("text/dlm") }, + { ".doc", new("application/msword") }, + { ".docm", new("application/vnd.ms-word.document.macroEnabled.12") }, + { ".docx", new("application/vnd.openxmlformats-officedocument.wordprocessingml.document") }, + { ".dot", new("application/msword") }, + { ".dotm", new("application/vnd.ms-word.template.macroEnabled.12") }, + { ".dotx", new("application/vnd.openxmlformats-officedocument.wordprocessingml.template") }, + { ".dsp", new("application/octet-stream") }, + { ".dsw", new("text/plain") }, + { ".dtd", new("text/xml") }, + { ".dtsConfig", new("text/xml") }, + { ".dv", new("video/x-dv") }, + { ".dvi", new("application/x-dvi") }, + { ".dwf", new("drawing/x-dwf") }, + { ".dwp", new("application/octet-stream") }, + { ".dxr", new("application/x-director") }, + { ".eml", new("message/rfc822") }, + { ".emz", new("application/octet-stream") }, + { ".eot", new("application/vnd.ms-fontobject") }, + { ".eps", new("application/postscript") }, + { ".etl", new("application/etl") }, + { ".etx", new("text/x-setext") }, + { ".evy", new("application/envoy") }, + { ".exe", new("application/octet-stream") }, + { ".exe.config", new("text/xml") }, + { ".fdf", new("application/vnd.fdf") }, + { ".fif", new("application/fractals") }, + { ".filters", new("application/xml") }, + { ".fla", new("application/octet-stream") }, + { ".flac", new("audio/flac") }, + { ".flr", new("x-world/x-vrml") }, + { ".flv", new("video/x-flv") }, + { ".fsscript", new("application/fsharp-script") }, + { ".fsx", new("application/fsharp-script") }, + { ".generictest", new("application/xml") }, + { ".gif", new("image/gif") }, + { ".gml", new("application/gml+xml") }, + { ".gpx", new("application/gpx+xml") }, + { ".group", new("text/x-ms-group") }, + { ".gsm", new("audio/x-gsm") }, + { ".gtar", new("application/x-gtar") }, + { ".gz", new("application/x-gzip") }, + { ".h", new("text/plain") }, + { ".hdf", new("application/x-hdf") }, + { ".hdml", new("text/x-hdml") }, + { ".hhc", new("application/x-oleobject") }, + { ".hhk", new("application/octet-stream") }, + { ".hhp", new("application/octet-stream") }, + { ".hlp", new("application/winhlp") }, + { ".hpp", new("text/plain") }, + { ".hqx", new("application/mac-binhex40") }, + { ".hta", new("application/hta") }, + { ".htc", new("text/x-component") }, + { ".htm", new("text/html") }, + { ".html", new("text/html") }, + { ".htt", new("text/webviewhtml") }, + { ".hxa", new("application/xml") }, + { ".hxc", new("application/xml") }, + { ".hxd", new("application/octet-stream") }, + { ".hxe", new("application/xml") }, + { ".hxf", new("application/xml") }, + { ".hxh", new("application/octet-stream") }, + { ".hxi", new("application/octet-stream") }, + { ".hxk", new("application/xml") }, + { ".hxq", new("application/octet-stream") }, + { ".hxr", new("application/octet-stream") }, + { ".hxs", new("application/octet-stream") }, + { ".hxt", new("text/html") }, + { ".hxv", new("application/xml") }, + { ".hxw", new("application/octet-stream") }, + { ".hxx", new("text/plain") }, + { ".i", new("text/plain") }, + { ".ico", new("image/x-icon") }, + { ".ics", new("application/octet-stream") }, + { ".idl", new("text/plain") }, + { ".ief", new("image/ief") }, + { ".iii", new("application/x-iphone") }, + { ".inc", new("text/plain") }, + { ".inf", new("application/octet-stream") }, + { ".ini", new("text/plain") }, + { ".inl", new("text/plain") }, + { ".ins", new("application/x-internet-signup") }, + { ".ipa", new("application/x-itunes-ipa") }, + { ".ipg", new("application/x-itunes-ipg") }, + { ".ipproj", new("text/plain") }, + { ".ipsw", new("application/x-itunes-ipsw") }, + { ".iqy", new("text/x-ms-iqy") }, + { ".isp", new("application/x-internet-signup") }, + { ".ite", new("application/x-itunes-ite") }, + { ".itlp", new("application/x-itunes-itlp") }, + { ".itms", new("application/x-itunes-itms") }, + { ".itpc", new("application/x-itunes-itpc") }, + { ".IVF", new("video/x-ivf") }, + { ".jar", new("application/java-archive") }, + { ".java", new("application/octet-stream") }, + { ".jck", new("application/liquidmotion") }, + { ".jcz", new("application/liquidmotion") }, + { ".jfif", new("image/pjpeg") }, + { ".jnlp", new("application/x-java-jnlp-file") }, + { ".jpb", new("application/octet-stream") }, + { ".jpe", new("image/jpeg") }, + { ".jpeg", new("image/jpeg") }, + { ".jpg", new("image/jpeg") }, + { ".js", new("application/javascript") }, + { ".json", new("application/json") }, + { ".jsx", new("text/jscript") }, + { ".jsxbin", new("text/plain") }, + { ".latex", new("application/x-latex") }, + { ".library-ms", new("application/windows-library+xml") }, + { ".lit", new("application/x-ms-reader") }, + { ".loadtest", new("application/xml") }, + { ".lpk", new("application/octet-stream") }, + { ".lsf", new("video/x-la-asf") }, + { ".lst", new("text/plain") }, + { ".lsx", new("video/x-la-asf") }, + { ".lzh", new("application/octet-stream") }, + { ".m13", new("application/x-msmediaview") }, + { ".m14", new("application/x-msmediaview") }, + { ".m1v", new("video/mpeg") }, + { ".m2t", new("video/vnd.dlna.mpeg-tts") }, + { ".m2ts", new("video/vnd.dlna.mpeg-tts") }, + { ".m2v", new("video/mpeg") }, + { ".m3u", new("audio/x-mpegurl") }, + { ".m3u8", new("audio/x-mpegurl") }, + { ".m4a", new("audio/m4a") }, + { ".m4b", new("audio/m4b") }, + { ".m4p", new("audio/m4p") }, + { ".m4r", new("audio/x-m4r") }, + { ".m4v", new("video/x-m4v") }, + { ".mac", new("image/x-macpaint") }, + { ".mak", new("text/plain") }, + { ".man", new("application/x-troff-man") }, + { ".manifest", new("application/x-ms-manifest") }, + { ".map", new("text/plain") }, + { ".master", new("application/xml") }, + { ".mda", new("application/msaccess") }, + { ".mdb", new("application/x-msaccess") }, + { ".mde", new("application/msaccess") }, + { ".mdp", new("application/octet-stream") }, + { ".me", new("application/x-troff-me") }, + { ".mfp", new("application/x-shockwave-flash") }, + { ".mht", new("message/rfc822") }, + { ".mhtml", new("message/rfc822") }, + { ".mid", new("audio/mid") }, + { ".midi", new("audio/mid") }, + { ".mix", new("application/octet-stream") }, + { ".mk", new("text/plain") }, + { ".mmf", new("application/x-smaf") }, + { ".mno", new("text/xml") }, + { ".mny", new("application/x-msmoney") }, + { ".mod", new("video/mpeg") }, + { ".mov", new("video/quicktime") }, + { ".movie", new("video/x-sgi-movie") }, + { ".mp2", new("video/mpeg") }, + { ".mp2v", new("video/mpeg") }, + { ".mp3", new("audio/mpeg") }, + { ".mp4", new("video/mp4") }, + { ".mp4v", new("video/mp4") }, + { ".mpa", new("video/mpeg") }, + { ".mpe", new("video/mpeg") }, + { ".mpeg", new("video/mpeg") }, + { ".mpf", new("application/vnd.ms-mediapackage") }, + { ".mpg", new("video/mpeg") }, + { ".mpp", new("application/vnd.ms-project") }, + { ".mpv2", new("video/mpeg") }, + { ".mqv", new("video/quicktime") }, + { ".ms", new("application/x-troff-ms") }, + { ".msi", new("application/octet-stream") }, + { ".mso", new("application/octet-stream") }, + { ".mts", new("video/vnd.dlna.mpeg-tts") }, + { ".mtx", new("application/xml") }, + { ".mvb", new("application/x-msmediaview") }, + { ".mvc", new("application/x-miva-compiled") }, + { ".mxp", new("application/x-mmxp") }, + { ".nc", new("application/x-netcdf") }, + { ".nsc", new("video/x-ms-asf") }, + { ".nws", new("message/rfc822") }, + { ".ocx", new("application/octet-stream") }, + { ".oda", new("application/oda") }, + { ".odb", new("application/vnd.oasis.opendocument.database") }, + { ".odc", new("application/vnd.oasis.opendocument.chart") }, + { ".odf", new("application/vnd.oasis.opendocument.formula") }, + { ".odg", new("application/vnd.oasis.opendocument.graphics") }, + { ".odh", new("text/plain") }, + { ".odi", new("application/vnd.oasis.opendocument.image") }, + { ".odl", new("text/plain") }, + { ".odm", new("application/vnd.oasis.opendocument.text-master") }, + { ".odp", new("application/vnd.oasis.opendocument.presentation") }, + { ".ods", new("application/vnd.oasis.opendocument.spreadsheet") }, + { ".odt", new("application/vnd.oasis.opendocument.text") }, + { ".oga", new("audio/ogg") }, + { ".ogg", new("audio/ogg") }, + { ".ogv", new("video/ogg") }, + { ".ogx", new("application/ogg") }, + { ".one", new("application/onenote") }, + { ".onea", new("application/onenote") }, + { ".onepkg", new("application/onenote") }, + { ".onetmp", new("application/onenote") }, + { ".onetoc", new("application/onenote") }, + { ".onetoc2", new("application/onenote") }, + { ".opus", new("audio/ogg") }, + { ".orderedtest", new("application/xml") }, + { ".osdx", new("application/opensearchdescription+xml") }, + { ".otf", new("application/font-sfnt") }, + { ".otg", new("application/vnd.oasis.opendocument.graphics-template") }, + { ".oth", new("application/vnd.oasis.opendocument.text-web") }, + { ".otp", new("application/vnd.oasis.opendocument.presentation-template") }, + { ".ots", new("application/vnd.oasis.opendocument.spreadsheet-template") }, + { ".ott", new("application/vnd.oasis.opendocument.text-template") }, + { ".oxt", new("application/vnd.openofficeorg.extension") }, + { ".p10", new("application/pkcs10") }, + { ".p12", new("application/x-pkcs12") }, + { ".p7b", new("application/x-pkcs7-certificates") }, + { ".p7c", new("application/pkcs7-mime") }, + { ".p7m", new("application/pkcs7-mime") }, + { ".p7r", new("application/x-pkcs7-certreqresp") }, + { ".p7s", new("application/pkcs7-signature") }, + { ".pbm", new("image/x-portable-bitmap") }, + { ".pcast", new("application/x-podcast") }, + { ".pct", new("image/pict") }, + { ".pcx", new("application/octet-stream") }, + { ".pcz", new("application/octet-stream") }, + { ".pdf", new("application/pdf") }, + { ".pfb", new("application/octet-stream") }, + { ".pfm", new("application/octet-stream") }, + { ".pfx", new("application/x-pkcs12") }, + { ".pgm", new("image/x-portable-graymap") }, + { ".pic", new("image/pict") }, + { ".pict", new("image/pict") }, + { ".pkgdef", new("text/plain") }, + { ".pkgundef", new("text/plain") }, + { ".pko", new("application/vnd.ms-pki.pko") }, + { ".pls", new("audio/scpls") }, + { ".pma", new("application/x-perfmon") }, + { ".pmc", new("application/x-perfmon") }, + { ".pml", new("application/x-perfmon") }, + { ".pmr", new("application/x-perfmon") }, + { ".pmw", new("application/x-perfmon") }, + { ".png", new("image/png") }, + { ".pnm", new("image/x-portable-anymap") }, + { ".pnt", new("image/x-macpaint") }, + { ".pntg", new("image/x-macpaint") }, + { ".pnz", new("image/png") }, + { ".pot", new("application/vnd.ms-powerpoint") }, + { ".potm", new("application/vnd.ms-powerpoint.template.macroEnabled.12") }, + { ".potx", new("application/vnd.openxmlformats-officedocument.presentationml.template") }, + { ".ppa", new("application/vnd.ms-powerpoint") }, + { ".ppam", new("application/vnd.ms-powerpoint.addin.macroEnabled.12") }, + { ".ppm", new("image/x-portable-pixmap") }, + { ".pps", new("application/vnd.ms-powerpoint") }, + { ".ppsm", new("application/vnd.ms-powerpoint.slideshow.macroEnabled.12") }, + { ".ppsx", new("application/vnd.openxmlformats-officedocument.presentationml.slideshow") }, + { ".ppt", new("application/vnd.ms-powerpoint") }, + { ".pptm", new("application/vnd.ms-powerpoint.presentation.macroEnabled.12") }, + { ".pptx", new("application/vnd.openxmlformats-officedocument.presentationml.presentation") }, + { ".prf", new("application/pics-rules") }, + { ".prm", new("application/octet-stream") }, + { ".prx", new("application/octet-stream") }, + { ".ps", new("application/postscript") }, + { ".psc1", new("application/PowerShell") }, + { ".psd", new("application/octet-stream") }, + { ".psess", new("application/xml") }, + { ".psm", new("application/octet-stream") }, + { ".psp", new("application/octet-stream") }, + { ".pub", new("application/x-mspublisher") }, + { ".pwz", new("application/vnd.ms-powerpoint") }, + { ".qht", new("text/x-html-insertion") }, + { ".qhtm", new("text/x-html-insertion") }, + { ".qt", new("video/quicktime") }, + { ".qti", new("image/x-quicktime") }, + { ".qtif", new("image/x-quicktime") }, + { ".qtl", new("application/x-quicktimeplayer") }, + { ".qxd", new("application/octet-stream") }, + { ".ra", new("audio/x-pn-realaudio") }, + { ".ram", new("audio/x-pn-realaudio") }, + { ".rar", new("application/x-rar-compressed") }, + { ".ras", new("image/x-cmu-raster") }, + { ".rat", new("application/rat-file") }, + { ".rc", new("text/plain") }, + { ".rc2", new("text/plain") }, + { ".rct", new("text/plain") }, + { ".rdlc", new("application/xml") }, + { ".reg", new("text/plain") }, + { ".resx", new("application/xml") }, + { ".rf", new("image/vnd.rn-realflash") }, + { ".rgb", new("image/x-rgb") }, + { ".rgs", new("text/plain") }, + { ".rm", new("application/vnd.rn-realmedia") }, + { ".rmi", new("audio/mid") }, + { ".rmp", new("application/vnd.rn-rn_music_package") }, + { ".roff", new("application/x-troff") }, + { ".rpm", new("audio/x-pn-realaudio-plugin") }, + { ".rqy", new("text/x-ms-rqy") }, + { ".rtf", new("application/rtf") }, + { ".rtx", new("text/richtext") }, + { ".ruleset", new("application/xml") }, + { ".s", new("text/plain") }, + { ".safariextz", new("application/x-safari-safariextz") }, + { ".scd", new("application/x-msschedule") }, + { ".scr", new("text/plain") }, + { ".sct", new("text/scriptlet") }, + { ".sd2", new("audio/x-sd2") }, + { ".sdp", new("application/sdp") }, + { ".sea", new("application/octet-stream") }, + { ".searchConnector-ms", new("application/windows-search-connector+xml") }, + { ".setpay", new("application/set-payment-initiation") }, + { ".setreg", new("application/set-registration-initiation") }, + { ".settings", new("application/xml") }, + { ".sgimb", new("application/x-sgimb") }, + { ".sgml", new("text/sgml") }, + { ".sh", new("application/x-sh") }, + { ".shar", new("application/x-shar") }, + { ".shtml", new("text/html") }, + { ".sit", new("application/x-stuffit") }, + { ".sitemap", new("application/xml") }, + { ".skin", new("application/xml") }, + { ".sldm", new("application/vnd.ms-powerpoint.slide.macroEnabled.12") }, + { ".sldx", new("application/vnd.openxmlformats-officedocument.presentationml.slide") }, + { ".slk", new("application/vnd.ms-excel") }, + { ".sln", new("text/plain") }, + { ".slupkg-ms", new("application/x-ms-license") }, + { ".smd", new("audio/x-smd") }, + { ".smi", new("application/octet-stream") }, + { ".smx", new("audio/x-smd") }, + { ".smz", new("audio/x-smd") }, + { ".snd", new("audio/basic") }, + { ".snippet", new("application/xml") }, + { ".snp", new("application/octet-stream") }, + { ".sol", new("text/plain") }, + { ".sor", new("text/plain") }, + { ".sos", new("text/vnd.sosi") }, + { ".spc", new("application/x-pkcs7-certificates") }, + { ".spl", new("application/futuresplash") }, + { ".spx", new("audio/ogg") }, + { ".src", new("application/x-wais-source") }, + { ".srf", new("text/plain") }, + { ".SSISDeploymentManifest", new("text/xml") }, + { ".ssm", new("application/streamingmedia") }, + { ".sst", new("application/vnd.ms-pki.certstore") }, + { ".stl", new("application/vnd.ms-pki.stl") }, + { ".sv4cpio", new("application/x-sv4cpio") }, + { ".sv4crc", new("application/x-sv4crc") }, + { ".svc", new("application/xml") }, + { ".svg", new("image/svg+xml") }, + { ".swf", new("application/x-shockwave-flash") }, + { ".step", new("application/step") }, + { ".stp", new("application/step") }, + { ".t", new("application/x-troff") }, + { ".tar", new("application/x-tar") }, + { ".tcl", new("application/x-tcl") }, + { ".testrunconfig", new("application/xml") }, + { ".testsettings", new("application/xml") }, + { ".tex", new("application/x-tex") }, + { ".texi", new("application/x-texinfo") }, + { ".texinfo", new("application/x-texinfo") }, + { ".tgz", new("application/x-compressed") }, + { ".thmx", new("application/vnd.ms-officetheme") }, + { ".thn", new("application/octet-stream") }, + { ".tif", new("image/tiff") }, + { ".tiff", new("image/tiff") }, + { ".tlh", new("text/plain") }, + { ".tli", new("text/plain") }, + { ".toc", new("application/octet-stream") }, + { ".tr", new("application/x-troff") }, + { ".trm", new("application/x-msterminal") }, + { ".trx", new("application/xml") }, + { ".ts", new("video/vnd.dlna.mpeg-tts") }, + { ".tsv", new("text/tab-separated-values") }, + { ".ttf", new("application/font-sfnt") }, + { ".tts", new("video/vnd.dlna.mpeg-tts") }, + { ".txt", new("text/plain") }, + { ".u32", new("application/octet-stream") }, + { ".uls", new("text/iuls") }, + { ".user", new("text/plain") }, + { ".ustar", new("application/x-ustar") }, + { ".vb", new("text/plain") }, + { ".vbdproj", new("text/plain") }, + { ".vbk", new("video/mpeg") }, + { ".vbproj", new("text/plain") }, + { ".vbs", new("text/vbscript") }, + { ".vcf", new("text/x-vcard") }, + { ".vcproj", new("application/xml") }, + { ".vcs", new("text/plain") }, + { ".vcxproj", new("application/xml") }, + { ".vddproj", new("text/plain") }, + { ".vdp", new("text/plain") }, + { ".vdproj", new("text/plain") }, + { ".vdx", new("application/vnd.ms-visio.viewer") }, + { ".vml", new("text/xml") }, + { ".vscontent", new("application/xml") }, + { ".vsct", new("text/xml") }, + { ".vsd", new("application/vnd.visio") }, + { ".vsi", new("application/ms-vsi") }, + { ".vsix", new("application/vsix") }, + { ".vsixlangpack", new("text/xml") }, + { ".vsixmanifest", new("text/xml") }, + { ".vsmdi", new("application/xml") }, + { ".vspscc", new("text/plain") }, + { ".vss", new("application/vnd.visio") }, + { ".vsscc", new("text/plain") }, + { ".vssettings", new("text/xml") }, + { ".vssscc", new("text/plain") }, + { ".vst", new("application/vnd.visio") }, + { ".vstemplate", new("text/xml") }, + { ".vsto", new("application/x-ms-vsto") }, + { ".vsw", new("application/vnd.visio") }, + { ".vsx", new("application/vnd.visio") }, + { ".vtx", new("application/vnd.visio") }, + { ".wav", new("audio/wav") }, + { ".wave", new("audio/wav") }, + { ".wax", new("audio/x-ms-wax") }, + { ".wbk", new("application/msword") }, + { ".wbmp", new("image/vnd.wap.wbmp") }, + { ".wcm", new("application/vnd.ms-works") }, + { ".wdb", new("application/vnd.ms-works") }, + { ".wdp", new("image/vnd.ms-photo") }, + { ".webarchive", new("application/x-safari-webarchive") }, + { ".webm", new("video/webm") }, + { ".webp", new("image/webp") }, /* https://en.wikipedia.org/wiki/WebP */ + { ".webtest", new("application/xml") }, + { ".wiq", new("application/xml") }, + { ".wiz", new("application/msword") }, + { ".wks", new("application/vnd.ms-works") }, + { ".WLMP", new("application/wlmoviemaker") }, + { ".wlpginstall", new("application/x-wlpg-detect") }, + { ".wlpginstall3", new("application/x-wlpg3-detect") }, + { ".wm", new("video/x-ms-wm") }, + { ".wma", new("audio/x-ms-wma") }, + { ".wmd", new("application/x-ms-wmd") }, + { ".wmf", new("application/x-msmetafile") }, + { ".wml", new("text/vnd.wap.wml") }, + { ".wmlc", new("application/vnd.wap.wmlc") }, + { ".wmls", new("text/vnd.wap.wmlscript") }, + { ".wmlsc", new("application/vnd.wap.wmlscriptc") }, + { ".wmp", new("video/x-ms-wmp") }, + { ".wmv", new("video/x-ms-wmv") }, + { ".wmx", new("video/x-ms-wmx") }, + { ".wmz", new("application/x-ms-wmz") }, + { ".woff", new("application/font-woff") }, + { ".wpl", new("application/vnd.ms-wpl") }, + { ".wps", new("application/vnd.ms-works") }, + { ".wri", new("application/x-mswrite") }, + { ".wrl", new("x-world/x-vrml") }, + { ".wrz", new("x-world/x-vrml") }, + { ".wsc", new("text/scriptlet") }, + { ".wsdl", new("text/xml") }, + { ".wvx", new("video/x-ms-wvx") }, + { ".x", new("application/directx") }, + { ".xaf", new("x-world/x-vrml") }, + { ".xaml", new("application/xaml+xml") }, + { ".xap", new("application/x-silverlight-app") }, + { ".xbap", new("application/x-ms-xbap") }, + { ".xbm", new("image/x-xbitmap") }, + { ".xdr", new("text/plain") }, + { ".xht", new("application/xhtml+xml") }, + { ".xhtml", new("application/xhtml+xml") }, + { ".xla", new("application/vnd.ms-excel") }, + { ".xlam", new("application/vnd.ms-excel.addin.macroEnabled.12") }, + { ".xlc", new("application/vnd.ms-excel") }, + { ".xld", new("application/vnd.ms-excel") }, + { ".xlk", new("application/vnd.ms-excel") }, + { ".xll", new("application/vnd.ms-excel") }, + { ".xlm", new("application/vnd.ms-excel") }, + { ".xls", new("application/vnd.ms-excel") }, + { ".xlsb", new("application/vnd.ms-excel.sheet.binary.macroEnabled.12") }, + { ".xlsm", new("application/vnd.ms-excel.sheet.macroEnabled.12") }, + { ".xlsx", new("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") }, + { ".xlt", new("application/vnd.ms-excel") }, + { ".xltm", new("application/vnd.ms-excel.template.macroEnabled.12") }, + { ".xltx", new("application/vnd.openxmlformats-officedocument.spreadsheetml.template") }, + { ".xlw", new("application/vnd.ms-excel") }, + { ".xml", new("application/xml", "text/xml") }, + { ".xmta", new("application/xml") }, + { ".xof", new("x-world/x-vrml") }, + { ".XOML", new("text/plain") }, + { ".xpm", new("image/x-xpixmap") }, + { ".xps", new("application/vnd.ms-xpsdocument") }, + { ".xrm-ms", new("text/xml") }, + { ".xsc", new("application/xml") }, + { ".xsd", new("text/xml") }, + { ".xsf", new("text/xml") }, + { ".xsl", new("text/xml") }, + { ".xslt", new("text/xml") }, + { ".xsn", new("application/octet-stream") }, + { ".xss", new("application/xml") }, + { ".xspf", new("application/xspf+xml") }, + { ".xtp", new("application/octet-stream") }, + { ".xwd", new("image/x-xwindowdump") }, + { ".z", new("application/x-compress") }, + { ".zip", new("application/zip", "application/x-zip-compressed", "application/zip-compressed") } - return mappings.ToFrozenDictionary(); - } + #endregion + }; - /// - /// Get mime type for the given file extension - /// - /// the file extension - /// The mime type - public static MimeType GetMimeType(string extension) - { - ArgumentNullException.ThrowIfNull(extension); + return mappings.ToFrozenDictionary(); + } - if (!extension.StartsWith('.')) - { - extension = "." + extension; - } + /// + /// Get mime type for the given file extension + /// + /// the file extension + /// The mime type + public static MimeType GetMimeType(string extension) + { + ArgumentNullException.ThrowIfNull(extension); - return _mappings.Value.TryGetValue(extension, out MimeType? mime) - ? mime - : new MimeType("application/octet-stream"); + if (!extension.StartsWith('.')) + { + extension = "." + extension; } + + return _mappings.Value.TryGetValue(extension, out MimeType? mime) + ? mime + : new MimeType("application/octet-stream"); } } diff --git a/src/Altinn.App.Core/Helpers/ObjectUtils.cs b/src/Altinn.App.Core/Helpers/ObjectUtils.cs index a81dc03c4..ba40cada1 100644 --- a/src/Altinn.App.Core/Helpers/ObjectUtils.cs +++ b/src/Altinn.App.Core/Helpers/ObjectUtils.cs @@ -1,7 +1,6 @@ using System.Collections; using System.Reflection; using System.Xml.Serialization; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Altinn.App.Core.Helpers; diff --git a/src/Altinn.App.Core/Helpers/PathHelper.cs b/src/Altinn.App.Core/Helpers/PathHelper.cs index f53c4e828..c3fb2d24e 100644 --- a/src/Altinn.App.Core/Helpers/PathHelper.cs +++ b/src/Altinn.App.Core/Helpers/PathHelper.cs @@ -1,35 +1,34 @@ -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// Helper class for Path manipulation and checks +/// +public static class PathHelper { /// - /// Helper class for Path manipulation and checks + /// Validates that the filePath is in the legalPath. The current path will be prefixed if the paths are relative. /// - public static class PathHelper + /// The legal path + /// The file path to check + /// True for legal paths, false otherwise + public static bool ValidateLegalFilePath(string legalPath, string filePath) { - /// - /// Validates that the filePath is in the legalPath. The current path will be prefixed if the paths are relative. - /// - /// The legal path - /// The file path to check - /// True for legal paths, false otherwise - public static bool ValidateLegalFilePath(string legalPath, string filePath) - { - var fullRootedFolder = Path.GetFullPath(legalPath + Path.DirectorySeparatorChar); - var expandedFilename = Path.GetFullPath(filePath); + var fullRootedFolder = Path.GetFullPath(legalPath + Path.DirectorySeparatorChar); + var expandedFilename = Path.GetFullPath(filePath); - return expandedFilename.StartsWith(fullRootedFolder); - } + return expandedFilename.StartsWith(fullRootedFolder); + } - /// - /// Ensures that the filePath is within the legalPath. Throws exception if the filePath is illegal. - /// - /// The legal path - /// The file path to check - public static void EnsureLegalPath(string legalPath, string filePath) + /// + /// Ensures that the filePath is within the legalPath. Throws exception if the filePath is illegal. + /// + /// The legal path + /// The file path to check + public static void EnsureLegalPath(string legalPath, string filePath) + { + if (!ValidateLegalFilePath(legalPath, filePath)) { - if (!ValidateLegalFilePath(legalPath, filePath)) - { - throw new ArgumentException("Invalid path", nameof(filePath)); - } + throw new ArgumentException("Invalid path", nameof(filePath)); } } } diff --git a/src/Altinn.App.Core/Helpers/PlatformHttpException.cs b/src/Altinn.App.Core/Helpers/PlatformHttpException.cs index 7d0c4d311..6d8930277 100644 --- a/src/Altinn.App.Core/Helpers/PlatformHttpException.cs +++ b/src/Altinn.App.Core/Helpers/PlatformHttpException.cs @@ -1,40 +1,37 @@ -using System.Runtime.Serialization; +namespace Altinn.App.Core.Helpers; -namespace Altinn.App.Core.Helpers +/// +/// Exception class to hold exceptions when talking to the platform REST services +/// +public class PlatformHttpException : Exception { /// - /// Exception class to hold exceptions when talking to the platform REST services + /// Responsible for holding an http request exception towards platform (storage). /// - public class PlatformHttpException : Exception - { - /// - /// Responsible for holding an http request exception towards platform (storage). - /// - public HttpResponseMessage Response { get; } + public HttpResponseMessage Response { get; } - /// - /// Create a new by reading the - /// content asynchronously. - /// - /// The to read. - /// A new . - public static async Task CreateAsync(HttpResponseMessage response) - { - string content = await response.Content.ReadAsStringAsync(); - string message = $"{(int)response.StatusCode} - {response.ReasonPhrase} - {content}"; + /// + /// Create a new by reading the + /// content asynchronously. + /// + /// The to read. + /// A new . + public static async Task CreateAsync(HttpResponseMessage response) + { + string content = await response.Content.ReadAsStringAsync(); + string message = $"{(int)response.StatusCode} - {response.ReasonPhrase} - {content}"; - return new PlatformHttpException(response, message); - } + return new PlatformHttpException(response, message); + } - /// - /// Copy the response for further investigations - /// - /// the response - /// A description of the cause of the exception. - public PlatformHttpException(HttpResponseMessage response, string message) - : base(message) - { - this.Response = response; - } + /// + /// Copy the response for further investigations + /// + /// the response + /// A description of the cause of the exception. + public PlatformHttpException(HttpResponseMessage response, string message) + : base(message) + { + this.Response = response; } } diff --git a/src/Altinn.App.Core/Helpers/ProcessError.cs b/src/Altinn.App.Core/Helpers/ProcessError.cs index 5fea7f5ef..80a95abff 100644 --- a/src/Altinn.App.Core/Helpers/ProcessError.cs +++ b/src/Altinn.App.Core/Helpers/ProcessError.cs @@ -1,20 +1,19 @@ -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// Represents an error from the process "system". +/// +public class ProcessError { +#nullable disable /// - /// Represents an error from the process "system". + /// Gets or sets a machine readable error code or test resource key. /// - public class ProcessError - { -#nullable disable - /// - /// Gets or sets a machine readable error code or test resource key. - /// - public string Code { get; set; } + public string Code { get; set; } - /// - /// Gets or sets a human readable error message. - /// - public string Text { get; set; } + /// + /// Gets or sets a human readable error message. + /// + public string Text { get; set; } #nullable restore - } } diff --git a/src/Altinn.App.Core/Helpers/ProcessHelper.cs b/src/Altinn.App.Core/Helpers/ProcessHelper.cs index 786f07dde..e0e872514 100644 --- a/src/Altinn.App.Core/Helpers/ProcessHelper.cs +++ b/src/Altinn.App.Core/Helpers/ProcessHelper.cs @@ -1,139 +1,136 @@ using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// Helper class for handling the process for an instance. +/// +public static class ProcessHelper { /// - /// Helper class for handling the process for an instance. + /// Validates that the process can start from the given start event. /// - public static class ProcessHelper + /// The name of the start event the process should start from. + /// List of possible start events + /// Any error preventing the process from starting. + /// The name of the start event or null if start event wasn't found. + // TODO: improve implementation of this so that we help out nullability flow analysis in the compiler + // i.e. startEventError is non-null when the function returns null + // this should probably also be internal... + public static string? GetValidStartEventOrError( + string? proposedStartEvent, + List possibleStartEvents, + out ProcessError? startEventError + ) { - /// - /// Validates that the process can start from the given start event. - /// - /// The name of the start event the process should start from. - /// List of possible start events - /// Any error preventing the process from starting. - /// The name of the start event or null if start event wasn't found. - // TODO: improve implementation of this so that we help out nullability flow analysis in the compiler - // i.e. startEventError is non-null when the function returns null - // this should probably also be internal... - public static string? GetValidStartEventOrError( - string? proposedStartEvent, - List possibleStartEvents, - out ProcessError? startEventError - ) - { - startEventError = null; + startEventError = null; - if (!string.IsNullOrEmpty(proposedStartEvent)) + if (!string.IsNullOrEmpty(proposedStartEvent)) + { + if (possibleStartEvents.Contains(proposedStartEvent)) { - if (possibleStartEvents.Contains(proposedStartEvent)) - { - return proposedStartEvent; - } - - startEventError = Conflict( - $"There is no such start event as '{proposedStartEvent}' in the process definition." - ); - return null; + return proposedStartEvent; } - if (possibleStartEvents.Count == 1) - { - return possibleStartEvents.First(); - } + startEventError = Conflict( + $"There is no such start event as '{proposedStartEvent}' in the process definition." + ); + return null; + } - if (possibleStartEvents.Count > 1) - { - startEventError = Conflict( - $"There are more than one start events available. Chose one: [{string.Join(", ", possibleStartEvents)}]" - ); - return null; - } + if (possibleStartEvents.Count == 1) + { + return possibleStartEvents.First(); + } - startEventError = Conflict($"There is no start events in process definition. Cannot start process!"); + if (possibleStartEvents.Count > 1) + { + startEventError = Conflict( + $"There are more than one start events available. Chose one: [{string.Join(", ", possibleStartEvents)}]" + ); return null; } - /// - /// Validates that the given element name is a valid next step in the process. - /// - /// The name of the proposed next element. - /// List of possible next elements - /// Any error preventing the logic to identify next element. - /// The name of the next element. - public static string? GetValidNextElementOrError( - string? proposedElementId, - List possibleNextElements, - out ProcessError? nextElementError - ) - { - nextElementError = null; + startEventError = Conflict($"There is no start events in process definition. Cannot start process!"); + return null; + } - if (!string.IsNullOrEmpty(proposedElementId)) - { - if (possibleNextElements.Contains(proposedElementId)) - { - return proposedElementId; - } - else - { - nextElementError = Conflict( - $"The proposed next element id '{proposedElementId}' is not among the available next process elements" - ); - return null; - } - } + /// + /// Validates that the given element name is a valid next step in the process. + /// + /// The name of the proposed next element. + /// List of possible next elements + /// Any error preventing the logic to identify next element. + /// The name of the next element. + public static string? GetValidNextElementOrError( + string? proposedElementId, + List possibleNextElements, + out ProcessError? nextElementError + ) + { + nextElementError = null; - if (possibleNextElements.Count == 1) + if (!string.IsNullOrEmpty(proposedElementId)) + { + if (possibleNextElements.Contains(proposedElementId)) { - return possibleNextElements.First(); + return proposedElementId; } - - if (possibleNextElements.Count > 1) + else { nextElementError = Conflict( - $"There are more than one outgoing sequence flows, please select one '{possibleNextElements}'" + $"The proposed next element id '{proposedElementId}' is not among the available next process elements" ); return null; } + } - if (possibleNextElements.Count == 0) - { - nextElementError = Conflict( - $"There are no outgoing sequence flows from current element. Cannot find next process element. Error in bpmn file!" - ); - return null; - } + if (possibleNextElements.Count == 1) + { + return possibleNextElements.First(); + } + if (possibleNextElements.Count > 1) + { + nextElementError = Conflict( + $"There are more than one outgoing sequence flows, please select one '{possibleNextElements}'" + ); return null; } - /// - /// Find the flowtype between - /// - public static ProcessSequenceFlowType GetSequenceFlowType(List flows) + if (possibleNextElements.Count == 0) { - foreach (SequenceFlow flow in flows) - { - if ( - !string.IsNullOrEmpty(flow.FlowType) - && Enum.TryParse(flow.FlowType, out ProcessSequenceFlowType flowType) - ) - { - return flowType; - } - } - - return ProcessSequenceFlowType.CompleteCurrentMoveToNext; + nextElementError = Conflict( + $"There are no outgoing sequence flows from current element. Cannot find next process element. Error in bpmn file!" + ); + return null; } - private static ProcessError Conflict(string text) + return null; + } + + /// + /// Find the flowtype between + /// + public static ProcessSequenceFlowType GetSequenceFlowType(List flows) + { + foreach (SequenceFlow flow in flows) { - return new ProcessError { Code = "Conflict", Text = text }; + if ( + !string.IsNullOrEmpty(flow.FlowType) + && Enum.TryParse(flow.FlowType, out ProcessSequenceFlowType flowType) + ) + { + return flowType; + } } + + return ProcessSequenceFlowType.CompleteCurrentMoveToNext; + } + + private static ProcessError Conflict(string text) + { + return new ProcessError { Code = "Conflict", Text = text }; } } diff --git a/src/Altinn.App.Core/Helpers/SelfLinkHelper.cs b/src/Altinn.App.Core/Helpers/SelfLinkHelper.cs index 2dbcde16a..d7ee798ae 100644 --- a/src/Altinn.App.Core/Helpers/SelfLinkHelper.cs +++ b/src/Altinn.App.Core/Helpers/SelfLinkHelper.cs @@ -2,97 +2,96 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// Helper class for setting application self links +/// +public static class SelfLinkHelper { /// - /// Helper class for setting application self links + /// Sets the application specific self links. /// - public static class SelfLinkHelper + /// the instance to set links for + /// the http request to extract host and path name + public static void SetInstanceAppSelfLinks(Instance instance, HttpRequest request) { - /// - /// Sets the application specific self links. - /// - /// the instance to set links for - /// the http request to extract host and path name - public static void SetInstanceAppSelfLinks(Instance instance, HttpRequest request) - { - string host = $"https://{request.Host.ToUriComponent()}"; - string url = request.Path; + string host = $"https://{request.Host.ToUriComponent()}"; + string url = request.Path; - string selfLink = $"{host}{url}"; + string selfLink = $"{host}{url}"; - int start = selfLink.IndexOf("/instances"); - if (start > 0) - { - selfLink = string.Concat(selfLink.AsSpan(0, start), "/instances"); - } + int start = selfLink.IndexOf("/instances"); + if (start > 0) + { + selfLink = string.Concat(selfLink.AsSpan(0, start), "/instances"); + } - selfLink += $"/{instance.Id}"; + selfLink += $"/{instance.Id}"; - if (!selfLink.EndsWith(instance.Id)) - { - selfLink += instance.Id; - } + if (!selfLink.EndsWith(instance.Id)) + { + selfLink += instance.Id; + } - instance.SelfLinks ??= new ResourceLinks(); - instance.SelfLinks.Apps = selfLink; + instance.SelfLinks ??= new ResourceLinks(); + instance.SelfLinks.Apps = selfLink; - if (instance.Data != null) + if (instance.Data != null) + { + foreach (DataElement dataElement in instance.Data) { - foreach (DataElement dataElement in instance.Data) - { - dataElement.SelfLinks ??= new ResourceLinks(); - dataElement.SelfLinks.Apps = $"{selfLink}/data/{dataElement.Id}"; - } + dataElement.SelfLinks ??= new ResourceLinks(); + dataElement.SelfLinks.Apps = $"{selfLink}/data/{dataElement.Id}"; } } + } - /// - /// Sets the application specific self links. - /// - /// the instance owner - /// the instance guid for the instance the data element belongs to - /// the data element to set links for - /// the http request to extract host and path name - public static void SetDataAppSelfLinks( - int instanceOwnerPartyId, - Guid instanceGuid, - DataElement dataElement, - HttpRequest request - ) - { - string host = $"https://{request.Host.ToUriComponent()}"; - string url = request.Path; + /// + /// Sets the application specific self links. + /// + /// the instance owner + /// the instance guid for the instance the data element belongs to + /// the data element to set links for + /// the http request to extract host and path name + public static void SetDataAppSelfLinks( + int instanceOwnerPartyId, + Guid instanceGuid, + DataElement dataElement, + HttpRequest request + ) + { + string host = $"https://{request.Host.ToUriComponent()}"; + string url = request.Path; - string selfLink = $"{host}{url}"; + string selfLink = $"{host}{url}"; - int start = selfLink.IndexOf("/instances"); - if (start > 0) - { - selfLink = string.Concat(selfLink.AsSpan(0, start), "/instances"); - } + int start = selfLink.IndexOf("/instances"); + if (start > 0) + { + selfLink = string.Concat(selfLink.AsSpan(0, start), "/instances"); + } - selfLink += $"/{instanceOwnerPartyId}/{instanceGuid.ToString()}"; + selfLink += $"/{instanceOwnerPartyId}/{instanceGuid.ToString()}"; - dataElement.SelfLinks ??= new ResourceLinks(); + dataElement.SelfLinks ??= new ResourceLinks(); - dataElement.SelfLinks.Apps = $"{selfLink}/data/{dataElement.Id}"; - } + dataElement.SelfLinks.Apps = $"{selfLink}/data/{dataElement.Id}"; + } - /// - /// Build a url that can be opened in a browser - /// - /// The instance metadata document. - /// The original http request. - /// - public static string BuildFrontendSelfLink(Instance instance, HttpRequest request) - { - StringBuilder urlBuilder = new($"https://{request.Host.ToUriComponent()}/"); - urlBuilder.Append(instance.AppId); - urlBuilder.Append("/#/instance/"); - urlBuilder.Append(instance.Id); + /// + /// Build a url that can be opened in a browser + /// + /// The instance metadata document. + /// The original http request. + /// + public static string BuildFrontendSelfLink(Instance instance, HttpRequest request) + { + StringBuilder urlBuilder = new($"https://{request.Host.ToUriComponent()}/"); + urlBuilder.Append(instance.AppId); + urlBuilder.Append("/#/instance/"); + urlBuilder.Append(instance.Id); - return urlBuilder.ToString(); - } + return urlBuilder.ToString(); } } diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs index 981c8362b..f095fb469 100644 --- a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs +++ b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs @@ -4,158 +4,157 @@ using System.Xml.Serialization; using Microsoft.Extensions.Logging; -namespace Altinn.App.Core.Helpers.Serialization +namespace Altinn.App.Core.Helpers.Serialization; + +/// +/// Represents logic to deserialize a stream of data to an instance of the given type +/// +public class ModelDeserializer { + private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); + + private readonly ILogger _logger; + private readonly Type _modelType; + + /// + /// Gets the error message describing what it was that went wrong if there was an issue during deserialization. + /// + public string? Error { get; private set; } + + /// + /// Initialize a new instance of with a logger and the Type the deserializer should target. + /// + /// A logger that can be used to write log information. + /// The Type the deserializer should target when deserializing data. + public ModelDeserializer(ILogger logger, Type modelType) + { + _logger = logger; + _modelType = modelType; + } + /// - /// Represents logic to deserialize a stream of data to an instance of the given type + /// Deserialize a stream with data of the given content type. The operation supports application/json and application/xml. /// - public class ModelDeserializer + /// The data stream to deserialize. + /// The content type of the stream. + /// An instance of the initialized type if deserializing succeed. + public async Task DeserializeAsync(Stream stream, string? contentType) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); - - private readonly ILogger _logger; - private readonly Type _modelType; - - /// - /// Gets the error message describing what it was that went wrong if there was an issue during deserialization. - /// - public string? Error { get; private set; } - - /// - /// Initialize a new instance of with a logger and the Type the deserializer should target. - /// - /// A logger that can be used to write log information. - /// The Type the deserializer should target when deserializing data. - public ModelDeserializer(ILogger logger, Type modelType) + Error = null; + + if (contentType == null) { - _logger = logger; - _modelType = modelType; + Error = $"Unknown content type \"null\". Cannot read the data."; + return null; } - /// - /// Deserialize a stream with data of the given content type. The operation supports application/json and application/xml. - /// - /// The data stream to deserialize. - /// The content type of the stream. - /// An instance of the initialized type if deserializing succeed. - public async Task DeserializeAsync(Stream stream, string? contentType) + if (contentType.Contains("application/json")) { - Error = null; + return await DeserializeJsonAsync(stream); + } - if (contentType == null) - { - Error = $"Unknown content type \"null\". Cannot read the data."; - return null; - } + if (contentType.Contains("application/xml")) + { + return await DeserializeXmlAsync(stream); + } - if (contentType.Contains("application/json")) - { - return await DeserializeJsonAsync(stream); - } + Error = $"Unknown content type {contentType}. Cannot read the data."; + return null; + } - if (contentType.Contains("application/xml")) - { - return await DeserializeXmlAsync(stream); - } + private async Task DeserializeJsonAsync(Stream stream) + { + Error = null; - Error = $"Unknown content type {contentType}. Cannot read the data."; + try + { + using StreamReader reader = new StreamReader(stream, Encoding.UTF8); + string content = await reader.ReadToEndAsync(); + return JsonSerializer.Deserialize(content, _modelType, _jsonSerializerOptions); + } + catch (JsonException jsonReaderException) + { + Error = jsonReaderException.Message; return null; } - - private async Task DeserializeJsonAsync(Stream stream) + catch (Exception ex) { - Error = null; - - try - { - using StreamReader reader = new StreamReader(stream, Encoding.UTF8); - string content = await reader.ReadToEndAsync(); - return JsonSerializer.Deserialize(content, _modelType, _jsonSerializerOptions); - } - catch (JsonException jsonReaderException) - { - Error = jsonReaderException.Message; - return null; - } - catch (Exception ex) - { - string message = $"Unexpected exception when attempting to deserialize JSON into '{_modelType}'"; - _logger.LogError(ex, message); - Error = message; - return null; - } + string message = $"Unexpected exception when attempting to deserialize JSON into '{_modelType}'"; + _logger.LogError(ex, message); + Error = message; + return null; } + } + + private async Task DeserializeXmlAsync(Stream stream) + { + Error = null; - private async Task DeserializeXmlAsync(Stream stream) + string? streamContent = null; + try { - Error = null; + // In this first try block we assume that the namespace is the same in the model + // and in the XML. This includes no namespace in both. + using StreamReader reader = new StreamReader(stream, Encoding.UTF8); + streamContent = await reader.ReadToEndAsync(); + + using XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(streamContent)); + XmlSerializer serializer = new XmlSerializer(_modelType); - string? streamContent = null; + return serializer.Deserialize(xmlTextReader); + } + catch (InvalidOperationException) + { try { - // In this first try block we assume that the namespace is the same in the model - // and in the XML. This includes no namespace in both. - using StreamReader reader = new StreamReader(stream, Encoding.UTF8); - streamContent = await reader.ReadToEndAsync(); + // In this backup try block we assume that the modelType has declared a namespace, + // but that the XML is without any namespace declaration. + string elementName = GetRootElementName(_modelType); - using XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(streamContent)); - XmlSerializer serializer = new XmlSerializer(_modelType); + XmlAttributeOverrides attributeOverrides = new XmlAttributeOverrides(); + XmlAttributes attributes = new XmlAttributes(); + attributes.XmlRoot = new XmlRootAttribute(elementName); + attributeOverrides.Add(_modelType, attributes); - return serializer.Deserialize(xmlTextReader); - } - catch (InvalidOperationException) - { - try + if (string.IsNullOrWhiteSpace(streamContent)) { - // In this backup try block we assume that the modelType has declared a namespace, - // but that the XML is without any namespace declaration. - string elementName = GetRootElementName(_modelType); - - XmlAttributeOverrides attributeOverrides = new XmlAttributeOverrides(); - XmlAttributes attributes = new XmlAttributes(); - attributes.XmlRoot = new XmlRootAttribute(elementName); - attributeOverrides.Add(_modelType, attributes); - - if (string.IsNullOrWhiteSpace(streamContent)) - { - throw new Exception("No XML content read from stream"); - } + throw new Exception("No XML content read from stream"); + } - using XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(streamContent)); - XmlSerializer serializer = new XmlSerializer(_modelType, attributeOverrides); + using XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(streamContent)); + XmlSerializer serializer = new XmlSerializer(_modelType, attributeOverrides); - return serializer.Deserialize(xmlTextReader); - } - catch (InvalidOperationException invalidOperationException) - { - // One possible fail condition is if the XML has a namespace, but the model does not, or that the namespaces are different. - Error = $"{invalidOperationException.Message} {invalidOperationException?.InnerException?.Message}"; - return null; - } + return serializer.Deserialize(xmlTextReader); } - catch (Exception ex) + catch (InvalidOperationException invalidOperationException) { - string message = $"Unexpected exception when attempting to deserialize XML into '{_modelType}'"; - _logger.LogError(ex, message); - Error = message; + // One possible fail condition is if the XML has a namespace, but the model does not, or that the namespaces are different. + Error = $"{invalidOperationException.Message} {invalidOperationException?.InnerException?.Message}"; return null; } } - - private static string GetRootElementName(Type modelType) + catch (Exception ex) { - Attribute[] attributes = Attribute.GetCustomAttributes(modelType); + string message = $"Unexpected exception when attempting to deserialize XML into '{_modelType}'"; + _logger.LogError(ex, message); + Error = message; + return null; + } + } + + private static string GetRootElementName(Type modelType) + { + Attribute[] attributes = Attribute.GetCustomAttributes(modelType); - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + var xmlRootAttribute = attribute as XmlRootAttribute; + if (xmlRootAttribute != null) { - var xmlRootAttribute = attribute as XmlRootAttribute; - if (xmlRootAttribute != null) - { - return xmlRootAttribute.ElementName; - } + return xmlRootAttribute.ElementName; } - - return modelType.Name; } + + return modelType.Name; } } diff --git a/src/Altinn.App.Core/Helpers/ServiceException.cs b/src/Altinn.App.Core/Helpers/ServiceException.cs index 61abf3b6e..393758b82 100644 --- a/src/Altinn.App.Core/Helpers/ServiceException.cs +++ b/src/Altinn.App.Core/Helpers/ServiceException.cs @@ -1,39 +1,37 @@ using System.Net; -using System.Runtime.Serialization; -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// Exception that is thrown by service implementation. +/// +public class ServiceException : Exception { /// - /// Exception that is thrown by service implementation. + /// The proposed return http status code. /// - public class ServiceException : Exception - { - /// - /// The proposed return http status code. - /// - public HttpStatusCode StatusCode { get; } + public HttpStatusCode StatusCode { get; } - /// - /// Add a proposed http status return code and message. - /// - /// the suggested return code - /// the message - public ServiceException(HttpStatusCode statusCode, string message) - : base(message) - { - StatusCode = statusCode; - } + /// + /// Add a proposed http status return code and message. + /// + /// the suggested return code + /// the message + public ServiceException(HttpStatusCode statusCode, string message) + : base(message) + { + StatusCode = statusCode; + } - /// - /// Add inner exception. - /// - /// the suggested return code - /// the message - /// the inner exception - public ServiceException(HttpStatusCode statusCode, string message, Exception innerException) - : base(message, innerException) - { - StatusCode = statusCode; - } + /// + /// Add inner exception. + /// + /// the suggested return code + /// the message + /// the inner exception + public ServiceException(HttpStatusCode statusCode, string message, Exception innerException) + : base(message, innerException) + { + StatusCode = statusCode; } } diff --git a/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs b/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs index e82ebce42..3bf92e0f7 100644 --- a/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs +++ b/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs @@ -1,48 +1,47 @@ using System.Text.Json.Serialization.Metadata; -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// This class is used to remove shadow fields from the JSON serialization. +/// +[Obsolete("This functionality will be removed in the future")] +public class IgnorePropertiesWithPrefix { + private readonly string _ignorePrefix; + /// - /// This class is used to remove shadow fields from the JSON serialization. + /// Initializes a new instance of the class. /// - [Obsolete("This functionality will be removed in the future")] - public class IgnorePropertiesWithPrefix - { - private readonly string _ignorePrefix; + public IgnorePropertiesWithPrefix(string prefix) => _ignorePrefix = prefix; - /// - /// Initializes a new instance of the class. - /// - public IgnorePropertiesWithPrefix(string prefix) => _ignorePrefix = prefix; - - /// - /// This method is called by the JSON serializer to remove all properties with the defined prefix. - /// - public void ModifyPrefixInfo(JsonTypeInfo ti) - { - if (ti.Kind != JsonTypeInfoKind.Object) - return; + /// + /// This method is called by the JSON serializer to remove all properties with the defined prefix. + /// + public void ModifyPrefixInfo(JsonTypeInfo ti) + { + if (ti.Kind != JsonTypeInfoKind.Object) + return; - ti.Properties.RemoveAll(prop => prop.Name.StartsWith(_ignorePrefix)); - } + ti.Properties.RemoveAll(prop => prop.Name.StartsWith(_ignorePrefix)); } +} +/// +/// This class extends the ]]> interface with a RemoveAll method. +/// +public static class ListHelpers +{ /// - /// This class extends the ]]> interface with a RemoveAll method. + /// ]]> implementation of .RemoveAll]]> method. /// - public static class ListHelpers + public static void RemoveAll(this IList list, Predicate predicate) { - /// - /// ]]> implementation of .RemoveAll]]> method. - /// - public static void RemoveAll(this IList list, Predicate predicate) + for (int i = 0; i < list.Count; i++) { - for (int i = 0; i < list.Count; i++) + if (predicate(list[i])) { - if (predicate(list[i])) - { - list.RemoveAt(i--); - } + list.RemoveAt(i--); } } } diff --git a/src/Altinn.App.Core/Helpers/UserHelper.cs b/src/Altinn.App.Core/Helpers/UserHelper.cs index 518751277..ecaa9c703 100644 --- a/src/Altinn.App.Core/Helpers/UserHelper.cs +++ b/src/Altinn.App.Core/Helpers/UserHelper.cs @@ -8,86 +8,85 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Helpers +namespace Altinn.App.Core.Helpers; + +/// +/// The helper for user functionality +/// +public class UserHelper { + private readonly IProfileClient _profileClient; + private readonly IAltinnPartyClient _altinnPartyClientService; + private readonly GeneralSettings _settings; + /// - /// The helper for user functionality + /// Initializes a new instance of the class /// - public class UserHelper + /// The ProfileService (defined in Startup.cs) + /// The RegisterService (defined in Startup.cs) + /// The general settings + public UserHelper( + IProfileClient profileClient, + IAltinnPartyClient altinnPartyClientService, + IOptions settings + ) { - private readonly IProfileClient _profileClient; - private readonly IAltinnPartyClient _altinnPartyClientService; - private readonly GeneralSettings _settings; + _profileClient = profileClient; + _altinnPartyClientService = altinnPartyClientService; + _settings = settings.Value; + } - /// - /// Initializes a new instance of the class - /// - /// The ProfileService (defined in Startup.cs) - /// The RegisterService (defined in Startup.cs) - /// The general settings - public UserHelper( - IProfileClient profileClient, - IAltinnPartyClient altinnPartyClientService, - IOptions settings - ) - { - _profileClient = profileClient; - _altinnPartyClientService = altinnPartyClientService; - _settings = settings.Value; - } + /// + /// Returns the user context + /// + /// The HttpContext + /// The UserContext + public async Task GetUserContext(HttpContext context) + { + UserContext userContext = new UserContext() { User = context.User }; - /// - /// Returns the user context - /// - /// The HttpContext - /// The UserContext - public async Task GetUserContext(HttpContext context) + foreach (Claim claim in context.User.Claims) { - UserContext userContext = new UserContext() { User = context.User }; - - foreach (Claim claim in context.User.Claims) + if (claim.Type.Equals(AltinnCoreClaimTypes.UserName)) { - if (claim.Type.Equals(AltinnCoreClaimTypes.UserName)) - { - userContext.UserName = claim.Value; - } - - if (claim.Type.Equals(AltinnCoreClaimTypes.UserId)) - { - userContext.UserId = Convert.ToInt32(claim.Value); - } - - if (claim.Type.Equals(AltinnCoreClaimTypes.PartyID)) - { - userContext.PartyId = Convert.ToInt32(claim.Value); - } - - if (claim.Type.Equals(AltinnCoreClaimTypes.AuthenticationLevel)) - { - userContext.AuthenticationLevel = Convert.ToInt32(claim.Value); - } + userContext.UserName = claim.Value; } - UserProfile userProfile = - await _profileClient.GetUserProfile(userContext.UserId) - ?? throw new Exception("Could not get user profile while getting user context"); - userContext.UserParty = userProfile.Party; - - if (context.Request.Cookies[_settings.GetAltinnPartyCookieName] != null) + if (claim.Type.Equals(AltinnCoreClaimTypes.UserId)) { - userContext.PartyId = Convert.ToInt32(context.Request.Cookies[_settings.GetAltinnPartyCookieName]); + userContext.UserId = Convert.ToInt32(claim.Value); } - if (userContext.PartyId == userProfile.PartyId) + if (claim.Type.Equals(AltinnCoreClaimTypes.PartyID)) { - userContext.Party = userProfile.Party; + userContext.PartyId = Convert.ToInt32(claim.Value); } - else + + if (claim.Type.Equals(AltinnCoreClaimTypes.AuthenticationLevel)) { - userContext.Party = await _altinnPartyClientService.GetParty(userContext.PartyId); + userContext.AuthenticationLevel = Convert.ToInt32(claim.Value); } + } - return userContext; + UserProfile userProfile = + await _profileClient.GetUserProfile(userContext.UserId) + ?? throw new Exception("Could not get user profile while getting user context"); + userContext.UserParty = userProfile.Party; + + if (context.Request.Cookies[_settings.GetAltinnPartyCookieName] != null) + { + userContext.PartyId = Convert.ToInt32(context.Request.Cookies[_settings.GetAltinnPartyCookieName]); } + + if (userContext.PartyId == userProfile.PartyId) + { + userContext.Party = userProfile.Party; + } + else + { + userContext.Party = await _altinnPartyClientService.GetParty(userContext.PartyId); + } + + return userContext; } } diff --git a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs index b9328c297..6a0e00a8d 100644 --- a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs +++ b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs @@ -13,469 +13,455 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; -namespace Altinn.App.Core.Implementation +namespace Altinn.App.Core.Implementation; + +/// +/// App implementation of the execution service needed for executing an Altinn Core Application (Functional term). +/// +public class AppResourcesSI : IAppResources { + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNameCaseInsensitive = true, + }; + + private readonly AppSettings _settings; + private readonly IAppMetadata _appMetadata; + private readonly IWebHostEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly Telemetry? _telemetry; + /// - /// App implementation of the execution service needed for executing an Altinn Core Application (Functional term). + /// Initializes a new instance of the class. /// - public class AppResourcesSI : IAppResources + /// The app repository settings. + /// App metadata service + /// The hosting environment + /// A logger from the built in logger factory. + /// Telemetry for traces and metrics. + public AppResourcesSI( + IOptions settings, + IAppMetadata appMetadata, + IWebHostEnvironment hostingEnvironment, + ILogger logger, + Telemetry? telemetry = null + ) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() - { - AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip, - PropertyNameCaseInsensitive = true, - }; - - private readonly AppSettings _settings; - private readonly IAppMetadata _appMetadata; - private readonly IWebHostEnvironment _hostingEnvironment; - private readonly ILogger _logger; - private readonly Telemetry? _telemetry; - - /// - /// Initializes a new instance of the class. - /// - /// The app repository settings. - /// App metadata service - /// The hosting environment - /// A logger from the built in logger factory. - /// Telemetry for traces and metrics. - public AppResourcesSI( - IOptions settings, - IAppMetadata appMetadata, - IWebHostEnvironment hostingEnvironment, - ILogger logger, - Telemetry? telemetry = null - ) + _settings = settings.Value; + _appMetadata = appMetadata; + _hostingEnvironment = hostingEnvironment; + _logger = logger; + _telemetry = telemetry; + } + + /// + public byte[] GetText(string org, string app, string textResource) + { + using var activity = _telemetry?.StartGetTextActivity(); + return ReadFileContentsFromLegalPath( + _settings.AppBasePath + _settings.ConfigurationFolder + _settings.TextFolder, + textResource + ); + } + + /// + public async Task GetTexts(string org, string app, string language) + { + using var activity = _telemetry?.StartGetTextsActivity(); + string pathTextsFolder = _settings.AppBasePath + _settings.ConfigurationFolder + _settings.TextFolder; + string fullFileName = Path.Join(pathTextsFolder, $"resource.{language}.json"); + + PathHelper.EnsureLegalPath(pathTextsFolder, fullFileName); + + if (!File.Exists(fullFileName)) { - _settings = settings.Value; - _appMetadata = appMetadata; - _hostingEnvironment = hostingEnvironment; - _logger = logger; - _telemetry = telemetry; + return null; } - /// - public byte[] GetText(string org, string app, string textResource) + using (FileStream fileStream = new(fullFileName, FileMode.Open, FileAccess.Read)) { - using var activity = _telemetry?.StartGetTextActivity(); - return ReadFileContentsFromLegalPath( - _settings.AppBasePath + _settings.ConfigurationFolder + _settings.TextFolder, - textResource - ); + TextResource textResource = + await System.Text.Json.JsonSerializer.DeserializeAsync(fileStream, _jsonSerializerOptions) + ?? throw new System.Text.Json.JsonException("Failed to deserialize text resource"); + textResource.Id = $"{org}-{app}-{language}"; + textResource.Org = org; + textResource.Language = language; + + return textResource; } + } - /// - public async Task GetTexts(string org, string app, string language) + /// + public Application GetApplication() + { + using var activity = _telemetry?.StartGetApplicationActivity(); + try { - using var activity = _telemetry?.StartGetTextsActivity(); - string pathTextsFolder = _settings.AppBasePath + _settings.ConfigurationFolder + _settings.TextFolder; - string fullFileName = Path.Join(pathTextsFolder, $"resource.{language}.json"); - - PathHelper.EnsureLegalPath(pathTextsFolder, fullFileName); - - if (!File.Exists(fullFileName)) + ApplicationMetadata applicationMetadata = _appMetadata.GetApplicationMetadata().Result; + Application application = applicationMetadata; + if (applicationMetadata.OnEntry != null) { - return null; + application.OnEntry = new OnEntryConfig() { Show = applicationMetadata.OnEntry.Show }; } - using (FileStream fileStream = new(fullFileName, FileMode.Open, FileAccess.Read)) - { - TextResource textResource = - await System.Text.Json.JsonSerializer.DeserializeAsync( - fileStream, - _jsonSerializerOptions - ) ?? throw new System.Text.Json.JsonException("Failed to deserialize text resource"); - textResource.Id = $"{org}-{app}-{language}"; - textResource.Org = org; - textResource.Language = language; - - return textResource; - } + return application; } - - /// - public Application GetApplication() + catch (AggregateException ex) { - using var activity = _telemetry?.StartGetApplicationActivity(); - try - { - ApplicationMetadata applicationMetadata = _appMetadata.GetApplicationMetadata().Result; - Application application = applicationMetadata; - if (applicationMetadata.OnEntry != null) - { - application.OnEntry = new OnEntryConfig() { Show = applicationMetadata.OnEntry.Show }; - } - - return application; - } - catch (AggregateException ex) - { - throw new ApplicationConfigException("Failed to read application metadata", ex.InnerException ?? ex); - } + throw new ApplicationConfigException("Failed to read application metadata", ex.InnerException ?? ex); } + } - /// - public string? GetApplicationXACMLPolicy() + /// + public string? GetApplicationXACMLPolicy() + { + using var activity = _telemetry?.StartClientGetApplicationXACMLPolicyActivity(); + try { - using var activity = _telemetry?.StartClientGetApplicationXACMLPolicyActivity(); - try - { - return _appMetadata.GetApplicationXACMLPolicy().Result; - } - catch (AggregateException ex) - { - _logger.LogError(ex, "Something went wrong fetching application policy"); - return null; - } + return _appMetadata.GetApplicationXACMLPolicy().Result; } - - /// - public string? GetApplicationBPMNProcess() + catch (AggregateException ex) { - using var activity = _telemetry?.StartClientGetApplicationBPMNProcessActivity(); - try - { - return _appMetadata.GetApplicationBPMNProcess().Result; - } - catch (AggregateException ex) - { - _logger.LogError(ex, "Something went wrong fetching application policy"); - return null; - } + _logger.LogError(ex, "Something went wrong fetching application policy"); + return null; } + } - /// - public string GetModelJsonSchema(string modelId) + /// + public string? GetApplicationBPMNProcess() + { + using var activity = _telemetry?.StartClientGetApplicationBPMNProcessActivity(); + try { - using var activity = _telemetry?.StartGetModelJsonSchemaActivity(); - string legalPath = $"{_settings.AppBasePath}{_settings.ModelsFolder}"; - string filename = $"{legalPath}{modelId}.{_settings.JsonSchemaFileName}"; - PathHelper.EnsureLegalPath(legalPath, filename); - - string filedata = File.ReadAllText(filename, Encoding.UTF8); - - return filedata; + return _appMetadata.GetApplicationBPMNProcess().Result; } - - /// - public string? GetPrefillJson(string dataModelName = "ServiceModel") + catch (AggregateException ex) { - using var activity = _telemetry?.StartGetPrefillJsonActivity(); - string legalPath = _settings.AppBasePath + _settings.ModelsFolder; - string filename = legalPath + dataModelName + ".prefill.json"; - PathHelper.EnsureLegalPath(legalPath, filename); + _logger.LogError(ex, "Something went wrong fetching application policy"); + return null; + } + } - string? filedata = null; - if (File.Exists(filename)) - { - filedata = File.ReadAllText(filename, Encoding.UTF8); - } + /// + public string GetModelJsonSchema(string modelId) + { + using var activity = _telemetry?.StartGetModelJsonSchemaActivity(); + string legalPath = $"{_settings.AppBasePath}{_settings.ModelsFolder}"; + string filename = $"{legalPath}{modelId}.{_settings.JsonSchemaFileName}"; + PathHelper.EnsureLegalPath(legalPath, filename); - return filedata; - } + string filedata = File.ReadAllText(filename, Encoding.UTF8); - /// - public string? GetLayoutSettingsString() - { - using var activity = _telemetry?.StartGetLayoutSettingsStringActivity(); - string filename = Path.Join( - _settings.AppBasePath, - _settings.UiFolder, - _settings.FormLayoutSettingsFileName - ); - string? filedata = null; - if (File.Exists(filename)) - { - filedata = File.ReadAllText(filename, Encoding.UTF8); - } + return filedata; + } - return filedata; - } + /// + public string? GetPrefillJson(string dataModelName = "ServiceModel") + { + using var activity = _telemetry?.StartGetPrefillJsonActivity(); + string legalPath = _settings.AppBasePath + _settings.ModelsFolder; + string filename = legalPath + dataModelName + ".prefill.json"; + PathHelper.EnsureLegalPath(legalPath, filename); - /// - public LayoutSettings GetLayoutSettings() + string? filedata = null; + if (File.Exists(filename)) { - using var activity = _telemetry?.StartGetLayoutSettingsActivity(); - string filename = Path.Join( - _settings.AppBasePath, - _settings.UiFolder, - _settings.FormLayoutSettingsFileName - ); - if (File.Exists(filename)) - { - var filedata = File.ReadAllText(filename, Encoding.UTF8); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - LayoutSettings layoutSettings = JsonConvert.DeserializeObject(filedata)!; - return layoutSettings; - } + filedata = File.ReadAllText(filename, Encoding.UTF8); + } - throw new FileNotFoundException($"Could not find layoutsettings file: {filename}"); + return filedata; + } + + /// + public string? GetLayoutSettingsString() + { + using var activity = _telemetry?.StartGetLayoutSettingsStringActivity(); + string filename = Path.Join(_settings.AppBasePath, _settings.UiFolder, _settings.FormLayoutSettingsFileName); + string? filedata = null; + if (File.Exists(filename)) + { + filedata = File.ReadAllText(filename, Encoding.UTF8); } - /// - public string GetClassRefForLogicDataType(string dataType) + return filedata; + } + + /// + public LayoutSettings GetLayoutSettings() + { + using var activity = _telemetry?.StartGetLayoutSettingsActivity(); + string filename = Path.Join(_settings.AppBasePath, _settings.UiFolder, _settings.FormLayoutSettingsFileName); + if (File.Exists(filename)) { - using var activity = _telemetry?.StartGetClassRefActivity(); - Application application = GetApplication(); - string classRef = string.Empty; + var filedata = File.ReadAllText(filename, Encoding.UTF8); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + LayoutSettings layoutSettings = JsonConvert.DeserializeObject(filedata)!; + return layoutSettings; + } - DataType? element = application.DataTypes.SingleOrDefault(d => d.Id.Equals(dataType)); + throw new FileNotFoundException($"Could not find layoutsettings file: {filename}"); + } - if (element != null) - { - classRef = element.AppLogic.ClassRef; - } + /// + public string GetClassRefForLogicDataType(string dataType) + { + using var activity = _telemetry?.StartGetClassRefActivity(); + Application application = GetApplication(); + string classRef = string.Empty; - return classRef; - } + DataType? element = application.DataTypes.SingleOrDefault(d => d.Id.Equals(dataType)); - /// - public string GetLayouts() + if (element != null) { - using var activity = _telemetry?.StartGetLayoutsActivity(); - Dictionary layouts = new Dictionary(); + classRef = element.AppLogic.ClassRef; + } - // Get FormLayout.json if it exists and return it (for backwards compatibility) - string fileName = _settings.AppBasePath + _settings.UiFolder + "FormLayout.json"; - if (File.Exists(fileName)) - { - string fileData = File.ReadAllText(fileName, Encoding.UTF8); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - layouts.Add("FormLayout", JsonConvert.DeserializeObject(fileData)!); - return JsonConvert.SerializeObject(layouts); - } + return classRef; + } - string layoutsPath = _settings.AppBasePath + _settings.UiFolder + "layouts/"; - if (Directory.Exists(layoutsPath)) - { - foreach (string file in Directory.GetFiles(layoutsPath)) - { - string data = File.ReadAllText(file, Encoding.UTF8); - string name = file.Replace(layoutsPath, string.Empty).Replace(".json", string.Empty); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - layouts.Add(name, JsonConvert.DeserializeObject(data)!); - } - } + /// + public string GetLayouts() + { + using var activity = _telemetry?.StartGetLayoutsActivity(); + Dictionary layouts = new Dictionary(); + // Get FormLayout.json if it exists and return it (for backwards compatibility) + string fileName = _settings.AppBasePath + _settings.UiFolder + "FormLayout.json"; + if (File.Exists(fileName)) + { + string fileData = File.ReadAllText(fileName, Encoding.UTF8); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + layouts.Add("FormLayout", JsonConvert.DeserializeObject(fileData)!); return JsonConvert.SerializeObject(layouts); } - /// - public string GetLayoutSets() + string layoutsPath = _settings.AppBasePath + _settings.UiFolder + "layouts/"; + if (Directory.Exists(layoutsPath)) { - using var activity = _telemetry?.StartGetLayoutSetsActivity(); - string filename = Path.Join(_settings.AppBasePath, _settings.UiFolder, _settings.LayoutSetsFileName); - string? filedata = null; - if (File.Exists(filename)) + foreach (string file in Directory.GetFiles(layoutsPath)) { - filedata = File.ReadAllText(filename, Encoding.UTF8); + string data = File.ReadAllText(file, Encoding.UTF8); + string name = file.Replace(layoutsPath, string.Empty).Replace(".json", string.Empty); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + layouts.Add(name, JsonConvert.DeserializeObject(data)!); } -#nullable disable - return filedata; -#nullable restore } - /// - public LayoutSets? GetLayoutSet() - { - using var activity = _telemetry?.StartGetLayoutSetActivity(); - string? layoutSetsString = GetLayoutSets(); - if (layoutSetsString is not null) - { - return System.Text.Json.JsonSerializer.Deserialize( - layoutSetsString, - _jsonSerializerOptions - ); - } + return JsonConvert.SerializeObject(layouts); + } - return null; + /// + public string GetLayoutSets() + { + using var activity = _telemetry?.StartGetLayoutSetsActivity(); + string filename = Path.Join(_settings.AppBasePath, _settings.UiFolder, _settings.LayoutSetsFileName); + string? filedata = null; + if (File.Exists(filename)) + { + filedata = File.ReadAllText(filename, Encoding.UTF8); } +#nullable disable + return filedata; +#nullable restore + } - /// - public LayoutSet? GetLayoutSetForTask(string taskId) + /// + public LayoutSets? GetLayoutSet() + { + using var activity = _telemetry?.StartGetLayoutSetActivity(); + string? layoutSetsString = GetLayoutSets(); + if (layoutSetsString is not null) { - using var activity = _telemetry?.StartGetLayoutSetsForTaskActivity(); - var sets = GetLayoutSet(); - return sets?.Sets?.FirstOrDefault(s => s?.Tasks?.Contains(taskId) ?? false); + return System.Text.Json.JsonSerializer.Deserialize(layoutSetsString, _jsonSerializerOptions); } - /// - public string GetLayoutsForSet(string layoutSetId) - { - using var activity = _telemetry?.StartGetLayoutsForSetActivity(); - Dictionary layouts = new Dictionary(); + return null; + } - string layoutsPath = _settings.AppBasePath + _settings.UiFolder + layoutSetId + "/layouts/"; - if (Directory.Exists(layoutsPath)) - { - foreach (string file in Directory.GetFiles(layoutsPath)) - { - string data = File.ReadAllText(file, Encoding.UTF8); - string name = file.Replace(layoutsPath, string.Empty).Replace(".json", string.Empty); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - layouts.Add(name, JsonConvert.DeserializeObject(data)!); - } - } + /// + public LayoutSet? GetLayoutSetForTask(string taskId) + { + using var activity = _telemetry?.StartGetLayoutSetsForTaskActivity(); + var sets = GetLayoutSet(); + return sets?.Sets?.FirstOrDefault(s => s?.Tasks?.Contains(taskId) ?? false); + } - return JsonConvert.SerializeObject(layouts); - } + /// + public string GetLayoutsForSet(string layoutSetId) + { + using var activity = _telemetry?.StartGetLayoutsForSetActivity(); + Dictionary layouts = new Dictionary(); - /// - public LayoutModel GetLayoutModel(string? layoutSetId = null) + string layoutsPath = _settings.AppBasePath + _settings.UiFolder + layoutSetId + "/layouts/"; + if (Directory.Exists(layoutsPath)) { - using var activity = _telemetry?.StartGetLayoutModelActivity(); - string folder = Path.Join(_settings.AppBasePath, _settings.UiFolder, layoutSetId, "layouts"); - var order = GetLayoutSettingsForSet(layoutSetId)?.Pages?.Order; - if (order is null) + foreach (string file in Directory.GetFiles(layoutsPath)) { - throw new InvalidDataException( - "No $Pages.Order field found" + (layoutSetId is null ? "" : $" for layoutSet {layoutSetId}") - ); - } - - var layoutModel = new LayoutModel(); - foreach (var page in order) - { - var pageBytes = File.ReadAllBytes(Path.Join(folder, page + ".json")); - // Set the PageName using AsyncLocal before deserializing. - PageComponentConverter.SetAsyncLocalPageName(page); - layoutModel.Pages[page] = - System.Text.Json.JsonSerializer.Deserialize( - pageBytes.RemoveBom(), - _jsonSerializerOptions - ) ?? throw new InvalidDataException(page + ".json is \"null\""); + string data = File.ReadAllText(file, Encoding.UTF8); + string name = file.Replace(layoutsPath, string.Empty).Replace(".json", string.Empty); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + layouts.Add(name, JsonConvert.DeserializeObject(data)!); } - - return layoutModel; } - /// - public string? GetLayoutSettingsStringForSet(string layoutSetId) + return JsonConvert.SerializeObject(layouts); + } + + /// + public LayoutModel GetLayoutModel(string? layoutSetId = null) + { + using var activity = _telemetry?.StartGetLayoutModelActivity(); + string folder = Path.Join(_settings.AppBasePath, _settings.UiFolder, layoutSetId, "layouts"); + var order = GetLayoutSettingsForSet(layoutSetId)?.Pages?.Order; + if (order is null) { - using var activity = _telemetry?.StartGetLayoutSettingsStringForSetActivity(); - string filename = Path.Join( - _settings.AppBasePath, - _settings.UiFolder, - layoutSetId, - _settings.FormLayoutSettingsFileName + throw new InvalidDataException( + "No $Pages.Order field found" + (layoutSetId is null ? "" : $" for layoutSet {layoutSetId}") ); - string? filedata = null; - if (File.Exists(filename)) - { - filedata = File.ReadAllText(filename, Encoding.UTF8); - } - - return filedata; } - /// - public LayoutSettings? GetLayoutSettingsForSet(string? layoutSetId) + var layoutModel = new LayoutModel(); + foreach (var page in order) { - using var activity = _telemetry?.StartGetLayoutSettingsForSetActivity(); - string filename = Path.Join( - _settings.AppBasePath, - _settings.UiFolder, - layoutSetId, - _settings.FormLayoutSettingsFileName - ); - if (File.Exists(filename)) - { - string? filedata = null; - filedata = File.ReadAllText(filename, Encoding.UTF8); - LayoutSettings? layoutSettings = JsonConvert.DeserializeObject(filedata); - return layoutSettings; - } - - return null; + var pageBytes = File.ReadAllBytes(Path.Join(folder, page + ".json")); + // Set the PageName using AsyncLocal before deserializing. + PageComponentConverter.SetAsyncLocalPageName(page); + layoutModel.Pages[page] = + System.Text.Json.JsonSerializer.Deserialize( + pageBytes.RemoveBom(), + _jsonSerializerOptions + ) ?? throw new InvalidDataException(page + ".json is \"null\""); } - /// - public byte[] GetRuleConfigurationForSet(string id) + return layoutModel; + } + + /// + public string? GetLayoutSettingsStringForSet(string layoutSetId) + { + using var activity = _telemetry?.StartGetLayoutSettingsStringForSetActivity(); + string filename = Path.Join( + _settings.AppBasePath, + _settings.UiFolder, + layoutSetId, + _settings.FormLayoutSettingsFileName + ); + string? filedata = null; + if (File.Exists(filename)) { - using var activity = _telemetry?.StartGetRuleConfigurationForSetActivity(); - string legalPath = Path.Join(_settings.AppBasePath, _settings.UiFolder); - string filename = Path.Join(legalPath, id, _settings.RuleConfigurationJSONFileName); + filedata = File.ReadAllText(filename, Encoding.UTF8); + } - PathHelper.EnsureLegalPath(legalPath, filename); + return filedata; + } - return ReadFileByte(filename); + /// + public LayoutSettings? GetLayoutSettingsForSet(string? layoutSetId) + { + using var activity = _telemetry?.StartGetLayoutSettingsForSetActivity(); + string filename = Path.Join( + _settings.AppBasePath, + _settings.UiFolder, + layoutSetId, + _settings.FormLayoutSettingsFileName + ); + if (File.Exists(filename)) + { + string? filedata = null; + filedata = File.ReadAllText(filename, Encoding.UTF8); + LayoutSettings? layoutSettings = JsonConvert.DeserializeObject(filedata); + return layoutSettings; } - /// - public byte[] GetRuleHandlerForSet(string id) - { - using var activity = _telemetry?.StartGetRuleHandlerForSetActivity(); - string legalPath = Path.Join(_settings.AppBasePath, _settings.UiFolder); - string filename = Path.Join(legalPath, id, _settings.RuleHandlerFileName); + return null; + } - PathHelper.EnsureLegalPath(legalPath, filename); + /// + public byte[] GetRuleConfigurationForSet(string id) + { + using var activity = _telemetry?.StartGetRuleConfigurationForSetActivity(); + string legalPath = Path.Join(_settings.AppBasePath, _settings.UiFolder); + string filename = Path.Join(legalPath, id, _settings.RuleConfigurationJSONFileName); - return ReadFileByte(filename); - } + PathHelper.EnsureLegalPath(legalPath, filename); + + return ReadFileByte(filename); + } + + /// + public byte[] GetRuleHandlerForSet(string id) + { + using var activity = _telemetry?.StartGetRuleHandlerForSetActivity(); + string legalPath = Path.Join(_settings.AppBasePath, _settings.UiFolder); + string filename = Path.Join(legalPath, id, _settings.RuleHandlerFileName); - private byte[] ReadFileByte(string fileName) + PathHelper.EnsureLegalPath(legalPath, filename); + + return ReadFileByte(filename); + } + + private byte[] ReadFileByte(string fileName) + { + byte[]? filedata = null; + if (File.Exists(fileName)) { - byte[]? filedata = null; - if (File.Exists(fileName)) - { - filedata = File.ReadAllBytes(fileName); - } + filedata = File.ReadAllBytes(fileName); + } #nullable disable - return filedata; + return filedata; #nullable restore - } + } - private byte[] ReadFileContentsFromLegalPath(string legalPath, string filePath) + private byte[] ReadFileContentsFromLegalPath(string legalPath, string filePath) + { + var fullFileName = legalPath + filePath; + if (!PathHelper.ValidateLegalFilePath(legalPath, fullFileName)) { - var fullFileName = legalPath + filePath; - if (!PathHelper.ValidateLegalFilePath(legalPath, fullFileName)) - { - throw new ArgumentException("Invalid argument", nameof(filePath)); - } + throw new ArgumentException("Invalid argument", nameof(filePath)); + } - if (File.Exists(fullFileName)) - { - return File.ReadAllBytes(fullFileName); - } + if (File.Exists(fullFileName)) + { + return File.ReadAllBytes(fullFileName); + } #nullable disable - return null; + return null; #nullable restore - } + } - /// - public async Task GetFooter() + /// + public async Task GetFooter() + { + using var activity = _telemetry?.StartGetFooterActivity(); + string filename = Path.Join(_settings.AppBasePath, _settings.UiFolder, _settings.FooterFileName); + string? filedata = null; + if (File.Exists(filename)) { - using var activity = _telemetry?.StartGetFooterActivity(); - string filename = Path.Join(_settings.AppBasePath, _settings.UiFolder, _settings.FooterFileName); - string? filedata = null; - if (File.Exists(filename)) - { - filedata = await File.ReadAllTextAsync(filename, Encoding.UTF8); - } - - return filedata; + filedata = await File.ReadAllTextAsync(filename, Encoding.UTF8); } - /// - public string? GetValidationConfiguration(string modelId) - { - using var activity = _telemetry?.StartGetValidationConfigurationActivity(); - string legalPath = $"{_settings.AppBasePath}{_settings.ModelsFolder}"; - string filename = $"{legalPath}{modelId}.{_settings.ValidationConfigurationFileName}"; - PathHelper.EnsureLegalPath(legalPath, filename); + return filedata; + } - string? filedata = null; - if (File.Exists(filename)) - { - filedata = File.ReadAllText(filename, Encoding.UTF8); - } + /// + public string? GetValidationConfiguration(string modelId) + { + using var activity = _telemetry?.StartGetValidationConfigurationActivity(); + string legalPath = $"{_settings.AppBasePath}{_settings.ModelsFolder}"; + string filename = $"{legalPath}{modelId}.{_settings.ValidationConfigurationFileName}"; + PathHelper.EnsureLegalPath(legalPath, filename); - return filedata; + string? filedata = null; + if (File.Exists(filename)) + { + filedata = File.ReadAllText(filename, Encoding.UTF8); } + + return filedata; } } diff --git a/src/Altinn.App.Core/Implementation/PrefillSI.cs b/src/Altinn.App.Core/Implementation/PrefillSI.cs index 1eeb85c5c..0a839cca1 100644 --- a/src/Altinn.App.Core/Implementation/PrefillSI.cs +++ b/src/Altinn.App.Core/Implementation/PrefillSI.cs @@ -11,311 +11,309 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -namespace Altinn.App.Core.Implementation +namespace Altinn.App.Core.Implementation; + +/// +public class PrefillSI : IPrefill { + private readonly ILogger _logger; + private readonly IProfileClient _profileClient; + private readonly IAppResources _appResourcesService; + private readonly IAltinnPartyClient _altinnPartyClientClient; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly Telemetry? _telemetry; + private static readonly string _erKey = "ER"; + private static readonly string _dsfKey = "DSF"; + private static readonly string _userProfileKey = "UserProfile"; + private static readonly string _allowOverwriteKey = "allowOverwrite"; + private bool _allowOverwrite = false; + + /// + /// Creates a new instance of the class + /// + /// The logger + /// The profile client + /// The app's resource service + /// The register client + /// A service with access to the http context. + /// Telemetry for traces and metrics. + public PrefillSI( + ILogger logger, + IProfileClient profileClient, + IAppResources appResourcesService, + IAltinnPartyClient altinnPartyClientClient, + IHttpContextAccessor httpContextAccessor, + Telemetry? telemetry = null + ) + { + _logger = logger; + _profileClient = profileClient; + _appResourcesService = appResourcesService; + _altinnPartyClientClient = altinnPartyClientClient; + _httpContextAccessor = httpContextAccessor; + _telemetry = telemetry; + } + /// - public class PrefillSI : IPrefill + public void PrefillDataModel( + object dataModel, + Dictionary externalPrefill, + bool continueOnError = false + ) { - private readonly ILogger _logger; - private readonly IProfileClient _profileClient; - private readonly IAppResources _appResourcesService; - private readonly IAltinnPartyClient _altinnPartyClientClient; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly Telemetry? _telemetry; - private static readonly string ER_KEY = "ER"; - private static readonly string DSF_KEY = "DSF"; - private static readonly string USER_PROFILE_KEY = "UserProfile"; - private static readonly string ALLOW_OVERWRITE_KEY = "allowOverwrite"; - private bool allowOverwrite = false; + using var activity = _telemetry?.StartPrefillDataModelActivity(); + LoopThroughDictionaryAndAssignValuesToDataModel(externalPrefill, null, dataModel, continueOnError); + } - /// - /// Creates a new instance of the class - /// - /// The logger - /// The profile client - /// The app's resource service - /// The register client - /// A service with access to the http context. - /// Telemetry for traces and metrics. - public PrefillSI( - ILogger logger, - IProfileClient profileClient, - IAppResources appResourcesService, - IAltinnPartyClient altinnPartyClientClient, - IHttpContextAccessor httpContextAccessor, - Telemetry? telemetry = null - ) + /// + public async Task PrefillDataModel( + string partyId, + string dataModelName, + object dataModel, + Dictionary? externalPrefill = null + ) + { + using var activity = _telemetry?.StartPrefillDataModelActivity(partyId); + // Prefill from external input. Only available during instansiation + if (externalPrefill != null && externalPrefill.Count > 0) { - _logger = logger; - _profileClient = profileClient; - _appResourcesService = appResourcesService; - _altinnPartyClientClient = altinnPartyClientClient; - _httpContextAccessor = httpContextAccessor; - _telemetry = telemetry; + PrefillDataModel(dataModel, externalPrefill, true); } - /// - public void PrefillDataModel( - object dataModel, - Dictionary externalPrefill, - bool continueOnError = false - ) + string? jsonConfig = _appResourcesService.GetPrefillJson(dataModelName); + if (jsonConfig == null || jsonConfig == string.Empty) { - using var activity = _telemetry?.StartPrefillDataModelActivity(); - LoopThroughDictionaryAndAssignValuesToDataModel(externalPrefill, null, dataModel, continueOnError); + return; } - /// - public async Task PrefillDataModel( - string partyId, - string dataModelName, - object dataModel, - Dictionary? externalPrefill = null - ) + JObject prefillConfiguration = JObject.Parse(jsonConfig); + JToken? allowOverwriteToken = prefillConfiguration.SelectToken(_allowOverwriteKey); + if (allowOverwriteToken != null) { - using var activity = _telemetry?.StartPrefillDataModelActivity(partyId); - // Prefill from external input. Only available during instansiation - if (externalPrefill != null && externalPrefill.Count > 0) - { - PrefillDataModel(dataModel, externalPrefill, true); - } - - string? jsonConfig = _appResourcesService.GetPrefillJson(dataModelName); - if (jsonConfig == null || jsonConfig == string.Empty) - { - return; - } - - JObject prefillConfiguration = JObject.Parse(jsonConfig); - JToken? allowOverwriteToken = prefillConfiguration.SelectToken(ALLOW_OVERWRITE_KEY); - if (allowOverwriteToken != null) - { - allowOverwrite = allowOverwriteToken.ToObject(); - } - - Party? party = await _altinnPartyClientClient.GetParty(int.Parse(partyId)); - if (party == null) - { - string errorMessage = $"Could find party for partyId: {partyId}"; - _logger.LogError(errorMessage); - throw new Exception(errorMessage); - } + _allowOverwrite = allowOverwriteToken.ToObject(); + } - // Prefill from user profile - JToken? profilePrefill = prefillConfiguration.SelectToken(USER_PROFILE_KEY); + Party? party = await _altinnPartyClientClient.GetParty(int.Parse(partyId)); + if (party == null) + { + string errorMessage = $"Could find party for partyId: {partyId}"; + _logger.LogError(errorMessage); + throw new Exception(errorMessage); + } - if (profilePrefill != null) - { - var userProfileDict = - profilePrefill.ToObject>() - ?? throw new Exception("Unexpectedly failed to convert profilePrefill JToken to dictionary"); + // Prefill from user profile + JToken? profilePrefill = prefillConfiguration.SelectToken(_userProfileKey); - if (userProfileDict.Count > 0) - { - var httpContext = - _httpContextAccessor.HttpContext - ?? throw new Exception( - "Could not get HttpContext - must be in a request context to get current user" - ); - int userId = AuthenticationHelper.GetUserId(httpContext); - UserProfile? userProfile = userId != 0 ? await _profileClient.GetUserProfile(userId) : null; - if (userProfile != null) - { - JObject userProfileJsonObject = JObject.FromObject(userProfile); - _logger.LogInformation($"Started prefill from {USER_PROFILE_KEY}"); - LoopThroughDictionaryAndAssignValuesToDataModel( - SwapKeyValuesForPrefill(userProfileDict), - userProfileJsonObject, - dataModel - ); - } - else - { - string errorMessage = - $"Could not prefill from {USER_PROFILE_KEY}, user profile is not defined."; - _logger.LogError(errorMessage); - } - } - } + if (profilePrefill != null) + { + var userProfileDict = + profilePrefill.ToObject>() + ?? throw new Exception("Unexpectedly failed to convert profilePrefill JToken to dictionary"); - // Prefill from ER (enhetsregisteret) - JToken? enhetsregisteret = prefillConfiguration.SelectToken(ER_KEY); - if (enhetsregisteret != null) + if (userProfileDict.Count > 0) { - var enhetsregisterPrefill = - enhetsregisteret.ToObject>() - ?? throw new Exception("Unexpectedly failed to convert enhetsregisteret JToken to dictionary"); - - if (enhetsregisterPrefill.Count > 0) + var httpContext = + _httpContextAccessor.HttpContext + ?? throw new Exception( + "Could not get HttpContext - must be in a request context to get current user" + ); + int userId = AuthenticationHelper.GetUserId(httpContext); + UserProfile? userProfile = userId != 0 ? await _profileClient.GetUserProfile(userId) : null; + if (userProfile != null) { - Organization org = party.Organization; - if (org != null) - { - JObject orgJsonObject = JObject.FromObject(org); - _logger.LogInformation($"Started prefill from {ER_KEY}"); - LoopThroughDictionaryAndAssignValuesToDataModel( - SwapKeyValuesForPrefill(enhetsregisterPrefill), - orgJsonObject, - dataModel - ); - } - else - { - string errorMessage = $"Could not prefill from {ER_KEY}, organisation is not defined."; - _logger.LogError(errorMessage); - } + JObject userProfileJsonObject = JObject.FromObject(userProfile); + _logger.LogInformation($"Started prefill from {_userProfileKey}"); + LoopThroughDictionaryAndAssignValuesToDataModel( + SwapKeyValuesForPrefill(userProfileDict), + userProfileJsonObject, + dataModel + ); } - } - - // Prefill from DSF (det sentrale folkeregisteret) - JToken? folkeregisteret = prefillConfiguration.SelectToken(DSF_KEY); - if (folkeregisteret != null) - { - var folkeregisterPrefill = - folkeregisteret.ToObject>() - ?? throw new Exception("Unexpectedly failed to convert folkeregisteret JToken to dictionary"); - - if (folkeregisterPrefill.Count > 0) + else { - Person person = party.Person; - if (person != null) - { - JObject personJsonObject = JObject.FromObject(person); - _logger.LogInformation($"Started prefill from {DSF_KEY}"); - LoopThroughDictionaryAndAssignValuesToDataModel( - SwapKeyValuesForPrefill(folkeregisterPrefill), - personJsonObject, - dataModel - ); - } - else - { - string errorMessage = $"Could not prefill from {DSF_KEY}, person is not defined."; - _logger.LogError(errorMessage); - } + string errorMessage = $"Could not prefill from {_userProfileKey}, user profile is not defined."; + _logger.LogError(errorMessage); } } } - /// - /// Recursivly navigates through the datamodel, initiating objects if needed, and assigns the value to the target field - /// - private void AssignValueToDataModel( - string[] keys, - JToken? value, - object currentObject, - int index = 0, - bool continueOnError = false - ) + // Prefill from ER (enhetsregisteret) + JToken? enhetsregisteret = prefillConfiguration.SelectToken(_erKey); + if (enhetsregisteret != null) { - string key = keys[index]; - bool isLastKey = (keys.Length - 1) == index; - Type current = currentObject.GetType(); - PropertyInfo? property = current.GetProperty( - key, - BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance - ); + var enhetsregisterPrefill = + enhetsregisteret.ToObject>() + ?? throw new Exception("Unexpectedly failed to convert enhetsregisteret JToken to dictionary"); - if (property == null) + if (enhetsregisterPrefill.Count > 0) { - if (!continueOnError) + Organization org = party.Organization; + if (org != null) + { + JObject orgJsonObject = JObject.FromObject(org); + _logger.LogInformation($"Started prefill from {_erKey}"); + LoopThroughDictionaryAndAssignValuesToDataModel( + SwapKeyValuesForPrefill(enhetsregisterPrefill), + orgJsonObject, + dataModel + ); + } + else { - string errorMessage = - $"Could not prefill the field {string.Join(".", keys)}, property {key} is not defined in the data model"; + string errorMessage = $"Could not prefill from {_erKey}, organisation is not defined."; _logger.LogError(errorMessage); - throw new Exception(errorMessage); } } - else - { - object? propertyValue = property.GetValue(currentObject, null); - if (isLastKey) - { - if (propertyValue == null || allowOverwrite) - { - ArgumentNullException.ThrowIfNull(value); + } - // create instance of the property type defined in the datamodel - var instance = value.ToObject(property.PropertyType); + // Prefill from DSF (det sentrale folkeregisteret) + JToken? folkeregisteret = prefillConfiguration.SelectToken(_dsfKey); + if (folkeregisteret != null) + { + var folkeregisterPrefill = + folkeregisteret.ToObject>() + ?? throw new Exception("Unexpectedly failed to convert folkeregisteret JToken to dictionary"); - // assign the value - property.SetValue(currentObject, instance); - } - else - { - // The target field has a value, and we do not have permission to overwrite values - } + if (folkeregisterPrefill.Count > 0) + { + Person person = party.Person; + if (person != null) + { + JObject personJsonObject = JObject.FromObject(person); + _logger.LogInformation($"Started prefill from {_dsfKey}"); + LoopThroughDictionaryAndAssignValuesToDataModel( + SwapKeyValuesForPrefill(folkeregisterPrefill), + personJsonObject, + dataModel + ); } else { - if (propertyValue == null) - { - // the object does not exsist, create a new one with the property type - propertyValue = - Activator.CreateInstance(property.PropertyType) - ?? throw new Exception( - $"Could not create instance of type {property.PropertyType.Name} while prefilling" - ); - property.SetValue(currentObject, propertyValue, null); - } - - // recursively assign values - // TODO: handle Nullable (nullable value types), propertyValue may be null here - // due to Activator.CreateInstance above. Right now there is an exception - // but we could handle this better - AssignValueToDataModel(keys, value, propertyValue, index + 1, continueOnError); + string errorMessage = $"Could not prefill from {_dsfKey}, person is not defined."; + _logger.LogError(errorMessage); } } } + } - /// - /// Loops through the key-value dictionary and assigns each value to the datamodel target field - /// - private void LoopThroughDictionaryAndAssignValuesToDataModel( - Dictionary dictionary, - JObject? sourceObject, - object serviceModel, - bool continueOnError = false - ) + /// + /// Recursivly navigates through the datamodel, initiating objects if needed, and assigns the value to the target field + /// + private void AssignValueToDataModel( + string[] keys, + JToken? value, + object currentObject, + int index = 0, + bool continueOnError = false + ) + { + string key = keys[index]; + bool isLastKey = (keys.Length - 1) == index; + Type current = currentObject.GetType(); + PropertyInfo? property = current.GetProperty( + key, + BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance + ); + + if (property == null) + { + if (!continueOnError) + { + string errorMessage = + $"Could not prefill the field {string.Join(".", keys)}, property {key} is not defined in the data model"; + _logger.LogError(errorMessage); + throw new Exception(errorMessage); + } + } + else { - foreach (KeyValuePair keyValuePair in dictionary) + object? propertyValue = property.GetValue(currentObject, null); + if (isLastKey) { - string source = keyValuePair.Value; - string target = keyValuePair.Key.Replace("-", string.Empty); - if (source == null || source == string.Empty) + if (propertyValue == null || _allowOverwrite) { - string errorMessage = $"Could not prefill, a source value was not set for target: {target}"; - _logger.LogError(errorMessage); - throw new Exception(errorMessage); - } + ArgumentNullException.ThrowIfNull(value); - if (target == null || target == string.Empty) - { - string errorMessage = $"Could not prefill, a target value was not set for source: {source}"; - _logger.LogError(errorMessage); - throw new Exception(errorMessage); - } + // create instance of the property type defined in the datamodel + var instance = value.ToObject(property.PropertyType); - JToken? sourceValue = null; - if (sourceObject != null) - { - sourceValue = sourceObject.SelectToken(source); + // assign the value + property.SetValue(currentObject, instance); } else { - sourceValue = JToken.Parse($"'{source}'"); + // The target field has a value, and we do not have permission to overwrite values + } + } + else + { + if (propertyValue == null) + { + // the object does not exsist, create a new one with the property type + propertyValue = + Activator.CreateInstance(property.PropertyType) + ?? throw new Exception( + $"Could not create instance of type {property.PropertyType.Name} while prefilling" + ); + property.SetValue(currentObject, propertyValue, null); } - _logger.LogInformation($"Source: {source}, target: {target}"); - _logger.LogInformation($"Value read from source object: {sourceValue?.ToString()}"); - string[] keys = target.Split("."); - AssignValueToDataModel(keys, sourceValue, serviceModel, 0, continueOnError); + // recursively assign values + // TODO: handle Nullable (nullable value types), propertyValue may be null here + // due to Activator.CreateInstance above. Right now there is an exception + // but we could handle this better + AssignValueToDataModel(keys, value, propertyValue, index + 1, continueOnError); } } + } - private Dictionary SwapKeyValuesForPrefill(Dictionary externalPrefil) + /// + /// Loops through the key-value dictionary and assigns each value to the datamodel target field + /// + private void LoopThroughDictionaryAndAssignValuesToDataModel( + Dictionary dictionary, + JObject? sourceObject, + object serviceModel, + bool continueOnError = false + ) + { + foreach (KeyValuePair keyValuePair in dictionary) { - return externalPrefil.ToDictionary(x => x.Value, x => x.Key); + string source = keyValuePair.Value; + string target = keyValuePair.Key.Replace("-", string.Empty); + if (source == null || source == string.Empty) + { + string errorMessage = $"Could not prefill, a source value was not set for target: {target}"; + _logger.LogError(errorMessage); + throw new Exception(errorMessage); + } + + if (target == null || target == string.Empty) + { + string errorMessage = $"Could not prefill, a target value was not set for source: {source}"; + _logger.LogError(errorMessage); + throw new Exception(errorMessage); + } + + JToken? sourceValue = null; + if (sourceObject != null) + { + sourceValue = sourceObject.SelectToken(source); + } + else + { + sourceValue = JToken.Parse($"'{source}'"); + } + + _logger.LogInformation($"Source: {source}, target: {target}"); + _logger.LogInformation($"Value read from source object: {sourceValue?.ToString()}"); + string[] keys = target.Split("."); + AssignValueToDataModel(keys, sourceValue, serviceModel, 0, continueOnError); } } + + private Dictionary SwapKeyValuesForPrefill(Dictionary externalPrefil) + { + return externalPrefil.ToDictionary(x => x.Value, x => x.Key); + } } diff --git a/src/Altinn.App.Core/Implementation/UserTokenProvider.cs b/src/Altinn.App.Core/Implementation/UserTokenProvider.cs index b0f1c9061..75607d7e1 100644 --- a/src/Altinn.App.Core/Implementation/UserTokenProvider.cs +++ b/src/Altinn.App.Core/Implementation/UserTokenProvider.cs @@ -5,40 +5,39 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Implementation +namespace Altinn.App.Core.Implementation; + +/// +/// Represents an implementation of using the HttpContext to obtain +/// the JSON Web Token needed for the application to make calls to other services. +/// +/// +/// This class is excluded from code doverage because we have no good way of mocking the HttpContext. +/// There are also very little code to test as most of the logic are in an imported package. +/// +[ExcludeFromCodeCoverage] +public class UserTokenProvider : IUserTokenProvider { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly string _jwtCookieName; + /// - /// Represents an implementation of using the HttpContext to obtain - /// the JSON Web Token needed for the application to make calls to other services. + /// Initialize a new instance of the class. /// - /// - /// This class is excluded from code doverage because we have no good way of mocking the HttpContext. - /// There are also very little code to test as most of the logic are in an imported package. - /// - [ExcludeFromCodeCoverage] - public class UserTokenProvider : IUserTokenProvider + /// A service providing access to the http context. + /// The application general settings. + public UserTokenProvider(IHttpContextAccessor httpContextAccessor, IOptions appSettings) { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly string _jwtCookieName; - - /// - /// Initialize a new instance of the class. - /// - /// A service providing access to the http context. - /// The application general settings. - public UserTokenProvider(IHttpContextAccessor httpContextAccessor, IOptions appSettings) - { - _httpContextAccessor = httpContextAccessor; - _jwtCookieName = appSettings.Value.RuntimeCookieName; - } + _httpContextAccessor = httpContextAccessor; + _jwtCookieName = appSettings.Value.RuntimeCookieName; + } - /// - /// Get the current JSON Web Token found on the HttpContext. - /// - /// The JSON Web Token of the current user. - public string GetUserToken() - { - return JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _jwtCookieName); - } + /// + /// Get the current JSON Web Token found on the HttpContext. + /// + /// The JSON Web Token of the current user. + public string GetUserToken() + { + return JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _jwtCookieName); } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs index 7e7d8b648..29b1de1ca 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs @@ -8,61 +8,54 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Infrastructure.Clients.Authentication +namespace Altinn.App.Core.Infrastructure.Clients.Authentication; + +/// +/// A client for authentication actions in Altinn Platform. +/// +public class AuthenticationClient : IAuthenticationClient { + private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly HttpClient _client; + /// - /// A client for authentication actions in Altinn Platform. + /// Initializes a new instance of the class /// - public class AuthenticationClient : IAuthenticationClient + /// The current platform settings. + /// the logger + /// The http context accessor + /// A HttpClient provided by the HttpClientFactory. + public AuthenticationClient( + IOptions platformSettings, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + HttpClient httpClient + ) { - private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly HttpClient _client; + _logger = logger; + _httpContextAccessor = httpContextAccessor; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiAuthenticationEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _client = httpClient; + } - /// - /// Initializes a new instance of the class - /// - /// The current platform settings. - /// the logger - /// The http context accessor - /// A HttpClient provided by the HttpClientFactory. - public AuthenticationClient( - IOptions platformSettings, - ILogger logger, - IHttpContextAccessor httpContextAccessor, - HttpClient httpClient - ) - { - _logger = logger; - _httpContextAccessor = httpContextAccessor; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiAuthenticationEndpoint); - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _client = httpClient; - } + /// + public async Task RefreshToken() + { + string endpointUrl = $"refresh"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, General.RuntimeCookieName); + HttpResponseMessage response = await _client.GetAsync(token, endpointUrl); - /// - public async Task RefreshToken() + if (response.StatusCode == System.Net.HttpStatusCode.OK) { - string endpointUrl = $"refresh"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - General.RuntimeCookieName - ); - HttpResponseMessage response = await _client.GetAsync(token, endpointUrl); - - if (response.StatusCode == System.Net.HttpStatusCode.OK) - { - string refreshedToken = await response.Content.ReadAsStringAsync(); - refreshedToken = refreshedToken.Replace('"', ' ').Trim(); - return refreshedToken; - } - - _logger.LogError($"Refreshing JwtToken failed with status code {response.StatusCode}"); - return string.Empty; + string refreshedToken = await response.Content.ReadAsStringAsync(); + refreshedToken = refreshedToken.Replace('"', ' ').Trim(); + return refreshedToken; } + + _logger.LogError($"Refreshing JwtToken failed with status code {response.StatusCode}"); + return string.Empty; } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs index 7224c17c0..f86ad9976 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs @@ -1,4 +1,3 @@ -using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; using Altinn.App.Core.Configuration; @@ -19,175 +18,165 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; -namespace Altinn.App.Core.Infrastructure.Clients.Authorization +namespace Altinn.App.Core.Infrastructure.Clients.Authorization; + +/// +/// Client for handling authorization actions in Altinn Platform. +/// +public class AuthorizationClient : IAuthorizationClient { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly AppSettings _settings; + private readonly HttpClient _client; + private readonly IPDP _pdp; + private readonly ILogger _logger; + private readonly Telemetry? _telemetry; + private const string ForwardedForHeaderName = "x-forwarded-for"; + /// - /// Client for handling authorization actions in Altinn Platform. + /// Initializes a new instance of the class /// - public class AuthorizationClient : IAuthorizationClient + /// The platform settings from configuration. + /// the http context accessor. + /// A Http client from the HttpClientFactory. + /// The application settings. + /// + /// the handler for logger service + /// Telemetry for traces and metrics. + public AuthorizationClient( + IOptions platformSettings, + IHttpContextAccessor httpContextAccessor, + HttpClient httpClient, + IOptionsMonitor settings, + IPDP pdp, + ILogger logger, + Telemetry? telemetry = null + ) { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly AppSettings _settings; - private readonly HttpClient _client; - private readonly IPDP _pdp; - private readonly ILogger _logger; - private readonly Telemetry? _telemetry; - private const string ForwardedForHeaderName = "x-forwarded-for"; + _httpContextAccessor = httpContextAccessor; + _settings = settings.CurrentValue; + _pdp = pdp; + _logger = logger; + _telemetry = telemetry; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiAuthorizationEndpoint); - /// - /// Initializes a new instance of the class - /// - /// The platform settings from configuration. - /// the http context accessor. - /// A Http client from the HttpClientFactory. - /// The application settings. - /// - /// the handler for logger service - /// Telemetry for traces and metrics. - public AuthorizationClient( - IOptions platformSettings, - IHttpContextAccessor httpContextAccessor, - HttpClient httpClient, - IOptionsMonitor settings, - IPDP pdp, - ILogger logger, - Telemetry? telemetry = null - ) + if (!httpClient.DefaultRequestHeaders.Contains(ForwardedForHeaderName)) { - _httpContextAccessor = httpContextAccessor; - _settings = settings.CurrentValue; - _pdp = pdp; - _logger = logger; - _telemetry = telemetry; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiAuthorizationEndpoint); - - if (!httpClient.DefaultRequestHeaders.Contains(ForwardedForHeaderName)) - { - string? clientIpAddress = _httpContextAccessor.HttpContext?.Request?.Headers?[ForwardedForHeaderName]; - httpClient.DefaultRequestHeaders.Add(ForwardedForHeaderName, clientIpAddress); - } - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _client = httpClient; + string? clientIpAddress = _httpContextAccessor.HttpContext?.Request?.Headers?[ForwardedForHeaderName]; + httpClient.DefaultRequestHeaders.Add(ForwardedForHeaderName, clientIpAddress); } + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _client = httpClient; + } - /// - public async Task?> GetPartyList(int userId) + /// + public async Task?> GetPartyList(int userId) + { + using var activity = _telemetry?.StartClientGetPartyListActivity(userId); + List? partyList = null; + string apiUrl = $"parties?userid={userId}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + try { - using var activity = _telemetry?.StartClientGetPartyListActivity(userId); - List? partyList = null; - string apiUrl = $"parties?userid={userId}"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - try - { - HttpResponseMessage response = await _client.GetAsync(token, apiUrl); + HttpResponseMessage response = await _client.GetAsync(token, apiUrl); - if (response.StatusCode == System.Net.HttpStatusCode.OK) - { - string partyListData = await response.Content.ReadAsStringAsync(); - partyList = JsonConvert.DeserializeObject>(partyListData); - } - } - catch (Exception e) + if (response.StatusCode == System.Net.HttpStatusCode.OK) { - _logger.LogError($"Unable to retrieve party list. An error occured {e.Message}"); + string partyListData = await response.Content.ReadAsStringAsync(); + partyList = JsonConvert.DeserializeObject>(partyListData); } - - return partyList; } - - /// - public async Task ValidateSelectedParty(int userId, int partyId) + catch (Exception e) { - using var activity = _telemetry?.StartClientValidateSelectedPartyActivity(userId, partyId); - bool? result; - string apiUrl = $"parties/{partyId}/validate?userid={userId}"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); + _logger.LogError($"Unable to retrieve party list. An error occured {e.Message}"); + } - HttpResponseMessage response = await _client.GetAsync(token, apiUrl); + return partyList; + } - if (response.StatusCode == System.Net.HttpStatusCode.OK) - { - string responseData = await response.Content.ReadAsStringAsync(); - result = JsonConvert.DeserializeObject(responseData); - } - else - { - _logger.LogError( - $"Validating selected party {partyId} for user {userId} failed with statuscode {response.StatusCode}" - ); - result = null; - } + /// + public async Task ValidateSelectedParty(int userId, int partyId) + { + using var activity = _telemetry?.StartClientValidateSelectedPartyActivity(userId, partyId); + bool? result; + string apiUrl = $"parties/{partyId}/validate?userid={userId}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); - return result; - } + HttpResponseMessage response = await _client.GetAsync(token, apiUrl); - /// - public async Task AuthorizeAction( - AppIdentifier appIdentifier, - InstanceIdentifier instanceIdentifier, - ClaimsPrincipal user, - string action, - string? taskId = null - ) + if (response.StatusCode == System.Net.HttpStatusCode.OK) { - using var activity = _telemetry?.StartClientAuthorizeActionActivity(instanceIdentifier, action, taskId); - XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest( - appIdentifier.Org, - appIdentifier.App, - user, - action, - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - taskId + string responseData = await response.Content.ReadAsStringAsync(); + result = JsonConvert.DeserializeObject(responseData); + } + else + { + _logger.LogError( + $"Validating selected party {partyId} for user {userId} failed with statuscode {response.StatusCode}" ); - XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); - if (response?.Response == null) - { - _logger.LogWarning( - "Failed to get decision from pdp: {SerializeObject}", - JsonConvert.SerializeObject(request) - ); - return false; - } + result = null; + } + + return result; + } - bool authorized = DecisionHelper.ValidatePdpDecision(response.Response, user); - return authorized; + /// + public async Task AuthorizeAction( + AppIdentifier appIdentifier, + InstanceIdentifier instanceIdentifier, + ClaimsPrincipal user, + string action, + string? taskId = null + ) + { + using var activity = _telemetry?.StartClientAuthorizeActionActivity(instanceIdentifier, action, taskId); + XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest( + appIdentifier.Org, + appIdentifier.App, + user, + action, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + taskId + ); + XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + if (response?.Response == null) + { + _logger.LogWarning( + "Failed to get decision from pdp: {SerializeObject}", + JsonConvert.SerializeObject(request) + ); + return false; } - /// - public async Task> AuthorizeActions( - Instance instance, - ClaimsPrincipal user, - List actions - ) + bool authorized = DecisionHelper.ValidatePdpDecision(response.Response, user); + return authorized; + } + + /// + public async Task> AuthorizeActions( + Instance instance, + ClaimsPrincipal user, + List actions + ) + { + using var activity = _telemetry?.StartClientAuthorizeActionsActivity(instance); + XacmlJsonRequestRoot request = MultiDecisionHelper.CreateMultiDecisionRequest(user, instance, actions); + XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + if (response?.Response == null) { - using var activity = _telemetry?.StartClientAuthorizeActionsActivity(instance); - XacmlJsonRequestRoot request = MultiDecisionHelper.CreateMultiDecisionRequest(user, instance, actions); - XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); - if (response?.Response == null) - { - _logger.LogWarning( - "Failed to get decision from pdp: {SerializeObject}", - JsonConvert.SerializeObject(request) - ); - return new Dictionary(); - } - Dictionary actionsResult = new Dictionary(); - foreach (var action in actions) - { - actionsResult.Add(action, false); - } - return MultiDecisionHelper.ValidatePdpMultiDecision(actionsResult, response.Response, user); + _logger.LogWarning( + "Failed to get decision from pdp: {SerializeObject}", + JsonConvert.SerializeObject(request) + ); + return new Dictionary(); + } + Dictionary actionsResult = new Dictionary(); + foreach (var action in actions) + { + actionsResult.Add(action, false); } + return MultiDecisionHelper.ValidatePdpMultiDecision(actionsResult, response.Response, user); } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs index 56b48b576..643255831 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs @@ -15,108 +15,101 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Infrastructure.Clients.Events +namespace Altinn.App.Core.Infrastructure.Clients.Events; + +/// +/// A client for handling actions on events in Altinn Platform. +/// +public class EventsClient : IEventsClient { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly AppSettings _settings; + private readonly GeneralSettings _generalSettings; + private readonly HttpClient _client; + private readonly Telemetry? _telemetry; + private readonly IAccessTokenGenerator _accessTokenGenerator; + private readonly IAppMetadata _appMetadata; + /// - /// A client for handling actions on events in Altinn Platform. + /// Initializes a new instance of the class. /// - public class EventsClient : IEventsClient + /// The platform settings. + /// The http context accessor. + /// A HttpClient. + /// The access token generator service. + /// The app metadata service + /// The application settings. + /// The general settings of the application. + /// Telemetry for metrics and traces. + public EventsClient( + IOptions platformSettings, + IHttpContextAccessor httpContextAccessor, + HttpClient httpClient, + IAccessTokenGenerator accessTokenGenerator, + IAppMetadata appMetadata, + IOptionsMonitor settings, + IOptions generalSettings, + Telemetry? telemetry = null + ) { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly AppSettings _settings; - private readonly GeneralSettings _generalSettings; - private readonly HttpClient _client; - private readonly Telemetry? _telemetry; - private readonly IAccessTokenGenerator _accessTokenGenerator; - private readonly IAppMetadata _appMetadata; + _httpContextAccessor = httpContextAccessor; + _settings = settings.CurrentValue; + _generalSettings = generalSettings.Value; + _accessTokenGenerator = accessTokenGenerator; + _appMetadata = appMetadata; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiEventsEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _client = httpClient; + _telemetry = telemetry; + } - /// - /// Initializes a new instance of the class. - /// - /// The platform settings. - /// The http context accessor. - /// A HttpClient. - /// The access token generator service. - /// The app metadata service - /// The application settings. - /// The general settings of the application. - /// Telemetry for metrics and traces. - public EventsClient( - IOptions platformSettings, - IHttpContextAccessor httpContextAccessor, - HttpClient httpClient, - IAccessTokenGenerator accessTokenGenerator, - IAppMetadata appMetadata, - IOptionsMonitor settings, - IOptions generalSettings, - Telemetry? telemetry = null - ) + /// + public async Task AddEvent(string eventType, Instance instance) + { + using var activity = _telemetry?.StartAddEventActivity(instance); + string? alternativeSubject = null; + if (!string.IsNullOrWhiteSpace(instance.InstanceOwner.OrganisationNumber)) { - _httpContextAccessor = httpContextAccessor; - _settings = settings.CurrentValue; - _generalSettings = generalSettings.Value; - _accessTokenGenerator = accessTokenGenerator; - _appMetadata = appMetadata; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiEventsEndpoint); - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _client = httpClient; - _telemetry = telemetry; + alternativeSubject = $"/org/{instance.InstanceOwner.OrganisationNumber}"; } - /// - public async Task AddEvent(string eventType, Instance instance) + if (!string.IsNullOrWhiteSpace(instance.InstanceOwner.PersonNumber)) { - using var activity = _telemetry?.StartAddEventActivity(instance); - string? alternativeSubject = null; - if (!string.IsNullOrWhiteSpace(instance.InstanceOwner.OrganisationNumber)) - { - alternativeSubject = $"/org/{instance.InstanceOwner.OrganisationNumber}"; - } - - if (!string.IsNullOrWhiteSpace(instance.InstanceOwner.PersonNumber)) - { - alternativeSubject = $"/person/{instance.InstanceOwner.PersonNumber}"; - } - - var baseUrl = _generalSettings.FormattedExternalAppBaseUrl(new AppIdentifier(instance)); + alternativeSubject = $"/person/{instance.InstanceOwner.PersonNumber}"; + } - CloudEvent cloudEvent = new CloudEvent - { - Subject = $"/party/{instance.InstanceOwner.PartyId}", - Type = eventType, - AlternativeSubject = alternativeSubject, - Time = DateTime.UtcNow, - SpecVersion = "1.0", - Source = new Uri($"{baseUrl}instances/{instance.Id}") - }; - Application app = await _appMetadata.GetApplicationMetadata(); - string accessToken = _accessTokenGenerator.GenerateAccessToken(app?.Org, app?.Id.Split("/")[1]); + var baseUrl = _generalSettings.FormattedExternalAppBaseUrl(new AppIdentifier(instance)); - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); + CloudEvent cloudEvent = new CloudEvent + { + Subject = $"/party/{instance.InstanceOwner.PartyId}", + Type = eventType, + AlternativeSubject = alternativeSubject, + Time = DateTime.UtcNow, + SpecVersion = "1.0", + Source = new Uri($"{baseUrl}instances/{instance.Id}") + }; + Application app = await _appMetadata.GetApplicationMetadata(); + string accessToken = _accessTokenGenerator.GenerateAccessToken(app?.Org, app?.Id.Split("/")[1]); - string serializedCloudEvent = JsonSerializer.Serialize(cloudEvent); + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); - HttpResponseMessage response = await _client.PostAsync( - token, - "app", - new StringContent(serializedCloudEvent, Encoding.UTF8, "application/json"), - accessToken - ); + string serializedCloudEvent = JsonSerializer.Serialize(cloudEvent); - if (response.IsSuccessStatusCode) - { - string eventId = await response.Content.ReadAsStringAsync(); - return eventId; - } + HttpResponseMessage response = await _client.PostAsync( + token, + "app", + new StringContent(serializedCloudEvent, Encoding.UTF8, "application/json"), + accessToken + ); - throw await PlatformHttpException.CreateAsync(response); + if (response.IsSuccessStatusCode) + { + string eventId = await response.Content.ReadAsStringAsync(); + return eventId; } + + throw await PlatformHttpException.CreateAsync(response); } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsSubscriptionClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsSubscriptionClient.cs index fe016fb01..8cef0e9d4 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsSubscriptionClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsSubscriptionClient.cs @@ -8,98 +8,91 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Infrastructure.Clients.Events +namespace Altinn.App.Core.Infrastructure.Clients.Events; + +/// +/// A client for handling actions on events in Altinn Platform. +/// +public class EventsSubscriptionClient : IEventsSubscription { + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { PropertyNameCaseInsensitive = true }; + + private readonly PlatformSettings _platformSettings; + private readonly GeneralSettings _generalSettings; + private readonly HttpClient _client; + private readonly IEventSecretCodeProvider _secretCodeProvider; + private readonly ILogger _logger; + /// - /// A client for handling actions on events in Altinn Platform. + /// Initializes a new instance of the class. /// - public class EventsSubscriptionClient : IEventsSubscription + public EventsSubscriptionClient( + IOptions platformSettings, + HttpClient httpClient, + IOptions generalSettings, + IEventSecretCodeProvider secretCodeProvider, + ILogger logger + ) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() { PropertyNameCaseInsensitive = true }; - - private readonly PlatformSettings _platformSettings; - private readonly GeneralSettings _generalSettings; - private readonly HttpClient _client; - private readonly IEventSecretCodeProvider _secretCodeProvider; - private readonly ILogger _logger; + _platformSettings = platformSettings.Value; + _generalSettings = generalSettings.Value; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiEventsEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _client = httpClient; + _secretCodeProvider = secretCodeProvider; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - public EventsSubscriptionClient( - IOptions platformSettings, - HttpClient httpClient, - IOptions generalSettings, - IEventSecretCodeProvider secretCodeProvider, - ILogger logger - ) - { - _platformSettings = platformSettings.Value; - _generalSettings = generalSettings.Value; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiEventsEndpoint); - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _client = httpClient; - _secretCodeProvider = secretCodeProvider; - _logger = logger; - } + /// + /// Creates a subscription on behalf of the org/app for the specified event type. + /// + /// The organization subscribing to the event. + /// The application the subscription should be deliverd to, will be combinded with org. + /// The event type to subscribe to. + /// Source filter will be automatially added, and set to the url of the application. + /// The created + public async Task AddSubscription(string org, string app, string eventType) + { + var appBaseUrl = _generalSettings.FormattedExternalAppBaseUrl(new Models.AppIdentifier(org, app)); - /// - /// Creates a subscription on behalf of the org/app for the specified event type. - /// - /// The organization subscribing to the event. - /// The application the subscription should be deliverd to, will be combinded with org. - /// The event type to subscribe to. - /// Source filter will be automatially added, and set to the url of the application. - /// The created - public async Task AddSubscription(string org, string app, string eventType) + var subscriptionRequest = new SubscriptionRequest() { - var appBaseUrl = _generalSettings.FormattedExternalAppBaseUrl(new Models.AppIdentifier(org, app)); - - var subscriptionRequest = new SubscriptionRequest() - { - TypeFilter = eventType, - EndPoint = new Uri( - $"{appBaseUrl}api/v1/eventsreceiver?code={await _secretCodeProvider.GetSecretCode()}" - ), - SourceFilter = new Uri(appBaseUrl.TrimEnd('/')) // The event system is requireing the source filter to be without trailing slash - }; + TypeFilter = eventType, + EndPoint = new Uri($"{appBaseUrl}api/v1/eventsreceiver?code={await _secretCodeProvider.GetSecretCode()}"), + SourceFilter = new Uri(appBaseUrl.TrimEnd('/')) // The event system is requireing the source filter to be without trailing slash + }; - string serializedSubscriptionRequest = JsonSerializer.Serialize(subscriptionRequest); + string serializedSubscriptionRequest = JsonSerializer.Serialize(subscriptionRequest); - _logger.LogInformation( - "About to send the following subscription request {subscriptionJson}", - serializedSubscriptionRequest - ); - HttpResponseMessage response = await _client.PostAsync( - "subscriptions", - new StringContent(serializedSubscriptionRequest, Encoding.UTF8, "application/json") - ); + _logger.LogInformation( + "About to send the following subscription request {subscriptionJson}", + serializedSubscriptionRequest + ); + HttpResponseMessage response = await _client.PostAsync( + "subscriptions", + new StringContent(serializedSubscriptionRequest, Encoding.UTF8, "application/json") + ); - if (response.IsSuccessStatusCode) - { - var content = await response.Content.ReadAsStringAsync(); - Subscription? subscription = JsonSerializer.Deserialize(content, _jsonSerializerOptions); + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + Subscription? subscription = JsonSerializer.Deserialize(content, _jsonSerializerOptions); - return subscription - ?? throw new NullReferenceException( - "Successfully added a subscription, but the returned subscription deserialized to null!" - ); - } - else - { - var content = await response.Content.ReadAsStringAsync(); - _logger.LogError( - "Unable to create subscription, received status {statusCode} with the following content {content}", - response.StatusCode, - content + return subscription + ?? throw new NullReferenceException( + "Successfully added a subscription, but the returned subscription deserialized to null!" ); - throw await PlatformHttpException.CreateAsync(response); - } + } + else + { + var content = await response.Content.ReadAsStringAsync(); + _logger.LogError( + "Unable to create subscription, received status {statusCode} with the following content {content}", + response.StatusCode, + content + ); + throw await PlatformHttpException.CreateAsync(response); } } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Events/Subscription.cs b/src/Altinn.App.Core/Infrastructure/Clients/Events/Subscription.cs index 39bac6dc3..8094fdad0 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Events/Subscription.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Events/Subscription.cs @@ -1,53 +1,52 @@ -namespace Altinn.App.Core.Infrastructure.Clients.Events +namespace Altinn.App.Core.Infrastructure.Clients.Events; + +/// +/// Class that describes an events subscription +/// +public class Subscription { /// - /// Class that describes an events subscription - /// - public class Subscription - { - /// - /// Subscription Id - /// - public int Id { get; set; } - - /// - /// Endpoint to receive matching events - /// - public Uri? EndPoint { get; set; } - - /// - /// Filter on source - /// - public Uri? SourceFilter { get; set; } - - /// - /// Filter on subject - /// - public string? SubjectFilter { get; set; } - - /// - /// Filter on alternative subject - /// - public string? AlternativeSubjectFilter { get; set; } - - /// - /// Filter for type. The different sources has different types. - /// - public string? TypeFilter { get; set; } - - /// - /// The events consumer - /// - public string? Consumer { get; set; } - - /// - /// Who created this subscription - /// - public string? CreatedBy { get; set; } - - /// - /// When subscription was created - /// - public DateTime Created { get; set; } - } + /// Subscription Id + /// + public int Id { get; set; } + + /// + /// Endpoint to receive matching events + /// + public Uri? EndPoint { get; set; } + + /// + /// Filter on source + /// + public Uri? SourceFilter { get; set; } + + /// + /// Filter on subject + /// + public string? SubjectFilter { get; set; } + + /// + /// Filter on alternative subject + /// + public string? AlternativeSubjectFilter { get; set; } + + /// + /// Filter for type. The different sources has different types. + /// + public string? TypeFilter { get; set; } + + /// + /// The events consumer + /// + public string? Consumer { get; set; } + + /// + /// Who created this subscription + /// + public string? CreatedBy { get; set; } + + /// + /// When subscription was created + /// + public DateTime Created { get; set; } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Events/SubscriptionRequest.cs b/src/Altinn.App.Core/Infrastructure/Clients/Events/SubscriptionRequest.cs index 158a770a2..6dbaabade 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Events/SubscriptionRequest.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Events/SubscriptionRequest.cs @@ -1,23 +1,22 @@ -namespace Altinn.App.Core.Infrastructure.Clients.Events +namespace Altinn.App.Core.Infrastructure.Clients.Events; + +/// +/// Class that describes the events subscription request model +/// +public class SubscriptionRequest { /// - /// Class that describes the events subscription request model + /// Endpoint to receive matching events /// - public class SubscriptionRequest - { - /// - /// Endpoint to receive matching events - /// - public Uri? EndPoint { get; set; } + public Uri? EndPoint { get; set; } - /// - /// Filter on source - /// - public Uri? SourceFilter { get; set; } + /// + /// Filter on source + /// + public Uri? SourceFilter { get; set; } - /// - /// Filter for type. The different sources has different types. - /// - public string? TypeFilter { get; set; } - } + /// + /// Filter for type. The different sources has different types. + /// + public string? TypeFilter { get; set; } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs index a76ee775a..c8bc37da3 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs @@ -6,72 +6,71 @@ using Microsoft.Azure.Services.AppAuthentication; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Infrastructure.Clients.KeyVault +namespace Altinn.App.Core.Infrastructure.Clients.KeyVault; + +/// +/// Class that handles integration with Azure Key Vault +/// +public class SecretsClient : ISecretsClient { + private readonly string _vaultUri; + private readonly AzureServiceTokenProvider _azureServiceTokenProvider; + /// - /// Class that handles integration with Azure Key Vault + /// Initializes a new instance of the class with a client using the credentials from the key vault settings. /// - public class SecretsClient : ISecretsClient + /// + /// The with information about the principal to use when getting secrets from a key vault. + /// + public SecretsClient(IOptions keyVaultSettings) { - private readonly string _vaultUri; - private readonly AzureServiceTokenProvider _azureServiceTokenProvider; - - /// - /// Initializes a new instance of the class with a client using the credentials from the key vault settings. - /// - /// - /// The with information about the principal to use when getting secrets from a key vault. - /// - public SecretsClient(IOptions keyVaultSettings) - { - string connectionString = - $"RunAs=App;AppId={keyVaultSettings.Value.ClientId};" - + $"TenantId={keyVaultSettings.Value.TenantId};" - + $"AppKey={keyVaultSettings.Value.ClientSecret}"; - _vaultUri = keyVaultSettings.Value.SecretUri; - _azureServiceTokenProvider = new AzureServiceTokenProvider(connectionString); - } + string connectionString = + $"RunAs=App;AppId={keyVaultSettings.Value.ClientId};" + + $"TenantId={keyVaultSettings.Value.TenantId};" + + $"AppKey={keyVaultSettings.Value.ClientSecret}"; + _vaultUri = keyVaultSettings.Value.SecretUri; + _azureServiceTokenProvider = new AzureServiceTokenProvider(connectionString); + } - /// - public async Task GetCertificateAsync(string certificateName) - { - using KeyVaultClient client = new KeyVaultClient( - new KeyVaultClient.AuthenticationCallback(_azureServiceTokenProvider.KeyVaultTokenCallback) - ); - CertificateBundle cert = await client.GetCertificateAsync(_vaultUri, certificateName); + /// + public async Task GetCertificateAsync(string certificateName) + { + using KeyVaultClient client = new KeyVaultClient( + new KeyVaultClient.AuthenticationCallback(_azureServiceTokenProvider.KeyVaultTokenCallback) + ); + CertificateBundle cert = await client.GetCertificateAsync(_vaultUri, certificateName); - return cert.Cer; - } + return cert.Cer; + } - /// - public async Task GetKeyAsync(string keyName) - { - using KeyVaultClient client = new KeyVaultClient( - new KeyVaultClient.AuthenticationCallback(_azureServiceTokenProvider.KeyVaultTokenCallback) - ); - KeyBundle kb = await client.GetKeyAsync(_vaultUri, keyName); + /// + public async Task GetKeyAsync(string keyName) + { + using KeyVaultClient client = new KeyVaultClient( + new KeyVaultClient.AuthenticationCallback(_azureServiceTokenProvider.KeyVaultTokenCallback) + ); + KeyBundle kb = await client.GetKeyAsync(_vaultUri, keyName); - return kb.Key; - } + return kb.Key; + } - /// - public KeyVaultClient GetKeyVaultClient() - { - KeyVaultClient client = new KeyVaultClient( - new KeyVaultClient.AuthenticationCallback(_azureServiceTokenProvider.KeyVaultTokenCallback) - ); - return client; - } + /// + public KeyVaultClient GetKeyVaultClient() + { + KeyVaultClient client = new KeyVaultClient( + new KeyVaultClient.AuthenticationCallback(_azureServiceTokenProvider.KeyVaultTokenCallback) + ); + return client; + } - /// - public async Task GetSecretAsync(string secretName) - { - using KeyVaultClient client = new KeyVaultClient( - new KeyVaultClient.AuthenticationCallback(_azureServiceTokenProvider.KeyVaultTokenCallback) - ); - SecretBundle sb = await client.GetSecretAsync(_vaultUri, secretName); + /// + public async Task GetSecretAsync(string secretName) + { + using KeyVaultClient client = new KeyVaultClient( + new KeyVaultClient.AuthenticationCallback(_azureServiceTokenProvider.KeyVaultTokenCallback) + ); + SecretBundle sb = await client.GetSecretAsync(_vaultUri, secretName); - return sb.Value; - } + return sb.Value; } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs index 9eed9b54e..042851d88 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs @@ -4,78 +4,77 @@ using Microsoft.Azure.KeyVault.WebKey; using Microsoft.Extensions.Configuration; -namespace Altinn.App.Core.Infrastructure.Clients.KeyVault +namespace Altinn.App.Core.Infrastructure.Clients.KeyVault; + +/// +/// Class that handles integration with Azure Key Vault +/// +public class SecretsLocalClient : ISecretsClient { + private readonly IConfiguration _configuration; + /// - /// Class that handles integration with Azure Key Vault + /// Initializes a new instance of the class. /// - public class SecretsLocalClient : ISecretsClient + /// IConfiguration + public SecretsLocalClient(IConfiguration configuration) { - private readonly IConfiguration _configuration; - - /// - /// Initializes a new instance of the class. - /// - /// IConfiguration - public SecretsLocalClient(IConfiguration configuration) - { - _configuration = configuration; - } + _configuration = configuration; + } - /// - public Task GetCertificateAsync(string certificateName) - { - string token = GetTokenFromSecrets(certificateName); - byte[] localCertBytes = Convert.FromBase64String(token); - return Task.FromResult(localCertBytes); - } + /// + public Task GetCertificateAsync(string certificateName) + { + string token = GetTokenFromSecrets(certificateName); + byte[] localCertBytes = Convert.FromBase64String(token); + return Task.FromResult(localCertBytes); + } - /// - public Task GetKeyAsync(string keyName) - { - string token = GetTokenFromSecrets(keyName); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - JsonWebKey key = JsonSerializer.Deserialize(token)!; - return Task.FromResult(key); - } + /// + public Task GetKeyAsync(string keyName) + { + string token = GetTokenFromSecrets(keyName); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + JsonWebKey key = JsonSerializer.Deserialize(token)!; + return Task.FromResult(key); + } - /// - public KeyVaultClient GetKeyVaultClient() - { - throw new NotSupportedException(); - } + /// + public KeyVaultClient GetKeyVaultClient() + { + throw new NotSupportedException(); + } - /// - public async Task GetSecretAsync(string secretId) - { - string token = GetTokenFromSecrets(secretId); - return await Task.FromResult(token); - } + /// + public async Task GetSecretAsync(string secretId) + { + string token = GetTokenFromSecrets(secretId); + return await Task.FromResult(token); + } - private string GetTokenFromSecrets(string secretId) => - GetTokenFromLocalSecrets(secretId) - ?? GetTokenFromConfiguration(secretId) - ?? throw new ArgumentException($"SecretId={secretId} does not exist in appsettings or secrets.json"); + private string GetTokenFromSecrets(string secretId) => + GetTokenFromLocalSecrets(secretId) + ?? GetTokenFromConfiguration(secretId) + ?? throw new ArgumentException($"SecretId={secretId} does not exist in appsettings or secrets.json"); - private string? GetTokenFromConfiguration(string tokenId) => _configuration[tokenId]; + private string? GetTokenFromConfiguration(string tokenId) => _configuration[tokenId]; - private static string? GetTokenFromLocalSecrets(string secretId) + private static string? GetTokenFromLocalSecrets(string secretId) + { + string path = Path.Combine(Directory.GetCurrentDirectory(), @"secrets.json"); + if (File.Exists(path)) { - string path = Path.Combine(Directory.GetCurrentDirectory(), @"secrets.json"); - if (File.Exists(path)) + string jsonString = File.ReadAllText(path); + var document = JsonDocument.Parse( + jsonString, + new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip } + ); + if (document.RootElement.TryGetProperty(secretId, out var jsonElement)) { - string jsonString = File.ReadAllText(path); - var document = JsonDocument.Parse( - jsonString, - new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip } - ); - if (document.RootElement.TryGetProperty(secretId, out var jsonElement)) - { - return jsonElement.GetString(); - } + return jsonElement.GetString(); } - - return null; } + + return null; } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Maskinporten/IX509CertificateProvider.cs b/src/Altinn.App.Core/Infrastructure/Clients/Maskinporten/IX509CertificateProvider.cs index 6da21baca..4a34c917d 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Maskinporten/IX509CertificateProvider.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Maskinporten/IX509CertificateProvider.cs @@ -1,19 +1,18 @@ using System.Security.Cryptography.X509Certificates; -namespace Altinn.App.Core.Infrastructure.Clients.Maskinporten +namespace Altinn.App.Core.Infrastructure.Clients.Maskinporten; + +/// +/// Provides a X509 certificate abstracted from the underlying implementation which +/// will vary based on environment. +/// +[Obsolete( + "This should only have been used to get an accesstoken from Maskinporten, and has been replaced by IMaskinportenTokenProvider." +)] +public interface IX509CertificateProvider { /// - /// Provides a X509 certificate abstracted from the underlying implementation which - /// will vary based on environment. + /// Gets the certificate. /// - [Obsolete( - "This should only have been used to get an accesstoken from Maskinporten, and has been replaced by IMaskinportenTokenProvider." - )] - public interface IX509CertificateProvider - { - /// - /// Gets the certificate. - /// - Task GetCertificate(); - } + Task GetCertificate(); } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs index c5e873ffe..8204b13c0 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs @@ -4,7 +4,6 @@ using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Pdf; using Altinn.App.Core.Models.Pdf; -using AltinnCore.Authentication.Utils; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs index d2375fe87..c75faba3a 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs @@ -14,93 +14,83 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Infrastructure.Clients.Profile +namespace Altinn.App.Core.Infrastructure.Clients.Profile; + +/// +/// A client for retrieving profiles from Altinn Platform. +/// +public class ProfileClient : IProfileClient { + private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly AppSettings _settings; + private readonly HttpClient _client; + private readonly IAppMetadata _appMetadata; + private readonly IAccessTokenGenerator _accessTokenGenerator; + private readonly Telemetry? _telemetry; + /// - /// A client for retrieving profiles from Altinn Platform. + /// Initializes a new instance of the class /// - public class ProfileClient : IProfileClient + /// the logger + /// the platform settings + /// The http context accessor + /// The application settings. + /// A HttpClient provided by the HttpClientFactory. + /// An instance of the IAppMetadata service. + /// An instance of the AccessTokenGenerator service. + /// Telemetry for traces and metrics. + public ProfileClient( + IOptions platformSettings, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IOptionsMonitor settings, + HttpClient httpClient, + IAppMetadata appMetadata, + IAccessTokenGenerator accessTokenGenerator, + Telemetry? telemetry = null + ) { - private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly AppSettings _settings; - private readonly HttpClient _client; - private readonly IAppMetadata _appMetadata; - private readonly IAccessTokenGenerator _accessTokenGenerator; - private readonly Telemetry? _telemetry; + _logger = logger; + _httpContextAccessor = httpContextAccessor; + _settings = settings.CurrentValue; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiProfileEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _client = httpClient; + _appMetadata = appMetadata; + _accessTokenGenerator = accessTokenGenerator; + _telemetry = telemetry; + } - /// - /// Initializes a new instance of the class - /// - /// the logger - /// the platform settings - /// The http context accessor - /// The application settings. - /// A HttpClient provided by the HttpClientFactory. - /// An instance of the IAppMetadata service. - /// An instance of the AccessTokenGenerator service. - /// Telemetry for traces and metrics. - public ProfileClient( - IOptions platformSettings, - ILogger logger, - IHttpContextAccessor httpContextAccessor, - IOptionsMonitor settings, - HttpClient httpClient, - IAppMetadata appMetadata, - IAccessTokenGenerator accessTokenGenerator, - Telemetry? telemetry = null - ) + /// + public async Task GetUserProfile(int userId) + { + using var activity = _telemetry?.StartGetUserProfileActivity(userId); + UserProfile? userProfile = null; + + string endpointUrl = $"users/{userId}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + HttpResponseMessage response = await _client.GetAsync( + token, + endpointUrl, + _accessTokenGenerator.GenerateAccessToken(applicationMetadata.Org, applicationMetadata.AppIdentifier.App) + ); + if (response.StatusCode == System.Net.HttpStatusCode.OK) { - _logger = logger; - _httpContextAccessor = httpContextAccessor; - _settings = settings.CurrentValue; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiProfileEndpoint); - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _client = httpClient; - _appMetadata = appMetadata; - _accessTokenGenerator = accessTokenGenerator; - _telemetry = telemetry; + userProfile = await JsonSerializerPermissive.DeserializeAsync(response.Content); } - - /// - public async Task GetUserProfile(int userId) + else { - using var activity = _telemetry?.StartGetUserProfileActivity(userId); - UserProfile? userProfile = null; - - string endpointUrl = $"users/{userId}"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - - ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - HttpResponseMessage response = await _client.GetAsync( - token, - endpointUrl, - _accessTokenGenerator.GenerateAccessToken( - applicationMetadata.Org, - applicationMetadata.AppIdentifier.App - ) + _logger.LogError( + "Getting user profile with userId {UserId} failed with statuscode {StatusCode}", + userId, + response.StatusCode ); - if (response.StatusCode == System.Net.HttpStatusCode.OK) - { - userProfile = await JsonSerializerPermissive.DeserializeAsync(response.Content); - } - else - { - _logger.LogError( - "Getting user profile with userId {UserId} failed with statuscode {StatusCode}", - userId, - response.StatusCode - ); - } - - return userProfile; } + + return userProfile; } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs index 6661d496e..d0a1e4133 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs @@ -4,54 +4,53 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Infrastructure.Clients.Profile +namespace Altinn.App.Core.Infrastructure.Clients.Profile; + +/// . +/// Decorates an implementation of IProfileClient by caching the party object. +/// If available, object is retrieved from cache without calling the service +/// +public class ProfileClientCachingDecorator : IProfileClient { - /// . - /// Decorates an implementation of IProfileClient by caching the party object. - /// If available, object is retrieved from cache without calling the service + private readonly IProfileClient _decoratedService; + private readonly IMemoryCache _memoryCache; + private readonly MemoryCacheEntryOptions _cacheOptions; + + /// + /// Initializes a new instance of the class. /// - public class ProfileClientCachingDecorator : IProfileClient + public ProfileClientCachingDecorator( + IProfileClient decoratedService, + IMemoryCache memoryCache, + IOptions _settings + ) { - private readonly IProfileClient _decoratedService; - private readonly IMemoryCache _memoryCache; - private readonly MemoryCacheEntryOptions _cacheOptions; - - /// - /// Initializes a new instance of the class. - /// - public ProfileClientCachingDecorator( - IProfileClient decoratedService, - IMemoryCache memoryCache, - IOptions _settings - ) - { - _decoratedService = decoratedService; - _memoryCache = memoryCache; + _decoratedService = decoratedService; + _memoryCache = memoryCache; - _cacheOptions = new() - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_settings.Value.ProfileCacheLifetimeSeconds) - }; - } - - /// - public async Task GetUserProfile(int userId) + _cacheOptions = new() { - string uniqueCacheKey = "User_UserId_" + userId; + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_settings.Value.ProfileCacheLifetimeSeconds) + }; + } - if (_memoryCache.TryGetValue(uniqueCacheKey, out UserProfile? user)) - { - return user; - } + /// + public async Task GetUserProfile(int userId) + { + string uniqueCacheKey = "User_UserId_" + userId; - user = await _decoratedService.GetUserProfile(userId); + if (_memoryCache.TryGetValue(uniqueCacheKey, out UserProfile? user)) + { + return user; + } - if (user != null) - { - _memoryCache.Set(uniqueCacheKey, user, _cacheOptions); - } + user = await _decoratedService.GetUserProfile(userId); - return user; + if (user != null) + { + _memoryCache.Set(uniqueCacheKey, user, _cacheOptions); } + + return user; } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs index 93c3e0e63..c32ba2c7e 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs @@ -15,143 +15,133 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Infrastructure.Clients.Register +namespace Altinn.App.Core.Infrastructure.Clients.Register; + +/// +/// A client for retrieving register data from Altinn Platform. +/// +public class AltinnPartyClient : IAltinnPartyClient { + private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly AppSettings _settings; + private readonly HttpClient _client; + private readonly IAppMetadata _appMetadata; + private readonly IAccessTokenGenerator _accessTokenGenerator; + private readonly Telemetry? _telemetry; + /// - /// A client for retrieving register data from Altinn Platform. + /// Initializes a new instance of the class /// - public class AltinnPartyClient : IAltinnPartyClient + /// The current platform settings. + /// The logger + /// The http context accessor + /// The application settings. + /// The http client + /// The app metadata service + /// The platform access token generator + /// Telemetry for metrics and traces. + public AltinnPartyClient( + IOptions platformSettings, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IOptionsMonitor settings, + HttpClient httpClient, + IAppMetadata appMetadata, + IAccessTokenGenerator accessTokenGenerator, + Telemetry? telemetry = null + ) + { + _logger = logger; + _httpContextAccessor = httpContextAccessor; + _settings = settings.CurrentValue; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiRegisterEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _client = httpClient; + _appMetadata = appMetadata; + _accessTokenGenerator = accessTokenGenerator; + _telemetry = telemetry; + } + + /// + public async Task GetParty(int partyId) { - private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly AppSettings _settings; - private readonly HttpClient _client; - private readonly IAppMetadata _appMetadata; - private readonly IAccessTokenGenerator _accessTokenGenerator; - private readonly Telemetry? _telemetry; + using var activity = _telemetry?.StartGetPartyActivity(partyId); + Party? party = null; - /// - /// Initializes a new instance of the class - /// - /// The current platform settings. - /// The logger - /// The http context accessor - /// The application settings. - /// The http client - /// The app metadata service - /// The platform access token generator - /// Telemetry for metrics and traces. - public AltinnPartyClient( - IOptions platformSettings, - ILogger logger, - IHttpContextAccessor httpContextAccessor, - IOptionsMonitor settings, - HttpClient httpClient, - IAppMetadata appMetadata, - IAccessTokenGenerator accessTokenGenerator, - Telemetry? telemetry = null - ) + string endpointUrl = $"parties/{partyId}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + HttpResponseMessage response = await _client.GetAsync( + token, + endpointUrl, + _accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App) + ); + if (response.StatusCode == HttpStatusCode.OK) { - _logger = logger; - _httpContextAccessor = httpContextAccessor; - _settings = settings.CurrentValue; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiRegisterEndpoint); - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _client = httpClient; - _appMetadata = appMetadata; - _accessTokenGenerator = accessTokenGenerator; - _telemetry = telemetry; + party = await JsonSerializerPermissive.DeserializeAsync(response.Content); } - - /// - public async Task GetParty(int partyId) + else if (response.StatusCode == HttpStatusCode.Unauthorized) { - using var activity = _telemetry?.StartGetPartyActivity(partyId); - Party? party = null; - - string endpointUrl = $"parties/{partyId}"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - HttpResponseMessage response = await _client.GetAsync( - token, - endpointUrl, - _accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App) + throw new ServiceException(HttpStatusCode.Unauthorized, "Unauthorized for party"); + } + else + { + _logger.LogError( + "// Getting party with partyID {PartyId} failed with statuscode {StatusCode}", + partyId, + response.StatusCode ); - if (response.StatusCode == HttpStatusCode.OK) - { - party = await JsonSerializerPermissive.DeserializeAsync(response.Content); - } - else if (response.StatusCode == HttpStatusCode.Unauthorized) - { - throw new ServiceException(HttpStatusCode.Unauthorized, "Unauthorized for party"); - } - else - { - _logger.LogError( - "// Getting party with partyID {PartyId} failed with statuscode {StatusCode}", - partyId, - response.StatusCode - ); - } - - return party; } - /// - public async Task LookupParty(PartyLookup partyLookup) - { - using var activity = _telemetry?.StartLookupPartyActivity(); - Party party; + return party; + } - string endpointUrl = "parties/lookup"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); + /// + public async Task LookupParty(PartyLookup partyLookup) + { + using var activity = _telemetry?.StartLookupPartyActivity(); + Party party; - StringContent content = new StringContent(JsonSerializerPermissive.Serialize(partyLookup)); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - HttpRequestMessage request = new HttpRequestMessage - { - RequestUri = new Uri(endpointUrl, UriKind.Relative), - Method = HttpMethod.Post, - Content = content - }; + string endpointUrl = "parties/lookup"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); - request.Headers.Add("Authorization", "Bearer " + token); - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - request.Headers.Add( - "PlatformAccessToken", - _accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App) - ); + StringContent content = new StringContent(JsonSerializerPermissive.Serialize(partyLookup)); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + HttpRequestMessage request = new HttpRequestMessage + { + RequestUri = new Uri(endpointUrl, UriKind.Relative), + Method = HttpMethod.Post, + Content = content + }; - HttpResponseMessage response = await _client.SendAsync(request); - if (response.StatusCode == HttpStatusCode.OK) - { - party = await JsonSerializerPermissive.DeserializeAsync(response.Content); - } - else - { - string reason = await response.Content.ReadAsStringAsync(); - _logger.LogError( - "// Getting party with orgNo: {OrgNo} or ssn: {Ssn} failed with statuscode {StatusCode} - {Reason}", - partyLookup.OrgNo, - partyLookup.Ssn, - response.StatusCode, - reason - ); + request.Headers.Add("Authorization", "Bearer " + token); + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + request.Headers.Add( + "PlatformAccessToken", + _accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App) + ); - throw await PlatformHttpException.CreateAsync(response); - } + HttpResponseMessage response = await _client.SendAsync(request); + if (response.StatusCode == HttpStatusCode.OK) + { + party = await JsonSerializerPermissive.DeserializeAsync(response.Content); + } + else + { + string reason = await response.Content.ReadAsStringAsync(); + _logger.LogError( + "// Getting party with orgNo: {OrgNo} or ssn: {Ssn} failed with statuscode {StatusCode} - {Reason}", + partyLookup.OrgNo, + partyLookup.Ssn, + response.StatusCode, + reason + ); - return party; + throw await PlatformHttpException.CreateAsync(response); } + + return party; } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs index 7b375ecef..a34ec5890 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs @@ -14,92 +14,91 @@ using Altinn.Platform.Register.Models; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Infrastructure.Clients.Register +namespace Altinn.App.Core.Infrastructure.Clients.Register; + +/// +/// Represents an implementation of that will call the Register +/// component to retrieve person information. +/// +public class PersonClient : IPersonClient { + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; + + private readonly HttpClient _httpClient; + private readonly IAppMetadata _appMetadata; + private readonly IAccessTokenGenerator _accessTokenGenerator; + private readonly IUserTokenProvider _userTokenProvider; + /// - /// Represents an implementation of that will call the Register - /// component to retrieve person information. + /// Initializes a new instance of the class. /// - public class PersonClient : IPersonClient + /// The HttpClient to be used to send requests to Register. + /// The platform settings from loaded configuration. + /// An access token generator to create an access token. + /// A service that can obtain the user JWT token. + /// The service providing appmetadata + public PersonClient( + HttpClient httpClient, + IOptions platformSettings, + IAppMetadata appMetadata, + IAccessTokenGenerator accessTokenGenerator, + IUserTokenProvider userTokenProvider + ) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; - - private readonly HttpClient _httpClient; - private readonly IAppMetadata _appMetadata; - private readonly IAccessTokenGenerator _accessTokenGenerator; - private readonly IUserTokenProvider _userTokenProvider; + _httpClient = httpClient; + _httpClient.BaseAddress = new Uri(platformSettings.Value.ApiRegisterEndpoint); + _httpClient.DefaultRequestHeaders.Add( + General.SubscriptionKeyHeaderName, + platformSettings.Value.SubscriptionKey + ); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _accessTokenGenerator = accessTokenGenerator; + _userTokenProvider = userTokenProvider; + _appMetadata = appMetadata; + } - /// - /// Initializes a new instance of the class. - /// - /// The HttpClient to be used to send requests to Register. - /// The platform settings from loaded configuration. - /// An access token generator to create an access token. - /// A service that can obtain the user JWT token. - /// The service providing appmetadata - public PersonClient( - HttpClient httpClient, - IOptions platformSettings, - IAppMetadata appMetadata, - IAccessTokenGenerator accessTokenGenerator, - IUserTokenProvider userTokenProvider - ) - { - _httpClient = httpClient; - _httpClient.BaseAddress = new Uri(platformSettings.Value.ApiRegisterEndpoint); - _httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _accessTokenGenerator = accessTokenGenerator; - _userTokenProvider = userTokenProvider; - _appMetadata = appMetadata; - } + /// + public async Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct) + { + using var request = new HttpRequestMessage(HttpMethod.Get, $"persons"); + await AddAuthHeaders(request); - /// - public async Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct) - { - using var request = new HttpRequestMessage(HttpMethod.Get, $"persons"); - await AddAuthHeaders(request); + request.Headers.Add("X-Ai-NationalIdentityNumber", nationalIdentityNumber); + request.Headers.Add("X-Ai-LastName", ConvertToBase64(lastName)); - request.Headers.Add("X-Ai-NationalIdentityNumber", nationalIdentityNumber); - request.Headers.Add("X-Ai-LastName", ConvertToBase64(lastName)); + var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, ct); - var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, ct); + return await ReadResponse(response, ct); + } - return await ReadResponse(response, ct); - } + private async Task AddAuthHeaders(HttpRequestMessage request) + { + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + string issuer = application.Org; + string appName = application.AppIdentifier.App; + request.Headers.Add("PlatformAccessToken", _accessTokenGenerator.GenerateAccessToken(issuer, appName)); + request.Headers.Add("Authorization", "Bearer " + _userTokenProvider.GetUserToken()); + } - private async Task AddAuthHeaders(HttpRequestMessage request) + private async Task ReadResponse(HttpResponseMessage response, CancellationToken ct) + { + if (response.StatusCode == HttpStatusCode.OK) { - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - string issuer = application.Org; - string appName = application.AppIdentifier.App; - request.Headers.Add("PlatformAccessToken", _accessTokenGenerator.GenerateAccessToken(issuer, appName)); - request.Headers.Add("Authorization", "Bearer " + _userTokenProvider.GetUserToken()); + return await response.Content.ReadFromJsonAsync(_jsonSerializerOptions, ct); } - private async Task ReadResponse(HttpResponseMessage response, CancellationToken ct) + if (response.StatusCode == HttpStatusCode.NotFound) { - if (response.StatusCode == HttpStatusCode.OK) - { - return await response.Content.ReadFromJsonAsync(_jsonSerializerOptions, ct); - } - - if (response.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - - throw await PlatformHttpException.CreateAsync(response); + return null; } - private static string ConvertToBase64(string text) - { - var bytes = Encoding.UTF8.GetBytes(text); - return Convert.ToBase64String(bytes); - } + throw await PlatformHttpException.CreateAsync(response); + } + + private static string ConvertToBase64(string text) + { + var bytes = Encoding.UTF8.GetBytes(text); + return Convert.ToBase64String(bytes); } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs index 872be9f62..3e94f5073 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs @@ -14,91 +14,84 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Infrastructure.Clients.Register +namespace Altinn.App.Core.Infrastructure.Clients.Register; + +/// +/// A client for retrieving ER data from Altinn Platform. +/// +public class RegisterERClient : IOrganizationClient { + private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly AppSettings _settings; + private readonly HttpClient _client; + private readonly IAppMetadata _appMetadata; + private readonly Telemetry? _telemetry; + private readonly IAccessTokenGenerator _accessTokenGenerator; + /// - /// A client for retrieving ER data from Altinn Platform. + /// Initializes a new instance of the class /// - public class RegisterERClient : IOrganizationClient + /// the logger + /// the platform settings + /// The http context accessor + /// The app settings + /// The http client + /// The platform access token generator + /// The app metadata service + /// Telemetry for metrics and traces. + public RegisterERClient( + IOptions platformSettings, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IOptionsMonitor settings, + HttpClient httpClient, + IAccessTokenGenerator accessTokenGenerator, + IAppMetadata appMetadata, + Telemetry? telemetry = null + ) { - private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly AppSettings _settings; - private readonly HttpClient _client; - private readonly IAppMetadata _appMetadata; - private readonly Telemetry? _telemetry; - private readonly IAccessTokenGenerator _accessTokenGenerator; + _logger = logger; + _httpContextAccessor = httpContextAccessor; + _settings = settings.CurrentValue; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiRegisterEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _client = httpClient; + _accessTokenGenerator = accessTokenGenerator; + _appMetadata = appMetadata; + _telemetry = telemetry; + } - /// - /// Initializes a new instance of the class - /// - /// the logger - /// the platform settings - /// The http context accessor - /// The app settings - /// The http client - /// The platform access token generator - /// The app metadata service - /// Telemetry for metrics and traces. - public RegisterERClient( - IOptions platformSettings, - ILogger logger, - IHttpContextAccessor httpContextAccessor, - IOptionsMonitor settings, - HttpClient httpClient, - IAccessTokenGenerator accessTokenGenerator, - IAppMetadata appMetadata, - Telemetry? telemetry = null - ) - { - _logger = logger; - _httpContextAccessor = httpContextAccessor; - _settings = settings.CurrentValue; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiRegisterEndpoint); - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _client = httpClient; - _accessTokenGenerator = accessTokenGenerator; - _appMetadata = appMetadata; - _telemetry = telemetry; - } + /// + public async Task GetOrganization(string OrgNr) + { + using var activity = _telemetry?.StartGetOrganizationActivity(OrgNr); + Organization? organization = null; - /// - public async Task GetOrganization(string OrgNr) - { - using var activity = _telemetry?.StartGetOrganizationActivity(OrgNr); - Organization? organization = null; + string endpointUrl = $"organizations/{OrgNr}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); - string endpointUrl = $"organizations/{OrgNr}"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); + HttpResponseMessage response = await _client.GetAsync( + token, + endpointUrl, + _accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App) + ); - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - HttpResponseMessage response = await _client.GetAsync( - token, - endpointUrl, - _accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App) + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + organization = await JsonSerializerPermissive.DeserializeAsync(response.Content); + } + else + { + _logger.LogError( + "Getting organisation with orgnr {OrgNr} failed with statuscode {StatusCode}", + OrgNr, + response.StatusCode ); - - if (response.StatusCode == System.Net.HttpStatusCode.OK) - { - organization = await JsonSerializerPermissive.DeserializeAsync(response.Content); - } - else - { - _logger.LogError( - "Getting organisation with orgnr {OrgNr} failed with statuscode {StatusCode}", - OrgNr, - response.StatusCode - ); - } - - return organization; } + + return organization; } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs index dfda83805..6e64f768d 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs @@ -8,59 +8,55 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; -namespace Altinn.App.Core.Infrastructure.Clients.Storage +namespace Altinn.App.Core.Infrastructure.Clients.Storage; + +/// +/// Client for retrieving application for Altinn Platform +/// +public class ApplicationClient : IApplicationClient { + private readonly ILogger _logger; + private readonly HttpClient _client; + /// - /// Client for retrieving application for Altinn Platform + /// Initializes a new instance of the class. /// - public class ApplicationClient : IApplicationClient + /// The current platform settings. + /// A logger. + /// An HttpClient provided by the HttpClientFactory. + public ApplicationClient( + IOptions platformSettings, + ILogger logger, + HttpClient httpClient + ) + { + _logger = logger; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + _client = httpClient; + } + + /// + public async Task GetApplication(string org, string app) { - private readonly ILogger _logger; - private readonly HttpClient _client; + string appId = $"{org}/{app}"; + + Application? application = null; + string getApplicationMetadataUrl = $"applications/{appId}"; - /// - /// Initializes a new instance of the class. - /// - /// The current platform settings. - /// A logger. - /// An HttpClient provided by the HttpClientFactory. - public ApplicationClient( - IOptions platformSettings, - ILogger logger, - HttpClient httpClient - ) + HttpResponseMessage response = await _client.GetAsync(getApplicationMetadataUrl); + if (response.StatusCode == HttpStatusCode.OK) { - _logger = logger; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); - _client = httpClient; + string applicationData = await response.Content.ReadAsStringAsync(); + application = JsonConvert.DeserializeObject(applicationData); } - - /// - public async Task GetApplication(string org, string app) + else { - string appId = $"{org}/{app}"; - - Application? application = null; - string getApplicationMetadataUrl = $"applications/{appId}"; - - HttpResponseMessage response = await _client.GetAsync(getApplicationMetadataUrl); - if (response.StatusCode == HttpStatusCode.OK) - { - string applicationData = await response.Content.ReadAsStringAsync(); - application = JsonConvert.DeserializeObject(applicationData); - } - else - { - _logger.LogError($"Unable to fetch application with application id {appId}"); - } - - return application; + _logger.LogError($"Unable to fetch application with application id {appId}"); } + + return application; } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index 73821ac39..d0e7e9119 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -19,578 +19,564 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; -namespace Altinn.App.Core.Infrastructure.Clients.Storage +namespace Altinn.App.Core.Infrastructure.Clients.Storage; + +/// +/// A client for handling actions on data in Altinn Platform. +/// +public class DataClient : IDataClient { + private readonly PlatformSettings _platformSettings; + private readonly ILogger _logger; + private readonly IUserTokenProvider _userTokenProvider; + private readonly Telemetry? _telemetry; + private readonly HttpClient _client; + /// - /// A client for handling actions on data in Altinn Platform. + /// Initializes a new data of the class. /// - public class DataClient : IDataClient + /// the platform settings + /// the logger + /// A HttpClient from the built in HttpClient factory. + /// Service to obtain json web token + /// Telemetry for traces and metrics. + public DataClient( + IOptions platformSettings, + ILogger logger, + HttpClient httpClient, + IUserTokenProvider userTokenProvider, + Telemetry? telemetry = null + ) { - private readonly PlatformSettings _platformSettings; - private readonly ILogger _logger; - private readonly IUserTokenProvider _userTokenProvider; - private readonly Telemetry? _telemetry; - private readonly HttpClient _client; - - /// - /// Initializes a new data of the class. - /// - /// the platform settings - /// the logger - /// A HttpClient from the built in HttpClient factory. - /// Service to obtain json web token - /// Telemetry for traces and metrics. - public DataClient( - IOptions platformSettings, - ILogger logger, - HttpClient httpClient, - IUserTokenProvider userTokenProvider, - Telemetry? telemetry = null - ) - { - _platformSettings = platformSettings.Value; - _logger = logger; - - httpClient.BaseAddress = new Uri(_platformSettings.ApiStorageEndpoint); - httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); - _client = httpClient; - _userTokenProvider = userTokenProvider; - _telemetry = telemetry; - } - - /// - public async Task InsertFormData( - T dataToSerialize, - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - string dataType - ) - { - using var activity = _telemetry?.StartInsertFormDataActivity(instanceGuid, instanceOwnerPartyId); - Instance instance = new() { Id = $"{instanceOwnerPartyId}/{instanceGuid}", }; - return await InsertFormData(instance, dataType, dataToSerialize, type); - } + _platformSettings = platformSettings.Value; + _logger = logger; + + httpClient.BaseAddress = new Uri(_platformSettings.ApiStorageEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + _client = httpClient; + _userTokenProvider = userTokenProvider; + _telemetry = telemetry; + } - /// - public async Task InsertFormData( - Instance instance, - string dataType, - T dataToSerialize, - Type type - ) - { - using var activity = _telemetry?.StartInsertFormDataActivity(instance); - string apiUrl = $"instances/{instance.Id}/data?dataType={dataType}"; - string token = _userTokenProvider.GetUserToken(); - DataElement dataElement; + /// + public async Task InsertFormData( + T dataToSerialize, + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + string dataType + ) + { + using var activity = _telemetry?.StartInsertFormDataActivity(instanceGuid, instanceOwnerPartyId); + Instance instance = new() { Id = $"{instanceOwnerPartyId}/{instanceGuid}", }; + return await InsertFormData(instance, dataType, dataToSerialize, type); + } - using MemoryStream stream = new MemoryStream(); - Serialize(dataToSerialize, type, stream); + /// + public async Task InsertFormData(Instance instance, string dataType, T dataToSerialize, Type type) + { + using var activity = _telemetry?.StartInsertFormDataActivity(instance); + string apiUrl = $"instances/{instance.Id}/data?dataType={dataType}"; + string token = _userTokenProvider.GetUserToken(); + DataElement dataElement; - stream.Position = 0; - StreamContent streamContent = new StreamContent(stream); - streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/xml"); - HttpResponseMessage response = await _client.PostAsync(token, apiUrl, streamContent); + using MemoryStream stream = new MemoryStream(); + Serialize(dataToSerialize, type, stream); - if (response.IsSuccessStatusCode) - { - string instanceData = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - dataElement = JsonConvert.DeserializeObject(instanceData)!; + stream.Position = 0; + StreamContent streamContent = new StreamContent(stream); + streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/xml"); + HttpResponseMessage response = await _client.PostAsync(token, apiUrl, streamContent); - return dataElement; - } + if (response.IsSuccessStatusCode) + { + string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + dataElement = JsonConvert.DeserializeObject(instanceData)!; - _logger.Log( - LogLevel.Error, - "unable to save form data for instance {InstanceId} due to response {StatusCode}", - instance.Id, - response.StatusCode - ); - throw await PlatformHttpException.CreateAsync(response); + return dataElement; } - /// - public async Task UpdateData( - T dataToSerialize, - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - Guid dataId - ) - { - using var activity = _telemetry?.StartUpdateDataActivity(instanceGuid, instanceOwnerPartyId); - string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; - string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; - string token = _userTokenProvider.GetUserToken(); + _logger.Log( + LogLevel.Error, + "unable to save form data for instance {InstanceId} due to response {StatusCode}", + instance.Id, + response.StatusCode + ); + throw await PlatformHttpException.CreateAsync(response); + } - using MemoryStream stream = new MemoryStream(); + /// + public async Task UpdateData( + T dataToSerialize, + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + Guid dataId + ) + { + using var activity = _telemetry?.StartUpdateDataActivity(instanceGuid, instanceOwnerPartyId); + string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; + string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; + string token = _userTokenProvider.GetUserToken(); - Serialize(dataToSerialize, type, stream); + using MemoryStream stream = new MemoryStream(); - stream.Position = 0; - StreamContent streamContent = new StreamContent(stream); - streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/xml"); + Serialize(dataToSerialize, type, stream); - HttpResponseMessage response = await _client.PutAsync(token, apiUrl, streamContent); + stream.Position = 0; + StreamContent streamContent = new StreamContent(stream); + streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/xml"); - if (response.IsSuccessStatusCode) - { - string instanceData = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - DataElement dataElement = JsonConvert.DeserializeObject(instanceData)!; - return dataElement; - } + HttpResponseMessage response = await _client.PutAsync(token, apiUrl, streamContent); - throw await PlatformHttpException.CreateAsync(response); + if (response.IsSuccessStatusCode) + { + string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + DataElement dataElement = JsonConvert.DeserializeObject(instanceData)!; + return dataElement; } - // Serializing using XmlWriter with UTF8 Encoding without generating BOM - // to avoid issue introduced with .Net when MS introduced BOM by default - // when serializing ref. https://github.com/dotnet/runtime/issues/63585 - // Will be fixed with https://github.com/dotnet/runtime/pull/75637 - internal static void Serialize(T dataToSerialize, Type type, Stream targetStream) + throw await PlatformHttpException.CreateAsync(response); + } + + // Serializing using XmlWriter with UTF8 Encoding without generating BOM + // to avoid issue introduced with .Net when MS introduced BOM by default + // when serializing ref. https://github.com/dotnet/runtime/issues/63585 + // Will be fixed with https://github.com/dotnet/runtime/pull/75637 + internal static void Serialize(T dataToSerialize, Type type, Stream targetStream) + { + XmlWriterSettings xmlWriterSettings = new XmlWriterSettings() { - XmlWriterSettings xmlWriterSettings = new XmlWriterSettings() - { - Encoding = new UTF8Encoding(false), - NewLineHandling = NewLineHandling.None, - }; - XmlWriter xmlWriter = XmlWriter.Create(targetStream, xmlWriterSettings); + Encoding = new UTF8Encoding(false), + NewLineHandling = NewLineHandling.None, + }; + XmlWriter xmlWriter = XmlWriter.Create(targetStream, xmlWriterSettings); - XmlSerializer serializer = new(type); + XmlSerializer serializer = new(type); - serializer.Serialize(xmlWriter, dataToSerialize); - } + serializer.Serialize(xmlWriter, dataToSerialize); + } - /// - public async Task GetBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataId - ) - { - using var activity = _telemetry?.StartGetBinaryDataActivity(instanceGuid, instanceOwnerPartyId); - string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; - string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; + /// + public async Task GetBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataId + ) + { + using var activity = _telemetry?.StartGetBinaryDataActivity(instanceGuid, instanceOwnerPartyId); + string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; + string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; - string token = _userTokenProvider.GetUserToken(); + string token = _userTokenProvider.GetUserToken(); - HttpResponseMessage response = await _client.GetAsync(token, apiUrl); + HttpResponseMessage response = await _client.GetAsync(token, apiUrl); - if (response.IsSuccessStatusCode) - { - return await response.Content.ReadAsStreamAsync(); - } - else if (response.StatusCode == HttpStatusCode.NotFound) - { + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadAsStreamAsync(); + } + else if (response.StatusCode == HttpStatusCode.NotFound) + { #nullable disable - return null; + return null; #nullable restore - } - - throw await PlatformHttpException.CreateAsync(response); } - /// - public async Task GetFormData( - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - Guid dataId - ) - { - using var activity = _telemetry?.StartGetFormDataActivity(instanceGuid, instanceOwnerPartyId); - string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; - string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; - string token = _userTokenProvider.GetUserToken(); + throw await PlatformHttpException.CreateAsync(response); + } - HttpResponseMessage response = await _client.GetAsync(token, apiUrl); - if (response.IsSuccessStatusCode) - { - using Stream stream = await response.Content.ReadAsStreamAsync(); - ModelDeserializer deserializer = new ModelDeserializer(_logger, type); - object? model = await deserializer.DeserializeAsync(stream, "application/xml"); + /// + public async Task GetFormData( + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + Guid dataId + ) + { + using var activity = _telemetry?.StartGetFormDataActivity(instanceGuid, instanceOwnerPartyId); + string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; + string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; + string token = _userTokenProvider.GetUserToken(); - if (deserializer.Error != null || model is null) - { - _logger.LogError($"Cannot deserialize XML form data read from storage: {deserializer.Error}"); - throw new ServiceException( - HttpStatusCode.Conflict, - $"Cannot deserialize XML form data from storage {deserializer.Error}" - ); - } + HttpResponseMessage response = await _client.GetAsync(token, apiUrl); + if (response.IsSuccessStatusCode) + { + using Stream stream = await response.Content.ReadAsStreamAsync(); + ModelDeserializer deserializer = new ModelDeserializer(_logger, type); + object? model = await deserializer.DeserializeAsync(stream, "application/xml"); - return model; + if (deserializer.Error != null || model is null) + { + _logger.LogError($"Cannot deserialize XML form data read from storage: {deserializer.Error}"); + throw new ServiceException( + HttpStatusCode.Conflict, + $"Cannot deserialize XML form data from storage {deserializer.Error}" + ); } - throw await PlatformHttpException.CreateAsync(response); + return model; } - /// - public async Task> GetBinaryDataList( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid - ) - { - using var activity = _telemetry?.StartGetBinaryDataListActivity(instanceGuid, instanceOwnerPartyId); - string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; - string apiUrl = $"instances/{instanceIdentifier}/dataelements"; - string token = _userTokenProvider.GetUserToken(); - - DataElementList dataList; - List attachmentList = new List(); + throw await PlatformHttpException.CreateAsync(response); + } - HttpResponseMessage response = await _client.GetAsync(token, apiUrl); - if (response.StatusCode == HttpStatusCode.OK) - { - string instanceData = await response.Content.ReadAsStringAsync(); - dataList = - JsonConvert.DeserializeObject(instanceData) - ?? throw new JsonException("Could not deserialize DataElementList"); + /// + public async Task> GetBinaryDataList( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid + ) + { + using var activity = _telemetry?.StartGetBinaryDataListActivity(instanceGuid, instanceOwnerPartyId); + string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; + string apiUrl = $"instances/{instanceIdentifier}/dataelements"; + string token = _userTokenProvider.GetUserToken(); - ExtractAttachments(dataList.DataElements, attachmentList); + DataElementList dataList; + List attachmentList = new List(); - return attachmentList; - } + HttpResponseMessage response = await _client.GetAsync(token, apiUrl); + if (response.StatusCode == HttpStatusCode.OK) + { + string instanceData = await response.Content.ReadAsStringAsync(); + dataList = + JsonConvert.DeserializeObject(instanceData) + ?? throw new JsonException("Could not deserialize DataElementList"); - _logger.Log(LogLevel.Error, "Unable to fetch attachment list {statusCode}", response.StatusCode); + ExtractAttachments(dataList.DataElements, attachmentList); - throw await PlatformHttpException.CreateAsync(response); + return attachmentList; } - private static void ExtractAttachments(List dataList, List attachmentList) - { - List? attachments = null; - IEnumerable attachmentTypes = dataList.GroupBy(m => m.DataType).Select(m => m.First()); + _logger.Log(LogLevel.Error, "Unable to fetch attachment list {statusCode}", response.StatusCode); - foreach (DataElement attachmentType in attachmentTypes) - { - attachments = new List(); - foreach (DataElement data in dataList) - { - if (data.DataType != "default" && data.DataType == attachmentType.DataType) - { - attachments.Add( - new Attachment - { - Id = data.Id, - Name = data.Filename, - Size = data.Size, - } - ); - } - } + throw await PlatformHttpException.CreateAsync(response); + } - if (attachments.Count > 0) + private static void ExtractAttachments(List dataList, List attachmentList) + { + List? attachments = null; + IEnumerable attachmentTypes = dataList.GroupBy(m => m.DataType).Select(m => m.First()); + + foreach (DataElement attachmentType in attachmentTypes) + { + attachments = new List(); + foreach (DataElement data in dataList) + { + if (data.DataType != "default" && data.DataType == attachmentType.DataType) { - attachmentList.Add( - new AttachmentList { Type = attachmentType.DataType, Attachments = attachments } + attachments.Add( + new Attachment + { + Id = data.Id, + Name = data.Filename, + Size = data.Size, + } ); } } - if (attachments != null && attachments.Count > 0) + if (attachments.Count > 0) { - attachmentList.Add(new AttachmentList { Type = "attachments", Attachments = attachments }); + attachmentList.Add(new AttachmentList { Type = attachmentType.DataType, Attachments = attachments }); } } - /// - public async Task DeleteBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid - ) + if (attachments != null && attachments.Count > 0) { - using var activity = _telemetry?.StartDeleteBinaryDataActivity(instanceGuid, instanceOwnerPartyId); - return await DeleteData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, false); + attachmentList.Add(new AttachmentList { Type = "attachments", Attachments = attachments }); } + } - /// - public async Task DeleteData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid, - bool delay - ) - { - using var activity = _telemetry?.StartDeleteDataActivity(instanceGuid, instanceOwnerPartyId); - string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; - string apiUrl = $"instances/{instanceIdentifier}/data/{dataGuid}?delay={delay}"; - string token = _userTokenProvider.GetUserToken(); + /// + public async Task DeleteBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid + ) + { + using var activity = _telemetry?.StartDeleteBinaryDataActivity(instanceGuid, instanceOwnerPartyId); + return await DeleteData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, false); + } - HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl); + /// + public async Task DeleteData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid, + bool delay + ) + { + using var activity = _telemetry?.StartDeleteDataActivity(instanceGuid, instanceOwnerPartyId); + string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; + string apiUrl = $"instances/{instanceIdentifier}/data/{dataGuid}?delay={delay}"; + string token = _userTokenProvider.GetUserToken(); - if (response.IsSuccessStatusCode) - { - return true; - } + HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl); - _logger.LogError( - $"Deleting data element {dataGuid} for instance {instanceIdentifier} failed with status code {response.StatusCode}" - ); - throw await PlatformHttpException.CreateAsync(response); + if (response.IsSuccessStatusCode) + { + return true; } - /// - public async Task InsertBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - string dataType, - HttpRequest request - ) - { - using var activity = _telemetry?.StartInsertBinaryDataActivity(instanceGuid, instanceOwnerPartyId); - string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; - string apiUrl = - $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data?dataType={dataType}"; - string token = _userTokenProvider.GetUserToken(); - DataElement dataElement; + _logger.LogError( + $"Deleting data element {dataGuid} for instance {instanceIdentifier} failed with status code {response.StatusCode}" + ); + throw await PlatformHttpException.CreateAsync(response); + } - StreamContent content = request.CreateContentStream(); + /// + public async Task InsertBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + string dataType, + HttpRequest request + ) + { + using var activity = _telemetry?.StartInsertBinaryDataActivity(instanceGuid, instanceOwnerPartyId); + string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; + string apiUrl = + $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data?dataType={dataType}"; + string token = _userTokenProvider.GetUserToken(); + DataElement dataElement; - HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content); + StreamContent content = request.CreateContentStream(); - if (response.IsSuccessStatusCode) - { - string instancedata = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - dataElement = JsonConvert.DeserializeObject(instancedata)!; + HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content); - return dataElement; - } + if (response.IsSuccessStatusCode) + { + string instancedata = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + dataElement = JsonConvert.DeserializeObject(instancedata)!; - _logger.LogError( - $"Storing attachment for instance {instanceGuid} failed with status code {response.StatusCode}" - ); - throw await PlatformHttpException.CreateAsync(response); + return dataElement; } - /// - public async Task InsertBinaryData( - string instanceId, - string dataType, - string contentType, - string? filename, - Stream stream, - string? generatedFromTask = null - ) + _logger.LogError( + $"Storing attachment for instance {instanceGuid} failed with status code {response.StatusCode}" + ); + throw await PlatformHttpException.CreateAsync(response); + } + + /// + public async Task InsertBinaryData( + string instanceId, + string dataType, + string contentType, + string? filename, + Stream stream, + string? generatedFromTask = null + ) + { + using var activity = _telemetry?.StartInsertBinaryDataActivity(instanceId); + string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceId}/data?dataType={dataType}"; + if (!string.IsNullOrEmpty(generatedFromTask)) { - using var activity = _telemetry?.StartInsertBinaryDataActivity(instanceId); - string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceId}/data?dataType={dataType}"; - if (!string.IsNullOrEmpty(generatedFromTask)) - { - apiUrl += $"&generatedFromTask={generatedFromTask}"; - } - string token = _userTokenProvider.GetUserToken(); - DataElement dataElement; + apiUrl += $"&generatedFromTask={generatedFromTask}"; + } + string token = _userTokenProvider.GetUserToken(); + DataElement dataElement; - StreamContent content = new StreamContent(stream); - content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - if (!string.IsNullOrEmpty(filename)) + StreamContent content = new StreamContent(stream); + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + if (!string.IsNullOrEmpty(filename)) + { + content.Headers.ContentDisposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment) { - content.Headers.ContentDisposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment) - { - FileName = filename, - FileNameStar = filename - }; - } - - HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content); + FileName = filename, + FileNameStar = filename + }; + } - if (response.IsSuccessStatusCode) - { - string instancedata = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - dataElement = JsonConvert.DeserializeObject(instancedata)!; + HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content); - return dataElement; - } + if (response.IsSuccessStatusCode) + { + string instancedata = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + dataElement = JsonConvert.DeserializeObject(instancedata)!; - _logger.LogError( - $"Storing attachment for instance {instanceId} failed with status code {response.StatusCode} - content {await response.Content.ReadAsStringAsync()}" - ); - throw await PlatformHttpException.CreateAsync(response); + return dataElement; } - /// - public async Task UpdateBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid, - HttpRequest request - ) - { - using var activity = _telemetry?.StartUpdateBinaryDataActivity(instanceGuid, instanceOwnerPartyId); - string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; - string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}"; - string token = _userTokenProvider.GetUserToken(); + _logger.LogError( + $"Storing attachment for instance {instanceId} failed with status code {response.StatusCode} - content {await response.Content.ReadAsStringAsync()}" + ); + throw await PlatformHttpException.CreateAsync(response); + } - StreamContent content = request.CreateContentStream(); + /// + public async Task UpdateBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid, + HttpRequest request + ) + { + using var activity = _telemetry?.StartUpdateBinaryDataActivity(instanceGuid, instanceOwnerPartyId); + string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; + string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}"; + string token = _userTokenProvider.GetUserToken(); - HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content); + StreamContent content = request.CreateContentStream(); - if (response.IsSuccessStatusCode) - { - string instancedata = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - DataElement dataElement = JsonConvert.DeserializeObject(instancedata)!; + HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content); - return dataElement; - } + if (response.IsSuccessStatusCode) + { + string instancedata = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + DataElement dataElement = JsonConvert.DeserializeObject(instancedata)!; - _logger.LogError( - $"Updating attachment {dataGuid} for instance {instanceGuid} failed with status code {response.StatusCode}" - ); - throw await PlatformHttpException.CreateAsync(response); + return dataElement; } - /// - public async Task UpdateBinaryData( - InstanceIdentifier instanceIdentifier, - string? contentType, - string filename, - Guid dataGuid, - Stream stream - ) + _logger.LogError( + $"Updating attachment {dataGuid} for instance {instanceGuid} failed with status code {response.StatusCode}" + ); + throw await PlatformHttpException.CreateAsync(response); + } + + /// + public async Task UpdateBinaryData( + InstanceIdentifier instanceIdentifier, + string? contentType, + string filename, + Guid dataGuid, + Stream stream + ) + { + using var activity = _telemetry?.StartUpdateBinaryDataActivity(instanceIdentifier.GetInstanceId()); + string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}"; + string token = _userTokenProvider.GetUserToken(); + StreamContent content = new StreamContent(stream); + ArgumentNullException.ThrowIfNull(contentType); + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + content.Headers.ContentDisposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment) { - using var activity = _telemetry?.StartUpdateBinaryDataActivity(instanceIdentifier.GetInstanceId()); - string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}"; - string token = _userTokenProvider.GetUserToken(); - StreamContent content = new StreamContent(stream); - ArgumentNullException.ThrowIfNull(contentType); - content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - content.Headers.ContentDisposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment) - { - FileName = filename, - FileNameStar = filename - }; - HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content); - _logger.LogInformation("Update binary data result: {ResultCode}", response.StatusCode); - if (response.IsSuccessStatusCode) - { - string instancedata = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - DataElement dataElement = JsonConvert.DeserializeObject(instancedata)!; + FileName = filename, + FileNameStar = filename + }; + HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content); + _logger.LogInformation("Update binary data result: {ResultCode}", response.StatusCode); + if (response.IsSuccessStatusCode) + { + string instancedata = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + DataElement dataElement = JsonConvert.DeserializeObject(instancedata)!; - return dataElement; - } - throw await PlatformHttpException.CreateAsync(response); + return dataElement; } + throw await PlatformHttpException.CreateAsync(response); + } - /// - public async Task Update(Instance instance, DataElement dataElement) + /// + public async Task Update(Instance instance, DataElement dataElement) + { + using var activity = _telemetry?.StartUpdateDataActivity(instance); + string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instance.Id}/dataelements/{dataElement.Id}"; + string token = _userTokenProvider.GetUserToken(); + + StringContent jsonString = new StringContent( + JsonConvert.SerializeObject(dataElement), + Encoding.UTF8, + "application/json" + ); + HttpResponseMessage response = await _client.PutAsync(token, apiUrl, jsonString); + + if (response.IsSuccessStatusCode) { - using var activity = _telemetry?.StartUpdateDataActivity(instance); - string apiUrl = - $"{_platformSettings.ApiStorageEndpoint}instances/{instance.Id}/dataelements/{dataElement.Id}"; - string token = _userTokenProvider.GetUserToken(); - - StringContent jsonString = new StringContent( - JsonConvert.SerializeObject(dataElement), - Encoding.UTF8, - "application/json" - ); - HttpResponseMessage response = await _client.PutAsync(token, apiUrl, jsonString); - - if (response.IsSuccessStatusCode) - { - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - DataElement result = JsonConvert.DeserializeObject( - await response.Content.ReadAsStringAsync() - )!; + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + DataElement result = JsonConvert.DeserializeObject( + await response.Content.ReadAsStringAsync() + )!; - return result; - } - - throw await PlatformHttpException.CreateAsync(response); + return result; } - /// - public async Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + throw await PlatformHttpException.CreateAsync(response); + } + + /// + public async Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + using var activity = _telemetry?.StartLockDataElementActivity(instanceIdentifier.GetInstanceId(), dataGuid); + string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock"; + string token = _userTokenProvider.GetUserToken(); + _logger.LogDebug( + "Locking data element {DataGuid} for instance {InstanceIdentifier} URL: {Url}", + dataGuid, + instanceIdentifier, + apiUrl + ); + HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content: null); + if (response.IsSuccessStatusCode) { - using var activity = _telemetry?.StartLockDataElementActivity(instanceIdentifier.GetInstanceId(), dataGuid); - string apiUrl = - $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock"; - string token = _userTokenProvider.GetUserToken(); - _logger.LogDebug( - "Locking data element {DataGuid} for instance {InstanceIdentifier} URL: {Url}", - dataGuid, - instanceIdentifier, - apiUrl - ); - HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content: null); - if (response.IsSuccessStatusCode) - { - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - DataElement result = JsonConvert.DeserializeObject( - await response.Content.ReadAsStringAsync() - )!; - return result; - } - _logger.LogError( - "Locking data element {DataGuid} for instance {InstanceIdentifier} failed with status code {StatusCode}", - dataGuid, - instanceIdentifier, - response.StatusCode - ); - throw await PlatformHttpException.CreateAsync(response); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + DataElement result = JsonConvert.DeserializeObject( + await response.Content.ReadAsStringAsync() + )!; + return result; } + _logger.LogError( + "Locking data element {DataGuid} for instance {InstanceIdentifier} failed with status code {StatusCode}", + dataGuid, + instanceIdentifier, + response.StatusCode + ); + throw await PlatformHttpException.CreateAsync(response); + } - /// - public async Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + /// + public async Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + using var activity = _telemetry?.StartUnlockDataElementActivity(instanceIdentifier.GetInstanceId(), dataGuid); + string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock"; + string token = _userTokenProvider.GetUserToken(); + _logger.LogDebug( + "Unlocking data element {DataGuid} for instance {InstanceIdentifier} URL: {Url}", + dataGuid, + instanceIdentifier, + apiUrl + ); + HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl); + if (response.IsSuccessStatusCode) { - using var activity = _telemetry?.StartUnlockDataElementActivity( - instanceIdentifier.GetInstanceId(), - dataGuid - ); - string apiUrl = - $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock"; - string token = _userTokenProvider.GetUserToken(); - _logger.LogDebug( - "Unlocking data element {DataGuid} for instance {InstanceIdentifier} URL: {Url}", - dataGuid, - instanceIdentifier, - apiUrl - ); - HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl); - if (response.IsSuccessStatusCode) - { - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - DataElement result = JsonConvert.DeserializeObject( - await response.Content.ReadAsStringAsync() - )!; - return result; - } - _logger.LogError( - "Unlocking data element {DataGuid} for instance {InstanceIdentifier} failed with status code {StatusCode}", - dataGuid, - instanceIdentifier, - response.StatusCode - ); - throw await PlatformHttpException.CreateAsync(response); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + DataElement result = JsonConvert.DeserializeObject( + await response.Content.ReadAsStringAsync() + )!; + return result; } + _logger.LogError( + "Unlocking data element {DataGuid} for instance {InstanceIdentifier} failed with status code {StatusCode}", + dataGuid, + instanceIdentifier, + response.StatusCode + ); + throw await PlatformHttpException.CreateAsync(response); } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs index b6b8b6f88..c6cecad87 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs @@ -16,358 +16,322 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; -using static Altinn.App.Core.Features.Telemetry.Instances; -namespace Altinn.App.Core.Infrastructure.Clients.Storage +namespace Altinn.App.Core.Infrastructure.Clients.Storage; + +/// +/// A client for handling actions on instances in Altinn Platform. +/// +public class InstanceClient : IInstanceClient { + private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly HttpClient _client; + private readonly Telemetry? _telemetry; + private readonly AppSettings _settings; + /// - /// A client for handling actions on instances in Altinn Platform. + /// Initializes a new instance of the class. /// - public class InstanceClient : IInstanceClient + /// the platform settings + /// the logger + /// The http context accessor + /// A HttpClient that can be used to perform HTTP requests against the platform. + /// The application settings. + /// Telemetry for traces and metrics. + public InstanceClient( + IOptions platformSettings, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + HttpClient httpClient, + IOptionsMonitor settings, + Telemetry? telemetry = null + ) + { + _logger = logger; + _httpContextAccessor = httpContextAccessor; + _settings = settings.CurrentValue; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + _client = httpClient; + _telemetry = telemetry; + } + + /// + public async Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceGuid) { - private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly HttpClient _client; - private readonly Telemetry? _telemetry; - private readonly AppSettings _settings; - - /// - /// Initializes a new instance of the class. - /// - /// the platform settings - /// the logger - /// The http context accessor - /// A HttpClient that can be used to perform HTTP requests against the platform. - /// The application settings. - /// Telemetry for traces and metrics. - public InstanceClient( - IOptions platformSettings, - ILogger logger, - IHttpContextAccessor httpContextAccessor, - HttpClient httpClient, - IOptionsMonitor settings, - Telemetry? telemetry = null - ) + using var activity = _telemetry?.StartGetInstanceByGuidActivity(instanceGuid); + string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; + + string apiUrl = $"instances/{instanceIdentifier}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + + HttpResponseMessage response = await _client.GetAsync(token, apiUrl); + if (response.StatusCode == HttpStatusCode.OK) { - _logger = logger; - _httpContextAccessor = httpContextAccessor; - _settings = settings.CurrentValue; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); - _client = httpClient; - _telemetry = telemetry; + string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + Instance instance = JsonConvert.DeserializeObject(instanceData)!; + return instance; } - - /// - public async Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceGuid) + else { - using var activity = _telemetry?.StartGetInstanceByGuidActivity(instanceGuid); - string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; - - string apiUrl = $"instances/{instanceIdentifier}"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - - HttpResponseMessage response = await _client.GetAsync(token, apiUrl); - if (response.StatusCode == HttpStatusCode.OK) - { - string instanceData = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - Instance instance = JsonConvert.DeserializeObject(instanceData)!; - return instance; - } - else - { - _logger.LogError($"Unable to fetch instance with instance id {instanceGuid}"); - throw await PlatformHttpException.CreateAsync(response); - } + _logger.LogError($"Unable to fetch instance with instance id {instanceGuid}"); + throw await PlatformHttpException.CreateAsync(response); } + } - /// - public async Task GetInstance(Instance instance) - { - Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - using var activity = _telemetry?.StartGetInstanceByInstanceActivity(instanceGuid); - string app = instance.AppId.Split("/")[1]; - string org = instance.Org; - int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); + /// + public async Task GetInstance(Instance instance) + { + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + using var activity = _telemetry?.StartGetInstanceByInstanceActivity(instanceGuid); + string app = instance.AppId.Split("/")[1]; + string org = instance.Org; + int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - return await GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - } + return await GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + } + + /// + public async Task> GetInstances(Dictionary queryParams) + { + using var activity = _telemetry?.StartGetInstancesActivity(); + var apiUrl = QueryHelpers.AddQueryString("instances", queryParams); + + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + QueryResponse queryResponse = await QueryInstances(token, apiUrl); - /// - public async Task> GetInstances(Dictionary queryParams) + if (queryResponse.Count == 0) { - using var activity = _telemetry?.StartGetInstancesActivity(); - var apiUrl = QueryHelpers.AddQueryString("instances", queryParams); - - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - QueryResponse queryResponse = await QueryInstances(token, apiUrl); - - if (queryResponse.Count == 0) - { - return []; - } - List instances = [.. queryResponse.Instances]; - - while (!string.IsNullOrEmpty(queryResponse.Next)) - { - queryResponse = await QueryInstances(token, queryResponse.Next); - instances.AddRange(queryResponse.Instances); - } - return instances; + return []; } + List instances = [.. queryResponse.Instances]; - private async Task> QueryInstances(string token, string url) + while (!string.IsNullOrEmpty(queryResponse.Next)) { - using var activity = _telemetry?.StartQueryInstancesActivity(); - HttpResponseMessage response = await _client.GetAsync(token, url); - - if (response.StatusCode == HttpStatusCode.OK) - { - string responseString = await response.Content.ReadAsStringAsync(); - QueryResponse queryResponse = - JsonConvert.DeserializeObject>(responseString) - ?? throw new JsonException("Could not deserialize Instance query response"); - return queryResponse; - } - else - { - _logger.LogError("Unable to query instances from Platform Storage"); - throw await PlatformHttpException.CreateAsync(response); - } + queryResponse = await QueryInstances(token, queryResponse.Next); + instances.AddRange(queryResponse.Instances); } + return instances; + } + + private async Task> QueryInstances(string token, string url) + { + using var activity = _telemetry?.StartQueryInstancesActivity(); + HttpResponseMessage response = await _client.GetAsync(token, url); - /// - public async Task UpdateProcess(Instance instance) + if (response.StatusCode == HttpStatusCode.OK) { - using var activity = _telemetry?.StartUpdateProcessActivity(instance); - ProcessState processState = instance.Process; - - string apiUrl = $"instances/{instance.Id}/process"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - - string processStateString = JsonConvert.SerializeObject(processState); - _logger.LogInformation($"update process state: {processStateString}"); - - StringContent httpContent = new(processStateString, Encoding.UTF8, "application/json"); - HttpResponseMessage response = await _client.PutAsync(token, apiUrl, httpContent); - if (response.StatusCode == HttpStatusCode.OK) - { - string instanceData = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - Instance updatedInstance = JsonConvert.DeserializeObject(instanceData)!; - return updatedInstance; - } - else - { - _logger.LogError($"Unable to update instance process with instance id {instance.Id}"); - throw await PlatformHttpException.CreateAsync(response); - } + string responseString = await response.Content.ReadAsStringAsync(); + QueryResponse queryResponse = + JsonConvert.DeserializeObject>(responseString) + ?? throw new JsonException("Could not deserialize Instance query response"); + return queryResponse; } - - /// - public async Task CreateInstance(string org, string app, Instance instanceTemplate) + else { - using var activity = _telemetry?.StartCreateInstanceActivity(); - string apiUrl = $"instances?appId={org}/{app}"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - - StringContent content = - new(JsonConvert.SerializeObject(instanceTemplate), Encoding.UTF8, "application/json"); - HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content); - - if (response.IsSuccessStatusCode) - { - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - Instance createdInstance = JsonConvert.DeserializeObject( - await response.Content.ReadAsStringAsync() - )!; - _telemetry?.InstanceCreated(createdInstance); - return createdInstance; - } - - _logger.LogError( - $"Unable to create instance {response.StatusCode} - {await response.Content.ReadAsStringAsync()}" - ); + _logger.LogError("Unable to query instances from Platform Storage"); throw await PlatformHttpException.CreateAsync(response); } + } - /// - public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid) - { - using var activity = _telemetry?.StartCompleteConfirmationActivity(instanceGuid, instanceOwnerPartyId); - string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/complete"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - - HttpResponseMessage response = await _client.PostAsync(token, apiUrl, new StringContent(string.Empty)); - - if (response.StatusCode == HttpStatusCode.OK) - { - string instanceData = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - Instance instance = JsonConvert.DeserializeObject(instanceData)!; - _telemetry?.InstanceCompleted(instance); - return instance; - } + /// + public async Task UpdateProcess(Instance instance) + { + using var activity = _telemetry?.StartUpdateProcessActivity(instance); + ProcessState processState = instance.Process; + + string apiUrl = $"instances/{instance.Id}/process"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + string processStateString = JsonConvert.SerializeObject(processState); + _logger.LogInformation($"update process state: {processStateString}"); + + StringContent httpContent = new(processStateString, Encoding.UTF8, "application/json"); + HttpResponseMessage response = await _client.PutAsync(token, apiUrl, httpContent); + if (response.StatusCode == HttpStatusCode.OK) + { + string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + Instance updatedInstance = JsonConvert.DeserializeObject(instanceData)!; + return updatedInstance; + } + else + { + _logger.LogError($"Unable to update instance process with instance id {instance.Id}"); throw await PlatformHttpException.CreateAsync(response); } + } - /// - public async Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus) + /// + public async Task CreateInstance(string org, string app, Instance instanceTemplate) + { + using var activity = _telemetry?.StartCreateInstanceActivity(); + string apiUrl = $"instances?appId={org}/{app}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + + StringContent content = new(JsonConvert.SerializeObject(instanceTemplate), Encoding.UTF8, "application/json"); + HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content); + + if (response.IsSuccessStatusCode) { - using var activity = _telemetry?.StartUpdateReadStatusActivity(instanceGuid, instanceOwnerPartyId); - string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/readstatus?status={readStatus}"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - - HttpResponseMessage response = await _client.PutAsync(token, apiUrl, new StringContent(string.Empty)); - - if (response.StatusCode == HttpStatusCode.OK) - { - string instanceData = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - Instance instance = JsonConvert.DeserializeObject(instanceData)!; - return instance; - } - - _logger.LogError( - $"Could not update read status for instance {instanceOwnerPartyId}/{instanceGuid}. Request failed with status code {response.StatusCode}" - ); -#nullable disable - return null; -#nullable restore + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + Instance createdInstance = JsonConvert.DeserializeObject( + await response.Content.ReadAsStringAsync() + )!; + _telemetry?.InstanceCreated(createdInstance); + return createdInstance; } - /// - public async Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus) - { - using var activity = _telemetry?.StartUpdateSubStatusActivity(instanceGuid, instanceOwnerPartyId); - string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/substatus"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - - HttpResponseMessage response = await _client.PutAsync( - token, - apiUrl, - new StringContent(JsonConvert.SerializeObject(substatus), Encoding.UTF8, "application/json") - ); - - if (response.StatusCode == HttpStatusCode.OK) - { - string instanceData = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - Instance instance = JsonConvert.DeserializeObject(instanceData)!; - return instance; - } + _logger.LogError( + $"Unable to create instance {response.StatusCode} - {await response.Content.ReadAsStringAsync()}" + ); + throw await PlatformHttpException.CreateAsync(response); + } - throw await PlatformHttpException.CreateAsync(response); - } + /// + public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid) + { + using var activity = _telemetry?.StartCompleteConfirmationActivity(instanceGuid, instanceOwnerPartyId); + string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/complete"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + + HttpResponseMessage response = await _client.PostAsync(token, apiUrl, new StringContent(string.Empty)); - /// - public async Task UpdatePresentationTexts( - int instanceOwnerPartyId, - Guid instanceGuid, - PresentationTexts presentationTexts - ) + if (response.StatusCode == HttpStatusCode.OK) { - using var activity = _telemetry?.StartUpdatePresentationTextActivity(instanceGuid, instanceOwnerPartyId); - string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/presentationtexts"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - - HttpResponseMessage response = await _client.PutAsync( - token, - apiUrl, - new StringContent(JsonConvert.SerializeObject(presentationTexts), Encoding.UTF8, "application/json") - ); - - if (response.StatusCode == HttpStatusCode.OK) - { - string instanceData = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - Instance instance = JsonConvert.DeserializeObject(instanceData)!; - return instance; - } + string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + Instance instance = JsonConvert.DeserializeObject(instanceData)!; + _telemetry?.InstanceCompleted(instance); + return instance; + } - throw await PlatformHttpException.CreateAsync(response); + throw await PlatformHttpException.CreateAsync(response); + } + + /// + public async Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus) + { + using var activity = _telemetry?.StartUpdateReadStatusActivity(instanceGuid, instanceOwnerPartyId); + string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/readstatus?status={readStatus}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + + HttpResponseMessage response = await _client.PutAsync(token, apiUrl, new StringContent(string.Empty)); + + if (response.StatusCode == HttpStatusCode.OK) + { + string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + Instance instance = JsonConvert.DeserializeObject(instanceData)!; + return instance; } - /// - public async Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues) + _logger.LogError( + $"Could not update read status for instance {instanceOwnerPartyId}/{instanceGuid}. Request failed with status code {response.StatusCode}" + ); +#nullable disable + return null; +#nullable restore + } + + /// + public async Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus) + { + using var activity = _telemetry?.StartUpdateSubStatusActivity(instanceGuid, instanceOwnerPartyId); + string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/substatus"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + + HttpResponseMessage response = await _client.PutAsync( + token, + apiUrl, + new StringContent(JsonConvert.SerializeObject(substatus), Encoding.UTF8, "application/json") + ); + + if (response.StatusCode == HttpStatusCode.OK) { - using var activity = _telemetry?.StartUpdateDataValuesActivity(instanceGuid, instanceOwnerPartyId); - string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/datavalues"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - - HttpResponseMessage response = await _client.PutAsync( - token, - apiUrl, - new StringContent(JsonConvert.SerializeObject(dataValues), Encoding.UTF8, "application/json") - ); - - if (response.StatusCode == HttpStatusCode.OK) - { - string instanceData = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - Instance instance = JsonConvert.DeserializeObject(instanceData)!; - return instance; - } + string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + Instance instance = JsonConvert.DeserializeObject(instanceData)!; + return instance; + } - throw await PlatformHttpException.CreateAsync(response); + throw await PlatformHttpException.CreateAsync(response); + } + + /// + public async Task UpdatePresentationTexts( + int instanceOwnerPartyId, + Guid instanceGuid, + PresentationTexts presentationTexts + ) + { + using var activity = _telemetry?.StartUpdatePresentationTextActivity(instanceGuid, instanceOwnerPartyId); + string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/presentationtexts"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + + HttpResponseMessage response = await _client.PutAsync( + token, + apiUrl, + new StringContent(JsonConvert.SerializeObject(presentationTexts), Encoding.UTF8, "application/json") + ); + + if (response.StatusCode == HttpStatusCode.OK) + { + string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + Instance instance = JsonConvert.DeserializeObject(instanceData)!; + return instance; } - /// - public async Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard) + throw await PlatformHttpException.CreateAsync(response); + } + + /// + public async Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues) + { + using var activity = _telemetry?.StartUpdateDataValuesActivity(instanceGuid, instanceOwnerPartyId); + string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/datavalues"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + + HttpResponseMessage response = await _client.PutAsync( + token, + apiUrl, + new StringContent(JsonConvert.SerializeObject(dataValues), Encoding.UTF8, "application/json") + ); + + if (response.StatusCode == HttpStatusCode.OK) { - using var activity = _telemetry?.StartDeleteInstanceActivity(instanceGuid, instanceOwnerPartyId); - string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}?hard={hard}"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - - HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl); - - if (response.StatusCode == HttpStatusCode.OK) - { - string instanceData = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - Instance instance = JsonConvert.DeserializeObject(instanceData)!; - _telemetry?.InstanceDeleted(instance); - return instance; - } + string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + Instance instance = JsonConvert.DeserializeObject(instanceData)!; + return instance; + } - throw await PlatformHttpException.CreateAsync(response); + throw await PlatformHttpException.CreateAsync(response); + } + + /// + public async Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard) + { + using var activity = _telemetry?.StartDeleteInstanceActivity(instanceGuid, instanceOwnerPartyId); + string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}?hard={hard}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + + HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl); + + if (response.StatusCode == HttpStatusCode.OK) + { + string instanceData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + Instance instance = JsonConvert.DeserializeObject(instanceData)!; + _telemetry?.InstanceDeleted(instance); + return instance; } + + throw await PlatformHttpException.CreateAsync(response); } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs index 545bc0c26..97db5f58e 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs @@ -12,124 +12,114 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; -namespace Altinn.App.Core.Infrastructure.Clients.Storage +namespace Altinn.App.Core.Infrastructure.Clients.Storage; + +/// +/// A client for handling actions on instance events in Altinn Platform. +/// +public class InstanceEventClient : IInstanceEventClient { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly AppSettings _settings; + private readonly HttpClient _client; + /// - /// A client for handling actions on instance events in Altinn Platform. + /// Initializes a new instance of the class. /// - public class InstanceEventClient : IInstanceEventClient + /// the platform settings + /// The http context accessor + /// The Http client + /// The application settings. + public InstanceEventClient( + IOptions platformSettings, + IHttpContextAccessor httpContextAccessor, + HttpClient httpClient, + IOptionsMonitor settings + ) { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly AppSettings _settings; - private readonly HttpClient _client; + _httpContextAccessor = httpContextAccessor; + _settings = settings.CurrentValue; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + _client = httpClient; + } - /// - /// Initializes a new instance of the class. - /// - /// the platform settings - /// The http context accessor - /// The Http client - /// The application settings. - public InstanceEventClient( - IOptions platformSettings, - IHttpContextAccessor httpContextAccessor, - HttpClient httpClient, - IOptionsMonitor settings - ) - { - _httpContextAccessor = httpContextAccessor; - _settings = settings.CurrentValue; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); - _client = httpClient; - } + /// + public async Task> GetInstanceEvents( + string instanceId, + string instanceOwnerPartyId, + string org, + string app, + string[] eventTypes, + string from, + string to + ) + { + string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceId}/events"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); - /// - public async Task> GetInstanceEvents( - string instanceId, - string instanceOwnerPartyId, - string org, - string app, - string[] eventTypes, - string from, - string to - ) + char paramSeparator = '?'; + if (eventTypes != null) { - string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceId}/events"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); - - char paramSeparator = '?'; - if (eventTypes != null) + StringBuilder bld = new StringBuilder(); + foreach (string type in eventTypes) { - StringBuilder bld = new StringBuilder(); - foreach (string type in eventTypes) - { - bld.Append($"{paramSeparator}eventTypes={type}"); - paramSeparator = '&'; - } - - apiUrl += bld.ToString(); + bld.Append($"{paramSeparator}eventTypes={type}"); + paramSeparator = '&'; } - if (!(string.IsNullOrEmpty(from) || string.IsNullOrEmpty(to))) - { - apiUrl += $"{paramSeparator}from={from}&to={to}"; - } + apiUrl += bld.ToString(); + } - HttpResponseMessage response = await _client.GetAsync(token, apiUrl); + if (!(string.IsNullOrEmpty(from) || string.IsNullOrEmpty(to))) + { + apiUrl += $"{paramSeparator}from={from}&to={to}"; + } - if (response.IsSuccessStatusCode) - { - string eventData = await response.Content.ReadAsStringAsync(); - InstanceEventList instanceEvents = - JsonConvert.DeserializeObject(eventData) - ?? throw new JsonException("Could not deserialize InstanceEventList"); + HttpResponseMessage response = await _client.GetAsync(token, apiUrl); - return instanceEvents.InstanceEvents; - } + if (response.IsSuccessStatusCode) + { + string eventData = await response.Content.ReadAsStringAsync(); + InstanceEventList instanceEvents = + JsonConvert.DeserializeObject(eventData) + ?? throw new JsonException("Could not deserialize InstanceEventList"); - throw await PlatformHttpException.CreateAsync(response); + return instanceEvents.InstanceEvents; } - /// - public async Task SaveInstanceEvent(object dataToSerialize, string org, string app) - { - InstanceEvent instanceEvent = (InstanceEvent)dataToSerialize; - instanceEvent.Created = DateTime.UtcNow; - string apiUrl = $"instances/{instanceEvent.InstanceId}/events"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName - ); + throw await PlatformHttpException.CreateAsync(response); + } - HttpResponseMessage response = await _client.PostAsync( - token, - apiUrl, - new StringContent(instanceEvent.ToString(), Encoding.UTF8, "application/json") - ); + /// + public async Task SaveInstanceEvent(object dataToSerialize, string org, string app) + { + InstanceEvent instanceEvent = (InstanceEvent)dataToSerialize; + instanceEvent.Created = DateTime.UtcNow; + string apiUrl = $"instances/{instanceEvent.InstanceId}/events"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); - if (response.IsSuccessStatusCode) - { - string eventData = await response.Content.ReadAsStringAsync(); - InstanceEvent result = - JsonConvert.DeserializeObject(eventData) - ?? throw new Exception("Failed to deserialize instance event"); - var id = result.Id.ToString(); - Debug.Assert(id is not null, "Nullable.ToString() never returns null"); - // ^ https://github.com/dotnet/runtime/blob/9b088ab8287a77c52ff7c4ed6fa96be6d3eb87f1/src/libraries/System.Private.CoreLib/src/System/Nullable.cs#L67 - // ^ https://github.com/dotnet/runtime/blob/9b088ab8287a77c52ff7c4ed6fa96be6d3eb87f1/src/libraries/System.Private.CoreLib/src/System/Guid.cs#L1124 - return id; - } + HttpResponseMessage response = await _client.PostAsync( + token, + apiUrl, + new StringContent(instanceEvent.ToString(), Encoding.UTF8, "application/json") + ); - throw await PlatformHttpException.CreateAsync(response); + if (response.IsSuccessStatusCode) + { + string eventData = await response.Content.ReadAsStringAsync(); + InstanceEvent result = + JsonConvert.DeserializeObject(eventData) + ?? throw new Exception("Failed to deserialize instance event"); + var id = result.Id.ToString(); + Debug.Assert(id is not null, "Nullable.ToString() never returns null"); + // ^ https://github.com/dotnet/runtime/blob/9b088ab8287a77c52ff7c4ed6fa96be6d3eb87f1/src/libraries/System.Private.CoreLib/src/System/Nullable.cs#L67 + // ^ https://github.com/dotnet/runtime/blob/9b088ab8287a77c52ff7c4ed6fa96be6d3eb87f1/src/libraries/System.Private.CoreLib/src/System/Guid.cs#L1124 + return id; } + + throw await PlatformHttpException.CreateAsync(response); } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs index 554209a9d..34438bbf0 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs @@ -12,93 +12,89 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; -namespace Altinn.App.Core.Infrastructure.Clients.Storage +namespace Altinn.App.Core.Infrastructure.Clients.Storage; + +/// +/// The app implementation of the process service. +/// +public class ProcessClient : IProcessClient { + private readonly AppSettings _appSettings; + private readonly ILogger _logger; + private readonly HttpClient _client; + private readonly Telemetry? _telemetry; + private readonly IHttpContextAccessor _httpContextAccessor; + /// - /// The app implementation of the process service. + /// Initializes a new instance of the class. /// - public class ProcessClient : IProcessClient + public ProcessClient( + IOptions platformSettings, + IOptions appSettings, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + HttpClient httpClient, + Telemetry? telemetry = null + ) { - private readonly AppSettings _appSettings; - private readonly ILogger _logger; - private readonly HttpClient _client; - private readonly Telemetry? _telemetry; - private readonly IHttpContextAccessor _httpContextAccessor; + _appSettings = appSettings.Value; + _httpContextAccessor = httpContextAccessor; + _logger = logger; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + _client = httpClient; + _telemetry = telemetry; + } - /// - /// Initializes a new instance of the class. - /// - public ProcessClient( - IOptions platformSettings, - IOptions appSettings, - ILogger logger, - IHttpContextAccessor httpContextAccessor, - HttpClient httpClient, - Telemetry? telemetry = null - ) - { - _appSettings = appSettings.Value; - _httpContextAccessor = httpContextAccessor; - _logger = logger; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); - _client = httpClient; - _telemetry = telemetry; - } + /// + public Stream GetProcessDefinition() + { + using var activity = _telemetry?.StartGetProcessDefinitionActivity(); + string bpmnFilePath = Path.Join( + _appSettings.AppBasePath, + _appSettings.ConfigurationFolder, + _appSettings.ProcessFolder, + _appSettings.ProcessFileName + ); - /// - public Stream GetProcessDefinition() + try { - using var activity = _telemetry?.StartGetProcessDefinitionActivity(); - string bpmnFilePath = Path.Join( - _appSettings.AppBasePath, - _appSettings.ConfigurationFolder, - _appSettings.ProcessFolder, - _appSettings.ProcessFileName - ); + Stream processModel = File.OpenRead(bpmnFilePath); - try - { - Stream processModel = File.OpenRead(bpmnFilePath); - - return processModel; - } - catch (Exception processDefinitionException) - { - _logger.LogError( - $"Cannot find process definition file for this app. Have tried file location {bpmnFilePath}. Exception {processDefinitionException}" - ); - throw; - } + return processModel; } - - /// - public async Task GetProcessHistory(string instanceGuid, string instanceOwnerPartyId) + catch (Exception processDefinitionException) { - using var activity = _telemetry?.StartGetProcessHistoryActivity(instanceGuid, instanceOwnerPartyId); - string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/process/history"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _appSettings.RuntimeCookieName + _logger.LogError( + $"Cannot find process definition file for this app. Have tried file location {bpmnFilePath}. Exception {processDefinitionException}" ); + throw; + } + } - HttpResponseMessage response = await _client.GetAsync(token, apiUrl); + /// + public async Task GetProcessHistory(string instanceGuid, string instanceOwnerPartyId) + { + using var activity = _telemetry?.StartGetProcessHistoryActivity(instanceGuid, instanceOwnerPartyId); + string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/process/history"; + string token = JwtTokenUtil.GetTokenFromContext( + _httpContextAccessor.HttpContext, + _appSettings.RuntimeCookieName + ); - if (response.IsSuccessStatusCode) - { - string eventData = await response.Content.ReadAsStringAsync(); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - ProcessHistoryList processHistoryList = JsonConvert.DeserializeObject(eventData)!; + HttpResponseMessage response = await _client.GetAsync(token, apiUrl); - return processHistoryList; - } + if (response.IsSuccessStatusCode) + { + string eventData = await response.Content.ReadAsStringAsync(); + // ! TODO: this null-forgiving operator should be fixed/removed for the next major release + ProcessHistoryList processHistoryList = JsonConvert.DeserializeObject(eventData)!; - throw await PlatformHttpException.CreateAsync(response); + return processHistoryList; } + + throw await PlatformHttpException.CreateAsync(response); } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs index bb5c3a2dd..51104b295 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs @@ -9,78 +9,77 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Infrastructure.Clients.Storage +namespace Altinn.App.Core.Infrastructure.Clients.Storage; + +/// +/// Implementation of that sends signing requests to platform +/// +public class SignClient : ISignClient { + private readonly IUserTokenProvider _userTokenProvider; + private readonly HttpClient _client; + /// - /// Implementation of that sends signing requests to platform + /// Create a new instance of /// - public class SignClient : ISignClient + /// Platform settings, used to get storage endpoint + /// HttpClient used to send requests + /// Service that can provide user token + public SignClient( + IOptions platformSettings, + HttpClient httpClient, + IUserTokenProvider userTokenProvider + ) { - private readonly IUserTokenProvider _userTokenProvider; - private readonly HttpClient _client; - - /// - /// Create a new instance of - /// - /// Platform settings, used to get storage endpoint - /// HttpClient used to send requests - /// Service that can provide user token - public SignClient( - IOptions platformSettings, - HttpClient httpClient, - IUserTokenProvider userTokenProvider - ) - { - var platformSettings1 = platformSettings.Value; + var platformSettings1 = platformSettings.Value; - httpClient.BaseAddress = new Uri(platformSettings1.ApiStorageEndpoint); - httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings1.SubscriptionKey); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); - _client = httpClient; - _userTokenProvider = userTokenProvider; - } + httpClient.BaseAddress = new Uri(platformSettings1.ApiStorageEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings1.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + _client = httpClient; + _userTokenProvider = userTokenProvider; + } - /// - public async Task SignDataElements(SignatureContext signatureContext) + /// + public async Task SignDataElements(SignatureContext signatureContext) + { + string apiUrl = $"instances/{signatureContext.InstanceIdentifier}/sign"; + string token = _userTokenProvider.GetUserToken(); + HttpResponseMessage response = await _client.PostAsync(token, apiUrl, BuildSignRequest(signatureContext)); + if (response.IsSuccessStatusCode) { - string apiUrl = $"instances/{signatureContext.InstanceIdentifier}/sign"; - string token = _userTokenProvider.GetUserToken(); - HttpResponseMessage response = await _client.PostAsync(token, apiUrl, BuildSignRequest(signatureContext)); - if (response.IsSuccessStatusCode) - { - return; - } - - throw new PlatformHttpException(response, "Failed to sign dataelements"); + return; } - private static JsonContent BuildSignRequest(SignatureContext signatureContext) + throw new PlatformHttpException(response, "Failed to sign dataelements"); + } + + private static JsonContent BuildSignRequest(SignatureContext signatureContext) + { + SignRequest signRequest = new SignRequest() { - SignRequest signRequest = new SignRequest() + Signee = new() { - Signee = new() + UserId = signatureContext.Signee.UserId, + PersonNumber = signatureContext.Signee.PersonNumber, + OrganisationNumber = signatureContext.Signee.OrganisationNumber + }, + SignatureDocumentDataType = signatureContext.SignatureDataTypeId, + DataElementSignatures = new(), + GeneratedFromTask = signatureContext.GeneratedFromTask + }; + foreach (var dataElementSignature in signatureContext.DataElementSignatures) + { + signRequest.DataElementSignatures.Add( + new SignRequest.DataElementSignature() { - UserId = signatureContext.Signee.UserId, - PersonNumber = signatureContext.Signee.PersonNumber, - OrganisationNumber = signatureContext.Signee.OrganisationNumber - }, - SignatureDocumentDataType = signatureContext.SignatureDataTypeId, - DataElementSignatures = new(), - GeneratedFromTask = signatureContext.GeneratedFromTask - }; - foreach (var dataElementSignature in signatureContext.DataElementSignatures) - { - signRequest.DataElementSignatures.Add( - new SignRequest.DataElementSignature() - { - DataElementId = dataElementSignature.DataElementId, - Signed = dataElementSignature.Signed - } - ); - } - - return JsonContent.Create(signRequest); + DataElementId = dataElementSignature.DataElementId, + Signed = dataElementSignature.Signed + } + ); } + + return JsonContent.Create(signRequest); } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs index 770faf09b..f037eec7e 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs @@ -11,86 +11,82 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Infrastructure.Clients.Storage +namespace Altinn.App.Core.Infrastructure.Clients.Storage; + +/// +/// A client forretrieving text resources from Altinn Platform. +/// +[Obsolete("Use IAppResources.GetTexts() instead")] +public class TextClient : IText { + private readonly ILogger _logger; + private readonly AppSettings _settings; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly HttpClient _client; + private readonly IMemoryCache _memoryCache; + private readonly MemoryCacheEntryOptions _cacheEntryOptions; + /// - /// A client forretrieving text resources from Altinn Platform. + /// Initializes a new instance of the class. /// - [Obsolete("Use IAppResources.GetTexts() instead")] - public class TextClient : IText + /// The app repository settings. + /// The platform settings object from configuration. + /// A logger from the built in logger factory. + /// An object with access to the http context. + /// A HttpClient provided by the built in HttpClientFactory. + /// The built in MemoryCache. + public TextClient( + IOptions settings, + IOptions platformSettings, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + HttpClient httpClient, + IMemoryCache memoryCache + ) { - private readonly ILogger _logger; - private readonly AppSettings _settings; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly HttpClient _client; - private readonly IMemoryCache _memoryCache; - private readonly MemoryCacheEntryOptions cacheEntryOptions; + _settings = settings.Value; + _logger = logger; + _httpContextAccessor = httpContextAccessor; + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _client = httpClient; - /// - /// Initializes a new instance of the class. - /// - /// The app repository settings. - /// The platform settings object from configuration. - /// A logger from the built in logger factory. - /// An object with access to the http context. - /// A HttpClient provided by the built in HttpClientFactory. - /// The built in MemoryCache. - public TextClient( - IOptions settings, - IOptions platformSettings, - ILogger logger, - IHttpContextAccessor httpContextAccessor, - HttpClient httpClient, - IMemoryCache memoryCache - ) - { - _settings = settings.Value; - _logger = logger; - _httpContextAccessor = httpContextAccessor; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); - httpClient.DefaultRequestHeaders.Add( - General.SubscriptionKeyHeaderName, - platformSettings.Value.SubscriptionKey - ); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _client = httpClient; + _memoryCache = memoryCache; + _cacheEntryOptions = new MemoryCacheEntryOptions() + .SetPriority(CacheItemPriority.High) + .SetAbsoluteExpiration(new TimeSpan(0, 0, settings.Value.CacheResourceLifeTimeInSeconds)); + } - _memoryCache = memoryCache; - cacheEntryOptions = new MemoryCacheEntryOptions() - .SetPriority(CacheItemPriority.High) - .SetAbsoluteExpiration(new TimeSpan(0, 0, settings.Value.CacheResourceLifeTimeInSeconds)); - } + /// + public async Task GetText(string org, string app, string language) + { + TextResource? textResource = null; + string cacheKey = $"{org}-{app}-{language.ToLower()}"; - /// - public async Task GetText(string org, string app, string language) + if (!_memoryCache.TryGetValue(cacheKey, out textResource)) { - TextResource? textResource = null; - string cacheKey = $"{org}-{app}-{language.ToLower()}"; + // Key not in cache, so get text from Platform Storage + string url = $"applications/{org}/{app}/texts/{language}"; + string token = JwtTokenUtil.GetTokenFromContext( + _httpContextAccessor.HttpContext, + _settings.RuntimeCookieName + ); - if (!_memoryCache.TryGetValue(cacheKey, out textResource)) + HttpResponseMessage response = await _client.GetAsync(token, url); + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + textResource = await JsonSerializerPermissive.DeserializeAsync(response.Content); + _memoryCache.Set(cacheKey, textResource, _cacheEntryOptions); + } + else { - // Key not in cache, so get text from Platform Storage - string url = $"applications/{org}/{app}/texts/{language}"; - string token = JwtTokenUtil.GetTokenFromContext( - _httpContextAccessor.HttpContext, - _settings.RuntimeCookieName + _logger.LogError( + $"Getting text resource for {org}/{app} with language code: {language} failed with statuscode {response.StatusCode}" ); - - HttpResponseMessage response = await _client.GetAsync(token, url); - if (response.StatusCode == System.Net.HttpStatusCode.OK) - { - textResource = await JsonSerializerPermissive.DeserializeAsync(response.Content); - _memoryCache.Set(cacheKey, textResource, cacheEntryOptions); - } - else - { - _logger.LogError( - $"Getting text resource for {org}/{app} with language code: {language} failed with statuscode {response.StatusCode}" - ); - } } - - return textResource; } + + return textResource; } } diff --git a/src/Altinn.App.Core/Interface/IAppResources.cs b/src/Altinn.App.Core/Interface/IAppResources.cs index d02463c48..463b90505 100644 --- a/src/Altinn.App.Core/Interface/IAppResources.cs +++ b/src/Altinn.App.Core/Interface/IAppResources.cs @@ -2,183 +2,182 @@ using Altinn.App.Core.Models.Layout; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface for execution functionality +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.App.IAppResources instead", error: true)] +public interface IAppResources { /// - /// Interface for execution functionality - /// - [Obsolete(message: "Use Altinn.App.Core.Internal.App.IAppResources instead", error: true)] - public interface IAppResources - { - /// - /// Get the app resource for the given parameters. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// the resource. - /// The app resource. - byte[] GetAppResource(string org, string app, string resource); - - /// - /// Get the app resource for the given parameters. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// the resource. - /// The app resource. - byte[] GetText(string org, string app, string textResource); - - /// - /// Get the text resources in a specific language. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The two letter language code. - /// The text resources in the specified language if they exist. Otherwise null. - Task GetTexts(string org, string app, string language); - - /// - /// Returns the model metadata for an app. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The ServiceMetadata for an app. - [Obsolete("GetModelMetaDataJSON is no longer used by app frontend. Use GetModelJsonSchema.")] - string GetModelMetaDataJSON(string org, string app); - - /// - /// Returns the json schema for the provided model id. - /// - /// Unique identifier for the model. - /// The JSON schema for the model - string GetModelJsonSchema(string modelId); - - /// - /// Method that fetches the runtime resources stored in wwwroot - /// - /// the resource - /// The filestream for the resource file - byte[]? GetRuntimeResource(string resource); - - /// - /// Returns the application metadata for an application. - /// - /// The application metadata for an application. - [Obsolete( - "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationMetadata instead", - false - )] - Application GetApplication(); - - /// - /// Returns the application XACML policy for an application. - /// - /// The application XACML policy for an application. - [Obsolete( - "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationXACMLPolicy instead", - false - )] - string? GetApplicationXACMLPolicy(); - - /// - /// Returns the application BPMN process for an application. - /// - /// The application BPMN process for an application. - [Obsolete( - "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationBPMNProcess instead", - false - )] - string? GetApplicationBPMNProcess(); - - /// - /// Gets the prefill json file - /// - /// the data model name - /// The prefill json file as a string - string? GetPrefillJson(string dataModelName = "ServiceModel"); - - /// - /// Get the class ref based on data type - /// - /// The datatype - /// Returns the class ref for a given datatype. An empty string is returned if no match is found. - string GetClassRefForLogicDataType(string dataType); - - /// - /// Gets the layouts for the app. - /// - /// A dictionary of FormLayout objects serialized to JSON - string GetLayouts(); - - /// - /// Gets the the layouts settings - /// - /// The layout settings as a JSON string - string? GetLayoutSettingsString(); - - /// - /// Gets the layout settings - /// - /// The layout settings - LayoutSettings GetLayoutSettings(); - - /// - /// Gets the the layout sets - /// - /// The layout sets - string GetLayoutSets(); - - /// - /// Gets the footer layout - /// - /// The footer layout - Task GetFooter(); - - /// - /// Get the layout set definition. Return null if no layoutsets exists - /// - LayoutSets? GetLayoutSet(); - - /// - /// - /// - LayoutSet? GetLayoutSetForTask(string taskId); - - /// - /// Gets the layouts for av given layoutset - /// - /// The layot set id - /// A dictionary of FormLayout objects serialized to JSON - string GetLayoutsForSet(string layoutSetId); - - /// - /// Gets the full layout model for the optional set - /// - LayoutModel GetLayoutModel(string? layoutSetId = null); - - /// - /// Gets the the layouts settings for a layoutset - /// - /// The layot set id - /// The layout settings as a JSON string - string? GetLayoutSettingsStringForSet(string layoutSetId); - - /// - /// Gets the the layouts settings for a layoutset - /// - /// The layout settings - LayoutSettings? GetLayoutSettingsForSet(string? layoutSetId); - - /// - /// Gets the ruleconfiguration for av given layoutset - /// - /// A dictionary of FormLayout objects serialized to JSON - byte[] GetRuleConfigurationForSet(string id); - - /// - /// Gets the the rule handler for a layoutset - /// - /// The layout settings - byte[] GetRuleHandlerForSet(string id); - } + /// Get the app resource for the given parameters. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// the resource. + /// The app resource. + byte[] GetAppResource(string org, string app, string resource); + + /// + /// Get the app resource for the given parameters. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// the resource. + /// The app resource. + byte[] GetText(string org, string app, string textResource); + + /// + /// Get the text resources in a specific language. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The two letter language code. + /// The text resources in the specified language if they exist. Otherwise null. + Task GetTexts(string org, string app, string language); + + /// + /// Returns the model metadata for an app. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The ServiceMetadata for an app. + [Obsolete("GetModelMetaDataJSON is no longer used by app frontend. Use GetModelJsonSchema.")] + string GetModelMetaDataJSON(string org, string app); + + /// + /// Returns the json schema for the provided model id. + /// + /// Unique identifier for the model. + /// The JSON schema for the model + string GetModelJsonSchema(string modelId); + + /// + /// Method that fetches the runtime resources stored in wwwroot + /// + /// the resource + /// The filestream for the resource file + byte[]? GetRuntimeResource(string resource); + + /// + /// Returns the application metadata for an application. + /// + /// The application metadata for an application. + [Obsolete( + "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationMetadata instead", + false + )] + Application GetApplication(); + + /// + /// Returns the application XACML policy for an application. + /// + /// The application XACML policy for an application. + [Obsolete( + "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationXACMLPolicy instead", + false + )] + string? GetApplicationXACMLPolicy(); + + /// + /// Returns the application BPMN process for an application. + /// + /// The application BPMN process for an application. + [Obsolete( + "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationBPMNProcess instead", + false + )] + string? GetApplicationBPMNProcess(); + + /// + /// Gets the prefill json file + /// + /// the data model name + /// The prefill json file as a string + string? GetPrefillJson(string dataModelName = "ServiceModel"); + + /// + /// Get the class ref based on data type + /// + /// The datatype + /// Returns the class ref for a given datatype. An empty string is returned if no match is found. + string GetClassRefForLogicDataType(string dataType); + + /// + /// Gets the layouts for the app. + /// + /// A dictionary of FormLayout objects serialized to JSON + string GetLayouts(); + + /// + /// Gets the the layouts settings + /// + /// The layout settings as a JSON string + string? GetLayoutSettingsString(); + + /// + /// Gets the layout settings + /// + /// The layout settings + LayoutSettings GetLayoutSettings(); + + /// + /// Gets the the layout sets + /// + /// The layout sets + string GetLayoutSets(); + + /// + /// Gets the footer layout + /// + /// The footer layout + Task GetFooter(); + + /// + /// Get the layout set definition. Return null if no layoutsets exists + /// + LayoutSets? GetLayoutSet(); + + /// + /// + /// + LayoutSet? GetLayoutSetForTask(string taskId); + + /// + /// Gets the layouts for av given layoutset + /// + /// The layot set id + /// A dictionary of FormLayout objects serialized to JSON + string GetLayoutsForSet(string layoutSetId); + + /// + /// Gets the full layout model for the optional set + /// + LayoutModel GetLayoutModel(string? layoutSetId = null); + + /// + /// Gets the the layouts settings for a layoutset + /// + /// The layot set id + /// The layout settings as a JSON string + string? GetLayoutSettingsStringForSet(string layoutSetId); + + /// + /// Gets the the layouts settings for a layoutset + /// + /// The layout settings + LayoutSettings? GetLayoutSettingsForSet(string? layoutSetId); + + /// + /// Gets the ruleconfiguration for av given layoutset + /// + /// A dictionary of FormLayout objects serialized to JSON + byte[] GetRuleConfigurationForSet(string id); + + /// + /// Gets the the rule handler for a layoutset + /// + /// The layout settings + byte[] GetRuleHandlerForSet(string id); } diff --git a/src/Altinn.App.Core/Interface/IApplication.cs b/src/Altinn.App.Core/Interface/IApplication.cs index b2caf3d88..4ca97271a 100644 --- a/src/Altinn.App.Core/Interface/IApplication.cs +++ b/src/Altinn.App.Core/Interface/IApplication.cs @@ -1,18 +1,17 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface for retrieving application metadata data related operations +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.App.IApplicationClient instead", error: true)] +public interface IApplication { /// - /// Interface for retrieving application metadata data related operations + /// Gets the application metdata /// - [Obsolete(message: "Use Altinn.App.Core.Internal.App.IApplicationClient instead", error: true)] - public interface IApplication - { - /// - /// Gets the application metdata - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - Task GetApplication(string org, string app); - } + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + Task GetApplication(string org, string app); } diff --git a/src/Altinn.App.Core/Interface/IAuthentication.cs b/src/Altinn.App.Core/Interface/IAuthentication.cs index e0c40ae5b..755417054 100644 --- a/src/Altinn.App.Core/Interface/IAuthentication.cs +++ b/src/Altinn.App.Core/Interface/IAuthentication.cs @@ -1,15 +1,14 @@ -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Authentication interface. +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IAuthenticationClient instead", error: true)] +public interface IAuthentication { /// - /// Authentication interface. + /// Refreshes the AltinnStudioRuntime JwtToken. /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IAuthenticationClient instead", error: true)] - public interface IAuthentication - { - /// - /// Refreshes the AltinnStudioRuntime JwtToken. - /// - /// Response message from Altinn Platform with refreshed token. - Task RefreshToken(); - } + /// Response message from Altinn Platform with refreshed token. + Task RefreshToken(); } diff --git a/src/Altinn.App.Core/Interface/IAuthorization.cs b/src/Altinn.App.Core/Interface/IAuthorization.cs index 0d714603c..e9b5d27eb 100644 --- a/src/Altinn.App.Core/Interface/IAuthorization.cs +++ b/src/Altinn.App.Core/Interface/IAuthorization.cs @@ -2,44 +2,43 @@ using Altinn.App.Core.Models; using Altinn.Platform.Register.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface for authorization functionality. +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IAuthorizationClient instead", error: true)] +public interface IAuthorization { /// - /// Interface for authorization functionality. + /// Returns the list of parties that user has any rights for. /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IAuthorizationClient instead", error: true)] - public interface IAuthorization - { - /// - /// Returns the list of parties that user has any rights for. - /// - /// The userId. - /// List of parties. - Task?> GetPartyList(int userId); + /// The userId. + /// List of parties. + Task?> GetPartyList(int userId); - /// - /// Verifies that the selected party is contained in the user's party list. - /// - /// The user id. - /// The party id. - /// Boolean indicating whether or not the user can represent the selected party. - Task ValidateSelectedParty(int userId, int partyId); + /// + /// Verifies that the selected party is contained in the user's party list. + /// + /// The user id. + /// The party id. + /// Boolean indicating whether or not the user can represent the selected party. + Task ValidateSelectedParty(int userId, int partyId); - /// - /// Check if the user is authorized to perform the given action on the given instance. - /// - /// - /// - /// - /// - /// - /// - Task AuthorizeAction( - AppIdentifier appIdentifier, - InstanceIdentifier instanceIdentifier, - ClaimsPrincipal user, - string action, - string? taskId = null - ); - } + /// + /// Check if the user is authorized to perform the given action on the given instance. + /// + /// + /// + /// + /// + /// + /// + Task AuthorizeAction( + AppIdentifier appIdentifier, + InstanceIdentifier instanceIdentifier, + ClaimsPrincipal user, + string action, + string? taskId = null + ); } diff --git a/src/Altinn.App.Core/Interface/IDSF.cs b/src/Altinn.App.Core/Interface/IDSF.cs index cf71a0a2b..5ad28c6a4 100644 --- a/src/Altinn.App.Core/Interface/IDSF.cs +++ b/src/Altinn.App.Core/Interface/IDSF.cs @@ -1,21 +1,17 @@ using Altinn.Platform.Register.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface for the resident registration database (DSF: Det sentrale folkeregisteret) +/// +[Obsolete(message: "Upstream API changed. Use Altinn.App.Core.Internal.Registers.IPersonClient instead", error: true)] +public interface IDSF { /// - /// Interface for the resident registration database (DSF: Det sentrale folkeregisteret) + /// Method for getting a person based on their social security number /// - [Obsolete( - message: "Upstream API changed. Use Altinn.App.Core.Internal.Registers.IPersonClient instead", - error: true - )] - public interface IDSF - { - /// - /// Method for getting a person based on their social security number - /// - /// The social security number - /// The person for the given social security number - Task GetPerson(string SSN); - } + /// The social security number + /// The person for the given social security number + Task GetPerson(string SSN); } diff --git a/src/Altinn.App.Core/Interface/IData.cs b/src/Altinn.App.Core/Interface/IData.cs index 840eae4f9..40164f402 100644 --- a/src/Altinn.App.Core/Interface/IData.cs +++ b/src/Altinn.App.Core/Interface/IData.cs @@ -2,218 +2,212 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface for data handling +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Data.IDataClient instead", error: true)] +public interface IData { /// - /// Interface for data handling + /// Stores the form model /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Data.IDataClient instead", error: true)] - public interface IData - { - /// - /// Stores the form model - /// - /// The type - /// The app model to serialize - /// The instance id - /// The type for serialization - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The data type to create, must be a valid data type defined in application metadata - Task InsertFormData( - T dataToSerialize, - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - string dataType - ); + /// The type + /// The app model to serialize + /// The instance id + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The data type to create, must be a valid data type defined in application metadata + Task InsertFormData( + T dataToSerialize, + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + string dataType + ); - /// - /// Stores the form - /// - /// The model type - /// The instance that the data element belongs to - /// The data type with requirements - /// The data element instance - /// The class type describing the data - /// The data element metadata - Task InsertFormData(Instance instance, string dataType, T dataToSerialize, Type type); + /// + /// Stores the form + /// + /// The model type + /// The instance that the data element belongs to + /// The data type with requirements + /// The data element instance + /// The class type describing the data + /// The data element metadata + Task InsertFormData(Instance instance, string dataType, T dataToSerialize, Type type); - /// - /// updates the form data - /// - /// The type - /// The form data to serialize - /// The instanceid - /// The type for serialization - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// the data id - Task UpdateData( - T dataToSerialize, - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - Guid dataId - ); + /// + /// updates the form data + /// + /// The type + /// The form data to serialize + /// The instanceid + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// the data id + Task UpdateData( + T dataToSerialize, + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + Guid dataId + ); - /// - /// Gets the form data - /// - /// The instanceid - /// The type for serialization - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// the data id - Task GetFormData( - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - Guid dataId - ); + /// + /// Gets the form data + /// + /// The instanceid + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// the data id + Task GetFormData( + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + Guid dataId + ); - /// - /// Gets the data as is. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instanceid - /// the data id - Task GetBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId); + /// + /// Gets the data as is. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instanceid + /// the data id + Task GetBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId); - /// - /// Method that gets metadata on form attachments ordered by attachmentType - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instance id - /// A list with attachments metadata ordered by attachmentType - Task> GetBinaryDataList( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid - ); + /// + /// Method that gets metadata on form attachments ordered by attachmentType + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// A list with attachments metadata ordered by attachmentType + Task> GetBinaryDataList(string org, string app, int instanceOwnerPartyId, Guid instanceGuid); - /// - /// Method that removes a form attachments from disk/storage - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instance id - /// The attachment id - [Obsolete("Use method DeleteData with delayed=false instead.", error: true)] - Task DeleteBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid); + /// + /// Method that removes a form attachments from disk/storage + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The attachment id + [Obsolete("Use method DeleteData with delayed=false instead.", error: true)] + Task DeleteBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid); - /// - /// Method that removes a data elemen from disk/storage immediatly or marks it as deleted. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instance id - /// The attachment id - /// A boolean indicating whether or not the delete should be executed immediately or delayed - Task DeleteData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid, - bool delay - ); + /// + /// Method that removes a data elemen from disk/storage immediatly or marks it as deleted. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The attachment id + /// A boolean indicating whether or not the delete should be executed immediately or delayed + Task DeleteData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid, + bool delay + ); - /// - /// Method that saves a form attachments to disk/storage and returns the new data element. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instance id - /// The data type to create, must be a valid data type defined in application metadata - /// Http request containing the attachment to be saved - Task InsertBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - string dataType, - HttpRequest request - ); + /// + /// Method that saves a form attachments to disk/storage and returns the new data element. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The data type to create, must be a valid data type defined in application metadata + /// Http request containing the attachment to be saved + Task InsertBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + string dataType, + HttpRequest request + ); - /// - /// Method that updates a form attachments to disk/storage and returns the updated data element. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instance id - /// The data id - /// Http request containing the attachment to be saved - [Obsolete( - message: "Deprecated please use UpdateBinaryData(InstanceIdentifier, string, string, Guid, Stream) instead", - error: false - )] - Task UpdateBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid, - HttpRequest request - ); + /// + /// Method that updates a form attachments to disk/storage and returns the updated data element. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The data id + /// Http request containing the attachment to be saved + [Obsolete( + message: "Deprecated please use UpdateBinaryData(InstanceIdentifier, string, string, Guid, Stream) instead", + error: false + )] + Task UpdateBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid, + HttpRequest request + ); - /// - /// Method that updates a form attachments to disk/storage and returns the updated data element. - /// - /// Instance identifier instanceOwnerPartyId and instanceGuid - /// Content type of the updated binary data - /// Filename of the updated binary data - /// Guid of the data element to update - /// Updated binary data - Task UpdateBinaryData( - InstanceIdentifier instanceIdentifier, - string? contentType, - string filename, - Guid dataGuid, - Stream stream - ); + /// + /// Method that updates a form attachments to disk/storage and returns the updated data element. + /// + /// Instance identifier instanceOwnerPartyId and instanceGuid + /// Content type of the updated binary data + /// Filename of the updated binary data + /// Guid of the data element to update + /// Updated binary data + Task UpdateBinaryData( + InstanceIdentifier instanceIdentifier, + string? contentType, + string filename, + Guid dataGuid, + Stream stream + ); - /// - /// Insert a binary data element. - /// - /// isntanceId = {instanceOwnerPartyId}/{instanceGuid} - /// data type - /// content type - /// filename - /// the stream to stream - /// - Task InsertBinaryData( - string instanceId, - string dataType, - string contentType, - string filename, - Stream stream - ); + /// + /// Insert a binary data element. + /// + /// isntanceId = {instanceOwnerPartyId}/{instanceGuid} + /// data type + /// content type + /// filename + /// the stream to stream + /// + Task InsertBinaryData( + string instanceId, + string dataType, + string contentType, + string filename, + Stream stream + ); - /// - /// Updates the data element metadata object. - /// - /// The instance which is not updated - /// The data element with values to update - /// the updated data element - Task Update(Instance instance, DataElement dataElement); - } + /// + /// Updates the data element metadata object. + /// + /// The instance which is not updated + /// The data element with values to update + /// the updated data element + Task Update(Instance instance, DataElement dataElement); } diff --git a/src/Altinn.App.Core/Interface/IER.cs b/src/Altinn.App.Core/Interface/IER.cs index 18f497dfa..c5593c75a 100644 --- a/src/Altinn.App.Core/Interface/IER.cs +++ b/src/Altinn.App.Core/Interface/IER.cs @@ -1,18 +1,17 @@ using Altinn.Platform.Register.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface for the entity registry (ER: Enhetsregisteret) +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IOrganizationClient instead", error: true)] +public interface IER { /// - /// Interface for the entity registry (ER: Enhetsregisteret) + /// Method for getting an organization based on a organization nr /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IOrganizationClient instead", error: true)] - public interface IER - { - /// - /// Method for getting an organization based on a organization nr - /// - /// the organization number - /// The organization for the given organization number - Task GetOrganization(string OrgNr); - } + /// the organization number + /// The organization for the given organization number + Task GetOrganization(string OrgNr); } diff --git a/src/Altinn.App.Core/Interface/IEvents.cs b/src/Altinn.App.Core/Interface/IEvents.cs index 5ed3a1ccf..b9961679d 100644 --- a/src/Altinn.App.Core/Interface/IEvents.cs +++ b/src/Altinn.App.Core/Interface/IEvents.cs @@ -1,16 +1,15 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface describing client implementations for the Events component in the Altinn 3 platform. +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Events.IEventsClient instead", error: true)] +public interface IEvents { /// - /// Interface describing client implementations for the Events component in the Altinn 3 platform. + /// Adds a new event to the events published by the Events component. /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Events.IEventsClient instead", error: true)] - public interface IEvents - { - /// - /// Adds a new event to the events published by the Events component. - /// - Task AddEvent(string eventType, Instance instance); - } + Task AddEvent(string eventType, Instance instance); } diff --git a/src/Altinn.App.Core/Interface/IInstance.cs b/src/Altinn.App.Core/Interface/IInstance.cs index e8bd4465c..5a612c10f 100644 --- a/src/Altinn.App.Core/Interface/IInstance.cs +++ b/src/Altinn.App.Core/Interface/IInstance.cs @@ -2,143 +2,138 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Primitives; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface for handling form data related operations +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Instances.IInstanceClient instead", error: true)] +public interface IInstance { /// - /// Interface for handling form data related operations + /// Gets the instance /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Instances.IInstanceClient instead", error: true)] - public interface IInstance - { - /// - /// Gets the instance - /// - Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceId); + Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceId); - /// - /// Gets the instance anew. Instance must have set appId, instanceOwner.PartyId and Id. - /// - Task GetInstance(Instance instance); - - /// - /// Gets a list of instances based on a dictionary of provided query parameters. - /// - Task> GetInstances(Dictionary queryParams); + /// + /// Gets the instance anew. Instance must have set appId, instanceOwner.PartyId and Id. + /// + Task GetInstance(Instance instance); - /// - /// Updates the process model of the instance and returns the updated instance. - /// - Task UpdateProcess(Instance instance); + /// + /// Gets a list of instances based on a dictionary of provided query parameters. + /// + Task> GetInstances(Dictionary queryParams); - /// - /// Creates an instance of an application with no data. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// the instance template to create (must have instanceOwner with partyId, personNumber or organisationNumber set) - /// The created instance - Task CreateInstance(string org, string app, Instance instanceTemplate); + /// + /// Updates the process model of the instance and returns the updated instance. + /// + Task UpdateProcess(Instance instance); - /// - /// Add complete confirmation. - /// - /// - /// Add to an instance that a given stakeholder considers the instance as no longer needed by them. The stakeholder has - /// collected all the data and information they needed from the instance and expect no additional data to be added to it. - /// The body of the request isn't used for anything despite this being a POST operation. - /// - /// The party id of the instance owner. - /// The id of the instance to confirm as complete. - /// Returns the updated instance. - Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid); + /// + /// Creates an instance of an application with no data. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// the instance template to create (must have instanceOwner with partyId, personNumber or organisationNumber set) + /// The created instance + Task CreateInstance(string org, string app, Instance instanceTemplate); - /// - /// Update read status. - /// - /// The party id of the instance owner. - /// The id of the instance to confirm as complete. - /// The new instance read status. - /// Returns the updated instance. - Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus); + /// + /// Add complete confirmation. + /// + /// + /// Add to an instance that a given stakeholder considers the instance as no longer needed by them. The stakeholder has + /// collected all the data and information they needed from the instance and expect no additional data to be added to it. + /// The body of the request isn't used for anything despite this being a POST operation. + /// + /// The party id of the instance owner. + /// The id of the instance to confirm as complete. + /// Returns the updated instance. + Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid); - /// - /// Update substatus. - /// - /// The party id of the instance owner. - /// The id of the instance to be updated. - /// The new substatus. - /// Returns the updated instance. - Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus); + /// + /// Update read status. + /// + /// The party id of the instance owner. + /// The id of the instance to confirm as complete. + /// The new instance read status. + /// Returns the updated instance. + Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus); - /// - /// Update presentation texts. - /// - /// - /// The provided presentation texts will be merged with the existing collection of presentation texts on the instance. - /// - /// The party id of the instance owner. - /// The id of the instance to update presentation texts for. - /// The presentation texts - /// Returns the updated instance. - Task UpdatePresentationTexts( - int instanceOwnerPartyId, - Guid instanceGuid, - PresentationTexts presentationTexts - ); + /// + /// Update substatus. + /// + /// The party id of the instance owner. + /// The id of the instance to be updated. + /// The new substatus. + /// Returns the updated instance. + Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus); - /// - /// Update data values. - /// - /// - /// The provided data values will be merged with the existing collection of data values on the instance. - /// - /// The party id of the instance owner. - /// The id of the instance to update data values for. - /// The data values - /// Returns the updated instance. - Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues); + /// + /// Update presentation texts. + /// + /// + /// The provided presentation texts will be merged with the existing collection of presentation texts on the instance. + /// + /// The party id of the instance owner. + /// The id of the instance to update presentation texts for. + /// The presentation texts + /// Returns the updated instance. + Task UpdatePresentationTexts( + int instanceOwnerPartyId, + Guid instanceGuid, + PresentationTexts presentationTexts + ); - /// - /// Update data data values. - /// - /// - /// The provided data value will be added with the existing collection of data values on the instance. - /// - /// The instance - /// The data value (null unsets the value) - /// Returns the updated instance. - async Task UpdateDataValues(Instance instance, Dictionary dataValues) - { - var id = new InstanceIdentifier(instance); - return await UpdateDataValues( - id.InstanceOwnerPartyId, - id.InstanceGuid, - new DataValues { Values = dataValues } - ); - } + /// + /// Update data values. + /// + /// + /// The provided data values will be merged with the existing collection of data values on the instance. + /// + /// The party id of the instance owner. + /// The id of the instance to update data values for. + /// The data values + /// Returns the updated instance. + Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues); - /// - /// Update single data value. - /// - /// - /// The provided data value will be added with the existing collection of data values on the instance. - /// - /// The instance - /// The key of the DataValues collection to be updated. - /// The data value (null unsets the value) - /// Returns the updated instance. - async Task UpdateDataValue(Instance instance, string key, string? value) - { - return await UpdateDataValues(instance, new Dictionary { { key, value } }); - } + /// + /// Update data data values. + /// + /// + /// The provided data value will be added with the existing collection of data values on the instance. + /// + /// The instance + /// The data value (null unsets the value) + /// Returns the updated instance. + async Task UpdateDataValues(Instance instance, Dictionary dataValues) + { + var id = new InstanceIdentifier(instance); + return await UpdateDataValues(id.InstanceOwnerPartyId, id.InstanceGuid, new DataValues { Values = dataValues }); + } - /// - /// Delete instance. - /// - /// The party id of the instance owner. - /// The id of the instance to delete. - /// Boolean to indicate if instance should be hard deleted. - /// Returns the deleted instance. - Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard); + /// + /// Update single data value. + /// + /// + /// The provided data value will be added with the existing collection of data values on the instance. + /// + /// The instance + /// The key of the DataValues collection to be updated. + /// The data value (null unsets the value) + /// Returns the updated instance. + async Task UpdateDataValue(Instance instance, string key, string? value) + { + return await UpdateDataValues(instance, new Dictionary { { key, value } }); } + + /// + /// Delete instance. + /// + /// The party id of the instance owner. + /// The id of the instance to delete. + /// Boolean to indicate if instance should be hard deleted. + /// Returns the deleted instance. + Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard); } diff --git a/src/Altinn.App.Core/Interface/IInstanceEvent.cs b/src/Altinn.App.Core/Interface/IInstanceEvent.cs index 2bd9c4181..fa967bd0c 100644 --- a/src/Altinn.App.Core/Interface/IInstanceEvent.cs +++ b/src/Altinn.App.Core/Interface/IInstanceEvent.cs @@ -1,29 +1,28 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface for handling instance event related operations +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Instances.IInstanceEventClient instead", error: true)] +public interface IInstanceEvent { /// - /// Interface for handling instance event related operations + /// Stores the instance event /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Instances.IInstanceEventClient instead", error: true)] - public interface IInstanceEvent - { - /// - /// Stores the instance event - /// - Task SaveInstanceEvent(object dataToSerialize, string org, string app); + Task SaveInstanceEvent(object dataToSerialize, string org, string app); - /// - /// Gets the instance events related to the instance matching the instance id. - /// - Task> GetInstanceEvents( - string instanceId, - string instanceOwnerPartyId, - string org, - string app, - string[] eventTypes, - string from, - string to - ); - } + /// + /// Gets the instance events related to the instance matching the instance id. + /// + Task> GetInstanceEvents( + string instanceId, + string instanceOwnerPartyId, + string org, + string app, + string[] eventTypes, + string from, + string to + ); } diff --git a/src/Altinn.App.Core/Interface/IPersonLookup.cs b/src/Altinn.App.Core/Interface/IPersonLookup.cs index e9676b41b..3d77eb916 100644 --- a/src/Altinn.App.Core/Interface/IPersonLookup.cs +++ b/src/Altinn.App.Core/Interface/IPersonLookup.cs @@ -1,24 +1,23 @@ using Altinn.Platform.Register.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Describes the methods required by a person lookup service. +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IPersonClient instead", error: true)] +public interface IPersonLookup { /// - /// Describes the methods required by a person lookup service. + /// Get the object for the person identified with the parameters. /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IPersonClient instead", error: true)] - public interface IPersonLookup - { - /// - /// Get the object for the person identified with the parameters. - /// - /// - /// The method requires both the national identity number and the last name of the person. This is used to - /// verify that entered information is correct and to prevent testing of random identity numbers. - /// - /// The national identity number of the person. - /// The last name of the person. - /// The cancellation token to cancel operation. - /// The identified person if found. - Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct); - } + /// + /// The method requires both the national identity number and the last name of the person. This is used to + /// verify that entered information is correct and to prevent testing of random identity numbers. + /// + /// The national identity number of the person. + /// The last name of the person. + /// The cancellation token to cancel operation. + /// The identified person if found. + Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct); } diff --git a/src/Altinn.App.Core/Interface/IPersonRetriever.cs b/src/Altinn.App.Core/Interface/IPersonRetriever.cs index 2dfc33963..8ded2727c 100644 --- a/src/Altinn.App.Core/Interface/IPersonRetriever.cs +++ b/src/Altinn.App.Core/Interface/IPersonRetriever.cs @@ -1,24 +1,23 @@ using Altinn.Platform.Register.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Describes the required methods for an implementation of a person repository client. +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IPersonClient instead", error: true)] +public interface IPersonRetriever { /// - /// Describes the required methods for an implementation of a person repository client. + /// Get the object for the person identified with the parameters. /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IPersonClient instead", error: true)] - public interface IPersonRetriever - { - /// - /// Get the object for the person identified with the parameters. - /// - /// - /// The method requires both the national identity number and the last name of the person. This is used to - /// verify that entered information is correct and to prevent testing of random identity numbers. - /// - /// The national identity number of the person. - /// The last name of the person. - /// The cancellation token to cancel operation. - /// The identified person if found. - Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct); - } + /// + /// The method requires both the national identity number and the last name of the person. This is used to + /// verify that entered information is correct and to prevent testing of random identity numbers. + /// + /// The national identity number of the person. + /// The last name of the person. + /// The cancellation token to cancel operation. + /// The identified person if found. + Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct); } diff --git a/src/Altinn.App.Core/Interface/IPrefill.cs b/src/Altinn.App.Core/Interface/IPrefill.cs index 47e676edc..1da354932 100644 --- a/src/Altinn.App.Core/Interface/IPrefill.cs +++ b/src/Altinn.App.Core/Interface/IPrefill.cs @@ -1,35 +1,30 @@ -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// The prefill service +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Prefill.IPrefill instead", error: true)] +public interface IPrefill { /// - /// The prefill service + /// Prefills the data model based on key/values in the dictionary. /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Prefill.IPrefill instead", error: true)] - public interface IPrefill - { - /// - /// Prefills the data model based on key/values in the dictionary. - /// - /// The data model object - /// External given prefill - /// Ignore errors when true, throw on errors when false - void PrefillDataModel( - object dataModel, - Dictionary externalPrefill, - bool continueOnError = false - ); + /// The data model object + /// External given prefill + /// Ignore errors when true, throw on errors when false + void PrefillDataModel(object dataModel, Dictionary externalPrefill, bool continueOnError = false); - /// - /// Prefills the data model based on the prefill json configuration file - /// - /// The partyId of the instance owner - /// The data model name - /// The data model object - /// External given prefill - Task PrefillDataModel( - string partyId, - string dataModelName, - object dataModel, - Dictionary? externalPrefill = null - ); - } + /// + /// Prefills the data model based on the prefill json configuration file + /// + /// The partyId of the instance owner + /// The data model name + /// The data model object + /// External given prefill + Task PrefillDataModel( + string partyId, + string dataModelName, + object dataModel, + Dictionary? externalPrefill = null + ); } diff --git a/src/Altinn.App.Core/Interface/IProcess.cs b/src/Altinn.App.Core/Interface/IProcess.cs index 332730d37..f99fccb36 100644 --- a/src/Altinn.App.Core/Interface/IProcess.cs +++ b/src/Altinn.App.Core/Interface/IProcess.cs @@ -1,22 +1,21 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Process service that encapsulate reading of the BPMN process definition. +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Process.IProcessClient instead", error: true)] +public interface IProcess { /// - /// Process service that encapsulate reading of the BPMN process definition. + /// Returns a stream that contains the process definition. /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Process.IProcessClient instead", error: true)] - public interface IProcess - { - /// - /// Returns a stream that contains the process definition. - /// - /// the stream - Stream GetProcessDefinition(); + /// the stream + Stream GetProcessDefinition(); - /// - /// Gets the instance process events related to the instance matching the instance id. - /// - Task GetProcessHistory(string instanceGuid, string instanceOwnerPartyId); - } + /// + /// Gets the instance process events related to the instance matching the instance id. + /// + Task GetProcessHistory(string instanceGuid, string instanceOwnerPartyId); } diff --git a/src/Altinn.App.Core/Interface/IProfile.cs b/src/Altinn.App.Core/Interface/IProfile.cs index 2c0db8a98..96a4adf1f 100644 --- a/src/Altinn.App.Core/Interface/IProfile.cs +++ b/src/Altinn.App.Core/Interface/IProfile.cs @@ -1,18 +1,17 @@ using Altinn.Platform.Profile.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface for profile functionality +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Profile.IProfileClient instead", error: true)] +public interface IProfile { /// - /// Interface for profile functionality + /// Method for getting the userprofile from a given user id /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Profile.IProfileClient instead", error: true)] - public interface IProfile - { - /// - /// Method for getting the userprofile from a given user id - /// - /// the user id - /// The userprofile for the given user id - Task GetUserProfile(int userId); - } + /// the user id + /// The userprofile for the given user id + Task GetUserProfile(int userId); } diff --git a/src/Altinn.App.Core/Interface/IRegister.cs b/src/Altinn.App.Core/Interface/IRegister.cs index edfc92f97..30e67d9bd 100644 --- a/src/Altinn.App.Core/Interface/IRegister.cs +++ b/src/Altinn.App.Core/Interface/IRegister.cs @@ -1,25 +1,24 @@ using Altinn.Platform.Register.Models; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface for register functionality +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IAltinnPartyClient instead", error: true)] +public interface IRegister { /// - /// Interface for register functionality + /// Returns party information /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IAltinnPartyClient instead", error: true)] - public interface IRegister - { - /// - /// Returns party information - /// - /// The partyId - /// The party for the given partyId - Task GetParty(int partyId); + /// The partyId + /// The party for the given partyId + Task GetParty(int partyId); - /// - /// Looks up a party by person or organisation number. - /// - /// A populated lookup object with information about what to look for. - /// The party lookup containing either SSN or organisation number. - Task LookupParty(PartyLookup partyLookup); - } + /// + /// Looks up a party by person or organisation number. + /// + /// A populated lookup object with information about what to look for. + /// The party lookup containing either SSN or organisation number. + Task LookupParty(PartyLookup partyLookup); } diff --git a/src/Altinn.App.Core/Interface/ISecrets.cs b/src/Altinn.App.Core/Interface/ISecrets.cs index 14e8a2ccd..1374c7cce 100644 --- a/src/Altinn.App.Core/Interface/ISecrets.cs +++ b/src/Altinn.App.Core/Interface/ISecrets.cs @@ -1,39 +1,38 @@ using Microsoft.Azure.KeyVault; using Microsoft.Azure.KeyVault.WebKey; -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Interface for secrets service +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Secrets.ISecretsClient instead", error: true)] +public interface ISecrets { /// - /// Interface for secrets service + /// Gets the latest version of a key from key vault. /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Secrets.ISecretsClient instead", error: true)] - public interface ISecrets - { - /// - /// Gets the latest version of a key from key vault. - /// - /// The name of the key. - /// The key as a JSON web key. - Task GetKeyAsync(string keyName); + /// The name of the key. + /// The key as a JSON web key. + Task GetKeyAsync(string keyName); - /// - /// Gets the latest version of a secret from key vault. - /// - /// The name of the secret. - /// The secret value. - Task GetSecretAsync(string secretName); + /// + /// Gets the latest version of a secret from key vault. + /// + /// The name of the secret. + /// The secret value. + Task GetSecretAsync(string secretName); - /// - /// Gets the latest version of a certificate from key vault. - /// - /// The name of certificate. - /// The certificate as a byte array. - Task GetCertificateAsync(string certificateName); + /// + /// Gets the latest version of a certificate from key vault. + /// + /// The name of certificate. + /// The certificate as a byte array. + Task GetCertificateAsync(string certificateName); - /// - /// Gets the key vault client. - /// - /// The key vault client. - KeyVaultClient GetKeyVaultClient(); - } + /// + /// Gets the key vault client. + /// + /// The key vault client. + KeyVaultClient GetKeyVaultClient(); } diff --git a/src/Altinn.App.Core/Interface/IUserTokenProvider.cs b/src/Altinn.App.Core/Interface/IUserTokenProvider.cs index c11c3f9c6..7872ef37d 100644 --- a/src/Altinn.App.Core/Interface/IUserTokenProvider.cs +++ b/src/Altinn.App.Core/Interface/IUserTokenProvider.cs @@ -1,17 +1,16 @@ -namespace Altinn.App.Core.Interface +namespace Altinn.App.Core.Interface; + +/// +/// Defines the methods required for an implementation of a user JSON Web Token provider. +/// The provider is used by client implementations that needs the user token in requests +/// against other systems. +/// +[Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IUserTokenProvider instead", error: true)] +public interface IUserTokenProvider { /// - /// Defines the methods required for an implementation of a user JSON Web Token provider. - /// The provider is used by client implementations that needs the user token in requests - /// against other systems. + /// Defines a method that can return a JSON Web Token of the current user. /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IUserTokenProvider instead", error: true)] - public interface IUserTokenProvider - { - /// - /// Defines a method that can return a JSON Web Token of the current user. - /// - /// The Json Web Token for the current user. - public string GetUserToken(); - } + /// The Json Web Token for the current user. + public string GetUserToken(); } diff --git a/src/Altinn.App.Core/Internal/App/AppMetadata.cs b/src/Altinn.App.Core/Internal/App/AppMetadata.cs index 9de52d66a..c5ebe2d98 100644 --- a/src/Altinn.App.Core/Internal/App/AppMetadata.cs +++ b/src/Altinn.App.Core/Internal/App/AppMetadata.cs @@ -5,127 +5,122 @@ using Altinn.App.Core.Models; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Internal.App +namespace Altinn.App.Core.Internal.App; + +/// +/// Default implementation of IAppMetadata +/// +public class AppMetadata : IAppMetadata { + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + }; + + private readonly AppSettings _settings; + private readonly IFrontendFeatures _frontendFeatures; + private readonly Telemetry? _telemetry; + private ApplicationMetadata? _application; + /// - /// Default implementation of IAppMetadata + /// Initializes a new instance of the class. /// - public class AppMetadata : IAppMetadata + /// The app repository settings. + /// Application features service + /// Telemetry for traces and metrics. + public AppMetadata(IOptions settings, IFrontendFeatures frontendFeatures, Telemetry? telemetry = null) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - AllowTrailingCommas = true - }; - - private readonly AppSettings _settings; - private readonly IFrontendFeatures _frontendFeatures; - private readonly Telemetry? _telemetry; - private ApplicationMetadata? _application; + _settings = settings.Value; + _frontendFeatures = frontendFeatures; + _telemetry = telemetry; + } - /// - /// Initializes a new instance of the class. - /// - /// The app repository settings. - /// Application features service - /// Telemetry for traces and metrics. - public AppMetadata( - IOptions settings, - IFrontendFeatures frontendFeatures, - Telemetry? telemetry = null - ) + /// + /// Thrown if deserialization fails + /// Thrown if applicationmetadata.json file not found + public async Task GetApplicationMetadata() + { + using var activity = _telemetry?.StartGetApplicationMetadataActivity(); + // Cache application metadata + if (_application != null) { - _settings = settings.Value; - _frontendFeatures = frontendFeatures; - _telemetry = telemetry; + return _application; } - /// - /// Thrown if deserialization fails - /// Thrown if applicationmetadata.json file not found - public async Task GetApplicationMetadata() + string filename = Path.Join( + _settings.AppBasePath, + _settings.ConfigurationFolder, + _settings.ApplicationMetadataFileName + ); + try { - using var activity = _telemetry?.StartGetApplicationMetadataActivity(); - // Cache application metadata - if (_application != null) - { - return _application; - } - - string filename = Path.Join( - _settings.AppBasePath, - _settings.ConfigurationFolder, - _settings.ApplicationMetadataFileName - ); - try + if (File.Exists(filename)) { - if (File.Exists(filename)) + using FileStream fileStream = File.OpenRead(filename); + var application = await JsonSerializer.DeserializeAsync( + fileStream, + _jsonSerializerOptions + ); + if (application == null) { - using FileStream fileStream = File.OpenRead(filename); - var application = await JsonSerializer.DeserializeAsync( - fileStream, - _jsonSerializerOptions + throw new ApplicationConfigException( + $"Deserialization returned null, Could indicate problems with deserialization of {filename}" ); - if (application == null) - { - throw new ApplicationConfigException( - $"Deserialization returned null, Could indicate problems with deserialization of {filename}" - ); - } - - application.Features = await _frontendFeatures.GetFrontendFeatures(); - _application = application; - - return _application; } - throw new ApplicationConfigException($"Unable to locate application metadata file: {filename}"); - } - catch (JsonException ex) - { - throw new ApplicationConfigException( - $"Something went wrong when parsing application metadata file: {filename}", - ex - ); + application.Features = await _frontendFeatures.GetFrontendFeatures(); + _application = application; + + return _application; } - } - /// - public async Task GetApplicationXACMLPolicy() + throw new ApplicationConfigException($"Unable to locate application metadata file: {filename}"); + } + catch (JsonException ex) { - using var activity = _telemetry?.StartGetApplicationXACMLPolicyActivity(); - string filename = Path.Join( - _settings.AppBasePath, - _settings.ConfigurationFolder, - _settings.AuthorizationFolder, - _settings.ApplicationXACMLPolicyFileName + throw new ApplicationConfigException( + $"Something went wrong when parsing application metadata file: {filename}", + ex ); - if (File.Exists(filename)) - { - return await File.ReadAllTextAsync(filename, Encoding.UTF8); - } - - throw new FileNotFoundException($"XACML file {filename} not found"); } + } - /// - public async Task GetApplicationBPMNProcess() + /// + public async Task GetApplicationXACMLPolicy() + { + using var activity = _telemetry?.StartGetApplicationXACMLPolicyActivity(); + string filename = Path.Join( + _settings.AppBasePath, + _settings.ConfigurationFolder, + _settings.AuthorizationFolder, + _settings.ApplicationXACMLPolicyFileName + ); + if (File.Exists(filename)) { - using var activity = _telemetry?.StartGetApplicationBPMNProcessActivity(); - string filename = Path.Join( - _settings.AppBasePath, - _settings.ConfigurationFolder, - _settings.ProcessFolder, - _settings.ProcessFileName - ); - if (File.Exists(filename)) - { - return await File.ReadAllTextAsync(filename, Encoding.UTF8); - } + return await File.ReadAllTextAsync(filename, Encoding.UTF8); + } - throw new ApplicationConfigException($"Unable to locate application process file: {filename}"); + throw new FileNotFoundException($"XACML file {filename} not found"); + } + + /// + public async Task GetApplicationBPMNProcess() + { + using var activity = _telemetry?.StartGetApplicationBPMNProcessActivity(); + string filename = Path.Join( + _settings.AppBasePath, + _settings.ConfigurationFolder, + _settings.ProcessFolder, + _settings.ProcessFileName + ); + if (File.Exists(filename)) + { + return await File.ReadAllTextAsync(filename, Encoding.UTF8); } + + throw new ApplicationConfigException($"Unable to locate application process file: {filename}"); } } diff --git a/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs b/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs index 21716a57e..50217e0a1 100644 --- a/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs +++ b/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs @@ -1,30 +1,27 @@ -using System.Runtime.Serialization; +namespace Altinn.App.Core.Internal.App; -namespace Altinn.App.Core.Internal.App +/// +/// Configuration is not valid for application +/// +public class ApplicationConfigException : Exception { /// - /// Configuration is not valid for application + /// Create ApplicationConfigException /// - public class ApplicationConfigException : Exception - { - /// - /// Create ApplicationConfigException - /// - public ApplicationConfigException() { } + public ApplicationConfigException() { } - /// - /// Create ApplicationConfigException - /// - /// Exception message - public ApplicationConfigException(string? message) - : base(message) { } + /// + /// Create ApplicationConfigException + /// + /// Exception message + public ApplicationConfigException(string? message) + : base(message) { } - /// - /// Create ApplicationConfigException - /// - /// Exception message - /// Inner exception - public ApplicationConfigException(string? message, Exception? innerException) - : base(message, innerException) { } - } + /// + /// Create ApplicationConfigException + /// + /// Exception message + /// Inner exception + public ApplicationConfigException(string? message, Exception? innerException) + : base(message, innerException) { } } diff --git a/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs b/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs index 2fb071f33..45c975a29 100644 --- a/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs +++ b/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs @@ -1,37 +1,36 @@ using Altinn.App.Core.Features; using Microsoft.FeatureManagement; -namespace Altinn.App.Core.Internal.App +namespace Altinn.App.Core.Internal.App; + +/// +/// Default implementation of IFrontendFeatures +/// +public class FrontendFeatures : IFrontendFeatures { + private readonly Dictionary _features = new(); + /// /// Default implementation of IFrontendFeatures /// - public class FrontendFeatures : IFrontendFeatures + public FrontendFeatures(IFeatureManager featureManager) { - private readonly Dictionary features = new(); + _features.Add("footer", true); + _features.Add("processActions", true); - /// - /// Default implementation of IFrontendFeatures - /// - public FrontendFeatures(IFeatureManager featureManager) + if (featureManager.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse).Result) { - features.Add("footer", true); - features.Add("processActions", true); - - if (featureManager.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse).Result) - { - features.Add("jsonObjectInDataResponse", true); - } - else - { - features.Add("jsonObjectInDataResponse", false); - } + _features.Add("jsonObjectInDataResponse", true); } - - /// - public Task> GetFrontendFeatures() + else { - return Task.FromResult(features); + _features.Add("jsonObjectInDataResponse", false); } } + + /// + public Task> GetFrontendFeatures() + { + return Task.FromResult(_features); + } } diff --git a/src/Altinn.App.Core/Internal/App/IAppMetadata.cs b/src/Altinn.App.Core/Internal/App/IAppMetadata.cs index fd12f3814..b9bcfb407 100644 --- a/src/Altinn.App.Core/Internal/App/IAppMetadata.cs +++ b/src/Altinn.App.Core/Internal/App/IAppMetadata.cs @@ -1,31 +1,30 @@ using Altinn.App.Core.Models; -namespace Altinn.App.Core.Internal.App +namespace Altinn.App.Core.Internal.App; + +/// +/// Interface for fetching app metadata +/// +public interface IAppMetadata { /// - /// Interface for fetching app metadata + /// Get Application metadata asynchronously /// - public interface IAppMetadata - { - /// - /// Get Application metadata asynchronously - /// - /// - /// - public Task GetApplicationMetadata(); + /// + /// + public Task GetApplicationMetadata(); - /// - /// Returns the application XACML policy for an application. - /// - /// The application XACML policy for an application. - /// - public Task GetApplicationXACMLPolicy(); + /// + /// Returns the application XACML policy for an application. + /// + /// The application XACML policy for an application. + /// + public Task GetApplicationXACMLPolicy(); - /// - /// Returns the application BPMN process for an application. - /// - /// The application BPMN process. - /// - public Task GetApplicationBPMNProcess(); - } + /// + /// Returns the application BPMN process for an application. + /// + /// The application BPMN process. + /// + public Task GetApplicationBPMNProcess(); } diff --git a/src/Altinn.App.Core/Internal/App/IAppResources.cs b/src/Altinn.App.Core/Internal/App/IAppResources.cs index fa9d72569..ca249d4dd 100644 --- a/src/Altinn.App.Core/Internal/App/IAppResources.cs +++ b/src/Altinn.App.Core/Internal/App/IAppResources.cs @@ -2,163 +2,162 @@ using Altinn.App.Core.Models.Layout; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.App +namespace Altinn.App.Core.Internal.App; + +/// +/// Interface for execution functionality +/// +public interface IAppResources { /// - /// Interface for execution functionality - /// - public interface IAppResources - { - /// - /// Get the app resource for the given parameters. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// the resource. - /// The app resource. - byte[] GetText(string org, string app, string textResource); - - /// - /// Get the text resources in a specific language. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The two letter language code. - /// The text resources in the specified language if they exist. Otherwise null. - Task GetTexts(string org, string app, string language); - - /// - /// Returns the json schema for the provided model id. - /// - /// Unique identifier for the model. - /// The JSON schema for the model - string GetModelJsonSchema(string modelId); - - /// - /// Returns the application metadata for an application. - /// - /// The application metadata for an application. - [Obsolete( - "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationMetadata instead", - false - )] - Application GetApplication(); - - /// - /// Returns the application XACML policy for an application. - /// - /// The application XACML policy for an application. - [Obsolete( - "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationXACMLPolicy instead", - false - )] - string? GetApplicationXACMLPolicy(); - - /// - /// Returns the application BPMN process for an application. - /// - /// The application BPMN process for an application. - [Obsolete( - "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationBPMNProcess instead", - false - )] - string? GetApplicationBPMNProcess(); - - /// - /// Gets the prefill json file - /// - /// the data model name - /// The prefill json file as a string - string? GetPrefillJson(string dataModelName = "ServiceModel"); - - /// - /// Get the class ref based on data type - /// - /// The datatype - /// Returns the class ref for a given datatype. An empty string is returned if no match is found. - string GetClassRefForLogicDataType(string dataType); - - /// - /// Gets the layouts for the app. - /// - /// A dictionary of FormLayout objects serialized to JSON - string GetLayouts(); - - /// - /// Gets the the layouts settings - /// - /// The layout settings as a JSON string - string? GetLayoutSettingsString(); - - /// - /// Gets the layout settings - /// - /// The layout settings - LayoutSettings GetLayoutSettings(); - - /// - /// Gets the the layout sets - /// - /// The layout sets - string GetLayoutSets(); - - /// - /// Gets the footer layout - /// - /// The footer layout - Task GetFooter(); - - /// - /// Get the layout set definition. Return null if no layoutsets exists - /// - LayoutSets? GetLayoutSet(); - - /// - /// - /// - LayoutSet? GetLayoutSetForTask(string taskId); - - /// - /// Gets the layouts for av given layoutset - /// - /// The layot set id - /// A dictionary of FormLayout objects serialized to JSON - string GetLayoutsForSet(string layoutSetId); - - /// - /// Gets the full layout model for the optional set - /// - LayoutModel GetLayoutModel(string? layoutSetId = null); - - /// - /// Gets the the layouts settings for a layoutset - /// - /// The layot set id - /// The layout settings as a JSON string - string? GetLayoutSettingsStringForSet(string layoutSetId); - - /// - /// Gets the the layouts settings for a layoutset - /// - /// The layout settings - LayoutSettings? GetLayoutSettingsForSet(string? layoutSetId); - - /// - /// Gets the ruleconfiguration for av given layoutset - /// - /// A dictionary of FormLayout objects serialized to JSON - byte[] GetRuleConfigurationForSet(string id); - - /// - /// Gets the the rule handler for a layoutset - /// - /// The layout settings - byte[] GetRuleHandlerForSet(string id); - - /// - /// Gets the the rule handler for a layoutset - /// - /// The layout settings - string? GetValidationConfiguration(string modelId); - } + /// Get the app resource for the given parameters. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// the resource. + /// The app resource. + byte[] GetText(string org, string app, string textResource); + + /// + /// Get the text resources in a specific language. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The two letter language code. + /// The text resources in the specified language if they exist. Otherwise null. + Task GetTexts(string org, string app, string language); + + /// + /// Returns the json schema for the provided model id. + /// + /// Unique identifier for the model. + /// The JSON schema for the model + string GetModelJsonSchema(string modelId); + + /// + /// Returns the application metadata for an application. + /// + /// The application metadata for an application. + [Obsolete( + "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationMetadata instead", + false + )] + Application GetApplication(); + + /// + /// Returns the application XACML policy for an application. + /// + /// The application XACML policy for an application. + [Obsolete( + "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationXACMLPolicy instead", + false + )] + string? GetApplicationXACMLPolicy(); + + /// + /// Returns the application BPMN process for an application. + /// + /// The application BPMN process for an application. + [Obsolete( + "GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationBPMNProcess instead", + false + )] + string? GetApplicationBPMNProcess(); + + /// + /// Gets the prefill json file + /// + /// the data model name + /// The prefill json file as a string + string? GetPrefillJson(string dataModelName = "ServiceModel"); + + /// + /// Get the class ref based on data type + /// + /// The datatype + /// Returns the class ref for a given datatype. An empty string is returned if no match is found. + string GetClassRefForLogicDataType(string dataType); + + /// + /// Gets the layouts for the app. + /// + /// A dictionary of FormLayout objects serialized to JSON + string GetLayouts(); + + /// + /// Gets the the layouts settings + /// + /// The layout settings as a JSON string + string? GetLayoutSettingsString(); + + /// + /// Gets the layout settings + /// + /// The layout settings + LayoutSettings GetLayoutSettings(); + + /// + /// Gets the the layout sets + /// + /// The layout sets + string GetLayoutSets(); + + /// + /// Gets the footer layout + /// + /// The footer layout + Task GetFooter(); + + /// + /// Get the layout set definition. Return null if no layoutsets exists + /// + LayoutSets? GetLayoutSet(); + + /// + /// + /// + LayoutSet? GetLayoutSetForTask(string taskId); + + /// + /// Gets the layouts for av given layoutset + /// + /// The layot set id + /// A dictionary of FormLayout objects serialized to JSON + string GetLayoutsForSet(string layoutSetId); + + /// + /// Gets the full layout model for the optional set + /// + LayoutModel GetLayoutModel(string? layoutSetId = null); + + /// + /// Gets the the layouts settings for a layoutset + /// + /// The layot set id + /// The layout settings as a JSON string + string? GetLayoutSettingsStringForSet(string layoutSetId); + + /// + /// Gets the the layouts settings for a layoutset + /// + /// The layout settings + LayoutSettings? GetLayoutSettingsForSet(string? layoutSetId); + + /// + /// Gets the ruleconfiguration for av given layoutset + /// + /// A dictionary of FormLayout objects serialized to JSON + byte[] GetRuleConfigurationForSet(string id); + + /// + /// Gets the the rule handler for a layoutset + /// + /// The layout settings + byte[] GetRuleHandlerForSet(string id); + + /// + /// Gets the the rule handler for a layoutset + /// + /// The layout settings + string? GetValidationConfiguration(string modelId); } diff --git a/src/Altinn.App.Core/Internal/App/IApplicationClient.cs b/src/Altinn.App.Core/Internal/App/IApplicationClient.cs index dd8859081..39ea204d9 100644 --- a/src/Altinn.App.Core/Internal/App/IApplicationClient.cs +++ b/src/Altinn.App.Core/Internal/App/IApplicationClient.cs @@ -1,17 +1,16 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.App +namespace Altinn.App.Core.Internal.App; + +/// +/// Interface for retrieving application metadata data related operations +/// +public interface IApplicationClient { /// - /// Interface for retrieving application metadata data related operations + /// Gets the application metdata /// - public interface IApplicationClient - { - /// - /// Gets the application metdata - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - Task GetApplication(string org, string app); - } + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + Task GetApplication(string org, string app); } diff --git a/src/Altinn.App.Core/Internal/App/IFrontendFeatures.cs b/src/Altinn.App.Core/Internal/App/IFrontendFeatures.cs index d76106571..40d487943 100644 --- a/src/Altinn.App.Core/Internal/App/IFrontendFeatures.cs +++ b/src/Altinn.App.Core/Internal/App/IFrontendFeatures.cs @@ -1,14 +1,13 @@ -namespace Altinn.App.Core.Internal.App +namespace Altinn.App.Core.Internal.App; + +/// +/// Interface reporting features needed by frontend and their status to support multiple versions of the backend +/// +public interface IFrontendFeatures { /// - /// Interface reporting features needed by frontend and their status to support multiple versions of the backend + /// Fetch frontend features that are supported by this backend /// - public interface IFrontendFeatures - { - /// - /// Fetch frontend features that are supported by this backend - /// - /// List of frontend features enabled/disabled for this backend - public Task> GetFrontendFeatures(); - } + /// List of frontend features enabled/disabled for this backend + public Task> GetFrontendFeatures(); } diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthenticationClient.cs b/src/Altinn.App.Core/Internal/Auth/IAuthenticationClient.cs index 061d7fa33..94c5e402f 100644 --- a/src/Altinn.App.Core/Internal/Auth/IAuthenticationClient.cs +++ b/src/Altinn.App.Core/Internal/Auth/IAuthenticationClient.cs @@ -1,14 +1,13 @@ -namespace Altinn.App.Core.Internal.Auth +namespace Altinn.App.Core.Internal.Auth; + +/// +/// Authentication interface. +/// +public interface IAuthenticationClient { /// - /// Authentication interface. + /// Refreshes the AltinnStudioRuntime JwtToken. /// - public interface IAuthenticationClient - { - /// - /// Refreshes the AltinnStudioRuntime JwtToken. - /// - /// Response message from Altinn Platform with refreshed token. - Task RefreshToken(); - } + /// Response message from Altinn Platform with refreshed token. + Task RefreshToken(); } diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs b/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs index 82f3adc7b..7cefe1a91 100644 --- a/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs +++ b/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs @@ -1,56 +1,53 @@ using System.Security.Claims; -using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Auth +namespace Altinn.App.Core.Internal.Auth; + +/// +/// Interface for authorization functionality. +/// +public interface IAuthorizationClient { /// - /// Interface for authorization functionality. + /// Returns the list of parties that user has any rights for. /// - public interface IAuthorizationClient - { - /// - /// Returns the list of parties that user has any rights for. - /// - /// The userId. - /// List of parties. - Task?> GetPartyList(int userId); + /// The userId. + /// List of parties. + Task?> GetPartyList(int userId); - /// - /// Verifies that the selected party is contained in the user's party list. - /// - /// The user id. - /// The party id. - /// Boolean indicating whether or not the user can represent the selected party. - Task ValidateSelectedParty(int userId, int partyId); + /// + /// Verifies that the selected party is contained in the user's party list. + /// + /// The user id. + /// The party id. + /// Boolean indicating whether or not the user can represent the selected party. + Task ValidateSelectedParty(int userId, int partyId); - /// - /// Check if the user is authorized to perform the given action on the given instance. - /// - /// - /// - /// - /// - /// - /// - Task AuthorizeAction( - AppIdentifier appIdentifier, - InstanceIdentifier instanceIdentifier, - ClaimsPrincipal user, - string action, - string? taskId = null - ); + /// + /// Check if the user is authorized to perform the given action on the given instance. + /// + /// + /// + /// + /// + /// + /// + Task AuthorizeAction( + AppIdentifier appIdentifier, + InstanceIdentifier instanceIdentifier, + ClaimsPrincipal user, + string action, + string? taskId = null + ); - /// - /// Check if the user is authorized to perform the given actions on the given instance. - /// - /// - /// - /// - /// - Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions); - } + /// + /// Check if the user is authorized to perform the given actions on the given instance. + /// + /// + /// + /// + /// + Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions); } diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs b/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs index ec0477825..385521f7d 100644 --- a/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs +++ b/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs @@ -5,52 +5,51 @@ using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Auth +namespace Altinn.App.Core.Internal.Auth; + +/// +/// Interface for authorization functionality. +/// +public interface IAuthorizationService { /// - /// Interface for authorization functionality. + /// Returns the list of parties that user has any rights for. /// - public interface IAuthorizationService - { - /// - /// Returns the list of parties that user has any rights for. - /// - /// The userId. - /// List of parties. - Task?> GetPartyList(int userId); + /// The userId. + /// List of parties. + Task?> GetPartyList(int userId); - /// - /// Verifies that the selected party is contained in the user's party list. - /// - /// The user id. - /// The party id. - /// Boolean indicating whether or not the user can represent the selected party. - Task ValidateSelectedParty(int userId, int partyId); + /// + /// Verifies that the selected party is contained in the user's party list. + /// + /// The user id. + /// The party id. + /// Boolean indicating whether or not the user can represent the selected party. + Task ValidateSelectedParty(int userId, int partyId); - /// - /// Check if the user is authorized to perform the given action on the given instance. - /// - /// - /// - /// - /// - /// - /// - Task AuthorizeAction( - AppIdentifier appIdentifier, - InstanceIdentifier instanceIdentifier, - ClaimsPrincipal user, - string action, - string? taskId = null - ); + /// + /// Check if the user is authorized to perform the given action on the given instance. + /// + /// + /// + /// + /// + /// + /// + Task AuthorizeAction( + AppIdentifier appIdentifier, + InstanceIdentifier instanceIdentifier, + ClaimsPrincipal user, + string action, + string? taskId = null + ); - /// - /// Check if the user is authorized to perform the given actions on the given instance. - /// - /// - /// - /// - /// Dictionary with actions and the auth decision - Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions); - } + /// + /// Check if the user is authorized to perform the given actions on the given instance. + /// + /// + /// + /// + /// Dictionary with actions and the auth decision + Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions); } diff --git a/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs b/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs index d444ef041..84cf52b48 100644 --- a/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs +++ b/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs @@ -1,16 +1,15 @@ -namespace Altinn.App.Core.Internal.Auth +namespace Altinn.App.Core.Internal.Auth; + +/// +/// Defines the methods required for an implementation of a user JSON Web Token provider. +/// The provider is used by client implementations that needs the user token in requests +/// against other systems. +/// +public interface IUserTokenProvider { /// - /// Defines the methods required for an implementation of a user JSON Web Token provider. - /// The provider is used by client implementations that needs the user token in requests - /// against other systems. + /// Defines a method that can return a JSON Web Token of the current user. /// - public interface IUserTokenProvider - { - /// - /// Defines a method that can return a JSON Web Token of the current user. - /// - /// The Json Web Token for the current user. - public string GetUserToken(); - } + /// The Json Web Token for the current user. + public string GetUserToken(); } diff --git a/src/Altinn.App.Core/Internal/Data/DataService.cs b/src/Altinn.App.Core/Internal/Data/DataService.cs index 645379b48..8f9e822fd 100644 --- a/src/Altinn.App.Core/Internal/Data/DataService.cs +++ b/src/Altinn.App.Core/Internal/Data/DataService.cs @@ -3,129 +3,126 @@ using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Data +namespace Altinn.App.Core.Internal.Data; + +/// +internal class DataService : IDataService { - /// - internal class DataService : IDataService + private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); + + private readonly IDataClient _dataClient; + private readonly IAppMetadata _appMetadata; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public DataService(IDataClient dataClient, IAppMetadata appMetadata) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); + _dataClient = dataClient; + _appMetadata = appMetadata; + } - private readonly IDataClient _dataClient; - private readonly IAppMetadata _appMetadata; + /// + public async Task<(Guid dataElementId, T? model)> GetByType(Instance instance, string dataTypeId) + { + DataElement? dataElement = instance.Data.SingleOrDefault(d => d.DataType.Equals(dataTypeId)); - /// - /// Initializes a new instance of the class. - /// - /// - /// - public DataService(IDataClient dataClient, IAppMetadata appMetadata) + if (dataElement == null) { - _dataClient = dataClient; - _appMetadata = appMetadata; + return (Guid.Empty, default); } - /// - public async Task<(Guid dataElementId, T? model)> GetByType(Instance instance, string dataTypeId) - { - DataElement? dataElement = instance.Data.SingleOrDefault(d => d.DataType.Equals(dataTypeId)); + var data = await GetDataForDataElement(new InstanceIdentifier(instance), dataElement); - if (dataElement == null) - { - return (Guid.Empty, default); - } + return (Guid.Parse(dataElement.Id), data); + } - var data = await GetDataForDataElement(new InstanceIdentifier(instance), dataElement); + /// + public async Task GetById(Instance instance, Guid dataElementId) + { + DataElement dataElement = + instance.Data.SingleOrDefault(d => d.Id == dataElementId.ToString()) + ?? throw new ArgumentNullException( + $"Failed to locate data element with id {dataElementId} in instance {instance.Id}" + ); + return await GetDataForDataElement(new InstanceIdentifier(instance), dataElement); + } - return (Guid.Parse(dataElement.Id), data); - } + /// + public async Task InsertJsonObject( + InstanceIdentifier instanceIdentifier, + string dataTypeId, + object data + ) + { + using var referenceStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(referenceStream, data, _jsonSerializerOptions); + referenceStream.Position = 0; + return await _dataClient.InsertBinaryData( + instanceIdentifier.ToString(), + dataTypeId, + "application/json", + dataTypeId + ".json", + referenceStream + ); + } - /// - public async Task GetById(Instance instance, Guid dataElementId) - { - DataElement dataElement = - instance.Data.SingleOrDefault(d => d.Id == dataElementId.ToString()) - ?? throw new ArgumentNullException( - $"Failed to locate data element with id {dataElementId} in instance {instance.Id}" - ); - return await GetDataForDataElement(new InstanceIdentifier(instance), dataElement); - } + /// + public async Task UpdateJsonObject( + InstanceIdentifier instanceIdentifier, + string dataTypeId, + Guid dataElementId, + object data + ) + { + using var referenceStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(referenceStream, data, _jsonSerializerOptions); + referenceStream.Position = 0; + return await _dataClient.UpdateBinaryData( + instanceIdentifier, + "application/json", + dataTypeId + ".json", + dataElementId, + referenceStream + ); + } - /// - public async Task InsertJsonObject( - InstanceIdentifier instanceIdentifier, - string dataTypeId, - object data - ) - { - using var referenceStream = new MemoryStream(); - await JsonSerializer.SerializeAsync(referenceStream, data, _jsonSerializerOptions); - referenceStream.Position = 0; - return await _dataClient.InsertBinaryData( - instanceIdentifier.ToString(), - dataTypeId, - "application/json", - dataTypeId + ".json", - referenceStream - ); - } + /// + public async Task DeleteById(InstanceIdentifier instanceIdentifier, Guid dataElementId) + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - /// - public async Task UpdateJsonObject( - InstanceIdentifier instanceIdentifier, - string dataTypeId, - Guid dataElementId, - object data - ) - { - using var referenceStream = new MemoryStream(); - await JsonSerializer.SerializeAsync(referenceStream, data, _jsonSerializerOptions); - referenceStream.Position = 0; - return await _dataClient.UpdateBinaryData( - instanceIdentifier, - "application/json", - dataTypeId + ".json", - dataElementId, - referenceStream - ); - } + return await _dataClient.DeleteData( + applicationMetadata.AppIdentifier.Org, + applicationMetadata.AppIdentifier.App, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + dataElementId, + false + ); + } - /// - public async Task DeleteById(InstanceIdentifier instanceIdentifier, Guid dataElementId) - { - ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - - return await _dataClient.DeleteData( - applicationMetadata.AppIdentifier.Org, - applicationMetadata.AppIdentifier.App, - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - dataElementId, - false - ); - } + private async Task GetDataForDataElement(InstanceIdentifier instanceIdentifier, DataElement dataElement) + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - private async Task GetDataForDataElement(InstanceIdentifier instanceIdentifier, DataElement dataElement) + Stream dataStream = await _dataClient.GetBinaryData( + applicationMetadata.AppIdentifier.Org, + applicationMetadata.AppIdentifier.App, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + new Guid(dataElement.Id) + ); + if (dataStream == null) { - ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - - Stream dataStream = await _dataClient.GetBinaryData( - applicationMetadata.AppIdentifier.Org, - applicationMetadata.AppIdentifier.App, - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - new Guid(dataElement.Id) + throw new ArgumentNullException( + $"Failed to retrieve binary dataStream from dataClient using dataElement.Id {dataElement.Id}." ); - if (dataStream == null) - { - throw new ArgumentNullException( - $"Failed to retrieve binary dataStream from dataClient using dataElement.Id {dataElement.Id}." - ); - } - - return await JsonSerializer.DeserializeAsync(dataStream, _jsonSerializerOptions) - ?? throw new InvalidOperationException( - $"Unable to deserialize data from dataStream to type {nameof(T)}." - ); } + + return await JsonSerializer.DeserializeAsync(dataStream, _jsonSerializerOptions) + ?? throw new InvalidOperationException($"Unable to deserialize data from dataStream to type {nameof(T)}."); } } diff --git a/src/Altinn.App.Core/Internal/Data/IDataClient.cs b/src/Altinn.App.Core/Internal/Data/IDataClient.cs index 63102a30c..dc065fa31 100644 --- a/src/Altinn.App.Core/Internal/Data/IDataClient.cs +++ b/src/Altinn.App.Core/Internal/Data/IDataClient.cs @@ -2,235 +2,229 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; -namespace Altinn.App.Core.Internal.Data +namespace Altinn.App.Core.Internal.Data; + +/// +/// Interface for data handling +/// +public interface IDataClient { /// - /// Interface for data handling + /// Stores the form model + /// + /// The type + /// The app model to serialize + /// The instance id + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The data type to create, must be a valid data type defined in application metadata + Task InsertFormData( + T dataToSerialize, + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + string dataType + ); + + /// + /// Stores the form + /// + /// The model type + /// The instance that the data element belongs to + /// The data type with requirements + /// The data element instance + /// The class type describing the data + /// The data element metadata + Task InsertFormData(Instance instance, string dataType, T dataToSerialize, Type type); + + /// + /// updates the form data + /// + /// The type + /// The form data to serialize + /// The instanceid + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// the data id + Task UpdateData( + T dataToSerialize, + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + Guid dataId + ); + + /// + /// Gets the form data + /// + /// The instanceid + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// the data id + Task GetFormData( + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + Guid dataId + ); + + /// + /// Gets the data as is. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instanceid + /// the data id + Task GetBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId); + + /// + /// Method that gets metadata on form attachments ordered by attachmentType + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// A list with attachments metadata ordered by attachmentType + Task> GetBinaryDataList(string org, string app, int instanceOwnerPartyId, Guid instanceGuid); + + /// + /// Method that removes a form attachments from disk/storage + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The attachment id + [Obsolete("Use method DeleteData with delayed=false instead.", error: true)] + Task DeleteBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid); + + /// + /// Method that removes a data elemen from disk/storage immediatly or marks it as deleted. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The attachment id + /// A boolean indicating whether or not the delete should be executed immediately or delayed + Task DeleteData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid, + bool delay + ); + + /// + /// Method that saves a form attachments to disk/storage and returns the new data element. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The data type to create, must be a valid data type defined in application metadata + /// Http request containing the attachment to be saved + Task InsertBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + string dataType, + HttpRequest request + ); + + /// + /// Method that updates a form attachments to disk/storage and returns the updated data element. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The data id + /// Http request containing the attachment to be saved + [Obsolete( + message: "Deprecated please use UpdateBinaryData(InstanceIdentifier, string, string, Guid, Stream) instead", + error: false + )] + Task UpdateBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid, + HttpRequest request + ); + + /// + /// Method that updates a form attachments to disk/storage and returns the updated data element. + /// + /// Instance identifier instanceOwnerPartyId and instanceGuid + /// Content type of the updated binary data + /// Filename of the updated binary data + /// Guid of the data element to update + /// Updated binary data + Task UpdateBinaryData( + InstanceIdentifier instanceIdentifier, + string? contentType, + string filename, + Guid dataGuid, + Stream stream + ); + + /// + /// Insert a binary data element. + /// + /// isntanceId = {instanceOwnerPartyId}/{instanceGuid} + /// data type + /// content type + /// filename + /// the stream to stream + /// Optional field to set what task the binary data was generated from + /// + Task InsertBinaryData( + string instanceId, + string dataType, + string contentType, + string? filename, + Stream stream, + string? generatedFromTask = null + ); + + /// + /// Updates the data element metadata object. + /// + /// The instance which is not updated + /// The data element with values to update + /// the updated data element + Task Update(Instance instance, DataElement dataElement); + + /// + /// Lock data element in storage + /// + /// InstanceIdentifier identifying the instance containing the DataElement to lock + /// Id of the DataElement to lock + /// + Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid); + + /// + /// Unlock data element in storage /// - public interface IDataClient - { - /// - /// Stores the form model - /// - /// The type - /// The app model to serialize - /// The instance id - /// The type for serialization - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The data type to create, must be a valid data type defined in application metadata - Task InsertFormData( - T dataToSerialize, - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - string dataType - ); - - /// - /// Stores the form - /// - /// The model type - /// The instance that the data element belongs to - /// The data type with requirements - /// The data element instance - /// The class type describing the data - /// The data element metadata - Task InsertFormData(Instance instance, string dataType, T dataToSerialize, Type type); - - /// - /// updates the form data - /// - /// The type - /// The form data to serialize - /// The instanceid - /// The type for serialization - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// the data id - Task UpdateData( - T dataToSerialize, - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - Guid dataId - ); - - /// - /// Gets the form data - /// - /// The instanceid - /// The type for serialization - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// the data id - Task GetFormData( - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - Guid dataId - ); - - /// - /// Gets the data as is. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instanceid - /// the data id - Task GetBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId); - - /// - /// Method that gets metadata on form attachments ordered by attachmentType - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instance id - /// A list with attachments metadata ordered by attachmentType - Task> GetBinaryDataList( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid - ); - - /// - /// Method that removes a form attachments from disk/storage - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instance id - /// The attachment id - [Obsolete("Use method DeleteData with delayed=false instead.", error: true)] - Task DeleteBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid); - - /// - /// Method that removes a data elemen from disk/storage immediatly or marks it as deleted. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instance id - /// The attachment id - /// A boolean indicating whether or not the delete should be executed immediately or delayed - Task DeleteData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid, - bool delay - ); - - /// - /// Method that saves a form attachments to disk/storage and returns the new data element. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instance id - /// The data type to create, must be a valid data type defined in application metadata - /// Http request containing the attachment to be saved - Task InsertBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - string dataType, - HttpRequest request - ); - - /// - /// Method that updates a form attachments to disk/storage and returns the updated data element. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// The instance owner id - /// The instance id - /// The data id - /// Http request containing the attachment to be saved - [Obsolete( - message: "Deprecated please use UpdateBinaryData(InstanceIdentifier, string, string, Guid, Stream) instead", - error: false - )] - Task UpdateBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid, - HttpRequest request - ); - - /// - /// Method that updates a form attachments to disk/storage and returns the updated data element. - /// - /// Instance identifier instanceOwnerPartyId and instanceGuid - /// Content type of the updated binary data - /// Filename of the updated binary data - /// Guid of the data element to update - /// Updated binary data - Task UpdateBinaryData( - InstanceIdentifier instanceIdentifier, - string? contentType, - string filename, - Guid dataGuid, - Stream stream - ); - - /// - /// Insert a binary data element. - /// - /// isntanceId = {instanceOwnerPartyId}/{instanceGuid} - /// data type - /// content type - /// filename - /// the stream to stream - /// Optional field to set what task the binary data was generated from - /// - Task InsertBinaryData( - string instanceId, - string dataType, - string contentType, - string? filename, - Stream stream, - string? generatedFromTask = null - ); - - /// - /// Updates the data element metadata object. - /// - /// The instance which is not updated - /// The data element with values to update - /// the updated data element - Task Update(Instance instance, DataElement dataElement); - - /// - /// Lock data element in storage - /// - /// InstanceIdentifier identifying the instance containing the DataElement to lock - /// Id of the DataElement to lock - /// - Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid); - - /// - /// Unlock data element in storage - /// - /// InstanceIdentifier identifying the instance containing the DataElement to unlock - /// Id of the DataElement to unlock - /// - Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid); - } + /// InstanceIdentifier identifying the instance containing the DataElement to unlock + /// Id of the DataElement to unlock + /// + Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid); } diff --git a/src/Altinn.App.Core/Internal/Data/IDataService.cs b/src/Altinn.App.Core/Internal/Data/IDataService.cs index 39924ac18..00a692fd1 100644 --- a/src/Altinn.App.Core/Internal/Data/IDataService.cs +++ b/src/Altinn.App.Core/Internal/Data/IDataService.cs @@ -1,62 +1,61 @@ using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Data +namespace Altinn.App.Core.Internal.Data; + +/// +/// DRAFT. Don't make public yet. +/// Service that simplifies access to data elements through the IDataClient. +/// +public interface IDataService { /// - /// DRAFT. Don't make public yet. - /// Service that simplifies access to data elements through the IDataClient. + /// Retrieves a single data element by dataTypeId and deserializes it to an object of type T. /// - public interface IDataService - { - /// - /// Retrieves a single data element by dataTypeId and deserializes it to an object of type T. - /// - /// The type of the data element. - /// The instance associated with the object. - /// The ID of the data type. - /// A tuple containing the ID of the data element and the retrieved model. - Task<(Guid dataElementId, T? model)> GetByType(Instance instance, string dataTypeId); + /// The type of the data element. + /// The instance associated with the object. + /// The ID of the data type. + /// A tuple containing the ID of the data element and the retrieved model. + Task<(Guid dataElementId, T? model)> GetByType(Instance instance, string dataTypeId); - /// - /// Retrieves a single data element by its ID and deserializes it to an object of type T. - /// - /// The type of the data element. - /// The instance associated with the object. - /// The ID of the data element. - /// The object of type T. - Task GetById(Instance instance, Guid dataElementId); + /// + /// Retrieves a single data element by its ID and deserializes it to an object of type T. + /// + /// The type of the data element. + /// The instance associated with the object. + /// The ID of the data element. + /// The object of type T. + Task GetById(Instance instance, Guid dataElementId); - /// - /// Inserts a data element for the instance. - /// - /// - /// - /// - /// - Task InsertJsonObject(InstanceIdentifier instanceIdentifier, string dataTypeId, object data); + /// + /// Inserts a data element for the instance. + /// + /// + /// + /// + /// + Task InsertJsonObject(InstanceIdentifier instanceIdentifier, string dataTypeId, object data); - /// - /// Updates a data element for the instance. - /// - /// - /// - /// - /// - /// - Task UpdateJsonObject( - InstanceIdentifier instanceIdentifier, - string dataTypeId, - Guid dataElementId, - object data - ); + /// + /// Updates a data element for the instance. + /// + /// + /// + /// + /// + /// + Task UpdateJsonObject( + InstanceIdentifier instanceIdentifier, + string dataTypeId, + Guid dataElementId, + object data + ); - /// - /// Deletes a data element by its ID. - /// - /// The instance associated with the object. - /// The ID of the data element to delete. - /// A boolean indicating success/failure. - Task DeleteById(InstanceIdentifier instanceIdentifier, Guid dataElementId); - } + /// + /// Deletes a data element by its ID. + /// + /// The instance associated with the object. + /// The ID of the data element to delete. + /// A boolean indicating success/failure. + Task DeleteById(InstanceIdentifier instanceIdentifier, Guid dataElementId); } diff --git a/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs b/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs index aaae48cee..08bb56e58 100644 --- a/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs +++ b/src/Altinn.App.Core/Internal/Events/EventHandlerResolver.cs @@ -1,37 +1,36 @@ using Altinn.App.Core.Features; -namespace Altinn.App.Core.Internal.Events +namespace Altinn.App.Core.Internal.Events; + +/// +public class EventHandlerResolver : IEventHandlerResolver { + private readonly IEnumerable _eventHandlers; + /// - public class EventHandlerResolver : IEventHandlerResolver + public EventHandlerResolver(IEnumerable eventHandlers) { - private readonly IEnumerable _eventHandlers; + _eventHandlers = eventHandlers; + } - /// - public EventHandlerResolver(IEnumerable eventHandlers) + /// + public IEventHandler ResolveEventHandler(string eventType) + { + if (eventType == null) { - _eventHandlers = eventHandlers; + return new UnhandledEventHandler(); } - /// - public IEventHandler ResolveEventHandler(string eventType) + foreach (var handler in _eventHandlers) { - if (eventType == null) + if (!handler.EventType.Equals(eventType, StringComparison.OrdinalIgnoreCase)) { - return new UnhandledEventHandler(); + continue; } - foreach (var handler in _eventHandlers) - { - if (!handler.EventType.Equals(eventType, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - return handler; - } - - return new UnhandledEventHandler(); + return handler; } + + return new UnhandledEventHandler(); } } diff --git a/src/Altinn.App.Core/Internal/Events/IEventHandlerResolver.cs b/src/Altinn.App.Core/Internal/Events/IEventHandlerResolver.cs index 851a1cbde..57e189729 100644 --- a/src/Altinn.App.Core/Internal/Events/IEventHandlerResolver.cs +++ b/src/Altinn.App.Core/Internal/Events/IEventHandlerResolver.cs @@ -1,17 +1,16 @@ using Altinn.App.Core.Features; -namespace Altinn.App.Core.Internal.Events +namespace Altinn.App.Core.Internal.Events; + +/// +/// Interface used to resolve the implementation that should be used for a given event. +/// +public interface IEventHandlerResolver { /// - /// Interface used to resolve the implementation that should be used for a given event. + /// Resolves the correct implementation based on the event type. /// - public interface IEventHandlerResolver - { - /// - /// Resolves the correct implementation based on the event type. - /// - /// The type of event that has occured - /// A implementation of that is dedicated to handle events of the given type. - public IEventHandler ResolveEventHandler(string eventType); - } + /// The type of event that has occured + /// A implementation of that is dedicated to handle events of the given type. + public IEventHandler ResolveEventHandler(string eventType); } diff --git a/src/Altinn.App.Core/Internal/Events/IEventSecretCodeProvider.cs b/src/Altinn.App.Core/Internal/Events/IEventSecretCodeProvider.cs index a115ab14b..1573e10c7 100644 --- a/src/Altinn.App.Core/Internal/Events/IEventSecretCodeProvider.cs +++ b/src/Altinn.App.Core/Internal/Events/IEventSecretCodeProvider.cs @@ -1,17 +1,16 @@ -namespace Altinn.App.Core.Internal.Events +namespace Altinn.App.Core.Internal.Events; + +/// +/// Interface for providing a secret code to be used when +/// validating events from the Event system. This code is passed with +/// the subscription to the Event system and returned back when posting +/// the event to the app. If the code is the same the event is accepted. +/// +public interface IEventSecretCodeProvider { /// - /// Interface for providing a secret code to be used when - /// validating events from the Event system. This code is passed with - /// the subscription to the Event system and returned back when posting - /// the event to the app. If the code is the same the event is accepted. + /// Gets a secret code that can be passed on to the event system + /// when subscribing. /// - public interface IEventSecretCodeProvider - { - /// - /// Gets a secret code that can be passed on to the event system - /// when subscribing. - /// - public Task GetSecretCode(); - } + public Task GetSecretCode(); } diff --git a/src/Altinn.App.Core/Internal/Events/IEventsClient.cs b/src/Altinn.App.Core/Internal/Events/IEventsClient.cs index 5888ec778..6a3f36216 100644 --- a/src/Altinn.App.Core/Internal/Events/IEventsClient.cs +++ b/src/Altinn.App.Core/Internal/Events/IEventsClient.cs @@ -1,15 +1,14 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Events +namespace Altinn.App.Core.Internal.Events; + +/// +/// Interface describing client implementations for the Events component in the Altinn 3 platform. +/// +public interface IEventsClient { /// - /// Interface describing client implementations for the Events component in the Altinn 3 platform. + /// Adds a new event to the events published by the Events component. /// - public interface IEventsClient - { - /// - /// Adds a new event to the events published by the Events component. - /// - Task AddEvent(string eventType, Instance instance); - } + Task AddEvent(string eventType, Instance instance); } diff --git a/src/Altinn.App.Core/Internal/Events/IEventsSubscription.cs b/src/Altinn.App.Core/Internal/Events/IEventsSubscription.cs index 380ea379f..ed2011057 100644 --- a/src/Altinn.App.Core/Internal/Events/IEventsSubscription.cs +++ b/src/Altinn.App.Core/Internal/Events/IEventsSubscription.cs @@ -1,15 +1,14 @@ using Altinn.App.Core.Infrastructure.Clients.Events; -namespace Altinn.App.Core.Internal.Events +namespace Altinn.App.Core.Internal.Events; + +/// +/// Interface describing client implementations for the Events component in the Altinn 3 platform. +/// +public interface IEventsSubscription { /// - /// Interface describing client implementations for the Events component in the Altinn 3 platform. + /// Adds a new event subscription in the Events component. /// - public interface IEventsSubscription - { - /// - /// Adds a new event subscription in the Events component. - /// - Task AddSubscription(string org, string app, string eventType); - } + Task AddSubscription(string org, string app, string eventType); } diff --git a/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs b/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs index 2f801d85a..16ea3dfc5 100644 --- a/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs +++ b/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs @@ -1,45 +1,44 @@ using Altinn.App.Core.Internal.Secrets; -namespace Altinn.App.Core.Internal.Events +namespace Altinn.App.Core.Internal.Events; + +/// +/// Provides a secret code from Azure Key Vault to be used with +/// the Event system. +/// +public class KeyVaultEventSecretCodeProvider : IEventSecretCodeProvider { + private readonly ISecretsClient _keyVaultClient; + private string _secretCode = string.Empty; + /// - /// Provides a secret code from Azure Key Vault to be used with - /// the Event system. + /// Provides a new instance of + /// This /// - public class KeyVaultEventSecretCodeProvider : IEventSecretCodeProvider + /// + public KeyVaultEventSecretCodeProvider(ISecretsClient keyVaultClient) { - private readonly ISecretsClient _keyVaultClient; - private string _secretCode = string.Empty; + _keyVaultClient = keyVaultClient; + } - /// - /// Provides a new instance of - /// This - /// - /// - public KeyVaultEventSecretCodeProvider(ISecretsClient keyVaultClient) + /// + public async Task GetSecretCode() + { + if (!string.IsNullOrEmpty(_secretCode)) { - _keyVaultClient = keyVaultClient; + return _secretCode; } - /// - public async Task GetSecretCode() + var secretKey = "EventSubscription--SecretCode"; + string secretCode = await _keyVaultClient.GetSecretAsync(secretKey); + if (secretCode == null) { - if (!string.IsNullOrEmpty(_secretCode)) - { - return _secretCode; - } - - var secretKey = "EventSubscription--SecretCode"; - string secretCode = await _keyVaultClient.GetSecretAsync(secretKey); - if (secretCode == null) - { - throw new ArgumentException( - $"Unable to fetch event subscription secret code from key vault with the specified secret {secretKey}." - ); - } - - _secretCode = secretCode; - return _secretCode; + throw new ArgumentException( + $"Unable to fetch event subscription secret code from key vault with the specified secret {secretKey}." + ); } + + _secretCode = secretCode; + return _secretCode; } } diff --git a/src/Altinn.App.Core/Internal/Events/SubscriptionValidationHandler.cs b/src/Altinn.App.Core/Internal/Events/SubscriptionValidationHandler.cs index a4c183d1c..2e9c7af82 100644 --- a/src/Altinn.App.Core/Internal/Events/SubscriptionValidationHandler.cs +++ b/src/Altinn.App.Core/Internal/Events/SubscriptionValidationHandler.cs @@ -1,21 +1,20 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Models; -namespace Altinn.App.Core.Internal.Events +namespace Altinn.App.Core.Internal.Events; + +/// +/// Implementation used to handled events that the Event system used to validate +/// the events receiver endpoint. +/// +public class SubscriptionValidationHandler : IEventHandler { - /// - /// Implementation used to handled events that the Event system used to validate - /// the events receiver endpoint. - /// - public class SubscriptionValidationHandler : IEventHandler - { - /// - public string EventType => "platform.events.validatesubscription"; + /// + public string EventType => "platform.events.validatesubscription"; - /// - public Task ProcessEvent(CloudEvent cloudEvent) - { - return Task.FromResult(true); - } + /// + public Task ProcessEvent(CloudEvent cloudEvent) + { + return Task.FromResult(true); } } diff --git a/src/Altinn.App.Core/Internal/Events/UnhandledEventHandler.cs b/src/Altinn.App.Core/Internal/Events/UnhandledEventHandler.cs index e0a1cc272..2660a37be 100644 --- a/src/Altinn.App.Core/Internal/Events/UnhandledEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Events/UnhandledEventHandler.cs @@ -2,22 +2,21 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Models; -namespace Altinn.App.Core.Internal.Events +namespace Altinn.App.Core.Internal.Events; + +/// +/// Implementation used to handled events that could not bee resolved and matched on type. +/// +public class UnhandledEventHandler : IEventHandler { - /// - /// Implementation used to handled events that could not bee resolved and matched on type. - /// - public class UnhandledEventHandler : IEventHandler - { - /// - public string EventType => "app.events.unhandled"; + /// + public string EventType => "app.events.unhandled"; - /// - public Task ProcessEvent(CloudEvent cloudEvent) - { - throw new NotImplementedException( - $"Received unhandled event {cloudEvent?.Type} with the following data: {JsonSerializer.Serialize(cloudEvent)}" - ); - } + /// + public Task ProcessEvent(CloudEvent cloudEvent) + { + throw new NotImplementedException( + $"Received unhandled event {cloudEvent?.Type} with the following data: {JsonSerializer.Serialize(cloudEvent)}" + ); } } diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs index 518348230..30ffbf3eb 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs @@ -306,7 +306,7 @@ string s "1" => true, "0" => false, _ - => parseNumber(s, throwException: false) switch + => ParseNumber(s, throwException: false) switch { 1 => true, 0 => false, @@ -385,7 +385,7 @@ bool ab => throw new ExpressionEvaluatorTypeErrorException( $"Expected number, got value {(ab ? "true" : "false")}" ), - string s => parseNumber(s), + string s => ParseNumber(s), IConvertible c => Convert.ToDouble(c), _ => null }; @@ -413,11 +413,11 @@ bool ab ); } - private static readonly Regex numberRegex = new Regex(@"^-?\d+(\.\d+)?$"); + private static readonly Regex _numberRegex = new Regex(@"^-?\d+(\.\d+)?$"); - private static double? parseNumber(string s, bool throwException = true) + private static double? ParseNumber(string s, bool throwException = true) { - if (numberRegex.IsMatch(s) && double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) + if (_numberRegex.IsMatch(s) && double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) { return d; } diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs index e8374ad0d..c38722aad 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs @@ -1,5 +1,3 @@ -using System.Runtime.Serialization; - namespace Altinn.App.Core.Internal.Expressions; /// diff --git a/src/Altinn.App.Core/Internal/Instances/IInstanceClient.cs b/src/Altinn.App.Core/Internal/Instances/IInstanceClient.cs index eb7ce7b89..b83a67096 100644 --- a/src/Altinn.App.Core/Internal/Instances/IInstanceClient.cs +++ b/src/Altinn.App.Core/Internal/Instances/IInstanceClient.cs @@ -2,142 +2,137 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Primitives; -namespace Altinn.App.Core.Internal.Instances +namespace Altinn.App.Core.Internal.Instances; + +/// +/// Interface for handling form data related operations +/// +public interface IInstanceClient { /// - /// Interface for handling form data related operations + /// Gets the instance /// - public interface IInstanceClient - { - /// - /// Gets the instance - /// - Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceId); + Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceId); - /// - /// Gets the instance anew. Instance must have set appId, instanceOwner.PartyId and Id. - /// - Task GetInstance(Instance instance); - - /// - /// Gets a list of instances based on a dictionary of provided query parameters. - /// - Task> GetInstances(Dictionary queryParams); + /// + /// Gets the instance anew. Instance must have set appId, instanceOwner.PartyId and Id. + /// + Task GetInstance(Instance instance); - /// - /// Updates the process model of the instance and returns the updated instance. - /// - Task UpdateProcess(Instance instance); + /// + /// Gets a list of instances based on a dictionary of provided query parameters. + /// + Task> GetInstances(Dictionary queryParams); - /// - /// Creates an instance of an application with no data. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// the instance template to create (must have instanceOwner with partyId, personNumber or organisationNumber set) - /// The created instance - Task CreateInstance(string org, string app, Instance instanceTemplate); + /// + /// Updates the process model of the instance and returns the updated instance. + /// + Task UpdateProcess(Instance instance); - /// - /// Add complete confirmation. - /// - /// - /// Add to an instance that a given stakeholder considers the instance as no longer needed by them. The stakeholder has - /// collected all the data and information they needed from the instance and expect no additional data to be added to it. - /// The body of the request isn't used for anything despite this being a POST operation. - /// - /// The party id of the instance owner. - /// The id of the instance to confirm as complete. - /// Returns the updated instance. - Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid); + /// + /// Creates an instance of an application with no data. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// the instance template to create (must have instanceOwner with partyId, personNumber or organisationNumber set) + /// The created instance + Task CreateInstance(string org, string app, Instance instanceTemplate); - /// - /// Update read status. - /// - /// The party id of the instance owner. - /// The id of the instance to confirm as complete. - /// The new instance read status. - /// Returns the updated instance. - Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus); + /// + /// Add complete confirmation. + /// + /// + /// Add to an instance that a given stakeholder considers the instance as no longer needed by them. The stakeholder has + /// collected all the data and information they needed from the instance and expect no additional data to be added to it. + /// The body of the request isn't used for anything despite this being a POST operation. + /// + /// The party id of the instance owner. + /// The id of the instance to confirm as complete. + /// Returns the updated instance. + Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid); - /// - /// Update substatus. - /// - /// The party id of the instance owner. - /// The id of the instance to be updated. - /// The new substatus. - /// Returns the updated instance. - Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus); + /// + /// Update read status. + /// + /// The party id of the instance owner. + /// The id of the instance to confirm as complete. + /// The new instance read status. + /// Returns the updated instance. + Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus); - /// - /// Update presentation texts. - /// - /// - /// The provided presentation texts will be merged with the existing collection of presentation texts on the instance. - /// - /// The party id of the instance owner. - /// The id of the instance to update presentation texts for. - /// The presentation texts - /// Returns the updated instance. - Task UpdatePresentationTexts( - int instanceOwnerPartyId, - Guid instanceGuid, - PresentationTexts presentationTexts - ); + /// + /// Update substatus. + /// + /// The party id of the instance owner. + /// The id of the instance to be updated. + /// The new substatus. + /// Returns the updated instance. + Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus); - /// - /// Update data values. - /// - /// - /// The provided data values will be merged with the existing collection of data values on the instance. - /// - /// The party id of the instance owner. - /// The id of the instance to update data values for. - /// The data values - /// Returns the updated instance. - Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues); + /// + /// Update presentation texts. + /// + /// + /// The provided presentation texts will be merged with the existing collection of presentation texts on the instance. + /// + /// The party id of the instance owner. + /// The id of the instance to update presentation texts for. + /// The presentation texts + /// Returns the updated instance. + Task UpdatePresentationTexts( + int instanceOwnerPartyId, + Guid instanceGuid, + PresentationTexts presentationTexts + ); - /// - /// Update data data values. - /// - /// - /// The provided data value will be added with the existing collection of data values on the instance. - /// - /// The instance - /// The data value (null unsets the value) - /// Returns the updated instance. - async Task UpdateDataValues(Instance instance, Dictionary dataValues) - { - var id = new InstanceIdentifier(instance); - return await UpdateDataValues( - id.InstanceOwnerPartyId, - id.InstanceGuid, - new DataValues { Values = dataValues } - ); - } + /// + /// Update data values. + /// + /// + /// The provided data values will be merged with the existing collection of data values on the instance. + /// + /// The party id of the instance owner. + /// The id of the instance to update data values for. + /// The data values + /// Returns the updated instance. + Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues); - /// - /// Update single data value. - /// - /// - /// The provided data value will be added with the existing collection of data values on the instance. - /// - /// The instance - /// The key of the DataValues collection to be updated. - /// The data value (null unsets the value) - /// Returns the updated instance. - async Task UpdateDataValue(Instance instance, string key, string? value) - { - return await UpdateDataValues(instance, new Dictionary { { key, value } }); - } + /// + /// Update data data values. + /// + /// + /// The provided data value will be added with the existing collection of data values on the instance. + /// + /// The instance + /// The data value (null unsets the value) + /// Returns the updated instance. + async Task UpdateDataValues(Instance instance, Dictionary dataValues) + { + var id = new InstanceIdentifier(instance); + return await UpdateDataValues(id.InstanceOwnerPartyId, id.InstanceGuid, new DataValues { Values = dataValues }); + } - /// - /// Delete instance. - /// - /// The party id of the instance owner. - /// The id of the instance to delete. - /// Boolean to indicate if instance should be hard deleted. - /// Returns the deleted instance. - Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard); + /// + /// Update single data value. + /// + /// + /// The provided data value will be added with the existing collection of data values on the instance. + /// + /// The instance + /// The key of the DataValues collection to be updated. + /// The data value (null unsets the value) + /// Returns the updated instance. + async Task UpdateDataValue(Instance instance, string key, string? value) + { + return await UpdateDataValues(instance, new Dictionary { { key, value } }); } + + /// + /// Delete instance. + /// + /// The party id of the instance owner. + /// The id of the instance to delete. + /// Boolean to indicate if instance should be hard deleted. + /// Returns the deleted instance. + Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard); } diff --git a/src/Altinn.App.Core/Internal/Instances/IInstanceEventClient.cs b/src/Altinn.App.Core/Internal/Instances/IInstanceEventClient.cs index a99789b09..8084bab4f 100644 --- a/src/Altinn.App.Core/Internal/Instances/IInstanceEventClient.cs +++ b/src/Altinn.App.Core/Internal/Instances/IInstanceEventClient.cs @@ -1,28 +1,27 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Instances +namespace Altinn.App.Core.Internal.Instances; + +/// +/// Interface for handling instance event related operations +/// +public interface IInstanceEventClient { /// - /// Interface for handling instance event related operations + /// Stores the instance event /// - public interface IInstanceEventClient - { - /// - /// Stores the instance event - /// - Task SaveInstanceEvent(object dataToSerialize, string org, string app); + Task SaveInstanceEvent(object dataToSerialize, string org, string app); - /// - /// Gets the instance events related to the instance matching the instance id. - /// - Task> GetInstanceEvents( - string instanceId, - string instanceOwnerPartyId, - string org, - string app, - string[] eventTypes, - string from, - string to - ); - } + /// + /// Gets the instance events related to the instance matching the instance id. + /// + Task> GetInstanceEvents( + string instanceId, + string instanceOwnerPartyId, + string org, + string app, + string[] eventTypes, + string from, + string to + ); } diff --git a/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs b/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs index c2680393a..1d1bf97df 100644 --- a/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs +++ b/src/Altinn.App.Core/Internal/Language/ApplicationLanguage.cs @@ -5,67 +5,66 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Internal.Language +namespace Altinn.App.Core.Internal.Language; + +/// +/// An implementation used to retrieve the supported application languages. +/// +public class ApplicationLanguage : IApplicationLanguage { + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + private readonly AppSettings _settings; + private readonly ILogger _logger; + private readonly Telemetry? _telemetry; + /// - /// An implementation used to retrieve the supported application languages. + /// Initializes a new instance of the class. /// - public class ApplicationLanguage : IApplicationLanguage + /// The app repository settings. + /// A logger from the built in logger factory. + /// Telemetry for traces and metrics. + public ApplicationLanguage( + IOptions settings, + ILogger logger, + Telemetry? telemetry = null + ) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - private readonly AppSettings _settings; - private readonly ILogger _logger; - private readonly Telemetry? _telemetry; + _settings = settings.Value; + _logger = logger; + _telemetry = telemetry; + } - /// - /// Initializes a new instance of the class. - /// - /// The app repository settings. - /// A logger from the built in logger factory. - /// Telemetry for traces and metrics. - public ApplicationLanguage( - IOptions settings, - ILogger logger, - Telemetry? telemetry = null - ) - { - _settings = settings.Value; - _logger = logger; - _telemetry = telemetry; - } + /// + public async Task> GetApplicationLanguages() + { + using var activity = _telemetry?.StartGetApplicationLanguageActivity(); + var pathTextsResourceFolder = Path.Join( + _settings.AppBasePath, + _settings.ConfigurationFolder, + _settings.TextFolder + ); + var directoryInfo = new DirectoryInfo(pathTextsResourceFolder); + var textResourceFilesInDirectory = directoryInfo.GetFiles(); + var applicationLanguages = new List(); - /// - public async Task> GetApplicationLanguages() + foreach (var fileInfo in textResourceFilesInDirectory) { - using var activity = _telemetry?.StartGetApplicationLanguageActivity(); - var pathTextsResourceFolder = Path.Join( - _settings.AppBasePath, - _settings.ConfigurationFolder, - _settings.TextFolder - ); - var directoryInfo = new DirectoryInfo(pathTextsResourceFolder); - var textResourceFilesInDirectory = directoryInfo.GetFiles(); - var applicationLanguages = new List(); - - foreach (var fileInfo in textResourceFilesInDirectory) + await using (FileStream fileStream = new(fileInfo.FullName, FileMode.Open, FileAccess.Read)) { - await using (FileStream fileStream = new(fileInfo.FullName, FileMode.Open, FileAccess.Read)) - { - // ! TODO: find a better way to deal with deserialization errors here, rather than adding nulls to the list - // ! JSON deserialization returns null if the input is literally "null" - var applicationLanguage = ( - await JsonSerializer.DeserializeAsync( - fileStream, - _jsonSerializerOptions - ) - )!; - applicationLanguages.Add(applicationLanguage); - } + // ! TODO: find a better way to deal with deserialization errors here, rather than adding nulls to the list + // ! JSON deserialization returns null if the input is literally "null" + var applicationLanguage = ( + await JsonSerializer.DeserializeAsync( + fileStream, + _jsonSerializerOptions + ) + )!; + applicationLanguages.Add(applicationLanguage); } - - return applicationLanguages; } + + return applicationLanguages; } } diff --git a/src/Altinn.App.Core/Internal/Language/IApplicationLanguage.cs b/src/Altinn.App.Core/Internal/Language/IApplicationLanguage.cs index 512183829..9f6bff1d4 100644 --- a/src/Altinn.App.Core/Internal/Language/IApplicationLanguage.cs +++ b/src/Altinn.App.Core/Internal/Language/IApplicationLanguage.cs @@ -1,14 +1,13 @@ -namespace Altinn.App.Core.Internal.Language +namespace Altinn.App.Core.Internal.Language; + +/// +/// Interface for retrieving languages supported by the application. +/// +public interface IApplicationLanguage { /// - /// Interface for retrieving languages supported by the application. + /// Gets the supported languages from the application located in the text resource folder. /// - public interface IApplicationLanguage - { - /// - /// Gets the supported languages from the application located in the text resource folder. - /// - /// Returns a list of the supported languages - Task> GetApplicationLanguages(); - } + /// Returns a list of the supported languages + Task> GetApplicationLanguages(); } diff --git a/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenExtensions.cs b/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenExtensions.cs index 4c7e7c6dc..7bc8f6962 100644 --- a/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenExtensions.cs +++ b/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenExtensions.cs @@ -4,31 +4,30 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Internal.Maskinporten +namespace Altinn.App.Core.Internal.Maskinporten; + +/// +/// Extends the IServiceCollection with methods for adding Maskinporten related services. +/// +public static class MaskinportenExtensions { /// - /// Extends the IServiceCollection with methods for adding Maskinporten related services. + /// Adds the default token provider for Maskinporten using Jwk as the authentication method. + /// The Jwk is fetched from the secret store using the provided secretKeyName. + /// When using this locally the secret should be fetched from the local secret store using dotnet user-secrets. /// - public static class MaskinportenExtensions + public static IServiceCollection AddMaskinportenJwkTokenProvider( + this IServiceCollection services, + string secretKeyName + ) { - /// - /// Adds the default token provider for Maskinporten using Jwk as the authentication method. - /// The Jwk is fetched from the secret store using the provided secretKeyName. - /// When using this locally the secret should be fetched from the local secret store using dotnet user-secrets. - /// - public static IServiceCollection AddMaskinportenJwkTokenProvider( - this IServiceCollection services, - string secretKeyName - ) - { - services.AddTransient(sp => new MaskinportenJwkTokenProvider( - sp.GetRequiredService(), - sp.GetRequiredService>(), - sp.GetRequiredService(), - secretKeyName - )); + services.AddTransient(sp => new MaskinportenJwkTokenProvider( + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService(), + secretKeyName + )); - return services; - } + return services; } } diff --git a/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenJwkTokenProvider.cs b/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenJwkTokenProvider.cs index 6eeb7653b..48b659124 100644 --- a/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenJwkTokenProvider.cs +++ b/src/Altinn.App.Core/Internal/Maskinporten/MaskinportenJwkTokenProvider.cs @@ -4,84 +4,83 @@ using Altinn.App.Core.Internal.Secrets; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Internal.Maskinporten +namespace Altinn.App.Core.Internal.Maskinporten; + +/// +/// Defines the implementation of a Maskinporten token provider using JWK as the authentication method. +/// +public class MaskinportenJwkTokenProvider : IMaskinportenTokenProvider { + private readonly IMaskinportenService _maskinportenService; + private readonly MaskinportenSettings _maskinportenSettings; + private readonly ISecretsClient _secretsClient; + private readonly string _secretKeyName; + private string _base64EncodedJwk = string.Empty; + /// - /// Defines the implementation of a Maskinporten token provider using JWK as the authentication method. + /// Creates an instance of the using the provided services and settings. + /// From the settings, the ClientId and Environment properties are used to get the token. + /// The encoded Jwek is fetched from the secret store. Allthough possible, the Jwk should NOT be stored in the settings. + /// Scopes defined in the settings are igonored as this is provided to the GetToken method. /// - public class MaskinportenJwkTokenProvider : IMaskinportenTokenProvider + public MaskinportenJwkTokenProvider( + IMaskinportenService maskinportenService, + IOptions maskinportenSettings, + ISecretsClient secretsClient, + string secretKeyName + ) { - private readonly IMaskinportenService _maskinportenService; - private readonly MaskinportenSettings _maskinportenSettings; - private readonly ISecretsClient _secretsClient; - private readonly string _secretKeyName; - private string _base64EncodedJwk = string.Empty; - - /// - /// Creates an instance of the using the provided services and settings. - /// From the settings, the ClientId and Environment properties are used to get the token. - /// The encoded Jwek is fetched from the secret store. Allthough possible, the Jwk should NOT be stored in the settings. - /// Scopes defined in the settings are igonored as this is provided to the GetToken method. - /// - public MaskinportenJwkTokenProvider( - IMaskinportenService maskinportenService, - IOptions maskinportenSettings, - ISecretsClient secretsClient, - string secretKeyName - ) - { - _maskinportenService = maskinportenService; - _maskinportenSettings = maskinportenSettings.Value; - _secretsClient = secretsClient; - _secretKeyName = secretKeyName; - } + _maskinportenService = maskinportenService; + _maskinportenSettings = maskinportenSettings.Value; + _secretsClient = secretsClient; + _secretKeyName = secretKeyName; + } - /// - /// This will get a Maskinporten token for the provided scopes. - /// - public async Task GetToken(string scopes) - { - string base64EncodedJwk = await GetBase64EncodedJwk(); - TokenResponse maskinportenToken = await _maskinportenService.GetToken( - base64EncodedJwk, - _maskinportenSettings.Environment, - _maskinportenSettings.ClientId, - scopes, - string.Empty - ); + /// + /// This will get a Maskinporten token for the provided scopes. + /// + public async Task GetToken(string scopes) + { + string base64EncodedJwk = await GetBase64EncodedJwk(); + TokenResponse maskinportenToken = await _maskinportenService.GetToken( + base64EncodedJwk, + _maskinportenSettings.Environment, + _maskinportenSettings.ClientId, + scopes, + string.Empty + ); - return maskinportenToken.AccessToken; - } + return maskinportenToken.AccessToken; + } - /// - /// This will first get a Maskinporten token for the provided scopes and then exchange it to an Altinn token providing addition claims. - /// - public async Task GetAltinnExchangedToken(string scopes) - { - string base64EncodedJwk = await GetBase64EncodedJwk(); - TokenResponse maskinportenToken = await _maskinportenService.GetToken( - base64EncodedJwk, - _maskinportenSettings.Environment, - _maskinportenSettings.ClientId, - scopes, - string.Empty - ); - TokenResponse altinnToken = await _maskinportenService.ExchangeToAltinnToken( - maskinportenToken, - _maskinportenSettings.Environment - ); + /// + /// This will first get a Maskinporten token for the provided scopes and then exchange it to an Altinn token providing addition claims. + /// + public async Task GetAltinnExchangedToken(string scopes) + { + string base64EncodedJwk = await GetBase64EncodedJwk(); + TokenResponse maskinportenToken = await _maskinportenService.GetToken( + base64EncodedJwk, + _maskinportenSettings.Environment, + _maskinportenSettings.ClientId, + scopes, + string.Empty + ); + TokenResponse altinnToken = await _maskinportenService.ExchangeToAltinnToken( + maskinportenToken, + _maskinportenSettings.Environment + ); - return altinnToken.AccessToken; - } + return altinnToken.AccessToken; + } - private async Task GetBase64EncodedJwk() + private async Task GetBase64EncodedJwk() + { + if (string.IsNullOrEmpty(_base64EncodedJwk)) { - if (string.IsNullOrEmpty(_base64EncodedJwk)) - { - _base64EncodedJwk = await _secretsClient.GetSecretAsync(_secretKeyName); - } - - return _base64EncodedJwk; + _base64EncodedJwk = await _secretsClient.GetSecretAsync(_secretKeyName); } + + return _base64EncodedJwk; } } diff --git a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs index c50ac756a..c4d275d54 100644 --- a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs @@ -1,27 +1,26 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Pdf +namespace Altinn.App.Core.Internal.Pdf; + +/// +/// Interface for handling generation and storing of PDF's +/// +public interface IPdfService { /// - /// Interface for handling generation and storing of PDF's + /// Generate a PDF of what the user can currently see from the given instance of an app. Saves the PDF + /// to storage as a new binary file associated with the predefined PDF data type in most apps. /// - public interface IPdfService - { - /// - /// Generate a PDF of what the user can currently see from the given instance of an app. Saves the PDF - /// to storage as a new binary file associated with the predefined PDF data type in most apps. - /// - /// The instance details. - /// The task id for witch the pdf is generated - /// Cancellation Token for when a request should be stopped before it's completed. - Task GenerateAndStorePdf(Instance instance, string taskId, CancellationToken ct); + /// The instance details. + /// The task id for witch the pdf is generated + /// Cancellation Token for when a request should be stopped before it's completed. + Task GenerateAndStorePdf(Instance instance, string taskId, CancellationToken ct); - /// - /// Generate a PDF of what the user can currently see from the given instance of an app. - /// - /// The instance details. - /// The task id for witch the pdf is generated - /// Cancellation Token for when a request should be stopped before it's completed. - Task GeneratePdf(Instance instance, string taskId, CancellationToken ct); - } + /// + /// Generate a PDF of what the user can currently see from the given instance of an app. + /// + /// The instance details. + /// The task id for witch the pdf is generated + /// Cancellation Token for when a request should be stopped before it's completed. + Task GeneratePdf(Instance instance, string taskId, CancellationToken ct); } diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs b/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs index 357ffa766..c49ab797d 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs @@ -1,30 +1,27 @@ -using System.Runtime.Serialization; +namespace Altinn.App.Core.Internal.Pdf; -namespace Altinn.App.Core.Internal.Pdf +/// +/// Class representing an exception throw when a PDF could not be created. +/// +public class PdfGenerationException : Exception { /// - /// Class representing an exception throw when a PDF could not be created. + /// Creates a new Exception of + /// Intended to be used when the generation of PDF fails. /// - public class PdfGenerationException : Exception - { - /// - /// Creates a new Exception of - /// Intended to be used when the generation of PDF fails. - /// - public PdfGenerationException() { } + public PdfGenerationException() { } - /// - /// Creates a new Exception of - /// Intended to be used when the generation of PDF fails. - /// - public PdfGenerationException(string? message) - : base(message) { } + /// + /// Creates a new Exception of + /// Intended to be used when the generation of PDF fails. + /// + public PdfGenerationException(string? message) + : base(message) { } - /// - /// Creates a new Exception of - /// Intended to be used when the generation of PDF fails. - /// - public PdfGenerationException(string? message, Exception? innerException) - : base(message, innerException) { } - } + /// + /// Creates a new Exception of + /// Intended to be used when the generation of PDF fails. + /// + public PdfGenerationException(string? message, Exception? innerException) + : base(message, innerException) { } } diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index d32e39221..7df8db7ba 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -1,5 +1,4 @@ using System.Security.Claims; -using System.Xml.Serialization; using Altinn.App.Core.Configuration; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; diff --git a/src/Altinn.App.Core/Internal/Prefill/IPrefill.cs b/src/Altinn.App.Core/Internal/Prefill/IPrefill.cs index dde4c1781..8f41eeea3 100644 --- a/src/Altinn.App.Core/Internal/Prefill/IPrefill.cs +++ b/src/Altinn.App.Core/Internal/Prefill/IPrefill.cs @@ -1,34 +1,29 @@ -namespace Altinn.App.Core.Internal.Prefill +namespace Altinn.App.Core.Internal.Prefill; + +/// +/// The prefill service +/// +public interface IPrefill { /// - /// The prefill service + /// Prefills the data model based on key/values in the dictionary. /// - public interface IPrefill - { - /// - /// Prefills the data model based on key/values in the dictionary. - /// - /// The data model object - /// External given prefill - /// Ignore errors when true, throw on errors when false - void PrefillDataModel( - object dataModel, - Dictionary externalPrefill, - bool continueOnError = false - ); + /// The data model object + /// External given prefill + /// Ignore errors when true, throw on errors when false + void PrefillDataModel(object dataModel, Dictionary externalPrefill, bool continueOnError = false); - /// - /// Prefills the data model based on the prefill json configuration file - /// - /// The partyId of the instance owner - /// The data model name - /// The data model object - /// External given prefill - Task PrefillDataModel( - string partyId, - string dataModelName, - object dataModel, - Dictionary? externalPrefill = null - ); - } + /// + /// Prefills the data model based on the prefill json configuration file + /// + /// The partyId of the instance owner + /// The data model name + /// The data model object + /// External given prefill + Task PrefillDataModel( + string partyId, + string dataModelName, + object dataModel, + Dictionary? externalPrefill = null + ); } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs index 3a893bb4f..ceb983a94 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs @@ -1,72 +1,69 @@ -using System.Runtime.Serialization; -using System.Text.Json.Serialization; using System.Xml.Serialization; -namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +/// +/// Defines an altinn action for a task +/// +public class AltinnAction { /// - /// Defines an altinn action for a task + /// Initializes a new instance of the class /// - public class AltinnAction + public AltinnAction() { - /// - /// Initializes a new instance of the class - /// - public AltinnAction() - { - Value = string.Empty; - ActionType = ActionType.ProcessAction; - } + Value = string.Empty; + ActionType = ActionType.ProcessAction; + } - /// - /// Initializes a new instance of the class with the given ID - /// - /// - public AltinnAction(string id) - { - Value = id; - ActionType = ActionType.ProcessAction; - } + /// + /// Initializes a new instance of the class with the given ID + /// + /// + public AltinnAction(string id) + { + Value = id; + ActionType = ActionType.ProcessAction; + } - /// - /// Initializes a new instance of the class with the given ID and action type - /// - /// - /// - public AltinnAction(string id, ActionType actionType) - { - Value = id; - ActionType = actionType; - } + /// + /// Initializes a new instance of the class with the given ID and action type + /// + /// + /// + public AltinnAction(string id, ActionType actionType) + { + Value = id; + ActionType = actionType; + } - /// - /// Gets or sets the ID of the action - /// - [XmlText] - public string Value { get; set; } + /// + /// Gets or sets the ID of the action + /// + [XmlText] + public string Value { get; set; } - /// - /// Gets or sets the type of action - /// - [XmlAttribute("type", Namespace = "http://altinn.no/process")] - public ActionType ActionType { get; set; } - } + /// + /// Gets or sets the type of action + /// + [XmlAttribute("type", Namespace = "http://altinn.no/process")] + public ActionType ActionType { get; set; } +} +/// +/// Defines the different types of actions +/// +public enum ActionType +{ /// - /// Defines the different types of actions + /// The action is a process action /// - public enum ActionType - { - /// - /// The action is a process action - /// - [XmlEnum("processAction")] - ProcessAction, + [XmlEnum("processAction")] + ProcessAction, - /// - /// The action is a generic server action - /// - [XmlEnum("serverAction")] - ServerAction - } + /// + /// The action is a generic server action + /// + [XmlEnum("serverAction")] + ServerAction } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnGatewayExtension.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnGatewayExtension.cs index 4a7852543..92cbda785 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnGatewayExtension.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnGatewayExtension.cs @@ -1,16 +1,15 @@ using System.Xml.Serialization; -namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +/// +/// Defines the altinn properties for a task +/// +public class AltinnGatewayExtension { /// - /// Defines the altinn properties for a task + /// Gets or sets the data type id connected to the task /// - public class AltinnGatewayExtension - { - /// - /// Gets or sets the data type id connected to the task - /// - [XmlElement("connectedDataTypeId", Namespace = "http://altinn.no/process", IsNullable = true)] - public string? ConnectedDataTypeId { get; set; } - } + [XmlElement("connectedDataTypeId", Namespace = "http://altinn.no/process", IsNullable = true)] + public string? ConnectedDataTypeId { get; set; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs index ed1746997..25c4d2ec1 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs @@ -2,57 +2,56 @@ using System.Xml.Serialization; using Altinn.App.Core.Internal.App; -namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +/// +/// Configuration properties for payments in a process task +/// +public class AltinnPaymentConfiguration { /// - /// Configuration properties for payments in a process task + /// Set what dataTypeId that should be used for storing the payment /// - public class AltinnPaymentConfiguration - { - /// - /// Set what dataTypeId that should be used for storing the payment - /// - [XmlElement("paymentDataType", Namespace = "http://altinn.no/process")] - public string? PaymentDataType { get; set; } + [XmlElement("paymentDataType", Namespace = "http://altinn.no/process")] + public string? PaymentDataType { get; set; } - internal ValidAltinnPaymentConfiguration Validate() - { - List? errorMessages = null; + internal ValidAltinnPaymentConfiguration Validate() + { + List? errorMessages = null; - var paymentDataType = PaymentDataType; + var paymentDataType = PaymentDataType; - if (paymentDataType.IsNullOrWhitespace(ref errorMessages, "PaymentDataType is missing.")) - ThrowApplicationConfigException(errorMessages); + if (paymentDataType.IsNullOrWhitespace(ref errorMessages, "PaymentDataType is missing.")) + ThrowApplicationConfigException(errorMessages); - return new ValidAltinnPaymentConfiguration(paymentDataType); - } + return new ValidAltinnPaymentConfiguration(paymentDataType); + } - [DoesNotReturn] - private static void ThrowApplicationConfigException(List errorMessages) - { - throw new ApplicationConfigException( - "Payment process task configuration is not valid: " + string.Join(",\n", errorMessages) - ); - } + [DoesNotReturn] + private static void ThrowApplicationConfigException(List errorMessages) + { + throw new ApplicationConfigException( + "Payment process task configuration is not valid: " + string.Join(",\n", errorMessages) + ); } +} - internal readonly record struct ValidAltinnPaymentConfiguration(string PaymentDataType); +internal readonly record struct ValidAltinnPaymentConfiguration(string PaymentDataType); - file static class ValidationExtensions +file static class ValidationExtensions +{ + internal static bool IsNullOrWhitespace( + [NotNullWhen(false)] this string? value, + [NotNullWhen(true)] ref List? errors, + string error + ) { - internal static bool IsNullOrWhitespace( - [NotNullWhen(false)] this string? value, - [NotNullWhen(true)] ref List? errors, - string error - ) + var result = string.IsNullOrWhiteSpace(value); + if (result) { - var result = string.IsNullOrWhiteSpace(value); - if (result) - { - errors ??= new List(1); - errors.Add(error); - } - return result; + errors ??= new List(1); + errors.Add(error); } + return result; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs index d52c05587..476283dbe 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs @@ -1,36 +1,35 @@ using System.Xml.Serialization; -namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +/// +/// Defines the altinn properties for a task +/// +public class AltinnTaskExtension { /// - /// Defines the altinn properties for a task + /// List of available actions for a task /// - public class AltinnTaskExtension - { - /// - /// List of available actions for a task - /// - [XmlArray(ElementName = "actions", Namespace = "http://altinn.no/process", IsNullable = true)] - [XmlArrayItem(ElementName = "action", Namespace = "http://altinn.no/process")] - public List? AltinnActions { get; set; } + [XmlArray(ElementName = "actions", Namespace = "http://altinn.no/process", IsNullable = true)] + [XmlArrayItem(ElementName = "action", Namespace = "http://altinn.no/process")] + public List? AltinnActions { get; set; } - /// - /// Gets or sets the task type - /// - //[XmlElement(ElementName = "taskType", Namespace = "http://altinn.no/process/task", IsNullable = true)] - [XmlElement("taskType", Namespace = "http://altinn.no/process")] - public string? TaskType { get; set; } + /// + /// Gets or sets the task type + /// + //[XmlElement(ElementName = "taskType", Namespace = "http://altinn.no/process/task", IsNullable = true)] + [XmlElement("taskType", Namespace = "http://altinn.no/process")] + public string? TaskType { get; set; } - /// - /// Gets or sets the configuration for signature - /// - [XmlElement("signatureConfig", Namespace = "http://altinn.no/process")] - public AltinnSignatureConfiguration? SignatureConfiguration { get; set; } = new AltinnSignatureConfiguration(); + /// + /// Gets or sets the configuration for signature + /// + [XmlElement("signatureConfig", Namespace = "http://altinn.no/process")] + public AltinnSignatureConfiguration? SignatureConfiguration { get; set; } = new AltinnSignatureConfiguration(); - /// - /// Gets or sets the configuration for signature - /// - [XmlElement("paymentConfig", Namespace = "http://altinn.no/process")] - public AltinnPaymentConfiguration? PaymentConfiguration { get; set; } = new AltinnPaymentConfiguration(); - } + /// + /// Gets or sets the configuration for signature + /// + [XmlElement("paymentConfig", Namespace = "http://altinn.no/process")] + public AltinnPaymentConfiguration? PaymentConfiguration { get; set; } = new AltinnPaymentConfiguration(); } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/Definitions.cs b/src/Altinn.App.Core/Internal/Process/Elements/Definitions.cs index 17af15f7f..c27c22701 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/Definitions.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/Definitions.cs @@ -1,32 +1,31 @@ using System.Xml.Serialization; -namespace Altinn.App.Core.Internal.Process.Elements +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Class representing the definitions +/// +[XmlType(Namespace = "http://www.omg.org/spec/BPMN/20100524/MODEL")] +[XmlRoot("definitions", Namespace = "http://www.omg.org/spec/BPMN/20100524/MODEL")] +public class Definitions { /// - /// Class representing the definitions + /// Gets or sets the ID of the definition /// - [XmlType(Namespace = "http://www.omg.org/spec/BPMN/20100524/MODEL")] - [XmlRoot("definitions", Namespace = "http://www.omg.org/spec/BPMN/20100524/MODEL")] - public class Definitions - { - /// - /// Gets or sets the ID of the definition - /// - [XmlAttribute("id")] + [XmlAttribute("id")] #nullable disable - public string Id { get; set; } + public string Id { get; set; } - /// - /// Gets or sets the target namespace of the definition - /// - [XmlAttribute("targetNamespace")] - public string TargetNamespace { get; set; } + /// + /// Gets or sets the target namespace of the definition + /// + [XmlAttribute("targetNamespace")] + public string TargetNamespace { get; set; } - /// - /// Gets or sets the process of the workflow - /// - [XmlElement("process")] - public Process Process { get; set; } + /// + /// Gets or sets the process of the workflow + /// + [XmlElement("process")] + public Process Process { get; set; } #nullable restore - } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/EndEvent.cs b/src/Altinn.App.Core/Internal/Process/Elements/EndEvent.cs index caeac4a59..5832abbfe 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/EndEvent.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/EndEvent.cs @@ -1,20 +1,18 @@ -using System.Xml.Serialization; using Altinn.App.Core.Internal.Process.Elements.Base; -namespace Altinn.App.Core.Internal.Process.Elements +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Class representing the end event of a process +/// +public class EndEvent : ProcessElement { /// - /// Class representing the end event of a process + /// String representation of process element type /// - public class EndEvent : ProcessElement + /// EndEvent + public override string ElementType() { - /// - /// String representation of process element type - /// - /// EndEvent - public override string ElementType() - { - return "EndEvent"; - } + return "EndEvent"; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs b/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs index a433bb92a..d27399685 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs @@ -1,32 +1,31 @@ using System.Xml.Serialization; using Altinn.App.Core.Internal.Process.Elements.Base; -namespace Altinn.App.Core.Internal.Process.Elements +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Represents an exclusive gateway from a BPMN process definition. +/// +public class ExclusiveGateway : ProcessElement { /// - /// Represents an exclusive gateway from a BPMN process definition. + /// Get or sets the default path of the exclusive gateway. /// - public class ExclusiveGateway : ProcessElement - { - /// - /// Get or sets the default path of the exclusive gateway. - /// - [XmlAttribute("default")] - public string? Default { get; set; } + [XmlAttribute("default")] + public string? Default { get; set; } - /// - /// - /// - [XmlElement("extensionElements")] - public ExtensionElements? ExtensionElements { get; set; } + /// + /// + /// + [XmlElement("extensionElements")] + public ExtensionElements? ExtensionElements { get; set; } - /// - /// String representation of process element type - /// - /// ExclusiveGateway - public override string ElementType() - { - return "ExclusiveGateway"; - } + /// + /// String representation of process element type + /// + /// ExclusiveGateway + public override string ElementType() + { + return "ExclusiveGateway"; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs b/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs index 68ec9f5d4..04f8b12db 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs @@ -1,23 +1,22 @@ using System.Xml.Serialization; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; -namespace Altinn.App.Core.Internal.Process.Elements +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Class representing the extension elements +/// +public class ExtensionElements { /// - /// Class representing the extension elements + /// Gets or sets the altinn properties /// - public class ExtensionElements - { - /// - /// Gets or sets the altinn properties - /// - [XmlElement("taskExtension", Namespace = "http://altinn.no/process")] - public AltinnTaskExtension? TaskExtension { get; set; } + [XmlElement("taskExtension", Namespace = "http://altinn.no/process")] + public AltinnTaskExtension? TaskExtension { get; set; } - /// - /// Gets or sets the altinn properties - /// - [XmlElement("gatewayExtension", Namespace = "http://altinn.no/process")] - public AltinnGatewayExtension? GatewayExtension { get; set; } - } + /// + /// Gets or sets the altinn properties + /// + [XmlElement("gatewayExtension", Namespace = "http://altinn.no/process")] + public AltinnGatewayExtension? GatewayExtension { get; set; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/Process.cs b/src/Altinn.App.Core/Internal/Process/Elements/Process.cs index 4f6fbfe58..70b4e3179 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/Process.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/Process.cs @@ -1,57 +1,56 @@ using System.Xml.Serialization; -namespace Altinn.App.Core.Internal.Process.Elements +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Class representing the process of a workflow +/// +public class Process { /// - /// Class representing the process of a workflow + /// Gets or sets the ID of the process of a workflow /// - public class Process - { - /// - /// Gets or sets the ID of the process of a workflow - /// - [XmlAttribute("id")] + [XmlAttribute("id")] #nullable disable - public string Id { get; set; } + public string Id { get; set; } #nullable restore - /// - /// Gets or sets if the process of a workflow is executable or not - /// - [XmlAttribute("isExecutable")] - public bool IsExecutable { get; set; } + /// + /// Gets or sets if the process of a workflow is executable or not + /// + [XmlAttribute("isExecutable")] + public bool IsExecutable { get; set; } - /// - /// Gets or sets the start event of the process of a workflow - /// - [XmlElement("startEvent")] + /// + /// Gets or sets the start event of the process of a workflow + /// + [XmlElement("startEvent")] #nullable disable - public List StartEvents { get; set; } - - /// - /// Gets or sets the list of tasks for the process of a workflow - /// - [XmlElement("task")] - public List Tasks { get; set; } - - /// - /// Gets or sets the end event of the process of a workflow - /// - [XmlElement("endEvent")] - public List EndEvents { get; set; } - - /// - /// Gets or sets the sequence flow of the process of a workflow - /// - [XmlElement("sequenceFlow")] - public List SequenceFlow { get; set; } - - /// - /// Gets or sets the exclusiveGateways of the process of a workflow - /// - [XmlElement("exclusiveGateway")] - public List ExclusiveGateway { get; set; } + public List StartEvents { get; set; } + + /// + /// Gets or sets the list of tasks for the process of a workflow + /// + [XmlElement("task")] + public List Tasks { get; set; } + + /// + /// Gets or sets the end event of the process of a workflow + /// + [XmlElement("endEvent")] + public List EndEvents { get; set; } + + /// + /// Gets or sets the sequence flow of the process of a workflow + /// + [XmlElement("sequenceFlow")] + public List SequenceFlow { get; set; } + + /// + /// Gets or sets the exclusiveGateways of the process of a workflow + /// + [XmlElement("exclusiveGateway")] + public List ExclusiveGateway { get; set; } #nullable restore - } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs index 8bde96975..770695be2 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs @@ -1,26 +1,25 @@ using System.Xml.Serialization; using Altinn.App.Core.Internal.Process.Elements.Base; -namespace Altinn.App.Core.Internal.Process.Elements +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Class representing the task of a process +/// +public class ProcessTask : ProcessElement { /// - /// Class representing the task of a process + /// Defines the extension elements /// - public class ProcessTask : ProcessElement - { - /// - /// Defines the extension elements - /// - [XmlElement("extensionElements")] - public ExtensionElements? ExtensionElements { get; set; } + [XmlElement("extensionElements")] + public ExtensionElements? ExtensionElements { get; set; } - /// - /// String representation of process element type - /// - /// Task - public override string ElementType() - { - return "Task"; - } + /// + /// String representation of process element type + /// + /// Task + public override string ElementType() + { + return "Task"; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs b/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs index 0cc32ef60..c6871b684 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs @@ -1,43 +1,42 @@ using System.Xml.Serialization; -namespace Altinn.App.Core.Internal.Process.Elements +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Class representing the sequence flow of a process +/// +public class SequenceFlow { /// - /// Class representing the sequence flow of a process + /// Gets or sets the ID of a sequence flow /// - public class SequenceFlow - { - /// - /// Gets or sets the ID of a sequence flow - /// - [XmlAttribute("id")] + [XmlAttribute("id")] #nullable disable - public string Id { get; set; } + public string Id { get; set; } - /// - /// Gets or sets the source reference of a sequence flow - /// - [XmlAttribute("sourceRef")] - public string SourceRef { get; set; } + /// + /// Gets or sets the source reference of a sequence flow + /// + [XmlAttribute("sourceRef")] + public string SourceRef { get; set; } - /// - /// Gets or sets the target reference of a sequence flow - /// - [XmlAttribute("targetRef")] - public string TargetRef { get; set; } + /// + /// Gets or sets the target reference of a sequence flow + /// + [XmlAttribute("targetRef")] + public string TargetRef { get; set; } - /// - /// Gets or sets the flowtype of a sequence flow. - /// - [XmlAttribute("flowtype", Namespace = "http://altinn.no")] - public string FlowType { get; set; } + /// + /// Gets or sets the flowtype of a sequence flow. + /// + [XmlAttribute("flowtype", Namespace = "http://altinn.no")] + public string FlowType { get; set; } #nullable restore - /// - /// Gets or sets the condition expression of a sequence flow - /// - [XmlElement("conditionExpression")] - public string? ConditionExpression { get; set; } - } + /// + /// Gets or sets the condition expression of a sequence flow + /// + [XmlElement("conditionExpression")] + public string? ConditionExpression { get; set; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/StartEvent.cs b/src/Altinn.App.Core/Internal/Process/Elements/StartEvent.cs index 029bd8db5..a58942a18 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/StartEvent.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/StartEvent.cs @@ -1,20 +1,18 @@ -using System.Xml.Serialization; using Altinn.App.Core.Internal.Process.Elements.Base; -namespace Altinn.App.Core.Internal.Process.Elements +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Class representing the start event of a process +/// +public class StartEvent : ProcessElement { /// - /// Class representing the start event of a process + /// String representation of process element type /// - public class StartEvent : ProcessElement + /// StartEvent + public override string ElementType() { - /// - /// String representation of process element type - /// - /// StartEvent - public override string ElementType() - { - return "StartEvent"; - } + return "StartEvent"; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/UserAction.cs b/src/Altinn.App.Core/Internal/Process/Elements/UserAction.cs index d289077e5..99fb3558c 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/UserAction.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/UserAction.cs @@ -1,31 +1,29 @@ -using System.Runtime.Serialization; using System.Text.Json.Serialization; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; -namespace Altinn.App.Core.Internal.Process.Elements +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Defines an altinn action for a task +/// +public class UserAction { /// - /// Defines an altinn action for a task + /// Gets or sets the ID of the action /// - public class UserAction - { - /// - /// Gets or sets the ID of the action - /// - [JsonPropertyName("id")] - public required string Id { get; set; } + [JsonPropertyName("id")] + public required string Id { get; set; } - /// - /// Gets or sets if the user is authorized to perform the action - /// - [JsonPropertyName("authorized")] - public bool Authorized { get; set; } + /// + /// Gets or sets if the user is authorized to perform the action + /// + [JsonPropertyName("authorized")] + public bool Authorized { get; set; } - /// - /// Gets or sets the type of action - /// - [JsonConverter(typeof(JsonStringEnumConverter))] - [JsonPropertyName("type")] - public ActionType ActionType { get; set; } - } + /// + /// Gets or sets the type of action + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonPropertyName("type")] + public ActionType ActionType { get; set; } } diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/EndEventEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/EndEventEventHandler.cs index ea4951aa2..2e9dbe69c 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/EndEventEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/EndEventEventHandler.cs @@ -3,54 +3,53 @@ using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.EventHandlers +namespace Altinn.App.Core.Internal.Process.EventHandlers; + +/// +/// This event handler is responsible for handling the end event for a process. +/// +public class EndEventEventHandler : IEndEventEventHandler { + private readonly IAppEvents _appEvents; + private readonly IInstanceClient _instanceClient; + private readonly IAppMetadata _appMetadata; + /// /// This event handler is responsible for handling the end event for a process. /// - public class EndEventEventHandler : IEndEventEventHandler + public EndEventEventHandler(IAppEvents appEvents, IInstanceClient instanceClient, IAppMetadata appMetadata) { - private readonly IAppEvents _appEvents; - private readonly IInstanceClient _instanceClient; - private readonly IAppMetadata _appMetadata; + _appEvents = appEvents; + _instanceClient = instanceClient; + _appMetadata = appMetadata; + } - /// - /// This event handler is responsible for handling the end event for a process. - /// - public EndEventEventHandler(IAppEvents appEvents, IInstanceClient instanceClient, IAppMetadata appMetadata) - { - _appEvents = appEvents; - _instanceClient = instanceClient; - _appMetadata = appMetadata; - } + /// + /// Execute the event handler logic. + /// + public async Task Execute(InstanceEvent instanceEvent, Instance instance) + { + string? endEvent = instanceEvent.ProcessInfo?.EndEvent; - /// - /// Execute the event handler logic. - /// - public async Task Execute(InstanceEvent instanceEvent, Instance instance) + if (string.IsNullOrEmpty(endEvent)) { - string? endEvent = instanceEvent.ProcessInfo?.EndEvent; - - if (string.IsNullOrEmpty(endEvent)) - { - throw new ArgumentException( - $"End event is not set for instance event {instanceEvent.EventType} {instanceEvent.Id} on instance {instance.Id}." - ); - } - - await _appEvents.OnEndAppEvent(endEvent, instance); - await AutoDeleteOnProcessEndIfEnabled(instance); + throw new ArgumentException( + $"End event is not set for instance event {instanceEvent.EventType} {instanceEvent.Id} on instance {instance.Id}." + ); } - private async Task AutoDeleteOnProcessEndIfEnabled(Instance instance) + await _appEvents.OnEndAppEvent(endEvent, instance); + await AutoDeleteOnProcessEndIfEnabled(instance); + } + + private async Task AutoDeleteOnProcessEndIfEnabled(Instance instance) + { + InstanceIdentifier instanceIdentifier = new(instance); + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + if (applicationMetadata.AutoDeleteOnProcessEnd && instance.Process?.Ended != null) { - InstanceIdentifier instanceIdentifier = new(instance); - ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - if (applicationMetadata.AutoDeleteOnProcessEnd && instance.Process?.Ended != null) - { - int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - await _instanceClient.DeleteInstance(instanceOwnerPartyId, instanceIdentifier.InstanceGuid, true); - } + int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); + await _instanceClient.DeleteInstance(instanceOwnerPartyId, instanceIdentifier.InstanceGuid, true); } } } diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs index 9b872f7a6..56f3c01d2 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs @@ -1,15 +1,14 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.EventHandlers +namespace Altinn.App.Core.Internal.Process.EventHandlers; + +/// +/// Interface for end event handlers, which are executed when a process end event is triggered. +/// +public interface IEndEventEventHandler { /// - /// Interface for end event handlers, which are executed when a process end event is triggered. + /// Execute the end event handler /// - public interface IEndEventEventHandler - { - /// - /// Execute the end event handler - /// - Task Execute(InstanceEvent instanceEvent, Instance instance); - } + Task Execute(InstanceEvent instanceEvent, Instance instance); } diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandler.cs index dc70e109a..9273af202 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandler.cs @@ -2,41 +2,40 @@ using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; + +/// +/// This event handler is responsible for handling the abandon event for a process task. +/// +public class AbandonTaskEventHandler : IAbandonTaskEventHandler { + private readonly IEnumerable _processTaskAbondons; + /// /// This event handler is responsible for handling the abandon event for a process task. /// - public class AbandonTaskEventHandler : IAbandonTaskEventHandler + public AbandonTaskEventHandler(IEnumerable processTaskAbondons) { - private readonly IEnumerable _processTaskAbondons; - - /// - /// This event handler is responsible for handling the abandon event for a process task. - /// - public AbandonTaskEventHandler(IEnumerable processTaskAbondons) - { - _processTaskAbondons = processTaskAbondons; - } + _processTaskAbondons = processTaskAbondons; + } - /// - /// Handles the abandon event for a process task. - /// - public async Task Execute(IProcessTask processTask, string taskId, Instance instance) - { - await processTask.Abandon(taskId, instance); - await RunAppDefinedProcessTaskAbandonHandlers(taskId, instance); - } + /// + /// Handles the abandon event for a process task. + /// + public async Task Execute(IProcessTask processTask, string taskId, Instance instance) + { + await processTask.Abandon(taskId, instance); + await RunAppDefinedProcessTaskAbandonHandlers(taskId, instance); + } - /// - /// Runs IProcessTaskAbandons defined in the app. - /// - private async Task RunAppDefinedProcessTaskAbandonHandlers(string taskId, Instance instance) + /// + /// Runs IProcessTaskAbandons defined in the app. + /// + private async Task RunAppDefinedProcessTaskAbandonHandlers(string taskId, Instance instance) + { + foreach (IProcessTaskAbandon taskAbandon in _processTaskAbondons) { - foreach (IProcessTaskAbandon taskAbandon in _processTaskAbondons) - { - await taskAbandon.Abandon(taskId, instance); - } + await taskAbandon.Abandon(taskId, instance); } } } diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs index 946abc2bd..fd408cbd4 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs @@ -4,77 +4,76 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; -namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; + +/// +/// This event handler is responsible for handling the end event for a process task. +/// +public class EndTaskEventHandler : IEndTaskEventHandler { + private readonly IProcessTaskDataLocker _processTaskDataLocker; + private readonly IProcessTaskFinalizer _processTaskFinisher; + private readonly IServiceTask _pdfServiceTask; + private readonly IServiceTask _eformidlingServiceTask; + private readonly IEnumerable _processTaskEnds; + private readonly ILogger _logger; + /// /// This event handler is responsible for handling the end event for a process task. /// - public class EndTaskEventHandler : IEndTaskEventHandler + public EndTaskEventHandler( + IProcessTaskDataLocker processTaskDataLocker, + IProcessTaskFinalizer processTaskFinisher, + IEnumerable serviceTasks, + IEnumerable processTaskEnds, + ILogger logger + ) + { + _processTaskDataLocker = processTaskDataLocker; + _processTaskFinisher = processTaskFinisher; + _pdfServiceTask = + serviceTasks.FirstOrDefault(x => x is IPdfServiceTask) + ?? throw new InvalidOperationException("PdfServiceTask not found in serviceTasks"); + _eformidlingServiceTask = + serviceTasks.FirstOrDefault(x => x is IEformidlingServiceTask) + ?? throw new InvalidOperationException("EformidlingServiceTask not found in serviceTasks"); + _processTaskEnds = processTaskEnds; + _logger = logger; + } + + /// + /// Execute the event handler logic. + /// + public async Task Execute(IProcessTask processTask, string taskId, Instance instance) { - private readonly IProcessTaskDataLocker _processTaskDataLocker; - private readonly IProcessTaskFinalizer _processTaskFinisher; - private readonly IServiceTask _pdfServiceTask; - private readonly IServiceTask _eformidlingServiceTask; - private readonly IEnumerable _processTaskEnds; - private readonly ILogger _logger; + await processTask.End(taskId, instance); + await _processTaskFinisher.Finalize(taskId, instance); + await RunAppDefinedProcessTaskEndHandlers(taskId, instance); + await _processTaskDataLocker.Lock(taskId, instance); - /// - /// This event handler is responsible for handling the end event for a process task. - /// - public EndTaskEventHandler( - IProcessTaskDataLocker processTaskDataLocker, - IProcessTaskFinalizer processTaskFinisher, - IEnumerable serviceTasks, - IEnumerable processTaskEnds, - ILogger logger - ) + //These two services are scheduled to be removed and replaced by services tasks defined in the processfile. + try { - _processTaskDataLocker = processTaskDataLocker; - _processTaskFinisher = processTaskFinisher; - _pdfServiceTask = - serviceTasks.FirstOrDefault(x => x is IPdfServiceTask) - ?? throw new InvalidOperationException("PdfServiceTask not found in serviceTasks"); - _eformidlingServiceTask = - serviceTasks.FirstOrDefault(x => x is IEformidlingServiceTask) - ?? throw new InvalidOperationException("EformidlingServiceTask not found in serviceTasks"); - _processTaskEnds = processTaskEnds; - _logger = logger; + await _pdfServiceTask.Execute(taskId, instance); } - - /// - /// Execute the event handler logic. - /// - public async Task Execute(IProcessTask processTask, string taskId, Instance instance) + catch (Exception e) { - await processTask.End(taskId, instance); - await _processTaskFinisher.Finalize(taskId, instance); - await RunAppDefinedProcessTaskEndHandlers(taskId, instance); - await _processTaskDataLocker.Lock(taskId, instance); - - //These two services are scheduled to be removed and replaced by services tasks defined in the processfile. - try - { - await _pdfServiceTask.Execute(taskId, instance); - } - catch (Exception e) - { - _logger.LogError(e, "Error executing pdf service task. Unlocking data again."); - await _processTaskDataLocker.Unlock(taskId, instance); - throw; - } - - await _eformidlingServiceTask.Execute(taskId, instance); + _logger.LogError(e, "Error executing pdf service task. Unlocking data again."); + await _processTaskDataLocker.Unlock(taskId, instance); + throw; } - /// - /// Runs IProcessTaskEnds defined in the app. - /// - private async Task RunAppDefinedProcessTaskEndHandlers(string endEvent, Instance instance) + await _eformidlingServiceTask.Execute(taskId, instance); + } + + /// + /// Runs IProcessTaskEnds defined in the app. + /// + private async Task RunAppDefinedProcessTaskEndHandlers(string endEvent, Instance instance) + { + foreach (IProcessTaskEnd taskEnd in _processTaskEnds) { - foreach (IProcessTaskEnd taskEnd in _processTaskEnds) - { - await taskEnd.End(endEvent, instance); - } + await taskEnd.End(endEvent, instance); } } } diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IAbandonTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IAbandonTaskEventHandler.cs index 0ff3ff7e1..8bfac90d0 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IAbandonTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IAbandonTaskEventHandler.cs @@ -1,20 +1,19 @@ using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; + +/// +/// Interface for abandon task event handlers, which are executed when a process abandon task event is triggered. +/// +public interface IAbandonTaskEventHandler { /// - /// Interface for abandon task event handlers, which are executed when a process abandon task event is triggered. + /// Execute the abandon task event handler /// - public interface IAbandonTaskEventHandler - { - /// - /// Execute the abandon task event handler - /// - /// - /// - /// - /// - Task Execute(IProcessTask processTask, string taskId, Instance instance); - } + /// + /// + /// + /// + Task Execute(IProcessTask processTask, string taskId, Instance instance); } diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IEndTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IEndTaskEventHandler.cs index 9b3efb66f..7fd082d67 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IEndTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IEndTaskEventHandler.cs @@ -1,20 +1,19 @@ using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; + +/// +/// Interface for end task event handlers, which are executed when a process end task event is triggered. +/// +public interface IEndTaskEventHandler { /// - /// Interface for end task event handlers, which are executed when a process end task event is triggered. + /// Execute the end task event handler /// - public interface IEndTaskEventHandler - { - /// - /// Execute the end task event handler - /// - /// - /// - /// - /// - Task Execute(IProcessTask processTask, string taskId, Instance instance); - } + /// + /// + /// + /// + Task Execute(IProcessTask processTask, string taskId, Instance instance); } diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IStartTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IStartTaskEventHandler.cs index d5caebc1d..48cc058ba 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IStartTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IStartTaskEventHandler.cs @@ -1,16 +1,15 @@ using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; + +/// +/// Interface for start task event handlers, which are executed when a process start task event is triggered. +/// +public interface IStartTaskEventHandler { /// - /// Interface for start task event handlers, which are executed when a process start task event is triggered. + /// Execute the start task event handler /// - public interface IStartTaskEventHandler - { - /// - /// Execute the start task event handler - /// - Task Execute(IProcessTask processTask, string taskId, Instance instance, Dictionary? prefill); - } + Task Execute(IProcessTask processTask, string taskId, Instance instance, Dictionary? prefill); } diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs index d14440af5..30dcb4ec3 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs @@ -2,60 +2,59 @@ using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; + +/// +/// This event handler is responsible for handling the start event for a process task. +/// +public class StartTaskEventHandler : IStartTaskEventHandler { + private readonly IProcessTaskDataLocker _processTaskDataLocker; + private readonly IProcessTaskInitializer _processTaskInitializer; + private readonly IEnumerable _processTaskStarts; + /// /// This event handler is responsible for handling the start event for a process task. /// - public class StartTaskEventHandler : IStartTaskEventHandler + public StartTaskEventHandler( + IProcessTaskDataLocker processTaskDataLocker, + IProcessTaskInitializer processTaskInitializer, + IEnumerable processTaskStarts + ) { - private readonly IProcessTaskDataLocker _processTaskDataLocker; - private readonly IProcessTaskInitializer _processTaskInitializer; - private readonly IEnumerable _processTaskStarts; - - /// - /// This event handler is responsible for handling the start event for a process task. - /// - public StartTaskEventHandler( - IProcessTaskDataLocker processTaskDataLocker, - IProcessTaskInitializer processTaskInitializer, - IEnumerable processTaskStarts - ) - { - _processTaskDataLocker = processTaskDataLocker; - _processTaskInitializer = processTaskInitializer; - _processTaskStarts = processTaskStarts; - } + _processTaskDataLocker = processTaskDataLocker; + _processTaskInitializer = processTaskInitializer; + _processTaskStarts = processTaskStarts; + } - /// - /// Execute the event handler logic. - /// - public async Task Execute( - IProcessTask processTask, - string taskId, - Instance instance, - Dictionary? prefill - ) - { - await _processTaskDataLocker.Unlock(taskId, instance); - await RunAppDefinedProcessTaskStartHandlers(taskId, instance, prefill); - await _processTaskInitializer.Initialize(taskId, instance, prefill); - await processTask.Start(taskId, instance); - } + /// + /// Execute the event handler logic. + /// + public async Task Execute( + IProcessTask processTask, + string taskId, + Instance instance, + Dictionary? prefill + ) + { + await _processTaskDataLocker.Unlock(taskId, instance); + await RunAppDefinedProcessTaskStartHandlers(taskId, instance, prefill); + await _processTaskInitializer.Initialize(taskId, instance, prefill); + await processTask.Start(taskId, instance); + } - /// - /// Runs IProcessTaskStarts defined in the app. - /// - private async Task RunAppDefinedProcessTaskStartHandlers( - string taskId, - Instance instance, - Dictionary? prefill - ) + /// + /// Runs IProcessTaskStarts defined in the app. + /// + private async Task RunAppDefinedProcessTaskStartHandlers( + string taskId, + Instance instance, + Dictionary? prefill + ) + { + foreach (IProcessTaskStart processTaskStarts in _processTaskStarts) { - foreach (IProcessTaskStart processTaskStarts in _processTaskStarts) - { - await processTaskStarts.Start(taskId, instance, prefill ?? []); - } + await processTaskStarts.Start(taskId, instance, prefill ?? []); } } } diff --git a/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs index fe2604934..a22f885dd 100644 --- a/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs +++ b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs @@ -11,199 +11,198 @@ using Altinn.App.Core.Models.Process; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process +namespace Altinn.App.Core.Internal.Process; + +/// +/// Class implementing for evaluating expressions on flows connected to a gateway +/// +public class ExpressionsExclusiveGateway : IProcessExclusiveGateway { + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNameCaseInsensitive = true, + }; + + private static readonly JsonSerializerOptions _jsonSerializerOptionsCamelCase = + new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + private readonly LayoutEvaluatorStateInitializer _layoutStateInit; + private readonly IAppResources _resources; + private readonly IAppMetadata _appMetadata; + private readonly IDataClient _dataClient; + private readonly IAppModel _appModel; + /// - /// Class implementing for evaluating expressions on flows connected to a gateway + /// Constructor for /// - public class ExpressionsExclusiveGateway : IProcessExclusiveGateway + /// Expressions state initalizer used to create context for expression evaluation + /// Service for fetching app resources + /// Service for fetching app model + /// Service for fetching app metadata + /// Service for interacting with Platform Storage + public ExpressionsExclusiveGateway( + LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, + IAppResources resources, + IAppModel appModel, + IAppMetadata appMetadata, + IDataClient dataClient + ) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() - { - AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip, - PropertyNameCaseInsensitive = true, - }; - - private static readonly JsonSerializerOptions _jsonSerializerOptionsCamelCase = - new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - private readonly LayoutEvaluatorStateInitializer _layoutStateInit; - private readonly IAppResources _resources; - private readonly IAppMetadata _appMetadata; - private readonly IDataClient _dataClient; - private readonly IAppModel _appModel; - - /// - /// Constructor for - /// - /// Expressions state initalizer used to create context for expression evaluation - /// Service for fetching app resources - /// Service for fetching app model - /// Service for fetching app metadata - /// Service for interacting with Platform Storage - public ExpressionsExclusiveGateway( - LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, - IAppResources resources, - IAppModel appModel, - IAppMetadata appMetadata, - IDataClient dataClient - ) - { - _layoutStateInit = layoutEvaluatorStateInitializer; - _resources = resources; - _appMetadata = appMetadata; - _dataClient = dataClient; - _appModel = appModel; - } + _layoutStateInit = layoutEvaluatorStateInitializer; + _resources = resources; + _appMetadata = appMetadata; + _dataClient = dataClient; + _appModel = appModel; + } - /// - public string GatewayId { get; } = "AltinnExpressionsExclusiveGateway"; + /// + public string GatewayId { get; } = "AltinnExpressionsExclusiveGateway"; - /// - public async Task> FilterAsync( - List outgoingFlows, - Instance instance, - ProcessGatewayInformation processGatewayInformation - ) - { - var state = await GetLayoutEvaluatorState( - instance, - processGatewayInformation.Action, - processGatewayInformation.DataTypeId - ); + /// + public async Task> FilterAsync( + List outgoingFlows, + Instance instance, + ProcessGatewayInformation processGatewayInformation + ) + { + var state = await GetLayoutEvaluatorState( + instance, + processGatewayInformation.Action, + processGatewayInformation.DataTypeId + ); - return outgoingFlows.Where(outgoingFlow => EvaluateSequenceFlow(state, outgoingFlow)).ToList(); - } + return outgoingFlows.Where(outgoingFlow => EvaluateSequenceFlow(state, outgoingFlow)).ToList(); + } - private async Task GetLayoutEvaluatorState( - Instance instance, - string? action, - string? dataTypeId - ) + private async Task GetLayoutEvaluatorState( + Instance instance, + string? action, + string? dataTypeId + ) + { + var layoutSet = GetLayoutSet(instance); + var (checkedDataTypeId, dataType) = await GetDataType(instance, layoutSet, dataTypeId); + object data = new object(); + if (checkedDataTypeId != null && dataType != null) { - var layoutSet = GetLayoutSet(instance); - var (checkedDataTypeId, dataType) = await GetDataType(instance, layoutSet, dataTypeId); - object data = new object(); - if (checkedDataTypeId != null && dataType != null) + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instance); + var dataGuid = GetDataId(instance, checkedDataTypeId); + Type dataElementType = dataType; + if (dataGuid != null) { - InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instance); - var dataGuid = GetDataId(instance, checkedDataTypeId); - Type dataElementType = dataType; - if (dataGuid != null) - { - data = await _dataClient.GetFormData( - instanceIdentifier.InstanceGuid, - dataElementType, - instance.Org, - instance.AppId.Split("/")[1], - int.Parse(instance.InstanceOwner.PartyId), - dataGuid.Value - ); - } + data = await _dataClient.GetFormData( + instanceIdentifier.InstanceGuid, + dataElementType, + instance.Org, + instance.AppId.Split("/")[1], + int.Parse(instance.InstanceOwner.PartyId), + dataGuid.Value + ); } - - var state = await _layoutStateInit.Init(instance, data, layoutSetId: layoutSet?.Id, gatewayAction: action); - return state; } - private static bool EvaluateSequenceFlow(LayoutEvaluatorState state, SequenceFlow sequenceFlow) + var state = await _layoutStateInit.Init(instance, data, layoutSetId: layoutSet?.Id, gatewayAction: action); + return state; + } + + private static bool EvaluateSequenceFlow(LayoutEvaluatorState state, SequenceFlow sequenceFlow) + { + if (sequenceFlow.ConditionExpression != null) { - if (sequenceFlow.ConditionExpression != null) + var expression = GetExpressionFromCondition(sequenceFlow.ConditionExpression); + // If there is no component context in the state, evaluate the expression once without a component context + var stateComponentContexts = state.GetComponentContexts().Any() + ? state.GetComponentContexts().ToList() + : [null]; + foreach (ComponentContext? componentContext in stateComponentContexts) { - var expression = GetExpressionFromCondition(sequenceFlow.ConditionExpression); - // If there is no component context in the state, evaluate the expression once without a component context - var stateComponentContexts = state.GetComponentContexts().Any() - ? state.GetComponentContexts().ToList() - : [null]; - foreach (ComponentContext? componentContext in stateComponentContexts) + var result = ExpressionEvaluator.EvaluateExpression(state, expression, componentContext); + if (result is bool boolResult && boolResult) { - var result = ExpressionEvaluator.EvaluateExpression(state, expression, componentContext); - if (result is bool boolResult && boolResult) - { - return true; - } + return true; } } - else - { - return true; - } - - return false; } - - private static Expression GetExpressionFromCondition(string condition) + else { - Utf8JsonReader reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(condition)); - reader.Read(); - var expressionFromCondition = ExpressionConverter.ReadNotNull(ref reader, _jsonSerializerOptions); - return expressionFromCondition; + return true; } - private LayoutSet? GetLayoutSet(Instance instance) - { - string taskId = instance.Process.CurrentTask.ElementId; + return false; + } - string layoutSetsString = _resources.GetLayoutSets(); - LayoutSet? layoutSet = null; - if (!string.IsNullOrEmpty(layoutSetsString)) - { - LayoutSets? layoutSets = JsonSerializer.Deserialize( - layoutSetsString, - _jsonSerializerOptionsCamelCase - ); - layoutSet = layoutSets?.Sets?.Find(t => t.Tasks.Contains(taskId)); - } + private static Expression GetExpressionFromCondition(string condition) + { + Utf8JsonReader reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(condition)); + reader.Read(); + var expressionFromCondition = ExpressionConverter.ReadNotNull(ref reader, _jsonSerializerOptions); + return expressionFromCondition; + } - return layoutSet; - } + private LayoutSet? GetLayoutSet(Instance instance) + { + string taskId = instance.Process.CurrentTask.ElementId; - //TODO: Find a better home for this method - private async Task<(string? DataTypeId, Type? DataTypeClassType)> GetDataType( - Instance instance, - LayoutSet? layoutSet, - string? dataTypeId - ) + string layoutSetsString = _resources.GetLayoutSets(); + LayoutSet? layoutSet = null; + if (!string.IsNullOrEmpty(layoutSetsString)) { - DataType? dataType; - if (dataTypeId != null) - { - dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => - d.Id == dataTypeId && d.AppLogic != null - ); - } - else if (layoutSet != null) - { - dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => - d.Id == layoutSet.DataType && d.AppLogic != null - ); - } - else - { - dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => - d.TaskId == instance.Process.CurrentTask.ElementId && d.AppLogic != null - ); - } + LayoutSets? layoutSets = JsonSerializer.Deserialize( + layoutSetsString, + _jsonSerializerOptionsCamelCase + ); + layoutSet = layoutSets?.Sets?.Find(t => t.Tasks.Contains(taskId)); + } - if (dataType != null) - { - return (dataType.Id, _appModel.GetModelType(dataType.AppLogic.ClassRef)); - } + return layoutSet; + } - return (null, null); + //TODO: Find a better home for this method + private async Task<(string? DataTypeId, Type? DataTypeClassType)> GetDataType( + Instance instance, + LayoutSet? layoutSet, + string? dataTypeId + ) + { + DataType? dataType; + if (dataTypeId != null) + { + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => + d.Id == dataTypeId && d.AppLogic != null + ); + } + else if (layoutSet != null) + { + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => + d.Id == layoutSet.DataType && d.AppLogic != null + ); + } + else + { + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => + d.TaskId == instance.Process.CurrentTask.ElementId && d.AppLogic != null + ); } - private static Guid? GetDataId(Instance instance, string dataType) + if (dataType != null) { - string? dataId = instance.Data.Find(d => d.DataType == dataType)?.Id; - if (dataId != null) - { - return new Guid(dataId); - } + return (dataType.Id, _appModel.GetModelType(dataType.AppLogic.ClassRef)); + } + + return (null, null); + } - return null; + private static Guid? GetDataId(Instance instance, string dataType) + { + string? dataId = instance.Data.Find(d => d.DataType == dataType)?.Id; + if (dataId != null) + { + return new Guid(dataId); } + + return null; } } diff --git a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessClient.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessClient.cs index 6a79e6633..6cfb80c93 100644 --- a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessClient.cs +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessClient.cs @@ -1,21 +1,20 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process +namespace Altinn.App.Core.Internal.Process; + +/// +/// Process service that encapsulate reading of the BPMN process definition. +/// +public interface IProcessClient { /// - /// Process service that encapsulate reading of the BPMN process definition. + /// Returns a stream that contains the process definition. /// - public interface IProcessClient - { - /// - /// Returns a stream that contains the process definition. - /// - /// the stream - Stream GetProcessDefinition(); + /// the stream + Stream GetProcessDefinition(); - /// - /// Gets the instance process events related to the instance matching the instance id. - /// - Task GetProcessHistory(string instanceGuid, string instanceOwnerPartyId); - } + /// + /// Gets the instance process events related to the instance matching the instance id. + /// + Task GetProcessHistory(string instanceGuid, string instanceOwnerPartyId); } diff --git a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs index 67e9a3215..e04f4b853 100644 --- a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs @@ -1,33 +1,32 @@ using Altinn.App.Core.Models.Process; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process +namespace Altinn.App.Core.Internal.Process; + +/// +/// Process engine interface that defines the Altinn App process engine +/// +public interface IProcessEngine { /// - /// Process engine interface that defines the Altinn App process engine + /// Method to start a new process /// - public interface IProcessEngine - { - /// - /// Method to start a new process - /// - Task GenerateProcessStartEvents(ProcessStartRequest processStartRequest); + Task GenerateProcessStartEvents(ProcessStartRequest processStartRequest); - /// - /// Method to move process to next task/event - /// - Task Next(ProcessNextRequest request); + /// + /// Method to move process to next task/event + /// + Task Next(ProcessNextRequest request); - /// - /// Handle process events and update storage - /// - /// - /// - /// - Task HandleEventsAndUpdateStorage( - Instance instance, - Dictionary? prefill, - List? events - ); - } + /// + /// Handle process events and update storage + /// + /// + /// + /// + Task HandleEventsAndUpdateStorage( + Instance instance, + Dictionary? prefill, + List? events + ); } diff --git a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventHandlerDelegator.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventHandlerDelegator.cs index 0616717c8..cf1510e24 100644 --- a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventHandlerDelegator.cs +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventHandlerDelegator.cs @@ -1,19 +1,18 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process +namespace Altinn.App.Core.Internal.Process; + +/// +/// This interface is responsible for delegating process events to the correct event handler. +/// +public interface IProcessEventHandlerDelegator { /// - /// This interface is responsible for delegating process events to the correct event handler. + /// Handle process events. /// - public interface IProcessEventHandlerDelegator - { - /// - /// Handle process events. - /// - /// - /// - /// - /// - Task HandleEvents(Instance instance, Dictionary? prefill, List? events); - } + /// + /// + /// + /// + Task HandleEvents(Instance instance, Dictionary? prefill, List? events); } diff --git a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessNavigator.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessNavigator.cs index b61396446..0be5ea527 100644 --- a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessNavigator.cs +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessNavigator.cs @@ -1,20 +1,19 @@ using Altinn.App.Core.Internal.Process.Elements.Base; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process +namespace Altinn.App.Core.Internal.Process; + +/// +/// Interface used to descipt the process navigator +/// +public interface IProcessNavigator { /// - /// Interface used to descipt the process navigator + /// Get the next task in the process from the current element based on the action and datadriven gateway decisions /// - public interface IProcessNavigator - { - /// - /// Get the next task in the process from the current element based on the action and datadriven gateway decisions - /// - /// Instance data - /// Current process element id - /// Action performed - /// The next process task - public Task GetNextTask(Instance instance, string currentElement, string? action); - } + /// Instance data + /// Current process element id + /// Action performed + /// The next process task + public Task GetNextTask(Instance instance, string currentElement, string? action); } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs b/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs index ac860b447..4c98db1c9 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs @@ -5,128 +5,119 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; -namespace Altinn.App.Core.Internal.Process +namespace Altinn.App.Core.Internal.Process; + +/// +/// This class is responsible for delegating process events to the correct event handler. +/// +public class ProcessEventHandlingDelegator : IProcessEventHandlerDelegator { + private readonly ILogger _logger; + private readonly IStartTaskEventHandler _startTaskEventHandler; + private readonly IEndTaskEventHandler _endTaskEventHandler; + private readonly IAbandonTaskEventHandler _abandonTaskEventHandler; + private readonly IEndEventEventHandler _endEventHandler; + private readonly IEnumerable _processTasks; + /// /// This class is responsible for delegating process events to the correct event handler. /// - public class ProcessEventHandlingDelegator : IProcessEventHandlerDelegator + public ProcessEventHandlingDelegator( + ILogger logger, + IStartTaskEventHandler startTaskEventHandler, + IEndTaskEventHandler endTaskEventHandler, + IAbandonTaskEventHandler abandonTaskEventHandler, + IEndEventEventHandler endEventHandler, + IEnumerable processTasks + ) { - private readonly ILogger _logger; - private readonly IStartTaskEventHandler _startTaskEventHandler; - private readonly IEndTaskEventHandler _endTaskEventHandler; - private readonly IAbandonTaskEventHandler _abandonTaskEventHandler; - private readonly IEndEventEventHandler _endEventHandler; - private readonly IEnumerable _processTasks; + _logger = logger; + _startTaskEventHandler = startTaskEventHandler; + _endTaskEventHandler = endTaskEventHandler; + _abandonTaskEventHandler = abandonTaskEventHandler; + _endEventHandler = endEventHandler; + _processTasks = processTasks; + } - /// - /// This class is responsible for delegating process events to the correct event handler. - /// - public ProcessEventHandlingDelegator( - ILogger logger, - IStartTaskEventHandler startTaskEventHandler, - IEndTaskEventHandler endTaskEventHandler, - IAbandonTaskEventHandler abandonTaskEventHandler, - IEndEventEventHandler endEventHandler, - IEnumerable processTasks - ) + /// + /// Loops through all events and delegates the event to the correct event handler. + /// + public async Task HandleEvents(Instance instance, Dictionary? prefill, List? events) + { + if (events == null) { - _logger = logger; - _startTaskEventHandler = startTaskEventHandler; - _endTaskEventHandler = endTaskEventHandler; - _abandonTaskEventHandler = abandonTaskEventHandler; - _endEventHandler = endEventHandler; - _processTasks = processTasks; + return; } - /// - /// Loops through all events and delegates the event to the correct event handler. - /// - public async Task HandleEvents( - Instance instance, - Dictionary? prefill, - List? events - ) + foreach (InstanceEvent instanceEvent in events) { - if (events == null) - { - return; - } - - foreach (InstanceEvent instanceEvent in events) + if (Enum.TryParse(instanceEvent.EventType, true, out InstanceEventType eventType)) { - if (Enum.TryParse(instanceEvent.EventType, true, out InstanceEventType eventType)) + string? taskId = instanceEvent.ProcessInfo?.CurrentTask?.ElementId; + if (instanceEvent.ProcessInfo?.CurrentTask != null && string.IsNullOrEmpty(taskId)) { - string? taskId = instanceEvent.ProcessInfo?.CurrentTask?.ElementId; - if (instanceEvent.ProcessInfo?.CurrentTask != null && string.IsNullOrEmpty(taskId)) - { - throw new ProcessException( - $"Unable to parse taskId from CurrentTask on instance event {eventType} ({instanceEvent.Id})" - ); - } + throw new ProcessException( + $"Unable to parse taskId from CurrentTask on instance event {eventType} ({instanceEvent.Id})" + ); + } - string? altinnTaskType = instanceEvent.ProcessInfo?.CurrentTask?.AltinnTaskType; + string? altinnTaskType = instanceEvent.ProcessInfo?.CurrentTask?.AltinnTaskType; - switch (eventType) - { - case InstanceEventType.process_StartEvent: - break; - case InstanceEventType.process_StartTask: - // ! TODO: figure out why taskId can be null here - await _startTaskEventHandler.Execute( - GetProcessTaskInstance(altinnTaskType), - taskId!, - instance, - prefill - ); - break; - case InstanceEventType.process_EndTask: - // ! TODO: figure out why taskId can be null here - await _endTaskEventHandler.Execute( - GetProcessTaskInstance(altinnTaskType), - taskId!, - instance - ); - break; - case InstanceEventType.process_AbandonTask: - // InstanceEventType is set to Abandon when action performed is `Reject`. This is to keep backwards compatability with existing code that only should be run when a task is abandoned/rejected. - // ! TODO: figure out why taskId can be null here - await _abandonTaskEventHandler.Execute( - GetProcessTaskInstance(altinnTaskType), - taskId!, - instance - ); - break; - case InstanceEventType.process_EndEvent: - await _endEventHandler.Execute(instanceEvent, instance); - break; - } - } - else + switch (eventType) { - _logger.LogError("Unable to parse instanceEvent eventType {EventType}", instanceEvent.EventType); + case InstanceEventType.process_StartEvent: + break; + case InstanceEventType.process_StartTask: + // ! TODO: figure out why taskId can be null here + await _startTaskEventHandler.Execute( + GetProcessTaskInstance(altinnTaskType), + taskId!, + instance, + prefill + ); + break; + case InstanceEventType.process_EndTask: + // ! TODO: figure out why taskId can be null here + await _endTaskEventHandler.Execute(GetProcessTaskInstance(altinnTaskType), taskId!, instance); + break; + case InstanceEventType.process_AbandonTask: + // InstanceEventType is set to Abandon when action performed is `Reject`. This is to keep backwards compatability with existing code that only should be run when a task is abandoned/rejected. + // ! TODO: figure out why taskId can be null here + await _abandonTaskEventHandler.Execute( + GetProcessTaskInstance(altinnTaskType), + taskId!, + instance + ); + break; + case InstanceEventType.process_EndEvent: + await _endEventHandler.Execute(instanceEvent, instance); + break; } } - } - - /// - /// Identify the correct task implementation - /// - private IProcessTask GetProcessTaskInstance(string? altinnTaskType) - { - if (string.IsNullOrEmpty(altinnTaskType)) + else { - altinnTaskType = "NullType"; + _logger.LogError("Unable to parse instanceEvent eventType {EventType}", instanceEvent.EventType); } + } + } - IProcessTask? processTask = _processTasks.FirstOrDefault(pt => pt.Type == altinnTaskType); + /// + /// Identify the correct task implementation + /// + private IProcessTask GetProcessTaskInstance(string? altinnTaskType) + { + if (string.IsNullOrEmpty(altinnTaskType)) + { + altinnTaskType = "NullType"; + } - if (processTask == null) - { - throw new ProcessException($"No process task instance found for altinnTaskType {altinnTaskType}"); - } + IProcessTask? processTask = _processTasks.FirstOrDefault(pt => pt.Type == altinnTaskType); - return processTask; + if (processTask == null) + { + throw new ProcessException($"No process task instance found for altinnTaskType {altinnTaskType}"); } + + return processTask; } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessException.cs b/src/Altinn.App.Core/Internal/Process/ProcessException.cs index cde7054da..093b9e185 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessException.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessException.cs @@ -1,15 +1,14 @@ -namespace Altinn.App.Core.Internal.Process +namespace Altinn.App.Core.Internal.Process; + +/// +/// Represents errors that occur while handling a process. +/// +public class ProcessException : Exception { /// - /// Represents errors that occur while handling a process. + /// Initializes a new instance of the class with a specified error message. /// - public class ProcessException : Exception - { - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public ProcessException(string message) - : base(message) { } - } + /// The message that describes the error. + public ProcessException(string message) + : base(message) { } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessSequenceFlowType.cs b/src/Altinn.App.Core/Internal/Process/ProcessSequenceFlowType.cs index 863434da6..3296c9653 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessSequenceFlowType.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessSequenceFlowType.cs @@ -1,23 +1,22 @@ -namespace Altinn.App.Core.Internal.Process +namespace Altinn.App.Core.Internal.Process; + +/// +/// Defines the +/// +public enum ProcessSequenceFlowType { /// - /// Defines the + /// Complete the current task and move to next process element. This is the default /// - public enum ProcessSequenceFlowType - { - /// - /// Complete the current task and move to next process element. This is the default - /// - CompleteCurrentMoveToNext = 0, + CompleteCurrentMoveToNext = 0, - /// - /// Abandon the current task and return to next process element. - /// - AbandonCurrentReturnToNext = 1, + /// + /// Abandon the current task and return to next process element. + /// + AbandonCurrentReturnToNext = 1, - /// - /// Abandon the current task and move to next process element. T - /// - AbandonCurrentMoveToNext = 2, - } + /// + /// Abandon the current task and move to next process element. T + /// + AbandonCurrentMoveToNext = 2, } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskDataLocker.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskDataLocker.cs index 9c9f67c06..c574fbcf2 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskDataLocker.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskDataLocker.cs @@ -1,26 +1,25 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +/// Can be used to lock data elements connected to a process task +/// +public interface IProcessTaskDataLocker { /// - /// Can be used to lock data elements connected to a process task + /// Unlock data elements connected to a specific task /// - public interface IProcessTaskDataLocker - { - /// - /// Unlock data elements connected to a specific task - /// - /// - /// - /// - Task Unlock(string taskId, Instance instance); + /// + /// + /// + Task Unlock(string taskId, Instance instance); - /// - /// Lock data elements connected to a specific task - /// - /// - /// - /// - Task Lock(string taskId, Instance instance); - } + /// + /// Lock data elements connected to a specific task + /// + /// + /// + /// + Task Lock(string taskId, Instance instance); } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskFinalizer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskFinalizer.cs index 8ff2bd781..1f1268c05 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskFinalizer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskFinalizer.cs @@ -1,18 +1,17 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +/// Contains common logic for ending a process task. +/// +public interface IProcessTaskFinalizer { /// - /// Contains common logic for ending a process task. + /// Runs common finalization logic for process tasks for a given task ID and instance. This method removes data elements generated from the task, removes hidden data and shadow fields. /// - public interface IProcessTaskFinalizer - { - /// - /// Runs common finalization logic for process tasks for a given task ID and instance. This method removes data elements generated from the task, removes hidden data and shadow fields. - /// - /// - /// - /// - Task Finalize(string taskId, Instance instance); - } + /// + /// + /// + Task Finalize(string taskId, Instance instance); } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskInitializer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskInitializer.cs index 93e317c72..7eb59e3ac 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskInitializer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskInitializer.cs @@ -1,18 +1,17 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +/// Contains common logic for initializing a process task. +/// +public interface IProcessTaskInitializer { /// - /// Contains common logic for initializing a process task. + /// Runs common initialization logic for process tasks for a given task ID and instance. This method initializes the data elements for the instance based on application metadata and prefill configurations. Also updates presentation texts and data values on the instance. /// - public interface IProcessTaskInitializer - { - /// - /// Runs common initialization logic for process tasks for a given task ID and instance. This method initializes the data elements for the instance based on application metadata and prefill configurations. Also updates presentation texts and data values on the instance. - /// - /// - /// - /// - Task Initialize(string taskId, Instance instance, Dictionary? prefill); - } + /// + /// + /// + Task Initialize(string taskId, Instance instance, Dictionary? prefill); } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs index e124f26bc..cd2850f72 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs @@ -6,7 +6,6 @@ using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Prefill; using Altinn.App.Core.Models; -using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ConfirmationProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ConfirmationProcessTask.cs index 7c625d43b..a4520a93d 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ConfirmationProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ConfirmationProcessTask.cs @@ -1,31 +1,30 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +/// Represents the process task responsible for collecting user confirmation. +/// +public class ConfirmationProcessTask : IProcessTask { - /// - /// Represents the process task responsible for collecting user confirmation. - /// - public class ConfirmationProcessTask : IProcessTask - { - /// - public string Type => "confirmation"; + /// + public string Type => "confirmation"; - /// - public async Task Abandon(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task Abandon(string taskId, Instance instance) + { + await Task.CompletedTask; + } - /// - public async Task End(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task End(string taskId, Instance instance) + { + await Task.CompletedTask; + } - /// - public async Task Start(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task Start(string taskId, Instance instance) + { + await Task.CompletedTask; } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/DataProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/DataProcessTask.cs index a112a7740..d664e7d49 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/DataProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/DataProcessTask.cs @@ -1,31 +1,30 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +/// Represents the process task responsible for form filling steps. +/// +public class DataProcessTask : IProcessTask { - /// - /// Represents the process task responsible for form filling steps. - /// - public class DataProcessTask : IProcessTask - { - /// - public string Type => "data"; + /// + public string Type => "data"; - /// - public async Task Abandon(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task Abandon(string taskId, Instance instance) + { + await Task.CompletedTask; + } - /// - public async Task End(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task End(string taskId, Instance instance) + { + await Task.CompletedTask; + } - /// - public async Task Start(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task Start(string taskId, Instance instance) + { + await Task.CompletedTask; } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/FeedbackProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/FeedbackProcessTask.cs index e81c58a06..65880baf0 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/FeedbackProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/FeedbackProcessTask.cs @@ -1,31 +1,30 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +/// Represents the process task responsible for waiting for feedback from application owner. +/// +public class FeedbackProcessTask : IProcessTask { - /// - /// Represents the process task responsible for waiting for feedback from application owner. - /// - public class FeedbackProcessTask : IProcessTask - { - /// - public string Type => "feedback"; + /// + public string Type => "feedback"; - /// - public async Task Abandon(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task Abandon(string taskId, Instance instance) + { + await Task.CompletedTask; + } - /// - public async Task End(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task End(string taskId, Instance instance) + { + await Task.CompletedTask; + } - /// - public async Task Start(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task Start(string taskId, Instance instance) + { + await Task.CompletedTask; } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Interfaces/IProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Interfaces/IProcessTask.cs index c8c095e75..11050d88b 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Interfaces/IProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Interfaces/IProcessTask.cs @@ -1,30 +1,29 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +/// Implement this interface to create a new type of task for the process engine. +/// +public interface IProcessTask { /// - /// Implement this interface to create a new type of task for the process engine. + /// The type is used to identify the correct task implementation for a given task type in the process config file. /// - public interface IProcessTask - { - /// - /// The type is used to identify the correct task implementation for a given task type in the process config file. - /// - string Type { get; } + string Type { get; } - /// - /// Any logic to be executed when a task is started should be put in this method. - /// - Task Start(string taskId, Instance instance); + /// + /// Any logic to be executed when a task is started should be put in this method. + /// + Task Start(string taskId, Instance instance); - /// - /// Any logic to be executed when a task is ended should be put in this method. - /// - Task End(string taskId, Instance instance); + /// + /// Any logic to be executed when a task is ended should be put in this method. + /// + Task End(string taskId, Instance instance); - /// - /// Any logic to be executed when a task is abandoned should be put in this method. - /// - Task Abandon(string taskId, Instance instance); - } + /// + /// Any logic to be executed when a task is abandoned should be put in this method. + /// + Task Abandon(string taskId, Instance instance); } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs index bd4ab9591..b989c5ea1 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs @@ -6,92 +6,89 @@ using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +/// Represents the process task responsible for collecting user payment. +/// +internal sealed class PaymentProcessTask : IProcessTask { + private readonly IPdfService _pdfService; + private readonly IDataClient _dataClient; + private readonly IProcessReader _processReader; + private readonly IPaymentService _paymentService; + + private const string PdfContentType = "application/pdf"; + private const string ReceiptFileName = "Betalingskvittering.pdf"; + /// - /// Represents the process task responsible for collecting user payment. + /// Initializes a new instance of the class. /// - internal sealed class PaymentProcessTask : IProcessTask + public PaymentProcessTask( + IPdfService pdfService, + IDataClient dataClient, + IProcessReader processReader, + IPaymentService paymentService + ) { - private readonly IPdfService _pdfService; - private readonly IDataClient _dataClient; - private readonly IProcessReader _processReader; - private readonly IPaymentService _paymentService; - - private const string PdfContentType = "application/pdf"; - private const string ReceiptFileName = "Betalingskvittering.pdf"; - - /// - /// Initializes a new instance of the class. - /// - public PaymentProcessTask( - IPdfService pdfService, - IDataClient dataClient, - IProcessReader processReader, - IPaymentService paymentService - ) - { - _pdfService = pdfService; - _dataClient = dataClient; - _processReader = processReader; - _paymentService = paymentService; - } + _pdfService = pdfService; + _dataClient = dataClient; + _processReader = processReader; + _paymentService = paymentService; + } - /// - public string Type => "payment"; + /// + public string Type => "payment"; - /// - public async Task Start(string taskId, Instance instance) - { - AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); - await _paymentService.CancelAndDeleteAnyExistingPayment(instance, paymentConfiguration.Validate()); - } + /// + public async Task Start(string taskId, Instance instance) + { + AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); + await _paymentService.CancelAndDeleteAnyExistingPayment(instance, paymentConfiguration.Validate()); + } - /// - public async Task End(string taskId, Instance instance) - { - AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); + /// + public async Task End(string taskId, Instance instance) + { + AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); - if (!await _paymentService.IsPaymentCompleted(instance, paymentConfiguration.Validate())) - throw new PaymentException("The payment is not completed."); + if (!await _paymentService.IsPaymentCompleted(instance, paymentConfiguration.Validate())) + throw new PaymentException("The payment is not completed."); - Stream pdfStream = await _pdfService.GeneratePdf(instance, taskId, CancellationToken.None); + Stream pdfStream = await _pdfService.GeneratePdf(instance, taskId, CancellationToken.None); - var validatedPaymentConfiguration = paymentConfiguration.Validate(); + var validatedPaymentConfiguration = paymentConfiguration.Validate(); - await _dataClient.InsertBinaryData( - instance.Id, - validatedPaymentConfiguration.PaymentDataType, - PdfContentType, - ReceiptFileName, - pdfStream, - taskId - ); - } + await _dataClient.InsertBinaryData( + instance.Id, + validatedPaymentConfiguration.PaymentDataType, + PdfContentType, + ReceiptFileName, + pdfStream, + taskId + ); + } - /// - public async Task Abandon(string taskId, Instance instance) - { - AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); - await _paymentService.CancelAndDeleteAnyExistingPayment(instance, paymentConfiguration.Validate()); - } + /// + public async Task Abandon(string taskId, Instance instance) + { + AltinnPaymentConfiguration paymentConfiguration = GetAltinnPaymentConfiguration(taskId); + await _paymentService.CancelAndDeleteAnyExistingPayment(instance, paymentConfiguration.Validate()); + } - private AltinnPaymentConfiguration GetAltinnPaymentConfiguration(string taskId) - { - AltinnPaymentConfiguration? paymentConfiguration = _processReader - .GetAltinnTaskExtension(taskId) - ?.PaymentConfiguration; + private AltinnPaymentConfiguration GetAltinnPaymentConfiguration(string taskId) + { + AltinnPaymentConfiguration? paymentConfiguration = _processReader + .GetAltinnTaskExtension(taskId) + ?.PaymentConfiguration; - if (paymentConfiguration == null) - { - throw new ApplicationConfigException( - "PaymentConfig is missing in the payment process task configuration." - ); - } + if (paymentConfiguration == null) + { + throw new ApplicationConfigException("PaymentConfig is missing in the payment process task configuration."); + } - _ = paymentConfiguration.Validate(); + _ = paymentConfiguration.Validate(); - return paymentConfiguration; - } + return paymentConfiguration; } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs index 7968395fd..9621e5a44 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs @@ -1,30 +1,29 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +/// Represents the process task responsible for signing. +/// +internal class SigningProcessTask : IProcessTask { - /// - /// Represents the process task responsible for signing. - /// - internal class SigningProcessTask : IProcessTask - { - public string Type => "signing"; + public string Type => "signing"; - /// - public async Task Start(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task Start(string taskId, Instance instance) + { + await Task.CompletedTask; + } - /// - public async Task End(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task End(string taskId, Instance instance) + { + await Task.CompletedTask; + } - /// - public async Task Abandon(string taskId, Instance instance) - { - await Task.CompletedTask; - } + /// + public async Task Abandon(string taskId, Instance instance) + { + await Task.CompletedTask; } } diff --git a/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs b/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs index 17206dd2a..f645f5d28 100644 --- a/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs +++ b/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs @@ -1,17 +1,16 @@ using Altinn.Platform.Profile.Models; -namespace Altinn.App.Core.Internal.Profile +namespace Altinn.App.Core.Internal.Profile; + +/// +/// Interface for profile functionality +/// +public interface IProfileClient { /// - /// Interface for profile functionality + /// Method for getting the userprofile from a given user id /// - public interface IProfileClient - { - /// - /// Method for getting the userprofile from a given user id - /// - /// the user id - /// The userprofile for the given user id - Task GetUserProfile(int userId); - } + /// the user id + /// The userprofile for the given user id + Task GetUserProfile(int userId); } diff --git a/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs b/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs index b6c38ecdb..6e9cfead0 100644 --- a/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs +++ b/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs @@ -1,24 +1,23 @@ using Altinn.Platform.Register.Models; -namespace Altinn.App.Core.Internal.Registers +namespace Altinn.App.Core.Internal.Registers; + +/// +/// Interface for register functionality +/// +public interface IAltinnPartyClient { /// - /// Interface for register functionality + /// Returns party information /// - public interface IAltinnPartyClient - { - /// - /// Returns party information - /// - /// The partyId - /// The party for the given partyId - Task GetParty(int partyId); + /// The partyId + /// The party for the given partyId + Task GetParty(int partyId); - /// - /// Looks up a party by person or organisation number. - /// - /// A populated lookup object with information about what to look for. - /// The party lookup containing either SSN or organisation number. - Task LookupParty(PartyLookup partyLookup); - } + /// + /// Looks up a party by person or organisation number. + /// + /// A populated lookup object with information about what to look for. + /// The party lookup containing either SSN or organisation number. + Task LookupParty(PartyLookup partyLookup); } diff --git a/src/Altinn.App.Core/Internal/Registers/IOrganizationClient.cs b/src/Altinn.App.Core/Internal/Registers/IOrganizationClient.cs index 5b18de260..b1e39e58b 100644 --- a/src/Altinn.App.Core/Internal/Registers/IOrganizationClient.cs +++ b/src/Altinn.App.Core/Internal/Registers/IOrganizationClient.cs @@ -1,17 +1,16 @@ using Altinn.Platform.Register.Models; -namespace Altinn.App.Core.Internal.Registers +namespace Altinn.App.Core.Internal.Registers; + +/// +/// Interface for the entity registry (ER: Enhetsregisteret) +/// +public interface IOrganizationClient { /// - /// Interface for the entity registry (ER: Enhetsregisteret) + /// Method for getting an organization based on a organization nr /// - public interface IOrganizationClient - { - /// - /// Method for getting an organization based on a organization nr - /// - /// the organization number - /// The organization for the given organization number - Task GetOrganization(string OrgNr); - } + /// the organization number + /// The organization for the given organization number + Task GetOrganization(string OrgNr); } diff --git a/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs b/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs index d5a912b84..97c8f27ca 100644 --- a/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs +++ b/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs @@ -1,23 +1,22 @@ using Altinn.Platform.Register.Models; -namespace Altinn.App.Core.Internal.Registers +namespace Altinn.App.Core.Internal.Registers; + +/// +/// Describes the required methods for an implementation of a person repository client. +/// +public interface IPersonClient { /// - /// Describes the required methods for an implementation of a person repository client. + /// Get the object for the person identified with the parameters. /// - public interface IPersonClient - { - /// - /// Get the object for the person identified with the parameters. - /// - /// - /// The method requires both the national identity number and the last name of the person. This is used to - /// verify that entered information is correct and to prevent testing of random identity numbers. - /// - /// The national identity number of the person. - /// The last name of the person. - /// The cancellation token to cancel operation. - /// The identified person if found. - Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct); - } + /// + /// The method requires both the national identity number and the last name of the person. This is used to + /// verify that entered information is correct and to prevent testing of random identity numbers. + /// + /// The national identity number of the person. + /// The last name of the person. + /// The cancellation token to cancel operation. + /// The identified person if found. + Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct); } diff --git a/src/Altinn.App.Core/Internal/Secrets/ISecretsClient.cs b/src/Altinn.App.Core/Internal/Secrets/ISecretsClient.cs index 0fcd06e1a..ee938ff6d 100644 --- a/src/Altinn.App.Core/Internal/Secrets/ISecretsClient.cs +++ b/src/Altinn.App.Core/Internal/Secrets/ISecretsClient.cs @@ -1,38 +1,37 @@ using Microsoft.Azure.KeyVault; using Microsoft.Azure.KeyVault.WebKey; -namespace Altinn.App.Core.Internal.Secrets +namespace Altinn.App.Core.Internal.Secrets; + +/// +/// Interface for secrets service +/// +public interface ISecretsClient { /// - /// Interface for secrets service + /// Gets the latest version of a key from key vault. /// - public interface ISecretsClient - { - /// - /// Gets the latest version of a key from key vault. - /// - /// The name of the key. - /// The key as a JSON web key. - Task GetKeyAsync(string keyName); + /// The name of the key. + /// The key as a JSON web key. + Task GetKeyAsync(string keyName); - /// - /// Gets the latest version of a secret from key vault. - /// - /// The name of the secret. - /// The secret value. - Task GetSecretAsync(string secretName); + /// + /// Gets the latest version of a secret from key vault. + /// + /// The name of the secret. + /// The secret value. + Task GetSecretAsync(string secretName); - /// - /// Gets the latest version of a certificate from key vault. - /// - /// The name of certificate. - /// The certificate as a byte array. - Task GetCertificateAsync(string certificateName); + /// + /// Gets the latest version of a certificate from key vault. + /// + /// The name of certificate. + /// The certificate as a byte array. + Task GetCertificateAsync(string certificateName); - /// - /// Gets the key vault client. - /// - /// The key vault client. - KeyVaultClient GetKeyVaultClient(); - } + /// + /// Gets the key vault client. + /// + /// The key vault client. + KeyVaultClient GetKeyVaultClient(); } diff --git a/src/Altinn.App.Core/Internal/Sign/ISignClient.cs b/src/Altinn.App.Core/Internal/Sign/ISignClient.cs index 17d11bd29..06e0b8c63 100644 --- a/src/Altinn.App.Core/Internal/Sign/ISignClient.cs +++ b/src/Altinn.App.Core/Internal/Sign/ISignClient.cs @@ -1,5 +1,3 @@ -using Altinn.App.Core.Models; - namespace Altinn.App.Core.Internal.Sign; /// diff --git a/src/Altinn.App.Core/Internal/Texts/IText.cs b/src/Altinn.App.Core/Internal/Texts/IText.cs index b4851b8e0..60d839cd6 100644 --- a/src/Altinn.App.Core/Internal/Texts/IText.cs +++ b/src/Altinn.App.Core/Internal/Texts/IText.cs @@ -1,20 +1,19 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Texts +namespace Altinn.App.Core.Internal.Texts; + +/// +/// Describes the public methods of a text resources service +/// +[Obsolete("Use IAppResources.GetTexts() instead")] +public interface IText { /// - /// Describes the public methods of a text resources service + /// Get text resource based on id. /// - [Obsolete("Use IAppResources.GetTexts() instead")] - public interface IText - { - /// - /// Get text resource based on id. - /// - /// Unique identifier of the organisation responsible for the app. - /// Application identifier which is unique within an organisation. - /// Language for the text resource - /// The text resource - Task GetText(string org, string app, string language); - } + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// Language for the text resource + /// The text resource + Task GetText(string org, string app, string language); } diff --git a/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs b/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs index a814df9ca..3b8cc9651 100644 --- a/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs +++ b/src/Altinn.App.Core/Internal/Validation/FileValidationService.cs @@ -4,54 +4,53 @@ using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Validation +namespace Altinn.App.Core.Internal.Validation; + +/// +/// Validates files according to the registered IFileValidation interfaces +/// +public class FileValidationService : IFileValidationService { + private readonly IFileValidatorFactory _fileValidatorFactory; + private readonly Telemetry? _telemetry; + /// - /// Validates files according to the registered IFileValidation interfaces + /// Initializes a new instance of the class. /// - public class FileValidationService : IFileValidationService + public FileValidationService(IFileValidatorFactory fileValidatorFactory, Telemetry? telemetry = null) { - private readonly IFileValidatorFactory _fileValidatorFactory; - private readonly Telemetry? _telemetry; + _fileValidatorFactory = fileValidatorFactory; + _telemetry = telemetry; + } - /// - /// Initializes a new instance of the class. - /// - public FileValidationService(IFileValidatorFactory fileValidatorFactory, Telemetry? telemetry = null) - { - _fileValidatorFactory = fileValidatorFactory; - _telemetry = telemetry; - } + /// + /// Runs all registered validators on the specified + /// + public async Task<(bool Success, List Errors)> Validate( + DataType dataType, + IEnumerable fileAnalysisResults + ) + { + using var activity = _telemetry?.StartFileValidateActivity(); + List allErrors = new(); + bool allSuccess = true; - /// - /// Runs all registered validators on the specified - /// - public async Task<(bool Success, List Errors)> Validate( - DataType dataType, - IEnumerable fileAnalysisResults - ) + List fileValidators = _fileValidatorFactory + .GetFileValidators(dataType.EnabledFileValidators) + .ToList(); + foreach (IFileValidator fileValidator in fileValidators) { - using var activity = _telemetry?.StartFileValidateActivity(); - List allErrors = new(); - bool allSuccess = true; - - List fileValidators = _fileValidatorFactory - .GetFileValidators(dataType.EnabledFileValidators) - .ToList(); - foreach (IFileValidator fileValidator in fileValidators) + (bool success, IEnumerable errors) = await fileValidator.Validate( + dataType, + fileAnalysisResults + ); + if (!success) { - (bool success, IEnumerable errors) = await fileValidator.Validate( - dataType, - fileAnalysisResults - ); - if (!success) - { - allSuccess = false; - allErrors.AddRange(errors); - } + allSuccess = false; + allErrors.AddRange(errors); } - - return (allSuccess, allErrors); } + + return (allSuccess, allErrors); } } diff --git a/src/Altinn.App.Core/Internal/Validation/FileValidatorFactory.cs b/src/Altinn.App.Core/Internal/Validation/FileValidatorFactory.cs index 0ff71a295..da3cb4c1a 100644 --- a/src/Altinn.App.Core/Internal/Validation/FileValidatorFactory.cs +++ b/src/Altinn.App.Core/Internal/Validation/FileValidatorFactory.cs @@ -1,29 +1,28 @@ using Altinn.App.Core.Features.Validation; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Validation +namespace Altinn.App.Core.Internal.Validation; + +/// +/// Factory class that resolves the correct file validators to run on against a . +/// +public class FileValidatorFactory : IFileValidatorFactory { + private readonly IEnumerable _fileValidators; + /// - /// Factory class that resolves the correct file validators to run on against a . + /// Initializes a new instance of the class. /// - public class FileValidatorFactory : IFileValidatorFactory + public FileValidatorFactory(IEnumerable fileValidators) { - private readonly IEnumerable _fileValidators; - - /// - /// Initializes a new instance of the class. - /// - public FileValidatorFactory(IEnumerable fileValidators) - { - _fileValidators = fileValidators; - } + _fileValidators = fileValidators; + } - /// - /// Finds the specified file analyser implementations based on the specified analyser id's. - /// - public IEnumerable GetFileValidators(IEnumerable validatorIds) - { - return _fileValidators.Where(x => validatorIds.Contains(x.Id)).ToList(); - } + /// + /// Finds the specified file analyser implementations based on the specified analyser id's. + /// + public IEnumerable GetFileValidators(IEnumerable validatorIds) + { + return _fileValidators.Where(x => validatorIds.Contains(x.Id)).ToList(); } } diff --git a/src/Altinn.App.Core/Internal/Validation/IFileValidationService.cs b/src/Altinn.App.Core/Internal/Validation/IFileValidationService.cs index b3bc9ac70..3b7b6fda1 100644 --- a/src/Altinn.App.Core/Internal/Validation/IFileValidationService.cs +++ b/src/Altinn.App.Core/Internal/Validation/IFileValidationService.cs @@ -2,19 +2,18 @@ using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Validation +namespace Altinn.App.Core.Internal.Validation; + +/// +/// Interface for running all file validators registered on a data type. +/// +public interface IFileValidationService { /// - /// Interface for running all file validators registered on a data type. + /// Validates the file based on the file analysis results. /// - public interface IFileValidationService - { - /// - /// Validates the file based on the file analysis results. - /// - Task<(bool Success, List Errors)> Validate( - DataType dataType, - IEnumerable fileAnalysisResults - ); - } + Task<(bool Success, List Errors)> Validate( + DataType dataType, + IEnumerable fileAnalysisResults + ); } diff --git a/src/Altinn.App.Core/Internal/Validation/IFileValidatorFactory.cs b/src/Altinn.App.Core/Internal/Validation/IFileValidatorFactory.cs index a77d2fcf9..5b9055b73 100644 --- a/src/Altinn.App.Core/Internal/Validation/IFileValidatorFactory.cs +++ b/src/Altinn.App.Core/Internal/Validation/IFileValidatorFactory.cs @@ -1,16 +1,15 @@ using Altinn.App.Core.Features.Validation; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Validation +namespace Altinn.App.Core.Internal.Validation; + +/// +/// Interface responsible for resolving the correct file validators to run on against a . +/// +public interface IFileValidatorFactory { /// - /// Interface responsible for resolving the correct file validators to run on against a . + /// Finds validator implementations based on the specified id's provided. /// - public interface IFileValidatorFactory - { - /// - /// Finds validator implementations based on the specified id's provided. - /// - IEnumerable GetFileValidators(IEnumerable validatorIds); - } + IEnumerable GetFileValidators(IEnumerable validatorIds); } diff --git a/src/Altinn.App.Core/Internal/Validation/IValidatorFactory.cs b/src/Altinn.App.Core/Internal/Validation/IValidatorFactory.cs index 67163b40c..1a677b199 100644 --- a/src/Altinn.App.Core/Internal/Validation/IValidatorFactory.cs +++ b/src/Altinn.App.Core/Internal/Validation/IValidatorFactory.cs @@ -1,6 +1,4 @@ using Altinn.App.Core.Features; -using Altinn.App.Core.Features.Validation; -using Microsoft.Extensions.DependencyInjection; namespace Altinn.App.Core.Internal.Validation; diff --git a/src/Altinn.App.Core/Models/AppIdentifier.cs b/src/Altinn.App.Core/Models/AppIdentifier.cs index 5bb3af0e1..0b61d1988 100644 --- a/src/Altinn.App.Core/Models/AppIdentifier.cs +++ b/src/Altinn.App.Core/Models/AppIdentifier.cs @@ -1,145 +1,144 @@ using Altinn.App.Core.Extensions; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Class representing the id of an instance. +/// +public sealed class AppIdentifier : IEquatable { /// - /// Class representing the id of an instance. + /// Organization that owns the app. + /// + public string Org { get; } + + /// + /// Application name + /// + public string App { get; } + + /// + /// Initializes a new instance of the class. /// - public sealed class AppIdentifier : IEquatable + /// The app owner. + /// The app name. + public AppIdentifier(string org, string app) { - /// - /// Organization that owns the app. - /// - public string Org { get; } - - /// - /// Application name - /// - public string App { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The app owner. - /// The app name. - public AppIdentifier(string org, string app) + if (string.IsNullOrEmpty(org)) { - if (string.IsNullOrEmpty(org)) - { - throw new ArgumentNullException(nameof(org)); - } - - if (string.IsNullOrEmpty(app)) - { - throw new ArgumentNullException(nameof(app)); - } - - Org = org; - App = app; + throw new ArgumentNullException(nameof(org)); } - /// - /// Initializes a new instance of the class. - /// - /// Application id on the form org/app - public AppIdentifier(string id) + if (string.IsNullOrEmpty(app)) { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException(nameof(id)); - } - - if (id.ContainsExactlyOne('/')) - { - (Org, App) = DeconstructAppId(id); - return; - } - - throw new ArgumentOutOfRangeException( - nameof(id), - "You must have exactly only one / (forward slash) in your id" - ); + throw new ArgumentNullException(nameof(app)); } - /// - /// Get an AppIdentifier from an Instance.AppID - /// - public AppIdentifier(Instance instance) - : this(instance.AppId) { } - - /// - /// Deconstructs an app id into it's two logical parts - org and app. - /// - /// App identifier on the form {org}/{app} - /// A 2-tuple with the org and the app - private static (string org, string app) DeconstructAppId(string appId) - { - var deconstructed = appId.Split("/"); - string org = deconstructed[0]; - string app = deconstructed[1]; - - return (org, app); - } + Org = org; + App = app; + } - /// - public override bool Equals(object? obj) + /// + /// Initializes a new instance of the class. + /// + /// Application id on the form org/app + public AppIdentifier(string id) + { + if (string.IsNullOrEmpty(id)) { - return Equals(obj as AppIdentifier); + throw new ArgumentNullException(nameof(id)); } - /// - public bool Equals(AppIdentifier? other) + if (id.ContainsExactlyOne('/')) { - return Org != null - && App != null - && Org.Equals(other?.Org, StringComparison.CurrentCultureIgnoreCase) - && App.Equals(other?.App, StringComparison.CurrentCultureIgnoreCase); + (Org, App) = DeconstructAppId(id); + return; } - /// - public override string ToString() - { - return $"{Org}/{App}"; - } + throw new ArgumentOutOfRangeException( + nameof(id), + "You must have exactly only one / (forward slash) in your id" + ); + } - /// - public override int GetHashCode() - { - return Org.GetHashCode() ^ App.GetHashCode(); - } + /// + /// Get an AppIdentifier from an Instance.AppID + /// + public AppIdentifier(Instance instance) + : this(instance.AppId) { } - /// - /// A absolute url expected to point to an Altinn 3 app ie. the first two segments of the path - /// has to be the organization and the app name for example: https://org.apps.altinn.no/{org}/{app}/api/... - /// - /// The url to parse - /// A new instance of - public static AppIdentifier CreateFromUrl(string url) - { - ArgumentNullException.ThrowIfNull(url); + /// + /// Deconstructs an app id into it's two logical parts - org and app. + /// + /// App identifier on the form {org}/{app} + /// A 2-tuple with the org and the app + private static (string org, string app) DeconstructAppId(string appId) + { + var deconstructed = appId.Split("/"); + string org = deconstructed[0]; + string app = deconstructed[1]; + + return (org, app); + } + + /// + public override bool Equals(object? obj) + { + return Equals(obj as AppIdentifier); + } + + /// + public bool Equals(AppIdentifier? other) + { + return Org != null + && App != null + && Org.Equals(other?.Org, StringComparison.CurrentCultureIgnoreCase) + && App.Equals(other?.App, StringComparison.CurrentCultureIgnoreCase); + } + + /// + public override string ToString() + { + return $"{Org}/{App}"; + } - if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) - { - throw new ArgumentException( - $"The provided url ({url}) is not well formed. Please check that it is an absolute url with at least two path segments." - ); - } + /// + public override int GetHashCode() + { + return Org.GetHashCode() ^ App.GetHashCode(); + } - Uri uri = new(url, UriKind.Absolute); + /// + /// A absolute url expected to point to an Altinn 3 app ie. the first two segments of the path + /// has to be the organization and the app name for example: https://org.apps.altinn.no/{org}/{app}/api/... + /// + /// The url to parse + /// A new instance of + public static AppIdentifier CreateFromUrl(string url) + { + ArgumentNullException.ThrowIfNull(url); - // Remove the first slash as this will only result in an empty first segment when splitting. - string[] segments = uri.PathAndQuery[1..].Split("/"); + if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) + { + throw new ArgumentException( + $"The provided url ({url}) is not well formed. Please check that it is an absolute url with at least two path segments." + ); + } - if (segments.Length < 2) - { - throw new ArgumentException($"The provided url ({url} must contain at least two path segments.)"); - } + Uri uri = new(url, UriKind.Absolute); - var org = segments[0]; - var app = segments[1]; + // Remove the first slash as this will only result in an empty first segment when splitting. + string[] segments = uri.PathAndQuery[1..].Split("/"); - return new AppIdentifier(org, app); + if (segments.Length < 2) + { + throw new ArgumentException($"The provided url ({url} must contain at least two path segments.)"); } + + var org = segments[0]; + var app = segments[1]; + + return new AppIdentifier(org, app); } } diff --git a/src/Altinn.App.Core/Models/AppOption.cs b/src/Altinn.App.Core/Models/AppOption.cs index 8aa752391..ac9a2cad9 100644 --- a/src/Altinn.App.Core/Models/AppOption.cs +++ b/src/Altinn.App.Core/Models/AppOption.cs @@ -2,204 +2,198 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Represents a key value pair to be used as options in dropdown selectors. +/// +[JsonConverter(typeof(AppOptionConverter))] +public class AppOption { /// - /// Represents a key value pair to be used as options in dropdown selectors. + /// The value of a given option /// - [JsonConverter(typeof(AppOptionConverter))] - public class AppOption - { - /// - /// The value of a given option - /// - [JsonPropertyName("value")] - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public required string? Value { get; set; } - - /// - /// The type of the value for Json serialization - /// - [JsonIgnore] - public AppOptionValueType ValueType { get; set; } - - /// - /// The label of a given option - /// - [JsonPropertyName("label")] - public required string Label { get; set; } - - /// - /// The description of a given option - /// - [JsonPropertyName("description")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Description { get; set; } - - /// - /// The help text of a given option - /// - [JsonPropertyName("helpText")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? HelpText { get; set; } - } + [JsonPropertyName("value")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public required string? Value { get; set; } /// /// The type of the value for Json serialization - /// Application developers must set this, if they want options that isn't strings. /// - public enum AppOptionValueType - { - /// - /// Default value, will be serialized as a string if not specified - /// - String, - - /// - /// The app option value is a number and can be bound to a numeric field - /// - Number, - - /// - /// The app option value is a boolean and can be bound to a boolean field - /// - Boolean, - - /// - /// The app option value is null and can be used to signal that the option is not set - /// - Null, - } + [JsonIgnore] + public AppOptionValueType ValueType { get; set; } + + /// + /// The label of a given option + /// + [JsonPropertyName("label")] + public required string Label { get; set; } /// - /// A converter for AppOption that can handle the different value types - /// This will override [JsonPropertyName] annotatinos, but I keep them for documentation purposes + /// The description of a given option /// - internal class AppOptionConverter : JsonConverter + [JsonPropertyName("description")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Description { get; set; } + + /// + /// The help text of a given option + /// + [JsonPropertyName("helpText")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? HelpText { get; set; } +} + +/// +/// The type of the value for Json serialization +/// Application developers must set this, if they want options that isn't strings. +/// +public enum AppOptionValueType +{ + /// + /// Default value, will be serialized as a string if not specified + /// + String, + + /// + /// The app option value is a number and can be bound to a numeric field + /// + Number, + + /// + /// The app option value is a boolean and can be bound to a boolean field + /// + Boolean, + + /// + /// The app option value is null and can be used to signal that the option is not set + /// + Null, +} + +/// +/// A converter for AppOption that can handle the different value types +/// This will override [JsonPropertyName] annotatinos, but I keep them for documentation purposes +/// +internal class AppOptionConverter : JsonConverter +{ + public override AppOption Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override AppOption Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - string? value = null; - string? label = null; - string? description = null; - string? helpText = null; - AppOptionValueType valueType = AppOptionValueType.Null; + string? value = null; + string? label = null; + string? description = null; + string? helpText = null; + AppOptionValueType valueType = AppOptionValueType.Null; - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType == JsonTokenType.PropertyName) { - if (reader.TokenType == JsonTokenType.PropertyName) + var propertyName = reader.GetString(); + reader.Read(); + switch (propertyName?.ToLowerInvariant()) { - var propertyName = reader.GetString(); - reader.Read(); - switch (propertyName?.ToLowerInvariant()) - { - case "value": - ReadValue(ref reader, out value, out valueType); - break; - case "label": - label = reader.GetString(); - break; - case "description": - description = reader.GetString(); - break; - case "helptext": - helpText = reader.GetString(); - break; - default: - throw new JsonException($"Unknown property {propertyName ?? "'NULL'"} on AppOption"); - } + case "value": + ReadValue(ref reader, out value, out valueType); + break; + case "label": + label = reader.GetString(); + break; + case "description": + description = reader.GetString(); + break; + case "helptext": + helpText = reader.GetString(); + break; + default: + throw new JsonException($"Unknown property {propertyName ?? "'NULL'"} on AppOption"); } } - - return new AppOption - { - Value = value, - ValueType = valueType, - Label = label ?? throw new JsonException("Missing required property 'label' on AppOption"), - Description = description, - HelpText = helpText, - }; } - private static void ReadValue(ref Utf8JsonReader reader, out string? value, out AppOptionValueType valueType) + return new AppOption { - switch (reader.TokenType) - { - case JsonTokenType.String: - value = reader.GetString(); - valueType = AppOptionValueType.String; - break; - case JsonTokenType.Number: - value = reader.GetDouble().ToString(CultureInfo.InvariantCulture); - valueType = AppOptionValueType.Number; - break; - case JsonTokenType.True: - case JsonTokenType.False: - value = reader.GetBoolean() ? "true" : "false"; - valueType = AppOptionValueType.Boolean; - break; - case JsonTokenType.Null: - value = null; - valueType = AppOptionValueType.Null; - break; - default: - throw new JsonException( - $"Unexpected token type {reader.TokenType} for property 'value' on AppOption" - ); - } + Value = value, + ValueType = valueType, + Label = label ?? throw new JsonException("Missing required property 'label' on AppOption"), + Description = description, + HelpText = helpText, + }; + } + + private static void ReadValue(ref Utf8JsonReader reader, out string? value, out AppOptionValueType valueType) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + value = reader.GetString(); + valueType = AppOptionValueType.String; + break; + case JsonTokenType.Number: + value = reader.GetDouble().ToString(CultureInfo.InvariantCulture); + valueType = AppOptionValueType.Number; + break; + case JsonTokenType.True: + case JsonTokenType.False: + value = reader.GetBoolean() ? "true" : "false"; + valueType = AppOptionValueType.Boolean; + break; + case JsonTokenType.Null: + value = null; + valueType = AppOptionValueType.Null; + break; + default: + throw new JsonException($"Unexpected token type {reader.TokenType} for property 'value' on AppOption"); } + } - public override void Write(Utf8JsonWriter writer, AppOption value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AppOption value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + switch (value.ValueType) { - writer.WriteStartObject(); - switch (value.ValueType) - { - case AppOptionValueType.String: - writer.WriteString("value", value.Value); - break; - case AppOptionValueType.Boolean: - writer.WriteBoolean( - "value", - value.Value switch - { - "true" => true, - "false" => false, - _ - => throw new JsonException( - $"Unable to parse value {value.Value} as a boolean on AppOption" - ) - } - ); - break; - case AppOptionValueType.Number: - if (double.TryParse(value.Value, out double doubleValue)) + case AppOptionValueType.String: + writer.WriteString("value", value.Value); + break; + case AppOptionValueType.Boolean: + writer.WriteBoolean( + "value", + value.Value switch { - writer.WriteNumber("value", doubleValue); - } - else - { - throw new JsonException($"Unable to parse value {value.Value} as a number on AppOption"); + "true" => true, + "false" => false, + _ => throw new JsonException($"Unable to parse value {value.Value} as a boolean on AppOption") } + ); + break; + case AppOptionValueType.Number: + if (double.TryParse(value.Value, out double doubleValue)) + { + writer.WriteNumber("value", doubleValue); + } + else + { + throw new JsonException($"Unable to parse value {value.Value} as a number on AppOption"); + } - break; - case AppOptionValueType.Null: - writer.WriteNull("value"); - break; - default: - throw new JsonException($"Unknown value type {value.ValueType} on AppOption"); - } + break; + case AppOptionValueType.Null: + writer.WriteNull("value"); + break; + default: + throw new JsonException($"Unknown value type {value.ValueType} on AppOption"); + } - writer.WriteString("label", value.Label); + writer.WriteString("label", value.Label); - if (!string.IsNullOrEmpty(value.Description)) - { - writer.WriteString("description", value.Description); - } - if (!string.IsNullOrEmpty(value.HelpText)) - { - writer.WriteString("helpText", value.HelpText); - } - writer.WriteEndObject(); + if (!string.IsNullOrEmpty(value.Description)) + { + writer.WriteString("description", value.Description); + } + if (!string.IsNullOrEmpty(value.HelpText)) + { + writer.WriteString("helpText", value.HelpText); } + writer.WriteEndObject(); } } diff --git a/src/Altinn.App.Core/Models/AppOptions.cs b/src/Altinn.App.Core/Models/AppOptions.cs index de1ca4c3b..b07fb85f8 100644 --- a/src/Altinn.App.Core/Models/AppOptions.cs +++ b/src/Altinn.App.Core/Models/AppOptions.cs @@ -1,25 +1,24 @@ -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Represents a collection of options for a dropdown selector or similar use. +/// +public class AppOptions { /// - /// Represents a collection of options for a dropdown selector or similar use. + /// Gets or sets the list of options. Null indicates that no options are found /// - public class AppOptions - { - /// - /// Gets or sets the list of options. Null indicates that no options are found - /// - public List? Options { get; set; } + public List? Options { get; set; } - /// - /// Gets or sets the parameters used to generate the options. - /// The dictionary key is the name of the parameter and the value is the value of the parameter. - /// This can be used to document the parameters used to generate the options. - /// - public Dictionary Parameters { get; set; } = new Dictionary(); + /// + /// Gets or sets the parameters used to generate the options. + /// The dictionary key is the name of the parameter and the value is the value of the parameter. + /// This can be used to document the parameters used to generate the options. + /// + public Dictionary Parameters { get; set; } = new Dictionary(); - /// - /// Gets or sets a value indicating whether the options can be cached. - /// - public bool IsCacheable { get; set; } - } + /// + /// Gets or sets a value indicating whether the options can be cached. + /// + public bool IsCacheable { get; set; } } diff --git a/src/Altinn.App.Core/Models/ApplicationLanguage.cs b/src/Altinn.App.Core/Models/ApplicationLanguage.cs index 6c35b14c2..a262b7a44 100644 --- a/src/Altinn.App.Core/Models/ApplicationLanguage.cs +++ b/src/Altinn.App.Core/Models/ApplicationLanguage.cs @@ -1,19 +1,18 @@ using System.Text.Json.Serialization; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Represents the supported language in the text resource folder. +/// +public class ApplicationLanguage { /// - /// Represents the supported language in the text resource folder. + /// Gets or sets the language code. Should be a two letter ISO name + /// Example: "nb" /// - public class ApplicationLanguage - { - /// - /// Gets or sets the language code. Should be a two letter ISO name - /// Example: "nb" - /// - [JsonPropertyName("language")] + [JsonPropertyName("language")] #nullable disable - public string Language { get; set; } + public string Language { get; set; } #nullable restore - } } diff --git a/src/Altinn.App.Core/Models/ApplicationMetadata.cs b/src/Altinn.App.Core/Models/ApplicationMetadata.cs index 2dae6f346..8e748c2a7 100644 --- a/src/Altinn.App.Core/Models/ApplicationMetadata.cs +++ b/src/Altinn.App.Core/Models/ApplicationMetadata.cs @@ -1,75 +1,73 @@ -using System.Reflection; using Altinn.Platform.Storage.Interface.Models; using Newtonsoft.Json; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Extension of Application model from Storage. Adds app specific attributes to the model +/// +public class ApplicationMetadata : Application { /// - /// Extension of Application model from Storage. Adds app specific attributes to the model + /// Create new instance of ApplicationMetadata /// - public class ApplicationMetadata : Application + /// + public ApplicationMetadata(string id) { - /// - /// Create new instance of ApplicationMetadata - /// - /// - public ApplicationMetadata(string id) - { - base.Id = id; - AppIdentifier = new AppIdentifier(id); - } + base.Id = id; + AppIdentifier = new AppIdentifier(id); + } - /// - /// Override Id from base to ensure AppIdentifier is set - /// - public new string Id + /// + /// Override Id from base to ensure AppIdentifier is set + /// + public new string Id + { + get { return base.Id; } + set { - get { return base.Id; } - set - { - AppIdentifier = new AppIdentifier(value); - base.Id = value; - } + AppIdentifier = new AppIdentifier(value); + base.Id = value; } + } - /// - /// List of features and status (enabled/disabled) - /// - [JsonProperty(PropertyName = "features")] - public Dictionary? Features { get; set; } + /// + /// List of features and status (enabled/disabled) + /// + [JsonProperty(PropertyName = "features")] + public Dictionary? Features { get; set; } - /// - /// Configure options for handling what happens when entering the application - /// - [JsonProperty(PropertyName = "onEntry")] - public new OnEntry? OnEntry { get; set; } + /// + /// Configure options for handling what happens when entering the application + /// + [JsonProperty(PropertyName = "onEntry")] + public new OnEntry? OnEntry { get; set; } - /// - /// Get AppIdentifier based on ApplicationMetadata.Id - /// Updated by setting ApplicationMetadata.Id - /// - [System.Text.Json.Serialization.JsonIgnore] - [Newtonsoft.Json.JsonIgnore] - public AppIdentifier AppIdentifier { get; private set; } + /// + /// Get AppIdentifier based on ApplicationMetadata.Id + /// Updated by setting ApplicationMetadata.Id + /// + [System.Text.Json.Serialization.JsonIgnore] + [Newtonsoft.Json.JsonIgnore] + public AppIdentifier AppIdentifier { get; private set; } - /// - /// Configure options for setting organisation logo - /// - [JsonProperty(PropertyName = "logo")] - public Logo? Logo { get; set; } + /// + /// Configure options for setting organisation logo + /// + [JsonProperty(PropertyName = "logo")] + public Logo? Logo { get; set; } - /// - /// Frontend sometimes need to have knowledge of the nuget package version for backwards compatibility - /// - [JsonProperty(PropertyName = "altinnNugetVersion")] - public string AltinnNugetVersion { get; set; } = - typeof(ApplicationMetadata).Assembly.GetName().Version?.ToString() - ?? throw new Exception("Assembly version is null"); + /// + /// Frontend sometimes need to have knowledge of the nuget package version for backwards compatibility + /// + [JsonProperty(PropertyName = "altinnNugetVersion")] + public string AltinnNugetVersion { get; set; } = + typeof(ApplicationMetadata).Assembly.GetName().Version?.ToString() + ?? throw new Exception("Assembly version is null"); - /// - /// Holds properties that are not mapped to other properties - /// - [System.Text.Json.Serialization.JsonExtensionData] - public Dictionary? UnmappedProperties { get; set; } - } + /// + /// Holds properties that are not mapped to other properties + /// + [System.Text.Json.Serialization.JsonExtensionData] + public Dictionary? UnmappedProperties { get; set; } } diff --git a/src/Altinn.App.Core/Models/Attachment.cs b/src/Altinn.App.Core/Models/Attachment.cs index 4e27315c3..943f9886f 100644 --- a/src/Altinn.App.Core/Models/Attachment.cs +++ b/src/Altinn.App.Core/Models/Attachment.cs @@ -1,26 +1,25 @@ -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Attachment metadata +/// +public class Attachment { /// - /// Attachment metadata + /// The file name /// - public class Attachment - { - /// - /// The file name - /// #nullable disable - public string Name { get; set; } + public string Name { get; set; } - /// - /// The id - /// - public string Id { get; set; } + /// + /// The id + /// + public string Id { get; set; } #nullable restore - /// - /// The file size in bytes - /// - public long Size { get; set; } - } + /// + /// The file size in bytes + /// + public long Size { get; set; } } diff --git a/src/Altinn.App.Core/Models/AttachmentList.cs b/src/Altinn.App.Core/Models/AttachmentList.cs index c2ae6a888..081934905 100644 --- a/src/Altinn.App.Core/Models/AttachmentList.cs +++ b/src/Altinn.App.Core/Models/AttachmentList.cs @@ -1,20 +1,19 @@ -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Attachment metadata list +/// +public class AttachmentList { /// - /// Attachment metadata list + /// The attachment type /// - public class AttachmentList - { - /// - /// The attachment type - /// #nullable disable - public string Type { get; set; } + public string Type { get; set; } - /// - /// The attachments - /// - public List Attachments { get; set; } + /// + /// The attachments + /// + public List Attachments { get; set; } #nullable restore - } } diff --git a/src/Altinn.App.Core/Models/CalculationResult.cs b/src/Altinn.App.Core/Models/CalculationResult.cs index 7b674780d..7ac1f684d 100644 --- a/src/Altinn.App.Core/Models/CalculationResult.cs +++ b/src/Altinn.App.Core/Models/CalculationResult.cs @@ -1,59 +1,58 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// The result of a calculation +/// +public class CalculationResult : DataElement { /// - /// The result of a calculation + /// Creates a instance of CalculationResult /// - public class CalculationResult : DataElement - { - /// - /// Creates a instance of CalculationResult - /// - public CalculationResult() { } + public CalculationResult() { } - /// - /// Creates a instance of CalculationResult - /// - /// The DataElement base object - public CalculationResult(DataElement dataElement) - { - MapDataElementToCalculationResult(dataElement); - } + /// + /// Creates a instance of CalculationResult + /// + /// The DataElement base object + public CalculationResult(DataElement dataElement) + { + MapDataElementToCalculationResult(dataElement); + } - /// - /// Creates an instance of CalculationResult - /// - /// The DataElement base object - /// The changed fields - public CalculationResult(DataElement dataElement, Dictionary changedFields) - { - MapDataElementToCalculationResult(dataElement); - ChangedFields = changedFields; - } + /// + /// Creates an instance of CalculationResult + /// + /// The DataElement base object + /// The changed fields + public CalculationResult(DataElement dataElement, Dictionary changedFields) + { + MapDataElementToCalculationResult(dataElement); + ChangedFields = changedFields; + } - /// - /// The key-value pair of fields changed by a calculation - /// - public Dictionary? ChangedFields { get; set; } + /// + /// The key-value pair of fields changed by a calculation + /// + public Dictionary? ChangedFields { get; set; } - private void MapDataElementToCalculationResult(DataElement dataElement) - { - BlobStoragePath = dataElement.BlobStoragePath; - ContentType = dataElement.ContentType; - Created = dataElement.Created; - CreatedBy = dataElement.CreatedBy; - DataType = dataElement.DataType; - Filename = dataElement.Filename; - Id = dataElement.Id; - InstanceGuid = dataElement.InstanceGuid; - IsRead = dataElement.IsRead; - LastChanged = dataElement.LastChanged; - LastChangedBy = dataElement.LastChangedBy; - Locked = dataElement.Locked; - Refs = dataElement.Refs; - SelfLinks = dataElement.SelfLinks; - Size = dataElement.Size; - } + private void MapDataElementToCalculationResult(DataElement dataElement) + { + BlobStoragePath = dataElement.BlobStoragePath; + ContentType = dataElement.ContentType; + Created = dataElement.Created; + CreatedBy = dataElement.CreatedBy; + DataType = dataElement.DataType; + Filename = dataElement.Filename; + Id = dataElement.Id; + InstanceGuid = dataElement.InstanceGuid; + IsRead = dataElement.IsRead; + LastChanged = dataElement.LastChanged; + LastChangedBy = dataElement.LastChangedBy; + Locked = dataElement.Locked; + Refs = dataElement.Refs; + SelfLinks = dataElement.SelfLinks; + Size = dataElement.Size; } } diff --git a/src/Altinn.App.Core/Models/CloudEvent.cs b/src/Altinn.App.Core/Models/CloudEvent.cs index a1ba92593..100b967f1 100644 --- a/src/Altinn.App.Core/Models/CloudEvent.cs +++ b/src/Altinn.App.Core/Models/CloudEvent.cs @@ -1,79 +1,78 @@ using System.Net.Mime; using System.Text.Json.Serialization; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Represents a cloud event. Based on CloudEvent: https://github.com/cloudevents/spec/blob/v1.0/spec.md. +/// +public class CloudEvent { /// - /// Represents a cloud event. Based on CloudEvent: https://github.com/cloudevents/spec/blob/v1.0/spec.md. + /// Gets or sets the id of the event. /// - public class CloudEvent - { - /// - /// Gets or sets the id of the event. - /// - [JsonPropertyName("id")] + [JsonPropertyName("id")] #nullable disable - public string Id { get; set; } + public string Id { get; set; } - /// - /// Gets or sets the source of the event. - /// - [JsonPropertyName("source")] - public Uri Source { get; set; } + /// + /// Gets or sets the source of the event. + /// + [JsonPropertyName("source")] + public Uri Source { get; set; } - /// - /// Gets or sets the specification version of the event. - /// - [JsonPropertyName("specversion")] - public string SpecVersion { get; set; } + /// + /// Gets or sets the specification version of the event. + /// + [JsonPropertyName("specversion")] + public string SpecVersion { get; set; } - /// - /// Gets or sets the type of the event. - /// - [JsonPropertyName("type")] - public string Type { get; set; } + /// + /// Gets or sets the type of the event. + /// + [JsonPropertyName("type")] + public string Type { get; set; } - /// - /// Gets or sets the subject of the event. - /// - [JsonPropertyName("subject")] - public string Subject { get; set; } + /// + /// Gets or sets the subject of the event. + /// + [JsonPropertyName("subject")] + public string Subject { get; set; } #nullable restore - /// - /// Gets or sets the time of the event. - /// - [JsonPropertyName("time")] - public DateTime Time { get; set; } + /// + /// Gets or sets the time of the event. + /// + [JsonPropertyName("time")] + public DateTime Time { get; set; } - /// - /// Gets or sets the alternative subject of the event. - /// - [JsonPropertyName("alternativesubject")] + /// + /// Gets or sets the alternative subject of the event. + /// + [JsonPropertyName("alternativesubject")] #nullable disable - public string AlternativeSubject { get; set; } + public string AlternativeSubject { get; set; } - /// - /// Gets or sets the cloudEvent data content. The event payload. - /// The payload depends on the type and the dataschema. - /// - [JsonPropertyName("data")] - public object Data { get; set; } + /// + /// Gets or sets the cloudEvent data content. The event payload. + /// The payload depends on the type and the dataschema. + /// + [JsonPropertyName("data")] + public object Data { get; set; } - /// - /// Gets or sets the cloudEvent dataschema attribute. - /// A link to the schema that the data attribute adheres to. - /// - [JsonPropertyName("dataschema")] - public Uri DataSchema { get; set; } + /// + /// Gets or sets the cloudEvent dataschema attribute. + /// A link to the schema that the data attribute adheres to. + /// + [JsonPropertyName("dataschema")] + public Uri DataSchema { get; set; } - /// - /// Gets or sets the cloudEvent datacontenttype attribute. - /// Content type of the data attribute value. - /// - [JsonPropertyName("contenttype")] - public ContentType DataContentType { get; set; } + /// + /// Gets or sets the cloudEvent datacontenttype attribute. + /// Content type of the data attribute value. + /// + [JsonPropertyName("contenttype")] + public ContentType DataContentType { get; set; } #nullable restore - } } diff --git a/src/Altinn.App.Core/Models/Components.cs b/src/Altinn.App.Core/Models/Components.cs index 7f24abc97..cc2ac63f9 100644 --- a/src/Altinn.App.Core/Models/Components.cs +++ b/src/Altinn.App.Core/Models/Components.cs @@ -1,13 +1,12 @@ -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// UI Components +/// +public class Components { /// - /// UI Components + /// Exclude from pdf /// - public class Components - { - /// - /// Exclude from pdf - /// - public List? ExcludeFromPdf { get; set; } - } + public List? ExcludeFromPdf { get; set; } } diff --git a/src/Altinn.App.Core/Models/DataList.cs b/src/Altinn.App.Core/Models/DataList.cs index ddf895ebb..7bf0060ad 100644 --- a/src/Altinn.App.Core/Models/DataList.cs +++ b/src/Altinn.App.Core/Models/DataList.cs @@ -1,24 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Altinn.App.Core.Models; -namespace Altinn.App.Core.Models +/// +/// Represents values to be used in a DataList. +/// +public class DataList { /// - /// Represents values to be used in a DataList. + /// Gets or sets the list of objects. /// - public class DataList - { - /// - /// Gets or sets the list of objects. - /// - public List ListItems { get; set; } = new List(); + public List ListItems { get; set; } = new List(); - /// - /// Gets or sets the metadata of the DataList. - /// - public DataListMetadata _metaData { get; set; } = new DataListMetadata(); - } + /// + /// Gets or sets the metadata of the DataList. + /// +#pragma warning disable IDE1006 // Naming Styles - public members should be PascalCase + public DataListMetadata _metaData { get; set; } = new DataListMetadata(); +#pragma warning restore IDE1006 // Naming Styles } diff --git a/src/Altinn.App.Core/Models/DataListMetadata.cs b/src/Altinn.App.Core/Models/DataListMetadata.cs index 8be37ed11..a94a8eb5d 100644 --- a/src/Altinn.App.Core/Models/DataListMetadata.cs +++ b/src/Altinn.App.Core/Models/DataListMetadata.cs @@ -1,39 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Altinn.App.Core.Models; -namespace Altinn.App.Core.Models +/// +/// Represents metadata values for a DataList. +/// +public class DataListMetadata { /// - /// Represents metadata values for a DataList. + /// Gets or sets the value of the current page to support pagination. /// - public class DataListMetadata - { - /// - /// Gets or sets the value of the current page to support pagination. - /// - public int Page { get; set; } + public int Page { get; set; } - /// - /// Gets or sets the total number of pages to support pagination. - /// - public int PageCount { get; set; } + /// + /// Gets or sets the total number of pages to support pagination. + /// + public int PageCount { get; set; } - /// - /// Gets or sets the number of objects per page to support pagination. - /// - public int PageSize { get; set; } + /// + /// Gets or sets the number of objects per page to support pagination. + /// + public int PageSize { get; set; } - /// - /// Gets or sets the totalt number of items. - /// - public int TotaltItemsCount { get; set; } + /// + /// Gets or sets the totalt number of items. + /// + public int TotaltItemsCount { get; set; } - /// - /// Gets or sets pagination links. - /// - public List Links { get; set; } = new List(); - } + /// + /// Gets or sets pagination links. + /// + public List Links { get; set; } = new List(); } diff --git a/src/Altinn.App.Core/Models/InstanceIdentifier.cs b/src/Altinn.App.Core/Models/InstanceIdentifier.cs index 28d5db126..6363edd29 100644 --- a/src/Altinn.App.Core/Models/InstanceIdentifier.cs +++ b/src/Altinn.App.Core/Models/InstanceIdentifier.cs @@ -1,152 +1,151 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Class representing the id of an instance. +/// +public class InstanceIdentifier { /// - /// Class representing the id of an instance. + /// Initializes a new instance of the class. /// - public class InstanceIdentifier + /// /// The id of the party owning this instance. + /// A identifying the instance. + public InstanceIdentifier(int instanceOwnerPartyId, Guid instanceGuid) { - /// - /// Initializes a new instance of the class. - /// - /// /// The id of the party owning this instance. - /// A identifying the instance. - public InstanceIdentifier(int instanceOwnerPartyId, Guid instanceGuid) - { - InstanceOwnerPartyId = instanceOwnerPartyId; - InstanceGuid = instanceGuid; - IsNoInstance = false; - } + InstanceOwnerPartyId = instanceOwnerPartyId; + InstanceGuid = instanceGuid; + IsNoInstance = false; + } - /// - /// Initializes a new instance of the class. - /// - /// InstanceId combined of the form instanceOwnerId/instanceGuid - public InstanceIdentifier(string instanceId) - { - (InstanceOwnerPartyId, InstanceGuid) = DeconstructInstanceId(instanceId); - IsNoInstance = false; - } + /// + /// Initializes a new instance of the class. + /// + /// InstanceId combined of the form instanceOwnerId/instanceGuid + public InstanceIdentifier(string instanceId) + { + (InstanceOwnerPartyId, InstanceGuid) = DeconstructInstanceId(instanceId); + IsNoInstance = false; + } + + /// + /// Initializes a new instance of the class. + /// + /// Is the instance you want to get an idenifier from + public InstanceIdentifier(Instance instance) + : this(instance.Id) { } + + /// + /// Initializes a new instance of the class. For instances without OwnerPartyId and InstanceId, ex: Stateless applications. + /// + private InstanceIdentifier() + { + InstanceOwnerPartyId = 0; + InstanceGuid = Guid.Empty; + IsNoInstance = true; + } + + /// + /// Creates a new instance of the class based on a url string. + /// + /// Parses the instance id from the given uri + public static InstanceIdentifier CreateFromUrl(string url) + { + var (instanceOwnerPartyId, instanceGuid) = DeconstructInstanceIdFromUrl(url); + + return new InstanceIdentifier(instanceOwnerPartyId, instanceGuid); + } + + /// + /// Get InstanceIdentifier for no instance. This is used for stateless applications. + /// INoInstance is set to true. + /// + public static readonly InstanceIdentifier NoInstance = new InstanceIdentifier(); + + /// + /// Party owning this instance. + /// + public int InstanceOwnerPartyId { get; } - /// - /// Initializes a new instance of the class. - /// - /// Is the instance you want to get an idenifier from - public InstanceIdentifier(Instance instance) - : this(instance.Id) { } - - /// - /// Initializes a new instance of the class. For instances without OwnerPartyId and InstanceId, ex: Stateless applications. - /// - private InstanceIdentifier() + /// + /// Unique id identifying this instance. + /// + public Guid InstanceGuid { get; } + + /// + /// Internal flag set to true if object is of type NoInstance. + /// + public bool IsNoInstance { get; } + + /// + /// Gets the instance id to be used when looking up this instance in storage api. + /// The url needs to conform to .../instances/{instanceOwnerId}/{instanceGuid}/... pattern. + /// + /// Instance id combining instance owner and instance guid. + public string GetInstanceId() + { + if (IsNoInstance) { - InstanceOwnerPartyId = 0; - InstanceGuid = Guid.Empty; - IsNoInstance = true; + throw new ArgumentNullException(nameof(InstanceGuid), "No instance id available for instance"); } - /// - /// Creates a new instance of the class based on a url string. - /// - /// Parses the instance id from the given uri - public static InstanceIdentifier CreateFromUrl(string url) - { - var (instanceOwnerPartyId, instanceGuid) = DeconstructInstanceIdFromUrl(url); + return $"{InstanceOwnerPartyId}/{InstanceGuid}"; + } - return new InstanceIdentifier(instanceOwnerPartyId, instanceGuid); - } + /// + /// A string on the format {instanceOwnerId}/{instanceGuid} without leading or trailing slashes. + /// + public override string ToString() + { + return GetInstanceId(); + } - /// - /// Get InstanceIdentifier for no instance. This is used for stateless applications. - /// INoInstance is set to true. - /// - public static readonly InstanceIdentifier NoInstance = new InstanceIdentifier(); - - /// - /// Party owning this instance. - /// - public int InstanceOwnerPartyId { get; } - - /// - /// Unique id identifying this instance. - /// - public Guid InstanceGuid { get; } - - /// - /// Internal flag set to true if object is of type NoInstance. - /// - public bool IsNoInstance { get; } - - /// - /// Gets the instance id to be used when looking up this instance in storage api. - /// The url needs to conform to .../instances/{instanceOwnerId}/{instanceGuid}/... pattern. - /// - /// Instance id combining instance owner and instance guid. - public string GetInstanceId() - { - if (IsNoInstance) - { - throw new ArgumentNullException(nameof(InstanceGuid), "No instance id available for instance"); - } + /// + /// Deconstructs an instance id into it's two logical parts - instanceOwnerPartyId and instanceGuid. + /// Party represents either the person or the organization that owns the instance. + /// + /// Instance identifier on the form {instanceOwnerPartyId}/{InstanceGuid} + /// A 2-tuple with the partyId (int) and the instanceGuid (Guid). + private static (int InstanceOwnerPartyId, Guid InstanceGuid) DeconstructInstanceId(string instanceId) + { + var deconstructed = instanceId.Split("/"); + int instanceOwnerPartyId = int.Parse(deconstructed[0]); + Guid instanceGuid = Guid.Parse(deconstructed[1]); - return $"{InstanceOwnerPartyId}/{InstanceGuid}"; - } + return (instanceOwnerPartyId, instanceGuid); + } - /// - /// A string on the format {instanceOwnerId}/{instanceGuid} without leading or trailing slashes. - /// - public override string ToString() + /// + /// Deconstructs an instance based url string into instanceOwnerId and InstanceGuid. + /// The url needs to conform to .../instances/{instanceOwerId}/{instanceOwnerGuid}/... or + /// .../instance/{instanceOwerId}/{instanceOwnerGuid}/... pattern. + /// + /// The url to parse + /// A 2-tuple with the partyId (int) and the instanceGuid (Guid). + private static (int InstanceOwnerId, Guid InstanceOwnerGuid) DeconstructInstanceIdFromUrl(string url) + { + string searchForPlural = "/instances/"; + string searchForSingular = "/instance/"; + string instanceSubpath = string.Empty; + + if (url.Contains(searchForPlural, StringComparison.InvariantCultureIgnoreCase)) { - return GetInstanceId(); + instanceSubpath = url.Substring(url.IndexOf(searchForPlural) + searchForPlural.Length); } - - /// - /// Deconstructs an instance id into it's two logical parts - instanceOwnerPartyId and instanceGuid. - /// Party represents either the person or the organization that owns the instance. - /// - /// Instance identifier on the form {instanceOwnerPartyId}/{InstanceGuid} - /// A 2-tuple with the partyId (int) and the instanceGuid (Guid). - private static (int InstanceOwnerPartyId, Guid InstanceGuid) DeconstructInstanceId(string instanceId) + else if (url.Contains(searchForSingular, StringComparison.InvariantCultureIgnoreCase)) { - var deconstructed = instanceId.Split("/"); - int instanceOwnerPartyId = int.Parse(deconstructed[0]); - Guid instanceGuid = Guid.Parse(deconstructed[1]); - - return (instanceOwnerPartyId, instanceGuid); + instanceSubpath = url.Substring(url.IndexOf(searchForSingular) + searchForSingular.Length); } - /// - /// Deconstructs an instance based url string into instanceOwnerId and InstanceGuid. - /// The url needs to conform to .../instances/{instanceOwerId}/{instanceOwnerGuid}/... or - /// .../instance/{instanceOwerId}/{instanceOwnerGuid}/... pattern. - /// - /// The url to parse - /// A 2-tuple with the partyId (int) and the instanceGuid (Guid). - private static (int InstanceOwnerId, Guid InstanceOwnerGuid) DeconstructInstanceIdFromUrl(string url) + if (string.IsNullOrEmpty(instanceSubpath)) { - string searchForPlural = "/instances/"; - string searchForSingular = "/instance/"; - string instanceSubpath = string.Empty; - - if (url.Contains(searchForPlural, StringComparison.InvariantCultureIgnoreCase)) - { - instanceSubpath = url.Substring(url.IndexOf(searchForPlural) + searchForPlural.Length); - } - else if (url.Contains(searchForSingular, StringComparison.InvariantCultureIgnoreCase)) - { - instanceSubpath = url.Substring(url.IndexOf(searchForSingular) + searchForSingular.Length); - } - - if (string.IsNullOrEmpty(instanceSubpath)) - { - throw new ArgumentException( - $"Parameter with value {url} is not recognised as a valid instance url.", - nameof(url) - ); - } - - return DeconstructInstanceId(instanceSubpath); + throw new ArgumentException( + $"Parameter with value {url} is not recognised as a valid instance url.", + nameof(url) + ); } + + return DeconstructInstanceId(instanceSubpath); } } diff --git a/src/Altinn.App.Core/Models/InstanceSelection.cs b/src/Altinn.App.Core/Models/InstanceSelection.cs index 44a294c6f..b30ce64ec 100644 --- a/src/Altinn.App.Core/Models/InstanceSelection.cs +++ b/src/Altinn.App.Core/Models/InstanceSelection.cs @@ -1,40 +1,39 @@ using Newtonsoft.Json; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Contains options for displaying the instance selection component +/// +public class InstanceSelection { + private int? _defaultSelectedOption; + /// - /// Contains options for displaying the instance selection component + /// A list of selectable options for amount of rows per page to show for pagination /// - public class InstanceSelection - { - private int? _defaultSelectedOption; - - /// - /// A list of selectable options for amount of rows per page to show for pagination - /// - [JsonProperty(PropertyName = "rowsPerPageOptions")] - public List? RowsPerPageOptions { get; set; } + [JsonProperty(PropertyName = "rowsPerPageOptions")] + public List? RowsPerPageOptions { get; set; } - /// - /// The default amount of rows per page to show for pagination - /// - [JsonProperty(PropertyName = "defaultRowsPerPage")] - public int? DefaultRowsPerPage { get; set; } - - /// - /// The default selected option for rows per page to show for pagination - /// - [JsonProperty(PropertyName = "defaultSelectedOption")] - public int? DefaultSelectedOption - { - get { return _defaultSelectedOption ?? DefaultRowsPerPage; } - set { _defaultSelectedOption = value; } - } + /// + /// The default amount of rows per page to show for pagination + /// + [JsonProperty(PropertyName = "defaultRowsPerPage")] + public int? DefaultRowsPerPage { get; set; } - /// - /// The direction of sorting the list of instances, asc or desc - /// - [JsonProperty(PropertyName = "sortDirection")] - public string? SortDirection { get; set; } + /// + /// The default selected option for rows per page to show for pagination + /// + [JsonProperty(PropertyName = "defaultSelectedOption")] + public int? DefaultSelectedOption + { + get { return _defaultSelectedOption ?? DefaultRowsPerPage; } + set { _defaultSelectedOption = value; } } + + /// + /// The direction of sorting the list of instances, asc or desc + /// + [JsonProperty(PropertyName = "sortDirection")] + public string? SortDirection { get; set; } } diff --git a/src/Altinn.App.Core/Models/Layout/Components/BaseComponent.cs b/src/Altinn.App.Core/Models/Layout/Components/BaseComponent.cs index 26667efc3..c4982edc8 100644 --- a/src/Altinn.App.Core/Models/Layout/Components/BaseComponent.cs +++ b/src/Altinn.App.Core/Models/Layout/Components/BaseComponent.cs @@ -1,5 +1,4 @@ using System.Collections.Immutable; -using System.Text.Json; using Altinn.App.Core.Models.Expressions; namespace Altinn.App.Core.Models.Layout.Components; diff --git a/src/Altinn.App.Core/Models/Layout/Components/GroupComponent.cs b/src/Altinn.App.Core/Models/Layout/Components/GroupComponent.cs index 2b13d4676..6a9724e31 100644 --- a/src/Altinn.App.Core/Models/Layout/Components/GroupComponent.cs +++ b/src/Altinn.App.Core/Models/Layout/Components/GroupComponent.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using Altinn.App.Core.Models.Expressions; namespace Altinn.App.Core.Models.Layout.Components; diff --git a/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs b/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs index 002c731ff..793a45f91 100644 --- a/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs +++ b/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs @@ -1,6 +1,3 @@ -using System.Collections.Immutable; -using System.Text.Json; -using System.Text.Json.Serialization; using Altinn.App.Core.Models.Expressions; namespace Altinn.App.Core.Models.Layout.Components; diff --git a/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs b/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs index a5a102283..934ec41b7 100644 --- a/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs +++ b/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs @@ -1,6 +1,3 @@ -using System.Collections.Immutable; -using System.Text.Json; -using System.Text.Json.Serialization; using Altinn.App.Core.Models.Expressions; namespace Altinn.App.Core.Models.Layout.Components; diff --git a/src/Altinn.App.Core/Models/Layout/LayoutModel.cs b/src/Altinn.App.Core/Models/Layout/LayoutModel.cs index 3ecb38466..57acefe95 100644 --- a/src/Altinn.App.Core/Models/Layout/LayoutModel.cs +++ b/src/Altinn.App.Core/Models/Layout/LayoutModel.cs @@ -1,6 +1,3 @@ -using System.Text.Json.Serialization; -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Models.Expressions; using Altinn.App.Core.Models.Layout.Components; namespace Altinn.App.Core.Models.Layout; diff --git a/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs b/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs index 26b790377..0ac0596b1 100644 --- a/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs +++ b/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs @@ -19,7 +19,7 @@ namespace Altinn.App.Core.Models.Layout; /// public class PageComponentConverter : JsonConverter { - private static readonly AsyncLocal PageName = new(); + private static readonly AsyncLocal _pageName = new(); /// /// Store pageName to be used for deserialization @@ -32,15 +32,15 @@ public class PageComponentConverter : JsonConverter /// public static void SetAsyncLocalPageName(string pageName) { - PageName.Value = pageName; + _pageName.Value = pageName; } /// public override PageComponent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Try to get pagename from metadata in this.AddPageName - var pageName = PageName.Value ?? "UnknownPageName"; - PageName.Value = null; + var pageName = _pageName.Value ?? "UnknownPageName"; + _pageName.Value = null; return ReadNotNull(ref reader, pageName, options); } @@ -151,7 +151,7 @@ private PageComponent ReadData(ref Utf8JsonReader reader, string pageName, JsonS throw new JsonException("Missing property \"layout\" on layout page"); } - var layout = processLayout(componentListFlat, componentLookup, childToGroupMapping); + var layout = ProcessLayout(componentListFlat, componentLookup, childToGroupMapping); return new PageComponent(pageName, layout, componentLookup, hidden, required, readOnly, additionalProperties); } @@ -186,7 +186,7 @@ JsonSerializerOptions options return (componentListFlat, componentLookup, childToGroupMapping); } - private static List processLayout( + private static List ProcessLayout( List componentListFlat, Dictionary componentLookup, Dictionary childToGroupMapping @@ -218,7 +218,7 @@ private static void AddToComponentLookup(BaseComponent component, Dictionary +/// A specific layoutset +/// +public class LayoutSet { /// - /// A specific layoutset + /// LayoutsetId for layout. This is the foldername /// - public class LayoutSet - { - /// - /// LayoutsetId for layout. This is the foldername - /// #nullable disable - public string Id { get; set; } + public string Id { get; set; } - /// - /// DataType for layout - /// - public string DataType { get; set; } + /// + /// DataType for layout + /// + public string DataType { get; set; } - /// - /// List of tasks where layuout should be used - /// - public List Tasks { get; set; } + /// + /// List of tasks where layuout should be used + /// + public List Tasks { get; set; } #nullable restore - } } diff --git a/src/Altinn.App.Core/Models/LayoutSets.cs b/src/Altinn.App.Core/Models/LayoutSets.cs index ef156499c..b0fcbe00a 100644 --- a/src/Altinn.App.Core/Models/LayoutSets.cs +++ b/src/Altinn.App.Core/Models/LayoutSets.cs @@ -1,13 +1,12 @@ -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Layoutsets for a specific app containg one or more layoytset +/// +public class LayoutSets { /// - /// Layoutsets for a specific app containg one or more layoytset + /// Sets /// - public class LayoutSets - { - /// - /// Sets - /// - public List? Sets { get; set; } - } + public List? Sets { get; set; } } diff --git a/src/Altinn.App.Core/Models/LayoutSettings.cs b/src/Altinn.App.Core/Models/LayoutSettings.cs index 5fa365d56..7f01756e8 100644 --- a/src/Altinn.App.Core/Models/LayoutSettings.cs +++ b/src/Altinn.App.Core/Models/LayoutSettings.cs @@ -1,18 +1,17 @@ -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Defines the layout settings +/// +public class LayoutSettings { /// - /// Defines the layout settings + /// Pages /// - public class LayoutSettings - { - /// - /// Pages - /// - public Pages? Pages { get; set; } + public Pages? Pages { get; set; } - /// - /// Components - /// - public Components? Components { get; set; } - } + /// + /// Components + /// + public Components? Components { get; set; } } diff --git a/src/Altinn.App.Core/Models/Logo.cs b/src/Altinn.App.Core/Models/Logo.cs index 66e03cd87..e700f7f4a 100644 --- a/src/Altinn.App.Core/Models/Logo.cs +++ b/src/Altinn.App.Core/Models/Logo.cs @@ -1,31 +1,28 @@ -using Altinn.Platform.Storage.Interface.Models; using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// The Logo configuration +/// +public class Logo { /// - /// The Logo configuration + /// A flag to specify that the form should display appOwner in header /// - public class Logo - { - /// - /// A flag to specify that the form should display appOwner in header - /// - [JsonProperty(PropertyName = "displayAppOwnerNameInHeader")] - public bool DisplayAppOwnerNameInHeader { get; set; } + [JsonProperty(PropertyName = "displayAppOwnerNameInHeader")] + public bool DisplayAppOwnerNameInHeader { get; set; } - /// - /// Specifies from where the logo url should be fetched - /// - [JsonProperty(PropertyName = "source")] - public string? Source { get; set; } + /// + /// Specifies from where the logo url should be fetched + /// + [JsonProperty(PropertyName = "source")] + public string? Source { get; set; } - /// - /// Specifies the size of the logo. Can have the values - /// 'small', 'medium', or 'large' - /// - [JsonProperty(PropertyName = "size")] - public string Size { get; set; } = "small"; - } + /// + /// Specifies the size of the logo. Can have the values + /// 'small', 'medium', or 'large' + /// + [JsonProperty(PropertyName = "size")] + public string Size { get; set; } = "small"; } diff --git a/src/Altinn.App.Core/Models/OnEntry.cs b/src/Altinn.App.Core/Models/OnEntry.cs index 847c067af..36ae33eb5 100644 --- a/src/Altinn.App.Core/Models/OnEntry.cs +++ b/src/Altinn.App.Core/Models/OnEntry.cs @@ -1,17 +1,16 @@ using Altinn.Platform.Storage.Interface.Models; using Newtonsoft.Json; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// The on entry configuration +/// +public class OnEntry : OnEntryConfig { /// - /// The on entry configuration + /// Options for displaying the instance selection component /// - public class OnEntry : OnEntryConfig - { - /// - /// Options for displaying the instance selection component - /// - [JsonProperty(PropertyName = "instanceSelection")] - public InstanceSelection? InstanceSelection { get; set; } - } + [JsonProperty(PropertyName = "instanceSelection")] + public InstanceSelection? InstanceSelection { get; set; } } diff --git a/src/Altinn.App.Core/Models/Pages.cs b/src/Altinn.App.Core/Models/Pages.cs index 59ed3933d..4bbeadd25 100644 --- a/src/Altinn.App.Core/Models/Pages.cs +++ b/src/Altinn.App.Core/Models/Pages.cs @@ -1,18 +1,17 @@ -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Pages +/// +public class Pages { /// - /// Pages + /// Order /// - public class Pages - { - /// - /// Order - /// - public List? Order { get; set; } + public List? Order { get; set; } - /// - /// Exclude from pdf - /// - public List? ExcludeFromPdf { get; set; } - } + /// + /// Exclude from pdf + /// + public List? ExcludeFromPdf { get; set; } } diff --git a/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs b/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs index 8744c3230..d30359b27 100644 --- a/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs @@ -1,58 +1,57 @@ using System.Diagnostics.CodeAnalysis; -namespace Altinn.App.Core.Models.Process +namespace Altinn.App.Core.Models.Process; + +/// +/// Class representing the result of a process change +/// +public class ProcessChangeResult { /// - /// Class representing the result of a process change - /// - public class ProcessChangeResult - { - /// - /// Gets or sets a value indicating whether the process change was successful - /// - [MemberNotNullWhen(true, nameof(ProcessStateChange))] - [MemberNotNullWhen(false, nameof(ErrorMessage), nameof(ErrorType))] - public bool Success { get; init; } - - /// - /// Gets or sets the error message if the process change was not successful - /// - public string? ErrorMessage { get; init; } - - /// - /// Gets or sets the error type if the process change was not successful - /// - public ProcessErrorType? ErrorType { get; init; } - - /// - /// Gets or sets the process state change if the process change was successful - /// - public ProcessStateChange? ProcessStateChange { get; init; } - } - - /// - /// Types of errors that can occur during a process change - /// - public enum ProcessErrorType - { - /// - /// The process change was not allowed due to the current state of the process - /// - Conflict, - - /// - /// The process change lead to an internal error - /// - Internal, - - /// - /// The user is not authorized to perform the process change - /// - Unauthorized, - - /// - /// The request was not valid - /// - BadRequest - } + /// Gets or sets a value indicating whether the process change was successful + /// + [MemberNotNullWhen(true, nameof(ProcessStateChange))] + [MemberNotNullWhen(false, nameof(ErrorMessage), nameof(ErrorType))] + public bool Success { get; init; } + + /// + /// Gets or sets the error message if the process change was not successful + /// + public string? ErrorMessage { get; init; } + + /// + /// Gets or sets the error type if the process change was not successful + /// + public ProcessErrorType? ErrorType { get; init; } + + /// + /// Gets or sets the process state change if the process change was successful + /// + public ProcessStateChange? ProcessStateChange { get; init; } +} + +/// +/// Types of errors that can occur during a process change +/// +public enum ProcessErrorType +{ + /// + /// The process change was not allowed due to the current state of the process + /// + Conflict, + + /// + /// The process change lead to an internal error + /// + Internal, + + /// + /// The user is not authorized to perform the process change + /// + Unauthorized, + + /// + /// The request was not valid + /// + BadRequest } diff --git a/src/Altinn.App.Core/Models/Process/ProcessStateChange.cs b/src/Altinn.App.Core/Models/Process/ProcessStateChange.cs index fcca5ac3c..87a8c39c6 100644 --- a/src/Altinn.App.Core/Models/Process/ProcessStateChange.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessStateChange.cs @@ -1,25 +1,24 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Models.Process +namespace Altinn.App.Core.Models.Process; + +/// +/// Represents a change in process state for an instance. +/// +public class ProcessStateChange { /// - /// Represents a change in process state for an instance. + /// Gets or sets the old process state /// - public class ProcessStateChange - { - /// - /// Gets or sets the old process state - /// - public ProcessState? OldProcessState { get; set; } + public ProcessState? OldProcessState { get; set; } - /// - /// Gets or sets the new process state - /// - public ProcessState? NewProcessState { get; set; } + /// + /// Gets or sets the new process state + /// + public ProcessState? NewProcessState { get; set; } - /// - /// Gets or sets a list of events to be registered. - /// - public List? Events { get; set; } - } + /// + /// Gets or sets a list of events to be registered. + /// + public List? Events { get; set; } } diff --git a/src/Altinn.App.Core/Models/QueryResponse.cs b/src/Altinn.App.Core/Models/QueryResponse.cs index 4e2bd0b39..44e3d4896 100644 --- a/src/Altinn.App.Core/Models/QueryResponse.cs +++ b/src/Altinn.App.Core/Models/QueryResponse.cs @@ -1,36 +1,35 @@ using Newtonsoft.Json; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Query response object +/// +public class QueryResponse { /// - /// Query response object + /// The number of items in this response. /// - public class QueryResponse - { - /// - /// The number of items in this response. - /// - [JsonProperty(PropertyName = "count")] - public long Count { get; set; } + [JsonProperty(PropertyName = "count")] + public long Count { get; set; } - /// - /// The current query. - /// - [JsonProperty(PropertyName = "self")] + /// + /// The current query. + /// + [JsonProperty(PropertyName = "self")] #nullable disable - public string Self { get; set; } + public string Self { get; set; } - /// - /// A link to the next page. - /// - [JsonProperty(PropertyName = "next")] - public string Next { get; set; } + /// + /// A link to the next page. + /// + [JsonProperty(PropertyName = "next")] + public string Next { get; set; } - /// - /// The metadata. - /// - [JsonProperty(PropertyName = "instances")] - public List Instances { get; set; } + /// + /// The metadata. + /// + [JsonProperty(PropertyName = "instances")] + public List Instances { get; set; } #nullable restore - } } diff --git a/src/Altinn.App.Core/Models/StylesConfig.cs b/src/Altinn.App.Core/Models/StylesConfig.cs index 1527b6085..a7abc20ff 100644 --- a/src/Altinn.App.Core/Models/StylesConfig.cs +++ b/src/Altinn.App.Core/Models/StylesConfig.cs @@ -1,18 +1,17 @@ -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Class for style config +/// +public class StylesConfig { /// - /// Class for style config + /// The internal styles /// - public class StylesConfig - { - /// - /// The internal styles - /// - public List InternalStyles { get; set; } = new(); + public List InternalStyles { get; set; } = new(); - /// - /// The external styles - /// - public List ExternalStyles { get; set; } = new(); - } + /// + /// The external styles + /// + public List ExternalStyles { get; set; } = new(); } diff --git a/src/Altinn.App.Core/Models/UserContext.cs b/src/Altinn.App.Core/Models/UserContext.cs index abcb72131..1a25bfbb5 100644 --- a/src/Altinn.App.Core/Models/UserContext.cs +++ b/src/Altinn.App.Core/Models/UserContext.cs @@ -1,54 +1,53 @@ using System.Security.Claims; using Altinn.Platform.Register.Models; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models; + +/// +/// Contains information about a user context +/// +public class UserContext { +#nullable disable /// - /// Contains information about a user context + /// Gets or sets the social security number /// - public class UserContext - { -#nullable disable - /// - /// Gets or sets the social security number - /// - public string SocialSecurityNumber { get; set; } - - /// - /// Gets or sets the username - /// - public string UserName { get; set; } - - /// - /// Gets or sets the reportee - /// - public Party Party { get; set; } - - /// - /// Gets or sets the party of the user - /// - public Party UserParty { get; set; } - - /// - /// Gets or sets the claims principal for the user - /// - public ClaimsPrincipal User { get; set; } + public string SocialSecurityNumber { get; set; } + + /// + /// Gets or sets the username + /// + public string UserName { get; set; } + + /// + /// Gets or sets the reportee + /// + public Party Party { get; set; } + + /// + /// Gets or sets the party of the user + /// + public Party UserParty { get; set; } + + /// + /// Gets or sets the claims principal for the user + /// + public ClaimsPrincipal User { get; set; } #nullable restore - /// - /// Gets or sets the ID of the user - /// - public int UserId { get; set; } - - /// - /// Gets or sets the party ID - /// - public int PartyId { get; set; } - - /// - /// Gets or sets the current - /// - public int AuthenticationLevel { get; set; } - } + /// + /// Gets or sets the ID of the user + /// + public int UserId { get; set; } + + /// + /// Gets or sets the party ID + /// + public int PartyId { get; set; } + + /// + /// Gets or sets the current + /// + public int AuthenticationLevel { get; set; } } diff --git a/src/Altinn.App.Core/Models/Validation/ExpressionValidation.cs b/src/Altinn.App.Core/Models/Validation/ExpressionValidation.cs index 6282d322e..f120fb064 100644 --- a/src/Altinn.App.Core/Models/Validation/ExpressionValidation.cs +++ b/src/Altinn.App.Core/Models/Validation/ExpressionValidation.cs @@ -1,39 +1,38 @@ using System.Text.Json.Serialization; using Altinn.App.Core.Models.Expressions; -namespace Altinn.App.Core.Models.Validation +namespace Altinn.App.Core.Models.Validation; + +/// +/// Resolved expression validation +/// +public class ExpressionValidation { - /// - /// Resolved expression validation - /// - public class ExpressionValidation - { - /// - public string? Message { get; set; } - - /// - public Expression? Condition { get; set; } - - /// - public ValidationIssueSeverity? Severity { get; set; } - } - - /// - /// Raw expression validation or definition from the validation configuration file - /// - public class RawExpressionValidation - { - /// - public string? Message { get; set; } - - /// - public Expression? Condition { get; set; } - - /// - [JsonConverter(typeof(FrontendSeverityConverter))] - public ValidationIssueSeverity? Severity { get; set; } - - /// - public string? Ref { get; set; } - } + /// + public string? Message { get; set; } + + /// + public Expression? Condition { get; set; } + + /// + public ValidationIssueSeverity? Severity { get; set; } +} + +/// +/// Raw expression validation or definition from the validation configuration file +/// +public class RawExpressionValidation +{ + /// + public string? Message { get; set; } + + /// + public Expression? Condition { get; set; } + + /// + [JsonConverter(typeof(FrontendSeverityConverter))] + public ValidationIssueSeverity? Severity { get; set; } + + /// + public string? Ref { get; set; } } diff --git a/src/Altinn.App.Core/Models/Validation/FrontendSeverityConverter.cs b/src/Altinn.App.Core/Models/Validation/FrontendSeverityConverter.cs index 1ce4dd7b5..b505a7610 100644 --- a/src/Altinn.App.Core/Models/Validation/FrontendSeverityConverter.cs +++ b/src/Altinn.App.Core/Models/Validation/FrontendSeverityConverter.cs @@ -1,46 +1,45 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Altinn.App.Core.Models.Validation +namespace Altinn.App.Core.Models.Validation; + +/// +public class FrontendSeverityConverter : JsonConverter { /// - public class FrontendSeverityConverter : JsonConverter + public override ValidationIssueSeverity Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) { - /// - public override ValidationIssueSeverity Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options - ) + if (reader.TokenType != JsonTokenType.String) { - if (reader.TokenType != JsonTokenType.String) - { - throw new JsonException(); - } - - return reader.GetString() switch - { - "error" => ValidationIssueSeverity.Error, - "warning" => ValidationIssueSeverity.Warning, - "info" => ValidationIssueSeverity.Informational, - "success" => ValidationIssueSeverity.Success, - _ => throw new JsonException(), - }; + throw new JsonException(); } - /// - public override void Write(Utf8JsonWriter writer, ValidationIssueSeverity value, JsonSerializerOptions options) + return reader.GetString() switch { - string output = value switch - { - ValidationIssueSeverity.Error => "error", - ValidationIssueSeverity.Warning => "warning", - ValidationIssueSeverity.Informational => "info", - ValidationIssueSeverity.Success => "success", - _ => throw new JsonException(), - }; + "error" => ValidationIssueSeverity.Error, + "warning" => ValidationIssueSeverity.Warning, + "info" => ValidationIssueSeverity.Informational, + "success" => ValidationIssueSeverity.Success, + _ => throw new JsonException(), + }; + } - JsonSerializer.Serialize(writer, output, options); - } + /// + public override void Write(Utf8JsonWriter writer, ValidationIssueSeverity value, JsonSerializerOptions options) + { + string output = value switch + { + ValidationIssueSeverity.Error => "error", + ValidationIssueSeverity.Warning => "warning", + ValidationIssueSeverity.Informational => "info", + ValidationIssueSeverity.Success => "success", + _ => throw new JsonException(), + }; + + JsonSerializer.Serialize(writer, output, options); } } diff --git a/src/Altinn.App.Core/Models/Validation/InstantiationValidationResult.cs b/src/Altinn.App.Core/Models/Validation/InstantiationValidationResult.cs index 60185075a..e940638d9 100644 --- a/src/Altinn.App.Core/Models/Validation/InstantiationValidationResult.cs +++ b/src/Altinn.App.Core/Models/Validation/InstantiationValidationResult.cs @@ -1,27 +1,26 @@ using Altinn.Platform.Register.Models; -namespace Altinn.App.Core.Models.Validation +namespace Altinn.App.Core.Models.Validation; + +/// +/// A status returned when validating instantiation +/// +public class InstantiationValidationResult { /// - /// A status returned when validating instantiation + /// Gets or sets if the validation was valid /// - public class InstantiationValidationResult - { - /// - /// Gets or sets if the validation was valid - /// - public bool Valid { get; set; } + public bool Valid { get; set; } #nullable disable - /// - /// Gets or sets a message - /// - public string Message { get; set; } + /// + /// Gets or sets a message + /// + public string Message { get; set; } - /// - /// Gets or sets a list of parties the user represents that can instantiate - /// - public List ValidParties { get; set; } + /// + /// Gets or sets a list of parties the user represents that can instantiate + /// + public List ValidParties { get; set; } #nullable restore - } } diff --git a/src/Altinn.App.Core/Models/Validation/ValidationException.cs b/src/Altinn.App.Core/Models/Validation/ValidationException.cs index 06a81cb41..19bbfa8d7 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationException.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationException.cs @@ -1,29 +1,28 @@ -namespace Altinn.App.Core.Models.Validation +namespace Altinn.App.Core.Models.Validation; + +/// +/// Represents errors that occur while handling a validation request. +/// +public class ValidationException : Exception { /// - /// Represents errors that occur while handling a validation request. + /// Initialises a new instance of the class. /// - public class ValidationException : Exception - { - /// - /// Initialises a new instance of the class. - /// - public ValidationException() { } + public ValidationException() { } - /// - /// Initialises a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public ValidationException(string message) - : base(message) { } + /// + /// Initialises a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public ValidationException(string message) + : base(message) { } - /// - /// Initialises a new instance of the class with a specified error - /// message and a reference to the inner exception that is the cause of this exception. - /// - /// The message that describes the error. - /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified - public ValidationException(string message, Exception inner) - : base(message, inner) { } - } + /// + /// Initialises a new instance of the class with a specified error + /// message and a reference to the inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified + public ValidationException(string message, Exception inner) + : base(message, inner) { } } diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs index b94a6a941..e89effe44 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs @@ -3,96 +3,95 @@ using Altinn.App.Core.Internal.Validation; using Newtonsoft.Json; -namespace Altinn.App.Core.Models.Validation +namespace Altinn.App.Core.Models.Validation; + +/// +/// Represents a detailed message from validation. +/// +public class ValidationIssue { /// - /// Represents a detailed message from validation. + /// The seriousness of the identified issue. /// - public class ValidationIssue - { - /// - /// The seriousness of the identified issue. - /// - /// - /// This property is serialized in json as a number - /// 1: Error (something needs to be fixed) - /// 2: Warning (does not prevent submission) - /// 3: Information (hint shown to the user) - /// 4: Fixed (obsolete, only used for v3 of frontend) - /// 5: Success (Inform the user that something was completed with success) - /// - [JsonProperty(PropertyName = "severity")] - [JsonPropertyName("severity")] - [System.Text.Json.Serialization.JsonConverter(typeof(JsonNumberEnumConverter))] - public required ValidationIssueSeverity Severity { get; set; } + /// + /// This property is serialized in json as a number + /// 1: Error (something needs to be fixed) + /// 2: Warning (does not prevent submission) + /// 3: Information (hint shown to the user) + /// 4: Fixed (obsolete, only used for v3 of frontend) + /// 5: Success (Inform the user that something was completed with success) + /// + [JsonProperty(PropertyName = "severity")] + [JsonPropertyName("severity")] + [System.Text.Json.Serialization.JsonConverter(typeof(JsonNumberEnumConverter))] + public required ValidationIssueSeverity Severity { get; set; } - /// - /// The unique id of the specific element with the identified issue. - /// - [System.Text.Json.Serialization.JsonIgnore] - [Newtonsoft.Json.JsonIgnore] - [Obsolete("Not in use", error: true)] - public string? InstanceId { get; set; } + /// + /// The unique id of the specific element with the identified issue. + /// + [System.Text.Json.Serialization.JsonIgnore] + [Newtonsoft.Json.JsonIgnore] + [Obsolete("Not in use", error: true)] + public string? InstanceId { get; set; } - /// - /// The unique id of the data element of a given instance with the identified issue. - /// - [JsonProperty(PropertyName = "dataElementId")] - [JsonPropertyName("dataElementId")] - public string? DataElementId { get; set; } + /// + /// The unique id of the data element of a given instance with the identified issue. + /// + [JsonProperty(PropertyName = "dataElementId")] + [JsonPropertyName("dataElementId")] + public string? DataElementId { get; set; } - /// - /// A reference to a property the issue is about. - /// - [JsonProperty(PropertyName = "field")] - [JsonPropertyName("field")] - public string? Field { get; set; } + /// + /// A reference to a property the issue is about. + /// + [JsonProperty(PropertyName = "field")] + [JsonPropertyName("field")] + public string? Field { get; set; } - /// - /// A system readable identification of the type of issue. - /// Eg: - /// - [JsonProperty(PropertyName = "code")] - [JsonPropertyName("code")] - public string? Code { get; set; } + /// + /// A system readable identification of the type of issue. + /// Eg: + /// + [JsonProperty(PropertyName = "code")] + [JsonPropertyName("code")] + public string? Code { get; set; } - /// - /// A human readable description of the issue. - /// - [JsonProperty(PropertyName = "description")] - [JsonPropertyName("description")] - public string? Description { get; set; } + /// + /// A human readable description of the issue. + /// + [JsonProperty(PropertyName = "description")] + [JsonPropertyName("description")] + public string? Description { get; set; } - /// - /// The short name of the class that crated the message (set automatically after return of list) - /// - /// - /// Intentionally not marked as "required", because it is set in - /// - [JsonProperty(PropertyName = "source")] - [JsonPropertyName("source")] + /// + /// The short name of the class that crated the message (set automatically after return of list) + /// + /// + /// Intentionally not marked as "required", because it is set in + /// + [JsonProperty(PropertyName = "source")] + [JsonPropertyName("source")] #nullable disable - public string Source { get; set; } + public string Source { get; set; } #nullable restore - /// - /// The custom text key to use for the localized text in the frontend. - /// - [JsonProperty(PropertyName = "customTextKey")] - [JsonPropertyName("customTextKey")] - public string? CustomTextKey { get; set; } + /// + /// The custom text key to use for the localized text in the frontend. + /// + [JsonProperty(PropertyName = "customTextKey")] + [JsonPropertyName("customTextKey")] + public string? CustomTextKey { get; set; } - /// - /// might include some parameters (typically the field value, or some derived value) - /// that should be included in error message. - /// - /// - /// The localized text for the key might be "Date must be between {0} and {1}" - /// and the param will provide the dynamical range of allowable dates (eg teh reporting period) - /// - [JsonProperty(PropertyName = "customTextParams")] - [JsonPropertyName("customTextParams")] - public List? CustomTextParams { get; set; } - } + /// + /// might include some parameters (typically the field value, or some derived value) + /// that should be included in error message. + /// + /// + /// The localized text for the key might be "Date must be between {0} and {1}" + /// and the param will provide the dynamical range of allowable dates (eg teh reporting period) + /// + [JsonProperty(PropertyName = "customTextParams")] + [JsonPropertyName("customTextParams")] + public List? CustomTextParams { get; set; } } diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssueCodes.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssueCodes.cs index 7d8d352a6..21b787289 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssueCodes.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssueCodes.cs @@ -1,70 +1,69 @@ -namespace Altinn.App.Core.Models.Validation +namespace Altinn.App.Core.Models.Validation; + +/// +/// Represents unique codes for different validation issues. The values should also exists in language files as lookup keys. +/// +public static class ValidationIssueCodes { /// - /// Represents unique codes for different validation issues. The values should also exists in language files as lookup keys. + /// Represents unique codes for validation issues on the instance level. /// - public static class ValidationIssueCodes + public static class InstanceCodes { /// - /// Represents unique codes for validation issues on the instance level. + /// Gets a value that represents a validation issue where an instance have more elements of a given type than the application allows. /// - public static class InstanceCodes - { - /// - /// Gets a value that represents a validation issue where an instance have more elements of a given type than the application allows. - /// - public static string TooManyDataElementsOfType => nameof(TooManyDataElementsOfType); + public static string TooManyDataElementsOfType => nameof(TooManyDataElementsOfType); - /// - /// Gets a value that represents a validation issue where an instance have fewer elements of a given type than the application requires. - /// - public static string TooFewDataElementsOfType => nameof(TooFewDataElementsOfType); - } + /// + /// Gets a value that represents a validation issue where an instance have fewer elements of a given type than the application requires. + /// + public static string TooFewDataElementsOfType => nameof(TooFewDataElementsOfType); + } + /// + /// Represents unique codes for validation issues on the data element level. + /// + public static class DataElementCodes + { /// - /// Represents unique codes for validation issues on the data element level. + /// Gets a value that represents a validation issue where a data element is missing content type. /// - public static class DataElementCodes - { - /// - /// Gets a value that represents a validation issue where a data element is missing content type. - /// - public static string MissingContentType => nameof(MissingContentType); + public static string MissingContentType => nameof(MissingContentType); - /// - /// Gets a value that represents a validation issue where a data element has a content type that is not in the list of allowed content types. - /// - public static string ContentTypeNotAllowed => nameof(ContentTypeNotAllowed); + /// + /// Gets a value that represents a validation issue where a data element has a content type that is not in the list of allowed content types. + /// + public static string ContentTypeNotAllowed => nameof(ContentTypeNotAllowed); - /// - /// Gets a value that represents a validation issue where a data element is too large for the given element type. - /// - public static string DataElementTooLarge => nameof(DataElementTooLarge); + /// + /// Gets a value that represents a validation issue where a data element is too large for the given element type. + /// + public static string DataElementTooLarge => nameof(DataElementTooLarge); - /// - /// Gets a value that represents a validation issue where a data element has been validated at a process task different from expected. - /// - public static string DataElementValidatedAtWrongTask => nameof(DataElementValidatedAtWrongTask); + /// + /// Gets a value that represents a validation issue where a data element has been validated at a process task different from expected. + /// + public static string DataElementValidatedAtWrongTask => nameof(DataElementValidatedAtWrongTask); - /// - /// Gets a value that represents a validation issue where the data element has a pending file virus scan. - /// - public static string DataElementFileScanPending => nameof(DataElementFileScanPending); + /// + /// Gets a value that represents a validation issue where the data element has a pending file virus scan. + /// + public static string DataElementFileScanPending => nameof(DataElementFileScanPending); - /// - /// Gets a value that represents a validation issue where the data element is infected with virus or malware of some form. - /// - public static string DataElementFileInfected => nameof(DataElementFileInfected); + /// + /// Gets a value that represents a validation issue where the data element is infected with virus or malware of some form. + /// + public static string DataElementFileInfected => nameof(DataElementFileInfected); - /// - /// Gets a value that represents a validation issue where the data element has a file name that is not allowed. - /// - public static string InvalidFileNameFormat => nameof(InvalidFileNameFormat); + /// + /// Gets a value that represents a validation issue where the data element has a file name that is not allowed. + /// + public static string InvalidFileNameFormat => nameof(InvalidFileNameFormat); - /// - /// Gets a value that represents a validation issue where the data element is missing a file name. - /// - public static string MissingFileName => nameof(MissingFileName); - } + /// + /// Gets a value that represents a validation issue where the data element is missing a file name. + /// + public static string MissingFileName => nameof(MissingFileName); } } diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs index 2b2264492..a606bbe76 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs @@ -1,39 +1,38 @@ -namespace Altinn.App.Core.Models.Validation +namespace Altinn.App.Core.Models.Validation; + +/// +/// Specifies the severity of a validation issue +/// +public enum ValidationIssueSeverity { /// - /// Specifies the severity of a validation issue + /// Severity has not been determined. /// - public enum ValidationIssueSeverity - { - /// - /// Severity has not been determined. - /// - Unspecified = 0, + Unspecified = 0, - /// - /// The issue requires attention and must be corrected in order to continue - /// - Error = 1, + /// + /// The issue requires attention and must be corrected in order to continue + /// + Error = 1, - /// - /// The issue should be corrected, but the process might continue without. - /// - Warning = 2, + /// + /// The issue should be corrected, but the process might continue without. + /// + Warning = 2, - /// - /// Immediate feedback provided through validation. - /// - Informational = 3, + /// + /// Immediate feedback provided through validation. + /// + Informational = 3, - /// - /// The issue has been corrected. - /// - [Obsolete("We run all validations from frontend version 4, so we don't need info about fixed issues")] - Fixed = 4, + /// + /// The issue has been corrected. + /// + [Obsolete("We run all validations from frontend version 4, so we don't need info about fixed issues")] + Fixed = 4, - /// - /// This validation indicates a success. Used for informational purposes. - /// - Success = 5, - } + /// + /// This validation indicates a success. Used for informational purposes. + /// + Success = 5, } diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssueSource.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssueSource.cs index 57434846b..8f56fb15d 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssueSource.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssueSource.cs @@ -1,33 +1,32 @@ -namespace Altinn.App.Core.Models.Validation +namespace Altinn.App.Core.Models.Validation; + +/// +/// Specifies the source of a validation issue +/// +public static class ValidationIssueSources { /// - /// Specifies the source of a validation issue + /// File attachment validation /// - public static class ValidationIssueSources - { - /// - /// File attachment validation - /// - public static readonly string File = nameof(File); + public static readonly string File = nameof(File); - /// - /// Data model validation - /// - public static readonly string ModelState = nameof(ModelState); + /// + /// Data model validation + /// + public static readonly string ModelState = nameof(ModelState); - /// - /// Required field validation - /// - public static readonly string Required = nameof(Required); + /// + /// Required field validation + /// + public static readonly string Required = nameof(Required); - /// - /// Required field validation - /// - public static readonly string Custom = nameof(Custom); + /// + /// Required field validation + /// + public static readonly string Custom = nameof(Custom); - /// - /// Expression validation - /// - public static readonly string Expression = nameof(Expression); - } + /// + /// Expression validation + /// + public static readonly string Expression = nameof(Expression); } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d827d60b7..6669928bf 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,19 +1,38 @@ + - all runtime; build; native; contentfiles; analyzers + + + + enable + enable + true + $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) preview.0 v + true + https://github.com/Altinn/app-lib-dotnet/releases + Altinn Platform Contributors + git + https://github.com/Altinn/app-lib-dotnet + README.md + LICENSE + + true + true + true + snupkg @@ -24,13 +43,6 @@ - - true - true - true - snupkg - - @@ -38,4 +50,4 @@ true - + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index f66ebf7c9..ae128f1d4 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -1,51 +1,51 @@ - - net8.0 - enable - enable - false - + + net8.0 + enable + enable + false + - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + - - - - + + + + - - - PreserveNewest - / - /%(RecursiveDir)%(Filename)%(Extension) - - + + + PreserveNewest + / + /%(RecursiveDir)%(Filename)%(Extension) + + - - - Always - - - Always - - - - + + + Always + + + Always + + + + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Constants/XacmlRequestAttribute.cs b/test/Altinn.App.Api.Tests/Constants/XacmlRequestAttribute.cs index b9c915e79..a1a5a692f 100644 --- a/test/Altinn.App.Api.Tests/Constants/XacmlRequestAttribute.cs +++ b/test/Altinn.App.Api.Tests/Constants/XacmlRequestAttribute.cs @@ -1,50 +1,49 @@ -namespace Altinn.App.Api.Tests.Constants +namespace Altinn.App.Api.Tests.Constants; + +public static class XacmlRequestAttribute { - public static class XacmlRequestAttribute - { - /// - /// xacml string that represents org - /// - public const string OrgAttribute = "urn:altinn:org"; - - /// - /// xacml string that represents app - /// - public const string AppAttribute = "urn:altinn:app"; - - /// - /// xacml string that represents isntance - /// - public const string InstanceAttribute = "urn:altinn:instance-id"; - - /// - /// xacm string that represents appresource - /// - public const string AppResourceAttribute = "urn:altinn:appresource"; - - /// - /// xacml string that represents task - /// - public const string TaskAttribute = "urn:altinn:task"; - - /// - /// xacml string that represents end event - /// - public const string EndEventAttribute = "urn:altinn:end-event"; - - /// - /// xacml string that represents party - /// - public const string PartyAttribute = "urn:altinn:partyid"; - - /// - /// xacml string that represents user - /// - public const string UserAttribute = "urn:altinn:userid"; - - /// - /// xacml string that represents role - /// - public const string RoleAttribute = "urn:altinn:rolecode"; - } + /// + /// xacml string that represents org + /// + public const string OrgAttribute = "urn:altinn:org"; + + /// + /// xacml string that represents app + /// + public const string AppAttribute = "urn:altinn:app"; + + /// + /// xacml string that represents isntance + /// + public const string InstanceAttribute = "urn:altinn:instance-id"; + + /// + /// xacm string that represents appresource + /// + public const string AppResourceAttribute = "urn:altinn:appresource"; + + /// + /// xacml string that represents task + /// + public const string TaskAttribute = "urn:altinn:task"; + + /// + /// xacml string that represents end event + /// + public const string EndEventAttribute = "urn:altinn:end-event"; + + /// + /// xacml string that represents party + /// + public const string PartyAttribute = "urn:altinn:partyid"; + + /// + /// xacml string that represents user + /// + public const string UserAttribute = "urn:altinn:userid"; + + /// + /// xacml string that represents role + /// + public const string RoleAttribute = "urn:altinn:rolecode"; } diff --git a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs index 4de0e326a..5dd838be4 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs @@ -11,7 +11,6 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; -using Xunit; using Xunit.Abstractions; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/ApplicationMetadataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ApplicationMetadataControllerTests.cs index 3be5e17cd..20d0560e8 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ApplicationMetadataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ApplicationMetadataControllerTests.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Moq; -using Xunit; using Xunit.Abstractions; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs index d0e773da3..86a82873e 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs @@ -9,247 +9,242 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; -using Xunit; using Xunit.Abstractions; -namespace Altinn.App.Api.Tests.Controllers +namespace Altinn.App.Api.Tests.Controllers; + +public class DataControllerTests : ApiTestBase, IClassFixture> { - public class DataControllerTests : ApiTestBase, IClassFixture> - { - public DataControllerTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) - : base(factory, outputHelper) { } + public DataControllerTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) + : base(factory, outputHelper) { } - [Fact] - public async Task PutDataElement_MissingDataType_ReturnsBadRequest() - { - // Setup test data - string org = "tdd"; - string app = "contributer-restriction"; - int instanceOwnerPartyId = 1337; - Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); - HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - - TestData.DeleteInstance(org, app, instanceOwnerPartyId, guid); - TestData.PrepareInstance(org, app, instanceOwnerPartyId, guid); - - using var content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json"); // empty valid json - var response = await client.PostAsync( - $"/{org}/{app}/instances/{instanceOwnerPartyId}/{guid}/data", - content - ); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - var responseContent = await response.Content.ReadAsStringAsync(); - responseContent.Should().Contain("dataType"); - } + [Fact] + public async Task PutDataElement_MissingDataType_ReturnsBadRequest() + { + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 1337; + Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + TestData.DeleteInstance(org, app, instanceOwnerPartyId, guid); + TestData.PrepareInstance(org, app, instanceOwnerPartyId, guid); + + using var content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json"); // empty valid json + var response = await client.PostAsync($"/{org}/{app}/instances/{instanceOwnerPartyId}/{guid}/data", content); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + var responseContent = await response.Content.ReadAsStringAsync(); + responseContent.Should().Contain("dataType"); + } - [Fact] - public async Task CreateDataElement_BinaryPdf_AnalyserShouldRunOk() + [Fact] + public async Task CreateDataElement_BinaryPdf_AnalyserShouldRunOk() + { + OverrideServicesForThisTest = (services) => { - OverrideServicesForThisTest = (services) => - { - services.AddTransient(); - services.AddTransient(); - }; + services.AddTransient(); + services.AddTransient(); + }; - // Setup test data - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); - Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); - TestData.DeleteInstance(org, app, 1337, guid); - TestData.PrepareInstance(org, app, 1337, guid); + Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); - // Setup the request - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.pdf", "application/pdf"); - string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; - var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; + // Setup the request + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.pdf", "application/pdf"); + string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; + var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; - // This is where it happens - HttpResponseMessage response = await client.SendAsync(request); + // This is where it happens + HttpResponseMessage response = await client.SendAsync(request); - // Cleanup testdata - TestData.DeleteInstanceAndData(org, app, 1337, guid); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - } - - [Fact] - public async Task CreateDataElement_ZeroBytes_BinaryPdf_AnalyserShouldReturnBadRequest() - { - OverrideServicesForThisTest = (services) => - { - services.AddTransient(); - services.AddTransient(); - }; - - // Setup test data - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); - - Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); - TestData.DeleteInstance(org, app, 1337, guid); - TestData.PrepareInstance(org, app, 1337, guid); - - // Setup the request - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - ByteArrayContent fileContent = await CreateBinaryContent(org, app, "zero.pdf", "application/pdf"); - string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; - var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; - - // This is where it happens - HttpResponseMessage response = await client.SendAsync(request); - - // Cleanup testdata - TestData.DeleteInstanceAndData(org, app, 1337, guid); - - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - var responseContent = await response.Content.ReadAsStringAsync(); - Assert.Equal("Invalid data provided. Error: The file is zero bytes.", responseContent); - } + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } - [Fact] - public async Task CreateDataElement_JpgFakedAsPdf_AnalyserShouldRunAndFail() + [Fact] + public async Task CreateDataElement_ZeroBytes_BinaryPdf_AnalyserShouldReturnBadRequest() + { + OverrideServicesForThisTest = (services) => { - OverrideServicesForThisTest = (services) => - { - services.AddTransient(); - services.AddTransient(); - }; - - // Setup test data - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); - - Guid guid = new Guid("1fc98a23-fe31-4ef5-8fb9-dd3f479354ce"); - TestData.DeleteInstance(org, app, 1337, guid); - TestData.PrepareInstance(org, app, 1337, guid); - - // Setup the request - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.jpg.pdf", "application/pdf"); - string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; - var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; - - // This is where it happens - HttpResponseMessage response = await client.SendAsync(request); - string responseContent = await response.Content.ReadAsStringAsync(); - - // Cleanup testdata - TestData.DeleteInstanceAndData(org, app, 1337, guid); - - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } + services.AddTransient(); + services.AddTransient(); + }; + + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + + // Setup the request + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + ByteArrayContent fileContent = await CreateBinaryContent(org, app, "zero.pdf", "application/pdf"); + string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; + var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; + + // This is where it happens + HttpResponseMessage response = await client.SendAsync(request); + + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var responseContent = await response.Content.ReadAsStringAsync(); + Assert.Equal("Invalid data provided. Error: The file is zero bytes.", responseContent); + } - private static async Task CreateBinaryContent( - string org, - string app, - string filename, - string mediaType - ) + [Fact] + public async Task CreateDataElement_JpgFakedAsPdf_AnalyserShouldRunAndFail() + { + OverrideServicesForThisTest = (services) => { - var pdfFilePath = TestData.GetAppSpecificTestdataFile(org, app, filename); - var fileBytes = await File.ReadAllBytesAsync(pdfFilePath); - var fileContent = new ByteArrayContent(fileBytes); - fileContent.Headers.ContentType = new MediaTypeHeaderValue(mediaType); - fileContent.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse( - $"attachment; filename=\"{filename}\"; filename*=UTF-8''{filename}" - ); - return fileContent; - } + services.AddTransient(); + services.AddTransient(); + }; + + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + Guid guid = new Guid("1fc98a23-fe31-4ef5-8fb9-dd3f479354ce"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + + // Setup the request + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.jpg.pdf", "application/pdf"); + string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; + var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; + + // This is where it happens + HttpResponseMessage response = await client.SendAsync(request); + string responseContent = await response.Content.ReadAsStringAsync(); + + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } - public class MimeTypeAnalyserSuccessStub : IFileAnalyser + private static async Task CreateBinaryContent( + string org, + string app, + string filename, + string mediaType + ) { - public string Id { get; private set; } = "mimeTypeAnalyser"; + var pdfFilePath = TestData.GetAppSpecificTestdataFile(org, app, filename); + var fileBytes = await File.ReadAllBytesAsync(pdfFilePath); + var fileContent = new ByteArrayContent(fileBytes); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(mediaType); + fileContent.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse( + $"attachment; filename=\"{filename}\"; filename*=UTF-8''{filename}" + ); + return fileContent; + } +} - public Task> Analyse(IEnumerable httpContents) - { - throw new NotImplementedException(); - } +public class MimeTypeAnalyserSuccessStub : IFileAnalyser +{ + public string Id { get; private set; } = "mimeTypeAnalyser"; - public Task Analyse(Stream stream, string? filename = null) - { - return Task.FromResult( - new FileAnalysisResult(Id) - { - MimeType = "application/pdf", - Filename = "example.pdf", - Extensions = new List() { "pdf" } - } - ); - } + public Task> Analyse(IEnumerable httpContents) + { + throw new NotImplementedException(); } - public class MimeTypeAnalyserFailureStub : IFileAnalyser + public Task Analyse(Stream stream, string? filename = null) { - public string Id { get; private set; } = "mimeTypeAnalyser"; + return Task.FromResult( + new FileAnalysisResult(Id) + { + MimeType = "application/pdf", + Filename = "example.pdf", + Extensions = new List() { "pdf" } + } + ); + } +} - public Task> Analyse(IEnumerable httpContents) - { - throw new NotImplementedException(); - } +public class MimeTypeAnalyserFailureStub : IFileAnalyser +{ + public string Id { get; private set; } = "mimeTypeAnalyser"; - public Task Analyse(Stream stream, string? filename = null) - { - return Task.FromResult( - new FileAnalysisResult(Id) - { - MimeType = "application/jpeg", - Filename = "example.jpg.pdf", - Extensions = new List() { "jpg" } - } - ); - } + public Task> Analyse(IEnumerable httpContents) + { + throw new NotImplementedException(); } - public class MimeTypeValidatorStub : IFileValidator + public Task Analyse(Stream stream, string? filename = null) { - public string Id { get; private set; } = "mimeTypeValidator"; + return Task.FromResult( + new FileAnalysisResult(Id) + { + MimeType = "application/jpeg", + Filename = "example.jpg.pdf", + Extensions = new List() { "jpg" } + } + ); + } +} + +public class MimeTypeValidatorStub : IFileValidator +{ + public string Id { get; private set; } = "mimeTypeValidator"; #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously. Suppressed because of the interface. - public async Task<(bool Success, IEnumerable Errors)> Validate( - DataType dataType, - IEnumerable fileAnalysisResults - ) + public async Task<(bool Success, IEnumerable Errors)> Validate( + DataType dataType, + IEnumerable fileAnalysisResults + ) #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - List errors = new(); + { + List errors = new(); - var fileMimeTypeResult = fileAnalysisResults.FirstOrDefault(x => x.MimeType != null); + var fileMimeTypeResult = fileAnalysisResults.FirstOrDefault(x => x.MimeType != null); - // Verify that file mime type is an allowed content-type - if ( - !dataType.AllowedContentTypes.Contains( - fileMimeTypeResult?.MimeType, - StringComparer.InvariantCultureIgnoreCase - ) && !dataType.AllowedContentTypes.Contains("application/octet-stream") - ) - { - ValidationIssue error = - new() - { - Source = ValidationIssueSources.File, - Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, - Severity = ValidationIssueSeverity.Error, - Description = - $"The {fileMimeTypeResult?.Filename + " "}file does not appear to be of the allowed content type according to the configuration for data type {dataType.Id}. Allowed content types are {string.Join(", ", dataType.AllowedContentTypes)}" - }; - - errors.Add(error); - - return (false, errors); - } + // Verify that file mime type is an allowed content-type + if ( + !dataType.AllowedContentTypes.Contains( + fileMimeTypeResult?.MimeType, + StringComparer.InvariantCultureIgnoreCase + ) && !dataType.AllowedContentTypes.Contains("application/octet-stream") + ) + { + ValidationIssue error = + new() + { + Source = ValidationIssueSources.File, + Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Severity = ValidationIssueSeverity.Error, + Description = + $"The {fileMimeTypeResult?.Filename + " "}file does not appear to be of the allowed content type according to the configuration for data type {dataType.Id}. Allowed content types are {string.Join(", ", dataType.AllowedContentTypes)}" + }; + + errors.Add(error); - return (true, errors); + return (false, errors); } + + return (true, errors); } } diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs index af874136a..b01772c24 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs @@ -19,7 +19,6 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Moq; -using Xunit; using Xunit.Abstractions; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs index f9d491cf0..b4a8b6af1 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Http.Headers; -using System.Text.Json; using Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Features; @@ -9,7 +8,6 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Moq; -using Xunit; using Xunit.Abstractions; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs index fa9fdc5cf..e58b03997 100644 --- a/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs @@ -1,100 +1,93 @@ -using System; using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; using System.Text; -using System.Threading.Tasks; -using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Models; using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; -using Xunit; using Xunit.Abstractions; -namespace Altinn.App.Api.Tests.Controllers -{ - public class EventsReceiverControllerTests : ApiTestBase, IClassFixture> - { - private readonly IEventSecretCodeProvider _secretCodeProvider; +namespace Altinn.App.Api.Tests.Controllers; - public EventsReceiverControllerTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) - : base(factory, outputHelper) - { - _secretCodeProvider = factory.Services.GetRequiredService(); - } +public class EventsReceiverControllerTests : ApiTestBase, IClassFixture> +{ + private readonly IEventSecretCodeProvider _secretCodeProvider; - [Fact] - public async Task Post_ValidEventType_ShouldReturnOk() - { - var org = "tdd"; - var app = "contributer-restriction"; + public EventsReceiverControllerTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) + : base(factory, outputHelper) + { + _secretCodeProvider = factory.Services.GetRequiredService(); + } - var client = GetRootedClient(org, app, 1338, null); - CloudEvent cloudEvent = - new() - { - Id = Guid.NewGuid().ToString(), - Source = new Uri( - "https://dihe.apps.altinn3local.no/dihe/redusert-foreldrebetaling-bhg/instances/510002/553a3ddc-4ca4-40af-9c2a-1e33e659c7e7" - ), - SpecVersion = "1.0", - Type = "app.event.dummy.success", - Subject = "/party/510002", - Time = DateTime.Parse("2022-10-13T09:33:46.6330634Z"), - AlternativeSubject = "/person/17858296439" - }; + [Fact] + public async Task Post_ValidEventType_ShouldReturnOk() + { + var org = "tdd"; + var app = "contributer-restriction"; - string requestUrl = $"{org}/{app}/api/v1/eventsreceiver?code={await _secretCodeProvider.GetSecretCode()}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUrl) + var client = GetRootedClient(org, app, 1338, null); + CloudEvent cloudEvent = + new() { - Content = new StringContent( - System.Text.Json.JsonSerializer.Serialize(cloudEvent), - Encoding.UTF8, - "application/json" - ) + Id = Guid.NewGuid().ToString(), + Source = new Uri( + "https://dihe.apps.altinn3local.no/dihe/redusert-foreldrebetaling-bhg/instances/510002/553a3ddc-4ca4-40af-9c2a-1e33e659c7e7" + ), + SpecVersion = "1.0", + Type = "app.event.dummy.success", + Subject = "/party/510002", + Time = DateTime.Parse("2022-10-13T09:33:46.6330634Z"), + AlternativeSubject = "/person/17858296439" }; - HttpResponseMessage response = await client.SendAsync(request); + string requestUrl = $"{org}/{app}/api/v1/eventsreceiver?code={await _secretCodeProvider.GetSecretCode()}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUrl) + { + Content = new StringContent( + System.Text.Json.JsonSerializer.Serialize(cloudEvent), + Encoding.UTF8, + "application/json" + ) + }; - response.StatusCode.Should().Be(HttpStatusCode.OK); - } + HttpResponseMessage response = await client.SendAsync(request); - [Fact] - public async Task Post_NonValidEventType_ShouldReturnBadRequest() - { - var org = "tdd"; - var app = "contributer-restriction"; + response.StatusCode.Should().Be(HttpStatusCode.OK); + } - var client = GetRootedClient(org, app, userId: 1338, partyId: null); - CloudEvent cloudEvent = - new() - { - Id = Guid.NewGuid().ToString(), - Source = new Uri( - "https://dihe.apps.altinn3local.no/dihe/redusert-foreldrebetaling-bhg/instances/510002/553a3ddc-4ca4-40af-9c2a-1e33e659c7e7" - ), - SpecVersion = "1.0", - Type = "no.event.handler.registered.for.this.type", - Subject = "/party/510002", - Time = DateTime.Parse("2022-10-13T09:33:46.6330634Z"), - AlternativeSubject = "/person/17858296439" - }; + [Fact] + public async Task Post_NonValidEventType_ShouldReturnBadRequest() + { + var org = "tdd"; + var app = "contributer-restriction"; - string requestUrl = $"{org}/{app}/api/v1/eventsreceiver?code={await _secretCodeProvider.GetSecretCode()}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUrl) + var client = GetRootedClient(org, app, userId: 1338, partyId: null); + CloudEvent cloudEvent = + new() { - Content = new StringContent( - System.Text.Json.JsonSerializer.Serialize(cloudEvent), - Encoding.UTF8, - "application/json" - ) + Id = Guid.NewGuid().ToString(), + Source = new Uri( + "https://dihe.apps.altinn3local.no/dihe/redusert-foreldrebetaling-bhg/instances/510002/553a3ddc-4ca4-40af-9c2a-1e33e659c7e7" + ), + SpecVersion = "1.0", + Type = "no.event.handler.registered.for.this.type", + Subject = "/party/510002", + Time = DateTime.Parse("2022-10-13T09:33:46.6330634Z"), + AlternativeSubject = "/person/17858296439" }; - HttpResponseMessage response = await client.SendAsync(request); + string requestUrl = $"{org}/{app}/api/v1/eventsreceiver?code={await _secretCodeProvider.GetSecretCode()}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUrl) + { + Content = new StringContent( + System.Text.Json.JsonSerializer.Serialize(cloudEvent), + Encoding.UTF8, + "application/json" + ) + }; + + HttpResponseMessage response = await client.SendAsync(request); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - } + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } } diff --git a/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs index 38b4579e5..c3eb607b3 100644 --- a/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs @@ -4,90 +4,73 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Moq; -using Xunit; -namespace Altinn.App.Api.Tests.Controllers +namespace Altinn.App.Api.Tests.Controllers; + +public class FileScanControllerTests { - public class FileScanControllerTests + [Fact] + public async Task InstanceAndDataExists_ShouldReturn200Ok() { - [Fact] - public async Task InstanceAndDataExists_ShouldReturn200Ok() - { - const string org = "org"; - const string app = "app"; - const int instanceOwnerPartyId = 12345; - Guid instanceId = Guid.NewGuid(); - Mock instanceClientMock = CreateInstanceClientMock( - org, - app, - instanceOwnerPartyId, - instanceId - ); + const string org = "org"; + const string app = "app"; + const int instanceOwnerPartyId = 12345; + Guid instanceId = Guid.NewGuid(); + Mock instanceClientMock = CreateInstanceClientMock(org, app, instanceOwnerPartyId, instanceId); - var fileScanController = new FileScanController(instanceClientMock.Object); - var fileScanResults = await fileScanController.GetFileScanResults( - org, - app, - instanceOwnerPartyId, - instanceId - ); + var fileScanController = new FileScanController(instanceClientMock.Object); + var fileScanResults = await fileScanController.GetFileScanResults(org, app, instanceOwnerPartyId, instanceId); - fileScanResults.Result.Should().BeOfType(); - fileScanResults.Value?.FileScanResult.Should().Be(Platform.Storage.Interface.Enums.FileScanResult.Infected); - } + fileScanResults.Result.Should().BeOfType(); + fileScanResults.Value?.FileScanResult.Should().Be(Platform.Storage.Interface.Enums.FileScanResult.Infected); + } - [Fact] - public async Task InstanceDoesNotExists_ShouldReturnNotFound() - { - const string org = "org"; - const string app = "app"; - const int instanceOwnerPartyId = 12345; - Guid instanceId = Guid.NewGuid(); - Mock instanceClientMock = CreateInstanceClientMock( - org, - app, - instanceOwnerPartyId, - instanceId - ); + [Fact] + public async Task InstanceDoesNotExists_ShouldReturnNotFound() + { + const string org = "org"; + const string app = "app"; + const int instanceOwnerPartyId = 12345; + Guid instanceId = Guid.NewGuid(); + Mock instanceClientMock = CreateInstanceClientMock(org, app, instanceOwnerPartyId, instanceId); - var fileScanController = new FileScanController(instanceClientMock.Object); - var fileScanResults = await fileScanController.GetFileScanResults( - org, - app, - instanceOwnerPartyId, - Guid.NewGuid() - ); + var fileScanController = new FileScanController(instanceClientMock.Object); + var fileScanResults = await fileScanController.GetFileScanResults( + org, + app, + instanceOwnerPartyId, + Guid.NewGuid() + ); - fileScanResults.Result.Should().BeOfType(); - } + fileScanResults.Result.Should().BeOfType(); + } - private static Mock CreateInstanceClientMock( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceId - ) + private static Mock CreateInstanceClientMock( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceId + ) + { + var instance = new Instance { - var instance = new Instance + Id = $"{instanceOwnerPartyId}/{instanceId}", + Process = null, + Data = new List() { - Id = $"{instanceOwnerPartyId}/{instanceId}", - Process = null, - Data = new List() + new() { - new() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = Platform.Storage.Interface.Enums.FileScanResult.Infected - } + Id = Guid.NewGuid().ToString(), + FileScanResult = Platform.Storage.Interface.Enums.FileScanResult.Infected } - }; + } + }; - var instanceClientMock = new Mock(); - instanceClientMock - .Setup(e => e.GetInstance(app, org, instanceOwnerPartyId, instanceId)) - .Returns(Task.FromResult(instance)); + var instanceClientMock = new Mock(); + instanceClientMock + .Setup(e => e.GetInstance(app, org, instanceOwnerPartyId, instanceId)) + .Returns(Task.FromResult(instance)); - return instanceClientMock; - } + return instanceClientMock; } } diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs index 9bee29b46..e08f1fb04 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs @@ -20,7 +20,6 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Moq; -using Xunit; using IProcessEngine = Altinn.App.Core.Internal.Process.IProcessEngine; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs index 21ce884fd..cf9dff8c9 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs @@ -22,7 +22,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using Xunit; using IProcessEngine = Altinn.App.Core.Internal.Process.IProcessEngine; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs index 1cad512aa..4fc904976 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Moq; -using Xunit; using Xunit.Abstractions; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs index edd68490a..7c8d3b3a1 100644 --- a/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs @@ -5,219 +5,217 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Moq; -using Xunit; using Xunit.Abstractions; -namespace Altinn.App.Api.Tests.Controllers +namespace Altinn.App.Api.Tests.Controllers; + +public class OptionsControllerTests : ApiTestBase, IClassFixture> { - public class OptionsControllerTests : ApiTestBase, IClassFixture> + public OptionsControllerTests(ITestOutputHelper outputHelper, WebApplicationFactory factory) + : base(factory, outputHelper) { } + + [Fact] + public async Task Get_ShouldReturnParametersInHeader() { - public OptionsControllerTests(ITestOutputHelper outputHelper, WebApplicationFactory factory) - : base(factory, outputHelper) { } + OverrideServicesForThisTest = (services) => + { + services.AddTransient(); + }; + + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + string url = $"/{org}/{app}/api/options/test?language=esperanto"; + HttpResponseMessage response = await client.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + _outputHelper.WriteLine(content); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var headerValue = response.Headers.GetValues("Altinn-DownstreamParameters"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + headerValue.Should().Contain("lang=esperanto"); + } - [Fact] - public async Task Get_ShouldReturnParametersInHeader() + [Fact] + public async Task Get_ShouldReturnParametersInHeaderWithSpecialChars() + { + var options = new AppOptions { - OverrideServicesForThisTest = (services) => + Options = new List() { - services.AddTransient(); - }; - - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); - - string url = $"/{org}/{app}/api/options/test?language=esperanto"; - HttpResponseMessage response = await client.GetAsync(url); - var content = await response.Content.ReadAsStringAsync(); - _outputHelper.WriteLine(content); - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var headerValue = response.Headers.GetValues("Altinn-DownstreamParameters"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - headerValue.Should().Contain("lang=esperanto"); - } - - [Fact] - public async Task Get_ShouldReturnParametersInHeaderWithSpecialChars() - { - var options = new AppOptions + new() { Value = "", Label = "" } + }, + Parameters = new Dictionary { - Options = new List() - { - new() { Value = "", Label = "" } - }, - Parameters = new Dictionary - { - { "language", "español" }, - { "level", "1" }, - { "variant", "Småviltjakt" }, - { "special", ",\".%" } - }, - }; - var provider = new Mock(MockBehavior.Strict); - provider - .Setup(p => p.GetAppOptionsAsync(It.IsAny(), It.IsAny>())) - .ReturnsAsync(options) - .Verifiable(Times.Once); - provider.Setup(p => p.Id).Returns("test").Verifiable(Times.Once); - - OverrideServicesForThisTest = (services) => - { - services.AddSingleton(provider.Object); - }; - - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); - - string url = $"/{org}/{app}/api/options/test?"; - HttpResponseMessage response = await client.GetAsync(url); - var content = await response.Content.ReadAsStringAsync(); - response.StatusCode.Should().Be(HttpStatusCode.OK, content); - - var headerValue = response - .Headers.Should() - .Contain((header) => header.Key == "Altinn-DownstreamParameters") - .Which.Value; - ; - response.Should().HaveStatusCode(HttpStatusCode.OK); - headerValue - .Should() - .ContainSingle() - .Which.Split(',') - .Should() - .Contain( - new List() - { - "language=espa%C3%B1ol", - "level=1", - "variant=Sm%C3%A5viltjakt", - "special=%2C%22.%25" - } - ); - provider.Verify(); - } - - [Fact] - public async Task Get_ShouldNotDefaultToNbLanguage() - { - OverrideServicesForThisTest = (services) => - { - services.AddTransient(); - }; - - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); - - string url = $"/{org}/{app}/api/options/test"; - HttpResponseMessage response = await client.GetAsync(url); - var content = await response.Content.ReadAsStringAsync(); - _outputHelper.WriteLine(content); - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var headerValue = response.Headers.GetValues("Altinn-DownstreamParameters"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - headerValue.Should().NotContain("nb"); - } - - [Fact] - public async Task Get_ShouldWorkWithFileSource() + { "language", "español" }, + { "level", "1" }, + { "variant", "Småviltjakt" }, + { "special", ",\".%" } + }, + }; + var provider = new Mock(MockBehavior.Strict); + provider + .Setup(p => p.GetAppOptionsAsync(It.IsAny(), It.IsAny>())) + .ReturnsAsync(options) + .Verifiable(Times.Once); + provider.Setup(p => p.Id).Returns("test").Verifiable(Times.Once); + + OverrideServicesForThisTest = (services) => { - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); - - string url = $"/{org}/{app}/api/options/fileSourceOptions"; - HttpResponseMessage response = await client.GetAsync(url); - var content = await response.Content.ReadAsStringAsync(); - _outputHelper.WriteLine(content); - response.Should().HaveStatusCode(HttpStatusCode.OK); - content - .Should() - .Be( - """[{"value":null,"label":""},{"value":"string-value","label":"string-label"},{"value":3,"label":"number"},{"value":true,"label":"boolean-true"},{"value":false,"label":"boolean-false"}]""" - ); - } - - [Fact] - public async Task GetNonExistentList_Return404() - { - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); - - string url = $"/{org}/{app}/api/options/non-existent-option-list"; - HttpResponseMessage response = await client.GetAsync(url); - var content = await response.Content.ReadAsStringAsync(); - _outputHelper.WriteLine(content); - response.Should().HaveStatusCode(HttpStatusCode.NotFound); - } - - [Fact] - public async Task Get_ShouldSerializeToCorrectTypes() + services.AddSingleton(provider.Object); + }; + + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + string url = $"/{org}/{app}/api/options/test?"; + HttpResponseMessage response = await client.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + response.StatusCode.Should().Be(HttpStatusCode.OK, content); + + var headerValue = response + .Headers.Should() + .Contain((header) => header.Key == "Altinn-DownstreamParameters") + .Which.Value; + ; + response.Should().HaveStatusCode(HttpStatusCode.OK); + headerValue + .Should() + .ContainSingle() + .Which.Split(',') + .Should() + .Contain( + new List() + { + "language=espa%C3%B1ol", + "level=1", + "variant=Sm%C3%A5viltjakt", + "special=%2C%22.%25" + } + ); + provider.Verify(); + } + + [Fact] + public async Task Get_ShouldNotDefaultToNbLanguage() + { + OverrideServicesForThisTest = (services) => { - OverrideServicesForThisTest = (services) => - { - services.AddTransient(); - }; - - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); - - string url = $"/{org}/{app}/api/options/test"; - HttpResponseMessage response = await client.GetAsync(url); - var content = await response.Content.ReadAsStringAsync(); - _outputHelper.WriteLine(content); - response.StatusCode.Should().Be(HttpStatusCode.OK); - - content - .Should() - .Be( - "[{\"value\":null,\"label\":\"\"},{\"value\":\"SomeString\",\"label\":\"False\"},{\"value\":true,\"label\":\"True\"},{\"value\":0,\"label\":\"Zero\"},{\"value\":1,\"label\":\"One\",\"description\":\"This is a description\",\"helpText\":\"This is a help text\"}]" - ); - } + services.AddTransient(); + }; + + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + string url = $"/{org}/{app}/api/options/test"; + HttpResponseMessage response = await client.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + _outputHelper.WriteLine(content); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var headerValue = response.Headers.GetValues("Altinn-DownstreamParameters"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + headerValue.Should().NotContain("nb"); } - public class DummyProvider : IAppOptionsProvider + [Fact] + public async Task Get_ShouldWorkWithFileSource() { - public string Id => "test"; + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + string url = $"/{org}/{app}/api/options/fileSourceOptions"; + HttpResponseMessage response = await client.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + _outputHelper.WriteLine(content); + response.Should().HaveStatusCode(HttpStatusCode.OK); + content + .Should() + .Be( + """[{"value":null,"label":""},{"value":"string-value","label":"string-label"},{"value":3,"label":"number"},{"value":true,"label":"boolean-true"},{"value":false,"label":"boolean-false"}]""" + ); + } + + [Fact] + public async Task GetNonExistentList_Return404() + { + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + string url = $"/{org}/{app}/api/options/non-existent-option-list"; + HttpResponseMessage response = await client.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + _outputHelper.WriteLine(content); + response.Should().HaveStatusCode(HttpStatusCode.NotFound); + } - public Task GetAppOptionsAsync(string? language, Dictionary keyValuePairs) + [Fact] + public async Task Get_ShouldSerializeToCorrectTypes() + { + OverrideServicesForThisTest = (services) => { - AppOptions appOptions = new AppOptions() + services.AddTransient(); + }; + + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + string url = $"/{org}/{app}/api/options/test"; + HttpResponseMessage response = await client.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + _outputHelper.WriteLine(content); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + content + .Should() + .Be( + "[{\"value\":null,\"label\":\"\"},{\"value\":\"SomeString\",\"label\":\"False\"},{\"value\":true,\"label\":\"True\"},{\"value\":0,\"label\":\"Zero\"},{\"value\":1,\"label\":\"One\",\"description\":\"This is a description\",\"helpText\":\"This is a help text\"}]" + ); + } +} + +public class DummyProvider : IAppOptionsProvider +{ + public string Id => "test"; + + public Task GetAppOptionsAsync(string? language, Dictionary keyValuePairs) + { + AppOptions appOptions = new AppOptions() + { + Parameters = new() { { "lang", language } }, + Options = new List() { - Parameters = new() { { "lang", language } }, - Options = new List() + new() { Value = null, Label = "", }, + new() { Value = "SomeString", Label = "False", }, + new() { - new() { Value = null, Label = "", }, - new() { Value = "SomeString", Label = "False", }, - new() - { - Value = "true", - ValueType = AppOptionValueType.Boolean, - Label = "True", - }, - new() - { - Value = "0", - ValueType = AppOptionValueType.Number, - Label = "Zero", - }, - new() - { - Value = "1", - ValueType = AppOptionValueType.Number, - Label = "One", - Description = "This is a description", - HelpText = "This is a help text" - }, - } - }; + Value = "true", + ValueType = AppOptionValueType.Boolean, + Label = "True", + }, + new() + { + Value = "0", + ValueType = AppOptionValueType.Number, + Label = "Zero", + }, + new() + { + Value = "1", + ValueType = AppOptionValueType.Number, + Label = "One", + Description = "This is a description", + HelpText = "This is a help text" + }, + } + }; - return Task.FromResult(appOptions); - } + return Task.FromResult(appOptions); } } diff --git a/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs index 339feaba5..ec94bdc59 100644 --- a/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs @@ -16,344 +16,334 @@ using Microsoft.Extensions.Options; using Moq; using Moq.Protected; -using Xunit; using IAppResources = Altinn.App.Core.Internal.App.IAppResources; -namespace Altinn.App.Api.Tests.Controllers +namespace Altinn.App.Api.Tests.Controllers; + +public class PdfControllerTests { - public class PdfControllerTests + private readonly string org = "org"; + private readonly string app = "app"; + private readonly Guid instanceId = new Guid("e11e3e0b-a45c-48fb-a968-8d4ddf868c80"); + private readonly int partyId = 12345; + private readonly string taskId = "Task_1"; + + private readonly Mock _appResources = new(); + private readonly Mock _dataClient = new(); + private readonly Mock _profile = new(); + private readonly IOptions _platformSettingsOptions = + Microsoft.Extensions.Options.Options.Create(new() { }); + private readonly Mock _instanceClient = new(); + private readonly Mock _pdfFormatter = new(); + private readonly Mock _appModel = new(); + private readonly Mock _userTokenProvider = new(); + + private readonly IOptions _pdfGeneratorSettingsOptions = + Microsoft.Extensions.Options.Options.Create(new() { }); + + public PdfControllerTests() { - private readonly string org = "org"; - private readonly string app = "app"; - private readonly Guid instanceId = new Guid("e11e3e0b-a45c-48fb-a968-8d4ddf868c80"); - private readonly int partyId = 12345; - private readonly string taskId = "Task_1"; - - private readonly Mock _appResources = new(); - private readonly Mock _dataClient = new(); - private readonly Mock _profile = new(); - private readonly IOptions _platformSettingsOptions = - Microsoft.Extensions.Options.Options.Create(new() { }); - private readonly Mock _instanceClient = new(); - private readonly Mock _pdfFormatter = new(); - private readonly Mock _appModel = new(); - private readonly Mock _userTokenProvider = new(); - - private readonly IOptions _pdfGeneratorSettingsOptions = - Microsoft.Extensions.Options.Options.Create(new() { }); - - public PdfControllerTests() - { - _instanceClient - .Setup(a => a.GetInstance(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns( - Task.FromResult( - new Instance() - { - Org = org, - AppId = $"{org}/{app}", - Id = $"{partyId}/{instanceId}", - Process = new ProcessState() - { - CurrentTask = new ProcessElementInfo() { ElementId = taskId, }, - } - } - ) - ); - } - - [Fact] - public async Task Request_In_Prod_Should_Be_Blocked() - { - var env = new Mock(); - env.Setup(a => a.EnvironmentName).Returns("Production"); - - IOptions generalSettingsOptions = - Microsoft.Extensions.Options.Options.Create(new() { HostName = "org.apps.altinn.no" }); - - var httpContextAccessor = new Mock(); - httpContextAccessor.Setup(x => x.HttpContext!.Request!.Query["lang"]).Returns("nb"); - - var handler = new Mock(); - var httpClient = new HttpClient(handler.Object); - - var pdfGeneratorClient = new PdfGeneratorClient( - httpClient, - _pdfGeneratorSettingsOptions, - _platformSettingsOptions, - _userTokenProvider.Object, - httpContextAccessor.Object - ); - var pdfService = new PdfService( - _appResources.Object, - _dataClient.Object, - httpContextAccessor.Object, - _profile.Object, - pdfGeneratorClient, - _pdfGeneratorSettingsOptions, - generalSettingsOptions - ); - var pdfController = new PdfController( - _instanceClient.Object, - _pdfFormatter.Object, - _appResources.Object, - _appModel.Object, - _dataClient.Object, - env.Object, - pdfService + _instanceClient + .Setup(a => a.GetInstance(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns( + Task.FromResult( + new Instance() + { + Org = org, + AppId = $"{org}/{app}", + Id = $"{partyId}/{instanceId}", + Process = new ProcessState() { CurrentTask = new ProcessElementInfo() { ElementId = taskId, }, } + } + ) ); + } - var result = await pdfController.GetPdfPreview(org, app, partyId, instanceId); + [Fact] + public async Task Request_In_Prod_Should_Be_Blocked() + { + var env = new Mock(); + env.Setup(a => a.EnvironmentName).Returns("Production"); + + IOptions generalSettingsOptions = Microsoft.Extensions.Options.Options.Create( + new() { HostName = "org.apps.altinn.no" } + ); + + var httpContextAccessor = new Mock(); + httpContextAccessor.Setup(x => x.HttpContext!.Request!.Query["lang"]).Returns("nb"); + + var handler = new Mock(); + var httpClient = new HttpClient(handler.Object); + + var pdfGeneratorClient = new PdfGeneratorClient( + httpClient, + _pdfGeneratorSettingsOptions, + _platformSettingsOptions, + _userTokenProvider.Object, + httpContextAccessor.Object + ); + var pdfService = new PdfService( + _appResources.Object, + _dataClient.Object, + httpContextAccessor.Object, + _profile.Object, + pdfGeneratorClient, + _pdfGeneratorSettingsOptions, + generalSettingsOptions + ); + var pdfController = new PdfController( + _instanceClient.Object, + _pdfFormatter.Object, + _appResources.Object, + _appModel.Object, + _dataClient.Object, + env.Object, + pdfService + ); + + var result = await pdfController.GetPdfPreview(org, app, partyId, instanceId); + + result.Should().BeOfType(typeof(NotFoundResult)); + handler + .Protected() + .Verify("SendAsync", Times.Never(), ItExpr.IsAny(), ItExpr.IsAny()); + } - result.Should().BeOfType(typeof(NotFoundResult)); + [Fact] + public async Task Request_In_Dev_Should_Generate() + { + var env = new Mock(); + env.Setup(a => a.EnvironmentName).Returns("Development"); + + IOptions generalSettingsOptions = Microsoft.Extensions.Options.Options.Create( + new() { HostName = "local.altinn.cloud" } + ); + + var httpContextAccessor = new Mock(); + httpContextAccessor.Setup(x => x.HttpContext!.Request!.Query["lang"]).Returns("nb"); + string? frontendVersion = null; + httpContextAccessor + .Setup(x => x.HttpContext!.Request!.Cookies.TryGetValue("frontendVersion", out frontendVersion)) + .Returns(false); + + var handler = new Mock(); + var httpClient = new HttpClient(handler.Object); + + var pdfGeneratorClient = new PdfGeneratorClient( + httpClient, + _pdfGeneratorSettingsOptions, + _platformSettingsOptions, + _userTokenProvider.Object, + httpContextAccessor.Object + ); + var pdfService = new PdfService( + _appResources.Object, + _dataClient.Object, + httpContextAccessor.Object, + _profile.Object, + pdfGeneratorClient, + _pdfGeneratorSettingsOptions, + generalSettingsOptions + ); + var pdfController = new PdfController( + _instanceClient.Object, + _pdfFormatter.Object, + _appResources.Object, + _appModel.Object, + _dataClient.Object, + env.Object, + pdfService + ); + + string? requestBody = null; + using ( + var mockResponse = new HttpResponseMessage() + { + StatusCode = System.Net.HttpStatusCode.OK, + Content = new StringContent("PDF") + } + ) + { handler .Protected() - .Verify( + .Setup>( "SendAsync", - Times.Never(), ItExpr.IsAny(), ItExpr.IsAny() - ); + ) + .Callback( + (m, c) => requestBody = m.Content!.ReadAsStringAsync().Result + ) + .ReturnsAsync(mockResponse); + + var result = await pdfController.GetPdfPreview(org, app, partyId, instanceId); + result.Should().BeOfType(typeof(FileStreamResult)); } - [Fact] - public async Task Request_In_Dev_Should_Generate() - { - var env = new Mock(); - env.Setup(a => a.EnvironmentName).Returns("Development"); - - IOptions generalSettingsOptions = - Microsoft.Extensions.Options.Options.Create(new() { HostName = "local.altinn.cloud" }); - - var httpContextAccessor = new Mock(); - httpContextAccessor.Setup(x => x.HttpContext!.Request!.Query["lang"]).Returns("nb"); - string? frontendVersion = null; - httpContextAccessor - .Setup(x => x.HttpContext!.Request!.Cookies.TryGetValue("frontendVersion", out frontendVersion)) - .Returns(false); - - var handler = new Mock(); - var httpClient = new HttpClient(handler.Object); - - var pdfGeneratorClient = new PdfGeneratorClient( - httpClient, - _pdfGeneratorSettingsOptions, - _platformSettingsOptions, - _userTokenProvider.Object, - httpContextAccessor.Object - ); - var pdfService = new PdfService( - _appResources.Object, - _dataClient.Object, - httpContextAccessor.Object, - _profile.Object, - pdfGeneratorClient, - _pdfGeneratorSettingsOptions, - generalSettingsOptions - ); - var pdfController = new PdfController( - _instanceClient.Object, - _pdfFormatter.Object, - _appResources.Object, - _appModel.Object, - _dataClient.Object, - env.Object, - pdfService + requestBody + .Should() + .Contain( + @"url"":""http://local.altinn.cloud/org/app/#/instance/12345/e11e3e0b-a45c-48fb-a968-8d4ddf868c80?pdf=1" ); + requestBody.Should().NotContain(@"name"":""frontendVersion"); + } - string? requestBody = null; - using ( - var mockResponse = new HttpResponseMessage() - { - StatusCode = System.Net.HttpStatusCode.OK, - Content = new StringContent("PDF") - } - ) + [Fact] + public async Task Request_In_Dev_Should_Include_Frontend_Version() + { + var env = new Mock(); + env.Setup(a => a.EnvironmentName).Returns("Development"); + + IOptions generalSettingsOptions = Microsoft.Extensions.Options.Options.Create( + new() { HostName = "local.altinn.cloud" } + ); + + var httpContextAccessor = new Mock(); + httpContextAccessor.Setup(x => x.HttpContext!.Request!.Query["lang"]).Returns("nb"); + string? frontendVersion = "https://altinncdn.no/toolkits/altinn-app-frontend/3/"; + httpContextAccessor + .Setup(x => x.HttpContext!.Request!.Cookies.TryGetValue("frontendVersion", out frontendVersion)) + .Returns(true); + + var handler = new Mock(); + var httpClient = new HttpClient(handler.Object); + + var pdfGeneratorClient = new PdfGeneratorClient( + httpClient, + _pdfGeneratorSettingsOptions, + _platformSettingsOptions, + _userTokenProvider.Object, + httpContextAccessor.Object + ); + var pdfService = new PdfService( + _appResources.Object, + _dataClient.Object, + httpContextAccessor.Object, + _profile.Object, + pdfGeneratorClient, + _pdfGeneratorSettingsOptions, + generalSettingsOptions + ); + var pdfController = new PdfController( + _instanceClient.Object, + _pdfFormatter.Object, + _appResources.Object, + _appModel.Object, + _dataClient.Object, + env.Object, + pdfService + ); + + string? requestBody = null; + using ( + var mockResponse = new HttpResponseMessage() { - handler - .Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny() - ) - .Callback( - (m, c) => requestBody = m.Content!.ReadAsStringAsync().Result - ) - .ReturnsAsync(mockResponse); - - var result = await pdfController.GetPdfPreview(org, app, partyId, instanceId); - result.Should().BeOfType(typeof(FileStreamResult)); + StatusCode = System.Net.HttpStatusCode.OK, + Content = new StringContent("PDF") } + ) + { + handler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .Callback( + (m, c) => requestBody = m.Content!.ReadAsStringAsync().Result + ) + .ReturnsAsync(mockResponse); - requestBody - .Should() - .Contain( - @"url"":""http://local.altinn.cloud/org/app/#/instance/12345/e11e3e0b-a45c-48fb-a968-8d4ddf868c80?pdf=1" - ); - requestBody.Should().NotContain(@"name"":""frontendVersion"); + var result = await pdfController.GetPdfPreview(org, app, partyId, instanceId); + result.Should().BeOfType(typeof(FileStreamResult)); } - [Fact] - public async Task Request_In_Dev_Should_Include_Frontend_Version() - { - var env = new Mock(); - env.Setup(a => a.EnvironmentName).Returns("Development"); - - IOptions generalSettingsOptions = - Microsoft.Extensions.Options.Options.Create(new() { HostName = "local.altinn.cloud" }); - - var httpContextAccessor = new Mock(); - httpContextAccessor.Setup(x => x.HttpContext!.Request!.Query["lang"]).Returns("nb"); - string? frontendVersion = "https://altinncdn.no/toolkits/altinn-app-frontend/3/"; - httpContextAccessor - .Setup(x => x.HttpContext!.Request!.Cookies.TryGetValue("frontendVersion", out frontendVersion)) - .Returns(true); - - var handler = new Mock(); - var httpClient = new HttpClient(handler.Object); - - var pdfGeneratorClient = new PdfGeneratorClient( - httpClient, - _pdfGeneratorSettingsOptions, - _platformSettingsOptions, - _userTokenProvider.Object, - httpContextAccessor.Object - ); - var pdfService = new PdfService( - _appResources.Object, - _dataClient.Object, - httpContextAccessor.Object, - _profile.Object, - pdfGeneratorClient, - _pdfGeneratorSettingsOptions, - generalSettingsOptions - ); - var pdfController = new PdfController( - _instanceClient.Object, - _pdfFormatter.Object, - _appResources.Object, - _appModel.Object, - _dataClient.Object, - env.Object, - pdfService + requestBody + .Should() + .Contain( + @"url"":""http://local.altinn.cloud/org/app/#/instance/12345/e11e3e0b-a45c-48fb-a968-8d4ddf868c80?pdf=1" ); + requestBody + .Should() + .Contain(@"name"":""frontendVersion"",""value"":""https://altinncdn.no/toolkits/altinn-app-frontend/3/"""); + } - string? requestBody = null; - using ( - var mockResponse = new HttpResponseMessage() - { - StatusCode = System.Net.HttpStatusCode.OK, - Content = new StringContent("PDF") - } - ) + [Fact] + public async Task Request_In_TT02_Should_Ignore_Frontend_Version() + { + var env = new Mock(); + env.Setup(a => a.EnvironmentName).Returns("Staging"); + + IOptions generalSettingsOptions = Microsoft.Extensions.Options.Options.Create( + new() { HostName = "org.apps.tt02.altinn.no" } + ); + + var httpContextAccessor = new Mock(); + httpContextAccessor.Setup(x => x.HttpContext!.Request!.Query["lang"]).Returns("nb"); + string? frontendVersion = "https://altinncdn.no/toolkits/altinn-app-frontend/3/"; + httpContextAccessor + .Setup(x => x.HttpContext!.Request!.Cookies.TryGetValue("frontendVersion", out frontendVersion)) + .Returns(true); + + var handler = new Mock(); + var httpClient = new HttpClient(handler.Object); + + var pdfGeneratorClient = new PdfGeneratorClient( + httpClient, + _pdfGeneratorSettingsOptions, + _platformSettingsOptions, + _userTokenProvider.Object, + httpContextAccessor.Object + ); + var pdfService = new PdfService( + _appResources.Object, + _dataClient.Object, + httpContextAccessor.Object, + _profile.Object, + pdfGeneratorClient, + _pdfGeneratorSettingsOptions, + generalSettingsOptions + ); + var pdfController = new PdfController( + _instanceClient.Object, + _pdfFormatter.Object, + _appResources.Object, + _appModel.Object, + _dataClient.Object, + env.Object, + pdfService + ); + + string? requestBody = null; + using ( + var mockResponse = new HttpResponseMessage() { - handler - .Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny() - ) - .Callback( - (m, c) => requestBody = m.Content!.ReadAsStringAsync().Result - ) - .ReturnsAsync(mockResponse); - - var result = await pdfController.GetPdfPreview(org, app, partyId, instanceId); - result.Should().BeOfType(typeof(FileStreamResult)); + StatusCode = System.Net.HttpStatusCode.OK, + Content = new StringContent("PDF") } + ) + { + handler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .Callback( + (m, c) => requestBody = m.Content!.ReadAsStringAsync().Result + ) + .ReturnsAsync(mockResponse); - requestBody - .Should() - .Contain( - @"url"":""http://local.altinn.cloud/org/app/#/instance/12345/e11e3e0b-a45c-48fb-a968-8d4ddf868c80?pdf=1" - ); - requestBody - .Should() - .Contain( - @"name"":""frontendVersion"",""value"":""https://altinncdn.no/toolkits/altinn-app-frontend/3/""" - ); + var result = await pdfController.GetPdfPreview(org, app, partyId, instanceId); + result.Should().BeOfType(typeof(FileStreamResult)); } - [Fact] - public async Task Request_In_TT02_Should_Ignore_Frontend_Version() - { - var env = new Mock(); - env.Setup(a => a.EnvironmentName).Returns("Staging"); - - IOptions generalSettingsOptions = - Microsoft.Extensions.Options.Options.Create( - new() { HostName = "org.apps.tt02.altinn.no" } - ); - - var httpContextAccessor = new Mock(); - httpContextAccessor.Setup(x => x.HttpContext!.Request!.Query["lang"]).Returns("nb"); - string? frontendVersion = "https://altinncdn.no/toolkits/altinn-app-frontend/3/"; - httpContextAccessor - .Setup(x => x.HttpContext!.Request!.Cookies.TryGetValue("frontendVersion", out frontendVersion)) - .Returns(true); - - var handler = new Mock(); - var httpClient = new HttpClient(handler.Object); - - var pdfGeneratorClient = new PdfGeneratorClient( - httpClient, - _pdfGeneratorSettingsOptions, - _platformSettingsOptions, - _userTokenProvider.Object, - httpContextAccessor.Object - ); - var pdfService = new PdfService( - _appResources.Object, - _dataClient.Object, - httpContextAccessor.Object, - _profile.Object, - pdfGeneratorClient, - _pdfGeneratorSettingsOptions, - generalSettingsOptions + requestBody + .Should() + .Contain( + @"url"":""http://org.apps.tt02.altinn.no/org/app/#/instance/12345/e11e3e0b-a45c-48fb-a968-8d4ddf868c80?pdf=1" ); - var pdfController = new PdfController( - _instanceClient.Object, - _pdfFormatter.Object, - _appResources.Object, - _appModel.Object, - _dataClient.Object, - env.Object, - pdfService - ); - - string? requestBody = null; - using ( - var mockResponse = new HttpResponseMessage() - { - StatusCode = System.Net.HttpStatusCode.OK, - Content = new StringContent("PDF") - } - ) - { - handler - .Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny() - ) - .Callback( - (m, c) => requestBody = m.Content!.ReadAsStringAsync().Result - ) - .ReturnsAsync(mockResponse); - - var result = await pdfController.GetPdfPreview(org, app, partyId, instanceId); - result.Should().BeOfType(typeof(FileStreamResult)); - } - - requestBody - .Should() - .Contain( - @"url"":""http://org.apps.tt02.altinn.no/org/app/#/instance/12345/e11e3e0b-a45c-48fb-a968-8d4ddf868c80?pdf=1" - ); - requestBody.Should().NotContain(@"name"":""frontendVersion"); - } + requestBody.Should().NotContain(@"name"":""frontendVersion"); } } diff --git a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs index 66d7bd62c..a9418fb59 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Moq; -using Xunit; using Xunit.Abstractions; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs index 71c12f373..98f864836 100644 --- a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs @@ -22,7 +22,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Xunit; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs index 6e5afd698..a9956f0c3 100644 --- a/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs @@ -7,7 +7,6 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Moq; -using Xunit; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs index 99be87e27..e614ae0e4 100644 --- a/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs @@ -4,7 +4,6 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Moq; -using Xunit; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs index 2dbcb58cc..69094a4b8 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs @@ -9,7 +9,6 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Moq; -using Xunit; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs index 662a971ab..688e1f35e 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs @@ -10,7 +10,6 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Moq; -using Xunit; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs index c6ed3a767..9b991b5a9 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs @@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Moq; -using Xunit; using Xunit.Abstractions; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Data/TestData.cs b/test/Altinn.App.Api.Tests/Data/TestData.cs index bbe083584..ba491d121 100644 --- a/test/Altinn.App.Api.Tests/Data/TestData.cs +++ b/test/Altinn.App.Api.Tests/Data/TestData.cs @@ -1,7 +1,5 @@ -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; -using Altinn.App.Api.Tests.Mocks; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/App.cs b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/App.cs index ccb4a224e..9c67d26b3 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/App.cs +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/App.cs @@ -1,48 +1,47 @@ using Altinn.App.Core.Internal.AppModel; using Microsoft.Extensions.Logging; -namespace App.IntegrationTests.Mocks.Apps.Ttd.EFormidling +namespace App.IntegrationTests.Mocks.Apps.Ttd.EFormidling; + +/// +/// Represents the core logic of an App +/// +public class App : IAppModel { + private readonly ILogger _logger; + /// - /// Represents the core logic of an App + /// Initialize a new instance of the class. /// - public class App : IAppModel + /// A logger from the built in LoggingFactory. + public App(ILogger logger) { - private readonly ILogger _logger; - - /// - /// Initialize a new instance of the class. - /// - /// A logger from the built in LoggingFactory. - public App(ILogger logger) - { - _logger = logger; - } - - /// - public object Create(string classRef) - { - _logger.LogInformation("CreateNewAppModel {classRef}", classRef); + _logger = logger; + } - Type? appType = Type.GetType(classRef); + /// + public object Create(string classRef) + { + _logger.LogInformation("CreateNewAppModel {classRef}", classRef); - if (appType == null) - { - throw new ArgumentException($"Could not find type {classRef}"); - } + Type? appType = Type.GetType(classRef); - object? appInstance = Activator.CreateInstance(appType); - return appInstance ?? throw new ArgumentException($"Could not create instance of {classRef}"); + if (appType == null) + { + throw new ArgumentException($"Could not find type {classRef}"); } - /// - public Type GetModelType(string classRef) - { - _logger.LogInformation("GetAppModelType {classRef}", classRef); + object? appInstance = Activator.CreateInstance(appType); + return appInstance ?? throw new ArgumentException($"Could not create instance of {classRef}"); + } + + /// + public Type GetModelType(string classRef) + { + _logger.LogInformation("GetAppModelType {classRef}", classRef); #pragma warning disable CS8603 // Possible null reference return. - return Type.GetType(classRef); + return Type.GetType(classRef); #pragma warning restore CS8603 // Possible null reference return. - } } } diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/EFormidlingMetadata.cs b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/EFormidlingMetadata.cs index 56bd9945c..642f7c2cc 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/EFormidlingMetadata.cs +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/EFormidlingMetadata.cs @@ -1,84 +1,78 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; using System.Net.Http.Headers; -using System.Threading.Tasks; using System.Xml.Serialization; using Altinn.App.Core.EFormidling.Interface; using Altinn.Common.EFormidlingClient.Models; using Altinn.Platform.Storage.Interface.Models; -namespace App.IntegrationTests.Mocks.Apps.Ttd.EFormidling +namespace App.IntegrationTests.Mocks.Apps.Ttd.EFormidling; + +public class EFormidlingMetadata : IEFormidlingMetadata { - public class EFormidlingMetadata : IEFormidlingMetadata + public async Task<(string MetadataFilename, Stream Metadata)> GenerateEFormidlingMetadata(Instance instance) { - public async Task<(string MetadataFilename, Stream Metadata)> GenerateEFormidlingMetadata(Instance instance) + var arkivmelding = new Arkivmelding { - var arkivmelding = new Arkivmelding + AntallFiler = 1, + Tidspunkt = DateTime.Now.ToString(), + MeldingId = Guid.NewGuid().ToString(), + System = "LandLord", + Mappe = new List { - AntallFiler = 1, - Tidspunkt = DateTime.Now.ToString(), - MeldingId = Guid.NewGuid().ToString(), - System = "LandLord", - Mappe = new List + new Mappe { - new Mappe + SystemID = Guid.NewGuid().ToString(), + Tittel = "Dette er en tittel", + OpprettetDato = DateTime.Now.ToString(), + Type = "saksmappe", + Basisregistrering = new Basisregistrering { + Type = "journalpost", SystemID = Guid.NewGuid().ToString(), - Tittel = "Dette er en tittel", - OpprettetDato = DateTime.Now.ToString(), - Type = "saksmappe", - Basisregistrering = new Basisregistrering + OpprettetDato = DateTime.UtcNow, + OpprettetAv = "LandLord", + ArkivertDato = DateTime.Now, + ArkivertAv = "LandLord", + Dokumentbeskrivelse = new Dokumentbeskrivelse { - Type = "journalpost", SystemID = Guid.NewGuid().ToString(), + Dokumenttype = "Bestilling", + Dokumentstatus = "Dokumentet er ferdigstilt", + Tittel = "Hei", OpprettetDato = DateTime.UtcNow, OpprettetAv = "LandLord", - ArkivertDato = DateTime.Now, - ArkivertAv = "LandLord", - Dokumentbeskrivelse = new Dokumentbeskrivelse + TilknyttetRegistreringSom = "hoveddokument", + Dokumentnummer = 1, + TilknyttetDato = DateTime.Now, + TilknyttetAv = "Landlord", + Dokumentobjekt = new Dokumentobjekt { - SystemID = Guid.NewGuid().ToString(), - Dokumenttype = "Bestilling", - Dokumentstatus = "Dokumentet er ferdigstilt", - Tittel = "Hei", + Versjonsnummer = 1, + Variantformat = "Produksjonsformat", OpprettetDato = DateTime.UtcNow, OpprettetAv = "LandLord", - TilknyttetRegistreringSom = "hoveddokument", - Dokumentnummer = 1, - TilknyttetDato = DateTime.Now, - TilknyttetAv = "Landlord", - Dokumentobjekt = new Dokumentobjekt - { - Versjonsnummer = 1, - Variantformat = "Produksjonsformat", - OpprettetDato = DateTime.UtcNow, - OpprettetAv = "LandLord", - ReferanseDokumentfil = "skjema.xml" - }, + ReferanseDokumentfil = "skjema.xml" }, - Tittel = "Nye lysrør", - OffentligTittel = "Nye lysrør", - Journalposttype = "Utgående dokument", - Journalstatus = "Journalført", - Journaldato = DateTime.Now }, + Tittel = "Nye lysrør", + OffentligTittel = "Nye lysrør", + Journalposttype = "Utgående dokument", + Journalstatus = "Journalført", + Journaldato = DateTime.Now }, }, - }; + }, + }; - var stream = new MemoryStream(); + var stream = new MemoryStream(); - var serializer = new XmlSerializer(typeof(Arkivmelding)); + var serializer = new XmlSerializer(typeof(Arkivmelding)); - serializer.Serialize(stream, arkivmelding); - stream.Position = 0; + serializer.Serialize(stream, arkivmelding); + stream.Position = 0; - var streamContent = new StreamContent(stream); - streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/xml"); + var streamContent = new StreamContent(stream); + streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/xml"); - return await Task.FromResult(("arkivmelding.xml", stream)); - } + return await Task.FromResult(("arkivmelding.xml", stream)); } } diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/models/Skjema.cs b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/models/Skjema.cs index 3fc9b311d..207e8882d 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/models/Skjema.cs +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/models/Skjema.cs @@ -1,18 +1,16 @@ -using System; using System.ComponentModel.DataAnnotations; using System.Xml.Serialization; using Microsoft.AspNetCore.Mvc.ModelBinding; -namespace Altinn.App.IntegrationTests.Mocks.Ttd.EFormidling +namespace Altinn.App.IntegrationTests.Mocks.Ttd.EFormidling; + +/// +/// The skjema +/// +public class Skjema { - /// - /// The skjema - /// - public class Skjema - { - [Range(int.MinValue, int.MaxValue)] - [XmlAttribute("skjemanummer")] - [BindNever] - public decimal Skjemanummer { get; set; } = 1472; - } + [Range(int.MinValue, int.MaxValue)] + [XmlAttribute("skjemanummer")] + [BindNever] + public decimal Skjemanummer { get; set; } = 1472; } diff --git a/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs b/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs index 020c78957..3c623bf73 100644 --- a/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs +++ b/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs @@ -14,7 +14,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using Xunit; namespace Altinn.App.Api.Tests.EFormidling; diff --git a/test/Altinn.App.Api.Tests/Helpers/RequestHandling/DataRestrictionValidationTests.cs b/test/Altinn.App.Api.Tests/Helpers/RequestHandling/DataRestrictionValidationTests.cs index a8be67fc9..48e2fdd4a 100644 --- a/test/Altinn.App.Api.Tests/Helpers/RequestHandling/DataRestrictionValidationTests.cs +++ b/test/Altinn.App.Api.Tests/Helpers/RequestHandling/DataRestrictionValidationTests.cs @@ -1,12 +1,8 @@ -using System.Collections.Generic; -using System.Configuration; using Altinn.App.Api.Helpers.RequestHandling; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Xunit; namespace Altinn.App.Api.Tests.Helpers.RequestHandling; diff --git a/test/Altinn.App.Api.Tests/Helpers/StartupHelperTests.cs b/test/Altinn.App.Api.Tests/Helpers/StartupHelperTests.cs index edfdf3d7a..7611fcca7 100644 --- a/test/Altinn.App.Api.Tests/Helpers/StartupHelperTests.cs +++ b/test/Altinn.App.Api.Tests/Helpers/StartupHelperTests.cs @@ -1,8 +1,6 @@ -using System; using Altinn.App.Api.Helpers; using Altinn.App.Api.Tests.TestStubs; using FluentAssertions; -using Xunit; namespace Altinn.App.Api.Tests.Helpers; diff --git a/test/Altinn.App.Api.Tests/Mappers/SimpleInstanceMapperTests.cs b/test/Altinn.App.Api.Tests/Mappers/SimpleInstanceMapperTests.cs index c4f25a54a..581073b0d 100644 --- a/test/Altinn.App.Api.Tests/Mappers/SimpleInstanceMapperTests.cs +++ b/test/Altinn.App.Api.Tests/Mappers/SimpleInstanceMapperTests.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; using Altinn.App.Api.Mappers; using Altinn.App.Api.Models; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.Api.Tests.Mappers; diff --git a/test/Altinn.App.Api.Tests/Middleware/SecurityHeadersMiddlewareTests.cs b/test/Altinn.App.Api.Tests/Middleware/SecurityHeadersMiddlewareTests.cs index 138c97d53..f5e35902d 100644 --- a/test/Altinn.App.Api.Tests/Middleware/SecurityHeadersMiddlewareTests.cs +++ b/test/Altinn.App.Api.Tests/Middleware/SecurityHeadersMiddlewareTests.cs @@ -1,14 +1,9 @@ -using System.Linq; using System.Net; -using System.Threading.Tasks; using Altinn.App.Api.Infrastructure.Middleware; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; -using Moq; -using Xunit; namespace Altinn.App.Api.Tests.Middleware; diff --git a/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs b/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs index 963ad2205..f748a26aa 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs @@ -10,165 +10,162 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Options; -namespace App.IntegrationTests.Mocks.Services +namespace App.IntegrationTests.Mocks.Services; + +public class AppMetadataMock : IAppMetadata { - public class AppMetadataMock : IAppMetadata + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + }; + + private readonly AppSettings _settings; + private readonly IFrontendFeatures _frontendFeatures; + private ApplicationMetadata? _application; + private readonly IHttpContextAccessor _contextAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// The app repository settings. + /// Application features service + public AppMetadataMock( + IOptions settings, + IFrontendFeatures frontendFeatures, + IHttpContextAccessor httpContextAccessor + ) { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - AllowTrailingCommas = true - }; - - private readonly AppSettings _settings; - private readonly IFrontendFeatures _frontendFeatures; - private ApplicationMetadata? _application; - private readonly IHttpContextAccessor _contextAccessor; - - /// - /// Initializes a new instance of the class. - /// - /// The app repository settings. - /// Application features service - public AppMetadataMock( - IOptions settings, - IFrontendFeatures frontendFeatures, - IHttpContextAccessor httpContextAccessor - ) + _settings = settings.Value; + _frontendFeatures = frontendFeatures; + _contextAccessor = httpContextAccessor; + } + + /// + /// Thrown if deserialization fails + /// Thrown if applicationmetadata.json file not found + public async Task GetApplicationMetadata() + { + // Cache application metadata + if (_application != null) { - _settings = settings.Value; - _frontendFeatures = frontendFeatures; - _contextAccessor = httpContextAccessor; + return _application; } - /// - /// Thrown if deserialization fails - /// Thrown if applicationmetadata.json file not found - public async Task GetApplicationMetadata() + if (_contextAccessor.HttpContext == null) { - // Cache application metadata - if (_application != null) - { - return _application; - } - - if (_contextAccessor.HttpContext == null) - { - throw new Exception("HttpContext is null"); - } + throw new Exception("HttpContext is null"); + } - AppIdentifier appIdentifier = AppIdentifier.CreateFromUrl( - _contextAccessor.HttpContext.Request.GetDisplayUrl() - ); - string filename = TestData.GetApplicationMetadataPath(appIdentifier.Org, appIdentifier.App); + AppIdentifier appIdentifier = AppIdentifier.CreateFromUrl(_contextAccessor.HttpContext.Request.GetDisplayUrl()); + string filename = TestData.GetApplicationMetadataPath(appIdentifier.Org, appIdentifier.App); - try + try + { + if (File.Exists(filename)) { - if (File.Exists(filename)) + using FileStream fileStream = File.OpenRead(filename); + var application = await JsonSerializer.DeserializeAsync( + fileStream, + _jsonSerializerOptions + ); + if (application == null) { - using FileStream fileStream = File.OpenRead(filename); - var application = await JsonSerializer.DeserializeAsync( - fileStream, - _jsonSerializerOptions + throw new ApplicationConfigException( + $"Deserialization returned null, Could indicate problems with deserialization of {filename}" ); - if (application == null) - { - throw new ApplicationConfigException( - $"Deserialization returned null, Could indicate problems with deserialization of {filename}" - ); - } - - application.Features = await _frontendFeatures.GetFrontendFeatures(); - _application = application; - - return _application; } - throw new ApplicationConfigException($"Unable to locate application metadata file: {filename}"); - } - catch (JsonException ex) - { - throw new ApplicationConfigException( - $"Something went wrong when parsing application metadata file: {filename}", - ex - ); - } - } + application.Features = await _frontendFeatures.GetFrontendFeatures(); + _application = application; - /// - public async Task GetApplicationXACMLPolicy() - { - string filename = Path.Join( - _settings.AppBasePath, - _settings.ConfigurationFolder, - _settings.AuthorizationFolder, - _settings.ApplicationXACMLPolicyFileName - ); - if (File.Exists(filename)) - { - return await File.ReadAllTextAsync(filename, Encoding.UTF8); + return _application; } - throw new FileNotFoundException($"XACML file {filename} not found"); + throw new ApplicationConfigException($"Unable to locate application metadata file: {filename}"); } - - /// - public async Task GetApplicationBPMNProcess() + catch (JsonException ex) { - string filename = Path.Join( - _settings.AppBasePath, - _settings.ConfigurationFolder, - _settings.ProcessFolder, - _settings.ProcessFileName + throw new ApplicationConfigException( + $"Something went wrong when parsing application metadata file: {filename}", + ex ); - if (File.Exists(filename)) - { - return await File.ReadAllTextAsync(filename, Encoding.UTF8); - } - - throw new ApplicationConfigException($"Unable to locate application process file: {filename}"); } + } - public static Task GetApplication(string org, string app) + /// + public async Task GetApplicationXACMLPolicy() + { + string filename = Path.Join( + _settings.AppBasePath, + _settings.ConfigurationFolder, + _settings.AuthorizationFolder, + _settings.ApplicationXACMLPolicyFileName + ); + if (File.Exists(filename)) { - return Task.FromResult(GetTestApplication(org, app)); + return await File.ReadAllTextAsync(filename, Encoding.UTF8); } - private static Application GetTestApplication(string org, string app) + throw new FileNotFoundException($"XACML file {filename} not found"); + } + + /// + public async Task GetApplicationBPMNProcess() + { + string filename = Path.Join( + _settings.AppBasePath, + _settings.ConfigurationFolder, + _settings.ProcessFolder, + _settings.ProcessFileName + ); + if (File.Exists(filename)) { - string applicationPath = Path.Combine(GetMetadataPath(), org, app, "applicationmetadata.json"); - if (File.Exists(applicationPath)) - { - string content = - File.ReadAllText(applicationPath) - ?? throw new Exception( - $"Unable to read application metadata file for {org}/{app}. Tried path: {applicationPath}" - ); + return await File.ReadAllTextAsync(filename, Encoding.UTF8); + } - if (JsonSerializer.Deserialize(content) is not Application application) - { - throw new Exception( - $"Unable to deserialize application metadata file for {org}/{app}. Tried path: {applicationPath}" - ); - } + throw new ApplicationConfigException($"Unable to locate application process file: {filename}"); + } - return application; + public static Task GetApplication(string org, string app) + { + return Task.FromResult(GetTestApplication(org, app)); + } + + private static Application GetTestApplication(string org, string app) + { + string applicationPath = Path.Combine(GetMetadataPath(), org, app, "applicationmetadata.json"); + if (File.Exists(applicationPath)) + { + string content = + File.ReadAllText(applicationPath) + ?? throw new Exception( + $"Unable to read application metadata file for {org}/{app}. Tried path: {applicationPath}" + ); + + if (JsonSerializer.Deserialize(content) is not Application application) + { + throw new Exception( + $"Unable to deserialize application metadata file for {org}/{app}. Tried path: {applicationPath}" + ); } - throw new Exception( - $"Unable to locate application metadata file for {org}/{app}. Tried path: {applicationPath}" - ); + return application; } - private static string GetMetadataPath() - { - var uri = new Uri(typeof(InstanceClientMockSi).Assembly.Location); - string unitTestFolder = - Path.GetDirectoryName(uri.LocalPath) ?? throw new Exception($"Unable to locate path {uri.LocalPath}"); + throw new Exception( + $"Unable to locate application metadata file for {org}/{app}. Tried path: {applicationPath}" + ); + } - return Path.Combine(unitTestFolder, @"../../../Data/Metadata"); - } + private static string GetMetadataPath() + { + var uri = new Uri(typeof(InstanceClientMockSi).Assembly.Location); + string unitTestFolder = + Path.GetDirectoryName(uri.LocalPath) ?? throw new Exception($"Unable to locate path {uri.LocalPath}"); + + return Path.Combine(unitTestFolder, @"../../../Data/Metadata"); } } diff --git a/test/Altinn.App.Api.Tests/Mocks/Authentication/ConfigurationManagerStub.cs b/test/Altinn.App.Api.Tests/Mocks/Authentication/ConfigurationManagerStub.cs index 99fbbf262..f0984404c 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Authentication/ConfigurationManagerStub.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Authentication/ConfigurationManagerStub.cs @@ -1,49 +1,44 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; -namespace Altinn.App.Api.Tests.Mocks.Authentication +namespace Altinn.App.Api.Tests.Mocks.Authentication; + +/// +/// Represents a stub of to be used in integration tests. +/// +public class ConfigurationManagerStub : IConfigurationManager { /// - /// Represents a stub of to be used in integration tests. + /// Initializes a new instance of /// - public class ConfigurationManagerStub : IConfigurationManager + /// The address to obtain configuration. + /// The + /// The that reaches out to obtain the configuration. + public ConfigurationManagerStub( + string metadataAddress, + IConfigurationRetriever configRetriever, + IDocumentRetriever docRetriever + ) { } + + /// + public async Task GetConfigurationAsync(CancellationToken cancel) { - /// - /// Initializes a new instance of - /// - /// The address to obtain configuration. - /// The - /// The that reaches out to obtain the configuration. - public ConfigurationManagerStub( - string metadataAddress, - IConfigurationRetriever configRetriever, - IDocumentRetriever docRetriever - ) { } + SigningKeysRetrieverStub signingKeysRetriever = new SigningKeysRetrieverStub(); + ICollection signingKeys = await signingKeysRetriever.GetSigningKeys(string.Empty); - /// - public async Task GetConfigurationAsync(CancellationToken cancel) + OpenIdConnectConfiguration configuration = new OpenIdConnectConfiguration(); + foreach (var securityKey in signingKeys) { - SigningKeysRetrieverStub signingKeysRetriever = new SigningKeysRetrieverStub(); - ICollection signingKeys = await signingKeysRetriever.GetSigningKeys(string.Empty); - - OpenIdConnectConfiguration configuration = new OpenIdConnectConfiguration(); - foreach (var securityKey in signingKeys) - { - configuration.SigningKeys.Add(securityKey); - } - - return configuration; + configuration.SigningKeys.Add(securityKey); } - /// - public void RequestRefresh() - { - throw new NotImplementedException(); - } + return configuration; + } + + /// + public void RequestRefresh() + { + throw new NotImplementedException(); } } diff --git a/test/Altinn.App.Api.Tests/Mocks/Authentication/ISigningKeysRetriever.cs b/test/Altinn.App.Api.Tests/Mocks/Authentication/ISigningKeysRetriever.cs index 8415a4fe8..f862d62e0 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Authentication/ISigningKeysRetriever.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Authentication/ISigningKeysRetriever.cs @@ -1,19 +1,16 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.IdentityModel.Tokens; -namespace Altinn.App.Api.Tests.Mocks.Authentication +namespace Altinn.App.Api.Tests.Mocks.Authentication; + +/// +/// Defines methods to retrieve signing keys from a remote well-known OpenID configuration endpoint +/// +public interface ISigningKeysRetriever { /// - /// Defines methods to retrieve signing keys from a remote well-known OpenID configuration endpoint + /// Get the signing keys published by the given endpoint. /// - public interface ISigningKeysRetriever - { - /// - /// Get the signing keys published by the given endpoint. - /// - /// The full address of the published configuration. - /// - Task> GetSigningKeys(string url); - } + /// The full address of the published configuration. + /// + Task> GetSigningKeys(string url); } diff --git a/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs b/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs index b69a60df2..0a7afbfaa 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs @@ -1,44 +1,42 @@ -using System; using AltinnCore.Authentication.JwtCookie; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; -namespace Altinn.App.Api.Tests.Mocks.Authentication +namespace Altinn.App.Api.Tests.Mocks.Authentication; + +/// +/// Represents a stub for the class to be used in integration tests. +/// +public class JwtCookiePostConfigureOptionsStub : IPostConfigureOptions { - /// - /// Represents a stub for the class to be used in integration tests. - /// - public class JwtCookiePostConfigureOptionsStub : IPostConfigureOptions + /// + public void PostConfigure(string? name, JwtCookieOptions options) { - /// - public void PostConfigure(string? name, JwtCookieOptions options) + if (string.IsNullOrEmpty(options.JwtCookieName)) { - if (string.IsNullOrEmpty(options.JwtCookieName)) - { - options.JwtCookieName = JwtCookieDefaults.CookiePrefix + name; - } + options.JwtCookieName = JwtCookieDefaults.CookiePrefix + name; + } - if (options.CookieManager == null) - { - options.CookieManager = new ChunkingCookieManager(); - } + if (options.CookieManager == null) + { + options.CookieManager = new ChunkingCookieManager(); + } - if (!string.IsNullOrEmpty(options.MetadataAddress)) + if (!string.IsNullOrEmpty(options.MetadataAddress)) + { + if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) { - if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) - { - options.MetadataAddress += "/"; - } + options.MetadataAddress += "/"; } - - options.MetadataAddress += ".well-known/openid-configuration"; - options.ConfigurationManager = new ConfigurationManagerStub( - options.MetadataAddress, - new OpenIdConnectConfigurationRetriever(), - new HttpDocumentRetriever() - ); } + + options.MetadataAddress += ".well-known/openid-configuration"; + options.ConfigurationManager = new ConfigurationManagerStub( + options.MetadataAddress, + new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever() + ); } } diff --git a/test/Altinn.App.Api.Tests/Mocks/Authentication/SigningKeysRetrieverStub.cs b/test/Altinn.App.Api.Tests/Mocks/Authentication/SigningKeysRetrieverStub.cs index 2c0cec14d..ab9edaecb 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Authentication/SigningKeysRetrieverStub.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Authentication/SigningKeysRetrieverStub.cs @@ -1,24 +1,21 @@ -using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; using Microsoft.IdentityModel.Tokens; -namespace Altinn.App.Api.Tests.Mocks.Authentication +namespace Altinn.App.Api.Tests.Mocks.Authentication; + +/// +public class SigningKeysRetrieverStub : ISigningKeysRetriever { /// - public class SigningKeysRetrieverStub : ISigningKeysRetriever + public async Task> GetSigningKeys(string url) { - /// - public async Task> GetSigningKeys(string url) - { - List signingKeys = new List(); + List signingKeys = new List(); - X509Certificate2 cert = new X509Certificate2("JWTValidationCert.cer"); - SecurityKey key = new X509SecurityKey(cert); + X509Certificate2 cert = new X509Certificate2("JWTValidationCert.cer"); + SecurityKey key = new X509SecurityKey(cert); - signingKeys.Add(key); + signingKeys.Add(key); - return await Task.FromResult(signingKeys); - } + return await Task.FromResult(signingKeys); } } diff --git a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs index c1aa8f9b6..25c51b7da 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs @@ -4,70 +4,69 @@ using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Api.Tests.Mocks +namespace Altinn.App.Api.Tests.Mocks; + +public class AuthorizationMock : IAuthorizationClient { - public class AuthorizationMock : IAuthorizationClient + public Task?> GetPartyList(int userId) { - public Task?> GetPartyList(int userId) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); + } - public Task ValidateSelectedParty(int userId, int partyId) - { - bool? isvalid = userId != 1; + public Task ValidateSelectedParty(int userId, int partyId) + { + bool? isvalid = userId != 1; + + return Task.FromResult(isvalid); + } - return Task.FromResult(isvalid); + /// + /// Mock method that returns false for actions ending with _unauthorized, and true for all other actions. + /// + /// + /// + /// + /// + /// + /// + /// + public async Task AuthorizeAction( + AppIdentifier appIdentifier, + InstanceIdentifier instanceIdentifier, + ClaimsPrincipal user, + string action, + string? taskId = null + ) + { + await Task.CompletedTask; + if (action.EndsWith("_unauthorized")) + { + return false; } - /// - /// Mock method that returns false for actions ending with _unauthorized, and true for all other actions. - /// - /// - /// - /// - /// - /// - /// - /// - public async Task AuthorizeAction( - AppIdentifier appIdentifier, - InstanceIdentifier instanceIdentifier, - ClaimsPrincipal user, - string action, - string? taskId = null - ) + return true; + } + + public async Task> AuthorizeActions( + Instance instance, + ClaimsPrincipal user, + List actions + ) + { + await Task.CompletedTask; + Dictionary authorizedActions = new Dictionary(); + foreach (var action in actions) { - await Task.CompletedTask; if (action.EndsWith("_unauthorized")) { - return false; + authorizedActions.Add(action, false); } - - return true; - } - - public async Task> AuthorizeActions( - Instance instance, - ClaimsPrincipal user, - List actions - ) - { - await Task.CompletedTask; - Dictionary authorizedActions = new Dictionary(); - foreach (var action in actions) + else { - if (action.EndsWith("_unauthorized")) - { - authorizedActions.Add(action, false); - } - else - { - authorizedActions.Add(action, true); - } + authorizedActions.Add(action, true); } - - return authorizedActions; } + + return authorizedActions; } } diff --git a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs index 6d18d0bb1..2320af831 100644 --- a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs @@ -11,579 +11,561 @@ using Microsoft.Extensions.Logging; using DataElement = Altinn.Platform.Storage.Interface.Models.DataElement; -namespace App.IntegrationTests.Mocks.Services +namespace App.IntegrationTests.Mocks.Services; + +public class DataClientMock : IDataClient { - public class DataClientMock : IDataClient - { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IAppMetadata _appMetadata; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAppMetadata _appMetadata; - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - private readonly ILogger _logger; + private readonly ILogger _logger; - public DataClientMock( - IAppMetadata appMetadata, - IHttpContextAccessor httpContextAccessor, - ILogger logger - ) - { - _httpContextAccessor = httpContextAccessor; - _logger = logger; - _appMetadata = appMetadata; - } + public DataClientMock( + IAppMetadata appMetadata, + IHttpContextAccessor httpContextAccessor, + ILogger logger + ) + { + _httpContextAccessor = httpContextAccessor; + _logger = logger; + _appMetadata = appMetadata; + } - public async Task DeleteBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid - ) - { - return await DeleteData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, false); - } + public async Task DeleteBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid + ) + { + return await DeleteData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, false); + } - public async Task DeleteData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid, - bool delay - ) + public async Task DeleteData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid, + bool delay + ) + { + await Task.CompletedTask; + string dataElementPath = TestData.GetDataElementPath(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); + + if (delay) { - await Task.CompletedTask; - string dataElementPath = TestData.GetDataElementPath( - org, - app, - instanceOwnerPartyId, - instanceGuid, - dataGuid - ); + string fileContent = await File.ReadAllTextAsync(dataElementPath); - if (delay) + if (fileContent == null) { - string fileContent = await File.ReadAllTextAsync(dataElementPath); - - if (fileContent == null) - { - return false; - } - - if ( - JsonSerializer.Deserialize(fileContent, _jsonSerializerOptions) - is not DataElement dataElement - ) - { - throw new Exception( - $"Unable to deserialize data element for org: {org}/{app} party: {instanceOwnerPartyId} instance: {instanceGuid} data: {dataGuid}. Tried path: {dataElementPath}" - ); - } - - dataElement.DeleteStatus = new() { IsHardDeleted = true, HardDeleted = DateTime.UtcNow }; - - WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); - - return true; + return false; } - else + + if ( + JsonSerializer.Deserialize(fileContent, _jsonSerializerOptions) + is not DataElement dataElement + ) { - string dataBlobPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); + throw new Exception( + $"Unable to deserialize data element for org: {org}/{app} party: {instanceOwnerPartyId} instance: {instanceGuid} data: {dataGuid}. Tried path: {dataElementPath}" + ); + } - if (File.Exists(dataElementPath)) - { - File.Delete(dataElementPath); - } + dataElement.DeleteStatus = new() { IsHardDeleted = true, HardDeleted = DateTime.UtcNow }; - if (File.Exists(dataBlobPath)) - { - File.Delete(dataBlobPath); - } + WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); - return true; - } + return true; } - - public Task GetBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataId - ) + else { - string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId); + string dataBlobPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); - Stream ms = new MemoryStream(); - using (FileStream file = new(dataPath, FileMode.Open, FileAccess.Read)) + if (File.Exists(dataElementPath)) { - file.CopyTo(ms); + File.Delete(dataElementPath); } - return Task.FromResult(ms); - } - -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public async Task> GetBinaryDataList( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid - ) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - var dataElements = GetDataElements(org, app, instanceOwnerPartyId, instanceGuid); - List list = new(); - foreach (DataElement dataElement in dataElements) + if (File.Exists(dataBlobPath)) { - AttachmentList al = - new() - { - Type = dataElement.DataType, - Attachments = new List() - { - new Attachment() - { - Id = dataElement.Id, - Name = dataElement.Filename, - Size = dataElement.Size - } - } - }; - list.Add(al); + File.Delete(dataBlobPath); } - return list; + return true; } + } - public async Task GetFormData( - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - Guid dataId - ) - { - string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId); - using var sourceStream = File.Open(dataPath, FileMode.OpenOrCreate); - - ModelDeserializer deserializer = new(_logger, type); - var formData = await deserializer.DeserializeAsync(sourceStream, "application/xml"); - - // var formData = serializer.Deserialize(sourceStream); - return formData ?? throw new Exception("Unable to deserialize form data"); - } + public Task GetBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId) + { + string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId); - public async Task InsertFormData( - Instance instance, - string dataType, - T dataToSerialize, - Type type - ) + Stream ms = new MemoryStream(); + using (FileStream file = new(dataPath, FileMode.Open, FileAccess.Read)) { - Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - string app = instance.AppId.Split("/")[1]; - string org = instance.Org; - int instanceOwnerId = int.Parse(instance.InstanceOwner.PartyId); - - return await InsertFormData(dataToSerialize, instanceGuid, type, org, app, instanceOwnerId, dataType); + file.CopyTo(ms); } - public Task InsertFormData( - T dataToSerialize, - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - string dataType - ) - { - Guid dataGuid = Guid.NewGuid(); - string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); + return Task.FromResult(ms); + } - DataElement dataElement = +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + public async Task> GetBinaryDataList( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid + ) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + var dataElements = GetDataElements(org, app, instanceOwnerPartyId, instanceGuid); + List list = new(); + foreach (DataElement dataElement in dataElements) + { + AttachmentList al = new() { - Id = dataGuid.ToString(), - InstanceGuid = instanceGuid.ToString(), - DataType = dataType, - ContentType = "application/xml", + Type = dataElement.DataType, + Attachments = new List() + { + new Attachment() + { + Id = dataElement.Id, + Name = dataElement.Filename, + Size = dataElement.Size + } + } }; + list.Add(al); + } - try - { - Directory.CreateDirectory(dataPath + @"blob"); + return list; + } - using (Stream stream = File.Open(dataPath + @"blob/" + dataGuid, FileMode.Create, FileAccess.ReadWrite)) - { - DataClient.Serialize(dataToSerialize, type, stream); - } + public async Task GetFormData( + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + Guid dataId + ) + { + string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId); + using var sourceStream = File.Open(dataPath, FileMode.OpenOrCreate); - WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); - } - catch -#pragma warning disable S108 // Nested blocks of code should not be left empty - { } -#pragma warning restore S108 // Nested blocks of code should not be left empty + ModelDeserializer deserializer = new(_logger, type); + var formData = await deserializer.DeserializeAsync(sourceStream, "application/xml"); - return Task.FromResult(dataElement); - } + // var formData = serializer.Deserialize(sourceStream); + return formData ?? throw new Exception("Unable to deserialize form data"); + } - public Task UpdateData( - T dataToSerialize, - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - Guid dataGuid - ) - { - ArgumentNullException.ThrowIfNull(dataToSerialize); - string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); + public async Task InsertFormData(Instance instance, string dataType, T dataToSerialize, Type type) + { + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + string app = instance.AppId.Split("/")[1]; + string org = instance.Org; + int instanceOwnerId = int.Parse(instance.InstanceOwner.PartyId); - DataElement? dataElement = GetDataElements(org, app, instanceOwnerPartyId, instanceGuid) - .FirstOrDefault(de => de.Id == dataGuid.ToString()); + return await InsertFormData(dataToSerialize, instanceGuid, type, org, app, instanceOwnerId, dataType); + } - if (dataElement == null) + public Task InsertFormData( + T dataToSerialize, + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + string dataType + ) + { + Guid dataGuid = Guid.NewGuid(); + string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); + + DataElement dataElement = + new() { - throw new Exception( - $"Unable to find data element for org: {org}/{app} party: {instanceOwnerPartyId} instance: {instanceGuid} data: {dataGuid}" - ); - } + Id = dataGuid.ToString(), + InstanceGuid = instanceGuid.ToString(), + DataType = dataType, + ContentType = "application/xml", + }; + try + { Directory.CreateDirectory(dataPath + @"blob"); - using ( - Stream stream = File.Open( - dataPath + $@"blob{Path.DirectorySeparatorChar}" + dataGuid, - FileMode.Create, - FileAccess.ReadWrite - ) - ) + using (Stream stream = File.Open(dataPath + @"blob/" + dataGuid, FileMode.Create, FileAccess.ReadWrite)) { DataClient.Serialize(dataToSerialize, type, stream); } - dataElement.LastChanged = DateTime.UtcNow; WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); - - return Task.FromResult(dataElement); } + catch +#pragma warning disable S108 // Nested blocks of code should not be left empty + { } +#pragma warning restore S108 // Nested blocks of code should not be left empty - public async Task InsertBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - string dataType, - HttpRequest request - ) - { - Guid dataGuid = Guid.NewGuid(); - string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); - DataElement dataElement = - new() - { - Id = dataGuid.ToString(), - InstanceGuid = instanceGuid.ToString(), - DataType = dataType, - ContentType = request.ContentType - }; + return Task.FromResult(dataElement); + } - if (!Directory.Exists(Path.GetDirectoryName(dataPath))) - { - var directory = - Path.GetDirectoryName(dataPath) - ?? throw new Exception($"Unable to get directory name from path {dataPath}"); + public Task UpdateData( + T dataToSerialize, + Guid instanceGuid, + Type type, + string org, + string app, + int instanceOwnerPartyId, + Guid dataGuid + ) + { + ArgumentNullException.ThrowIfNull(dataToSerialize); + string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); - Directory.CreateDirectory(directory); - } + DataElement? dataElement = GetDataElements(org, app, instanceOwnerPartyId, instanceGuid) + .FirstOrDefault(de => de.Id == dataGuid.ToString()); - Directory.CreateDirectory(dataPath + @"blob"); + if (dataElement == null) + { + throw new Exception( + $"Unable to find data element for org: {org}/{app} party: {instanceOwnerPartyId} instance: {instanceGuid} data: {dataGuid}" + ); + } - long filesize; + Directory.CreateDirectory(dataPath + @"blob"); - using ( - Stream streamToWriteTo = File.Open( - dataPath + @"blob/" + dataGuid, - FileMode.OpenOrCreate, - FileAccess.ReadWrite, - FileShare.ReadWrite - ) + using ( + Stream stream = File.Open( + dataPath + $@"blob{Path.DirectorySeparatorChar}" + dataGuid, + FileMode.Create, + FileAccess.ReadWrite ) - { - await request.Body.CopyToAsync(streamToWriteTo); - streamToWriteTo.Flush(); - filesize = streamToWriteTo.Length; - streamToWriteTo.Close(); - } + ) + { + DataClient.Serialize(dataToSerialize, type, stream); + } - dataElement.Size = filesize; + dataElement.LastChanged = DateTime.UtcNow; + WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); - WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); + return Task.FromResult(dataElement); + } + + public async Task InsertBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + string dataType, + HttpRequest request + ) + { + Guid dataGuid = Guid.NewGuid(); + string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); + DataElement dataElement = + new() + { + Id = dataGuid.ToString(), + InstanceGuid = instanceGuid.ToString(), + DataType = dataType, + ContentType = request.ContentType + }; - return dataElement; + if (!Directory.Exists(Path.GetDirectoryName(dataPath))) + { + var directory = + Path.GetDirectoryName(dataPath) + ?? throw new Exception($"Unable to get directory name from path {dataPath}"); + + Directory.CreateDirectory(directory); } - public async Task InsertBinaryData( - string instanceId, - string dataType, - string contentType, - string filename, - Stream stream + Directory.CreateDirectory(dataPath + @"blob"); + + long filesize; + + using ( + Stream streamToWriteTo = File.Open( + dataPath + @"blob/" + dataGuid, + FileMode.OpenOrCreate, + FileAccess.ReadWrite, + FileShare.ReadWrite + ) ) { - Application application = await _appMetadata.GetApplicationMetadata(); - var instanceIdParts = instanceId.Split("/"); + await request.Body.CopyToAsync(streamToWriteTo); + streamToWriteTo.Flush(); + filesize = streamToWriteTo.Length; + streamToWriteTo.Close(); + } - Guid dataGuid = Guid.NewGuid(); + dataElement.Size = filesize; - string org = application.Org; - string app = application.Id.Split("/")[1]; - int instanceOwnerId = int.Parse(instanceIdParts[0]); - Guid instanceGuid = Guid.Parse(instanceIdParts[1]); + WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); - string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerId, instanceGuid); + return dataElement; + } - DataElement dataElement = - new() - { - Id = dataGuid.ToString(), - InstanceGuid = instanceGuid.ToString(), - DataType = dataType, - ContentType = contentType, - }; + public async Task InsertBinaryData( + string instanceId, + string dataType, + string contentType, + string filename, + Stream stream + ) + { + Application application = await _appMetadata.GetApplicationMetadata(); + var instanceIdParts = instanceId.Split("/"); - if (!Directory.Exists(Path.GetDirectoryName(dataPath))) - { - var directory = Path.GetDirectoryName(dataPath); - if (directory != null) - Directory.CreateDirectory(directory); - } + Guid dataGuid = Guid.NewGuid(); - Directory.CreateDirectory(dataPath + @"blob"); + string org = application.Org; + string app = application.Id.Split("/")[1]; + int instanceOwnerId = int.Parse(instanceIdParts[0]); + Guid instanceGuid = Guid.Parse(instanceIdParts[1]); - long filesize; + string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerId, instanceGuid); - using ( - Stream streamToWriteTo = File.Open( - dataPath + @"blob/" + dataGuid, - FileMode.OpenOrCreate, - FileAccess.ReadWrite, - FileShare.ReadWrite - ) - ) + DataElement dataElement = + new() { - stream.Seek(0, SeekOrigin.Begin); - await stream.CopyToAsync(streamToWriteTo); - streamToWriteTo.Flush(); - filesize = streamToWriteTo.Length; - } - - dataElement.Size = filesize; - WriteDataElementToFile(dataElement, org, app, instanceOwnerId); + Id = dataGuid.ToString(), + InstanceGuid = instanceGuid.ToString(), + DataType = dataType, + ContentType = contentType, + }; - return dataElement; + if (!Directory.Exists(Path.GetDirectoryName(dataPath))) + { + var directory = Path.GetDirectoryName(dataPath); + if (directory != null) + Directory.CreateDirectory(directory); } - public Task UpdateBinaryData( - InstanceIdentifier instanceIdentifier, - string? contentType, - string filename, - Guid dataGuid, - Stream stream + Directory.CreateDirectory(dataPath + @"blob"); + + long filesize; + + using ( + Stream streamToWriteTo = File.Open( + dataPath + @"blob/" + dataGuid, + FileMode.OpenOrCreate, + FileAccess.ReadWrite, + FileShare.ReadWrite + ) ) { - throw new NotImplementedException(); + stream.Seek(0, SeekOrigin.Begin); + await stream.CopyToAsync(streamToWriteTo); + streamToWriteTo.Flush(); + filesize = streamToWriteTo.Length; } - public async Task InsertBinaryData( - string instanceId, - string dataType, - string contentType, - string? filename, - Stream stream, - string? generatedFromTask = null - ) - { - Application application = await _appMetadata.GetApplicationMetadata(); - var instanceIdParts = instanceId.Split("/"); + dataElement.Size = filesize; + WriteDataElementToFile(dataElement, org, app, instanceOwnerId); - Guid dataGuid = Guid.NewGuid(); + return dataElement; + } - string org = application.Org; - string app = application.Id.Split("/")[1]; - int instanceOwnerId = int.Parse(instanceIdParts[0]); - Guid instanceGuid = Guid.Parse(instanceIdParts[1]); + public Task UpdateBinaryData( + InstanceIdentifier instanceIdentifier, + string? contentType, + string filename, + Guid dataGuid, + Stream stream + ) + { + throw new NotImplementedException(); + } - string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerId, instanceGuid); + public async Task InsertBinaryData( + string instanceId, + string dataType, + string contentType, + string? filename, + Stream stream, + string? generatedFromTask = null + ) + { + Application application = await _appMetadata.GetApplicationMetadata(); + var instanceIdParts = instanceId.Split("/"); - DataElement dataElement = - new() - { - Id = dataGuid.ToString(), - InstanceGuid = instanceGuid.ToString(), - DataType = dataType, - ContentType = contentType, - }; + Guid dataGuid = Guid.NewGuid(); - if (!Directory.Exists(Path.GetDirectoryName(dataPath))) - { - var directory = Path.GetDirectoryName(dataPath); - if (directory != null) - Directory.CreateDirectory(directory); - } + string org = application.Org; + string app = application.Id.Split("/")[1]; + int instanceOwnerId = int.Parse(instanceIdParts[0]); + Guid instanceGuid = Guid.Parse(instanceIdParts[1]); - Directory.CreateDirectory(dataPath + @"blob"); + string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerId, instanceGuid); - long filesize; - - using ( - Stream streamToWriteTo = File.Open( - dataPath + @"blob/" + dataGuid, - FileMode.OpenOrCreate, - FileAccess.ReadWrite, - FileShare.ReadWrite - ) - ) + DataElement dataElement = + new() { - stream.Seek(0, SeekOrigin.Begin); - await stream.CopyToAsync(streamToWriteTo); - streamToWriteTo.Flush(); - filesize = streamToWriteTo.Length; - } + Id = dataGuid.ToString(), + InstanceGuid = instanceGuid.ToString(), + DataType = dataType, + ContentType = contentType, + }; - dataElement.Size = filesize; - WriteDataElementToFile(dataElement, org, app, instanceOwnerId); - - return dataElement; + if (!Directory.Exists(Path.GetDirectoryName(dataPath))) + { + var directory = Path.GetDirectoryName(dataPath); + if (directory != null) + Directory.CreateDirectory(directory); } - public Task UpdateBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid, - HttpRequest request + Directory.CreateDirectory(dataPath + @"blob"); + + long filesize; + + using ( + Stream streamToWriteTo = File.Open( + dataPath + @"blob/" + dataGuid, + FileMode.OpenOrCreate, + FileAccess.ReadWrite, + FileShare.ReadWrite + ) ) { - throw new NotImplementedException(); + stream.Seek(0, SeekOrigin.Begin); + await stream.CopyToAsync(streamToWriteTo); + streamToWriteTo.Flush(); + filesize = streamToWriteTo.Length; } - public Task Update(Instance instance, DataElement dataElement) - { - string org = instance.Org; - string app = instance.AppId.Split("/")[1]; - int instanceOwnerId = int.Parse(instance.InstanceOwner.PartyId); + dataElement.Size = filesize; + WriteDataElementToFile(dataElement, org, app, instanceOwnerId); - WriteDataElementToFile(dataElement, org, app, instanceOwnerId); + return dataElement; + } - return Task.FromResult(dataElement); - } + public Task UpdateBinaryData( + string org, + string app, + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid, + HttpRequest request + ) + { + throw new NotImplementedException(); + } + + public Task Update(Instance instance, DataElement dataElement) + { + string org = instance.Org; + string app = instance.AppId.Split("/")[1]; + int instanceOwnerId = int.Parse(instance.InstanceOwner.PartyId); + + WriteDataElementToFile(dataElement, org, app, instanceOwnerId); + + return Task.FromResult(dataElement); + } - public Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + public Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + // 🤬The signature does not take org/app, + // but our test data is organized by org/app. + (string org, string app) = TestData.GetInstanceOrgApp(instanceIdentifier); + List dataElement = GetDataElements( + org, + app, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid + ); + DataElement? element = dataElement.FirstOrDefault(d => d.Id == dataGuid.ToString()); + if (element is null) { - // 🤬The signature does not take org/app, - // but our test data is organized by org/app. - (string org, string app) = TestData.GetInstanceOrgApp(instanceIdentifier); - List dataElement = GetDataElements( - org, - app, - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid - ); - DataElement? element = dataElement.FirstOrDefault(d => d.Id == dataGuid.ToString()); - if (element is null) - { - throw new Exception("Data element not found."); - } - element.Locked = true; - WriteDataElementToFile(element, org, app, instanceIdentifier.InstanceOwnerPartyId); - return Task.FromResult(element); + throw new Exception("Data element not found."); } + element.Locked = true; + WriteDataElementToFile(element, org, app, instanceIdentifier.InstanceOwnerPartyId); + return Task.FromResult(element); + } - public Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + public Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + // 🤬The signature does not take org/app, + // but our test data is organized by org/app. + (string org, string app) = TestData.GetInstanceOrgApp(instanceIdentifier); + List dataElement = GetDataElements( + org, + app, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid + ); + DataElement? element = dataElement.FirstOrDefault(d => d.Id == dataGuid.ToString()); + if (element is null) { - // 🤬The signature does not take org/app, - // but our test data is organized by org/app. - (string org, string app) = TestData.GetInstanceOrgApp(instanceIdentifier); - List dataElement = GetDataElements( - org, - app, - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid - ); - DataElement? element = dataElement.FirstOrDefault(d => d.Id == dataGuid.ToString()); - if (element is null) - { - throw new Exception("Data element not found."); - } - element.Locked = false; - WriteDataElementToFile(element, org, app, instanceIdentifier.InstanceOwnerPartyId); - return Task.FromResult(element); + throw new Exception("Data element not found."); } + element.Locked = false; + WriteDataElementToFile(element, org, app, instanceIdentifier.InstanceOwnerPartyId); + return Task.FromResult(element); + } - private static void WriteDataElementToFile( - DataElement dataElement, - string org, - string app, - int instanceOwnerPartyId - ) - { - string dataElementPath = TestData.GetDataElementPath( - org, - app, - instanceOwnerPartyId, - Guid.Parse(dataElement.InstanceGuid), - Guid.Parse(dataElement.Id) - ); + private static void WriteDataElementToFile( + DataElement dataElement, + string org, + string app, + int instanceOwnerPartyId + ) + { + string dataElementPath = TestData.GetDataElementPath( + org, + app, + instanceOwnerPartyId, + Guid.Parse(dataElement.InstanceGuid), + Guid.Parse(dataElement.Id) + ); - string jsonData = JsonSerializer.Serialize(dataElement, _jsonSerializerOptions); + string jsonData = JsonSerializer.Serialize(dataElement, _jsonSerializerOptions); - using StreamWriter sw = new(dataElementPath); + using StreamWriter sw = new(dataElementPath); - sw.Write(jsonData.ToString()); - sw.Close(); - } + sw.Write(jsonData.ToString()); + sw.Close(); + } - private List GetDataElements(string org, string app, int instanceOwnerId, Guid instanceId) + private List GetDataElements(string org, string app, int instanceOwnerId, Guid instanceId) + { + string path = TestData.GetDataDirectory(org, app, instanceOwnerId, instanceId); + List dataElements = new(); + + if (!Directory.Exists(path)) { - string path = TestData.GetDataDirectory(org, app, instanceOwnerId, instanceId); - List dataElements = new(); + return new List(); + } - if (!Directory.Exists(path)) - { - return new List(); - } + string[] files = Directory.GetFiles(path); - string[] files = Directory.GetFiles(path); + foreach (string file in files) + { + string content = File.ReadAllText(Path.Combine(path, file)); + DataElement? dataElement = JsonSerializer.Deserialize(content, _jsonSerializerOptions); - foreach (string file in files) + if (dataElement != null) { - string content = File.ReadAllText(Path.Combine(path, file)); - DataElement? dataElement = JsonSerializer.Deserialize(content, _jsonSerializerOptions); - - if (dataElement != null) + if ( + dataElement.DeleteStatus?.IsHardDeleted == true + && string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.User.GetOrg()) + ) { - if ( - dataElement.DeleteStatus?.IsHardDeleted == true - && string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.User.GetOrg()) - ) - { - continue; - } - - dataElements.Add(dataElement); + continue; } - } - return dataElements; + dataElements.Add(dataElement); + } } + + return dataElements; } } diff --git a/test/Altinn.App.Api.Tests/Mocks/Event/DummyFailureEventHandler.cs b/test/Altinn.App.Api.Tests/Mocks/Event/DummyFailureEventHandler.cs index f87147dd2..fee835259 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Event/DummyFailureEventHandler.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Event/DummyFailureEventHandler.cs @@ -1,16 +1,14 @@ -using System.Threading.Tasks; using Altinn.App.Core.Features; using Altinn.App.Core.Models; -namespace Altinn.App.Api.Tests.Mocks.Event +namespace Altinn.App.Api.Tests.Mocks.Event; + +public class DummyFailureEventHandler : IEventHandler { - public class DummyFailureEventHandler : IEventHandler - { - public string EventType => "app.event.dummy.failure"; + public string EventType => "app.event.dummy.failure"; - public Task ProcessEvent(CloudEvent cloudEvent) - { - return Task.FromResult(true); - } + public Task ProcessEvent(CloudEvent cloudEvent) + { + return Task.FromResult(true); } } diff --git a/test/Altinn.App.Api.Tests/Mocks/Event/DummySuccessEventHandler.cs b/test/Altinn.App.Api.Tests/Mocks/Event/DummySuccessEventHandler.cs index f27350997..f4b357d78 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Event/DummySuccessEventHandler.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Event/DummySuccessEventHandler.cs @@ -1,16 +1,14 @@ -using System.Threading.Tasks; using Altinn.App.Core.Features; using Altinn.App.Core.Models; -namespace Altinn.App.Api.Tests.Mocks.Event +namespace Altinn.App.Api.Tests.Mocks.Event; + +public class DummySuccessEventHandler : IEventHandler { - public class DummySuccessEventHandler : IEventHandler - { - public string EventType => "app.event.dummy.success"; + public string EventType => "app.event.dummy.success"; - public Task ProcessEvent(CloudEvent cloudEvent) - { - return Task.FromResult(true); - } + public Task ProcessEvent(CloudEvent cloudEvent) + { + return Task.FromResult(true); } } diff --git a/test/Altinn.App.Api.Tests/Mocks/Event/EventSecretCodeProviderStub.cs b/test/Altinn.App.Api.Tests/Mocks/Event/EventSecretCodeProviderStub.cs index b9dbfd2ba..d1c6ff281 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Event/EventSecretCodeProviderStub.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Event/EventSecretCodeProviderStub.cs @@ -1,13 +1,11 @@ -using System.Threading.Tasks; using Altinn.App.Core.Internal.Events; -namespace Altinn.App.Api.Tests.Mocks.Event +namespace Altinn.App.Api.Tests.Mocks.Event; + +public class EventSecretCodeProviderStub : IEventSecretCodeProvider { - public class EventSecretCodeProviderStub : IEventSecretCodeProvider + public Task GetSecretCode() { - public Task GetSecretCode() - { - return Task.FromResult("42"); - } + return Task.FromResult("42"); } } diff --git a/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs index 452c52d13..ad103a398 100644 --- a/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs +++ b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs @@ -8,608 +8,603 @@ using Microsoft.Extensions.Primitives; using Newtonsoft.Json; -namespace Altinn.App.Api.Tests.Mocks +namespace Altinn.App.Api.Tests.Mocks; + +public class InstanceClientMockSi : IInstanceClient { - public class InstanceClientMockSi : IInstanceClient + private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + + public InstanceClientMockSi(ILogger logger, IHttpContextAccessor httpContextAccessor) { - private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; + _logger = logger; + _httpContextAccessor = httpContextAccessor; + } - public InstanceClientMockSi(ILogger logger, IHttpContextAccessor httpContextAccessor) - { - _logger = logger; - _httpContextAccessor = httpContextAccessor; - } + public Task CreateInstance(string org, string app, Instance instanceTemplate) + { + string partyId = instanceTemplate.InstanceOwner.PartyId; + Guid instanceGuid = Guid.NewGuid(); + + Instance instance = + new() + { + Id = $"{partyId}/{instanceGuid}", + AppId = $"{org}/{app}", + Org = org, + InstanceOwner = instanceTemplate.InstanceOwner, + Process = instanceTemplate.Process, + Data = new List(), + }; - public Task CreateInstance(string org, string app, Instance instanceTemplate) + if (instanceTemplate.DataValues != null) { - string partyId = instanceTemplate.InstanceOwner.PartyId; - Guid instanceGuid = Guid.NewGuid(); - - Instance instance = - new() - { - Id = $"{partyId}/{instanceGuid}", - AppId = $"{org}/{app}", - Org = org, - InstanceOwner = instanceTemplate.InstanceOwner, - Process = instanceTemplate.Process, - Data = new List(), - }; + instance.DataValues = instanceTemplate.DataValues; + } - if (instanceTemplate.DataValues != null) - { - instance.DataValues = instanceTemplate.DataValues; - } + string instancePath = GetInstancePath(app, org, int.Parse(partyId), instanceGuid); + string directory = + Path.GetDirectoryName(instancePath) + ?? throw new IOException($"Could not get directory name of specified path {instancePath}"); + _ = Directory.CreateDirectory(directory); + File.WriteAllText(instancePath, instance.ToString()); + + _logger.LogInformation( + "Created instance for app {org}/{app}. writing to path: {instancePath}", + org, + app, + instancePath + ); + + return Task.FromResult(instance); + } - string instancePath = GetInstancePath(app, org, int.Parse(partyId), instanceGuid); - string directory = - Path.GetDirectoryName(instancePath) - ?? throw new IOException($"Could not get directory name of specified path {instancePath}"); - _ = Directory.CreateDirectory(directory); - File.WriteAllText(instancePath, instance.ToString()); + /// + public async Task GetInstance(Instance instance) + { + string app = instance.AppId.Split("/")[1]; + string org = instance.Org; + int instanceOwnerId = int.Parse(instance.InstanceOwner.PartyId); + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - _logger.LogInformation( - "Created instance for app {org}/{app}. writing to path: {instancePath}", - org, - app, - instancePath - ); + return await GetInstance(app, org, instanceOwnerId, instanceGuid); + } - return Task.FromResult(instance); - } + public Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceId) + { + Instance instance = GetTestInstance(app, org, instanceOwnerPartyId, instanceId); - /// - public async Task GetInstance(Instance instance) + if (instance is null) { - string app = instance.AppId.Split("/")[1]; - string org = instance.Org; - int instanceOwnerId = int.Parse(instance.InstanceOwner.PartyId); - Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - - return await GetInstance(app, org, instanceOwnerId, instanceGuid); + throw new IOException($"Could not load instance {instanceId} from app {org}/{app}"); } - public Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceId) - { - Instance instance = GetTestInstance(app, org, instanceOwnerPartyId, instanceId); + instance.Data = GetDataElements(org, app, instanceOwnerPartyId, instanceId); + (instance.LastChangedBy, instance.LastChanged) = FindLastChanged(instance); - if (instance is null) - { - throw new IOException($"Could not load instance {instanceId} from app {org}/{app}"); - } + return Task.FromResult(instance); + } - instance.Data = GetDataElements(org, app, instanceOwnerPartyId, instanceId); - (instance.LastChangedBy, instance.LastChanged) = FindLastChanged(instance); + public Task UpdateProcess(Instance instance) + { + ProcessState process = instance.Process; - return Task.FromResult(instance); - } + string app = instance.AppId.Split("/")[1]; + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - public Task UpdateProcess(Instance instance) + string instancePath = GetInstancePath( + app, + instance.Org, + int.Parse(instance.InstanceOwner.PartyId), + instanceGuid + ); + + if (!File.Exists(instancePath)) { - ProcessState process = instance.Process; + throw new IOException($"Could not find instance {instance.Id} on path {instancePath}"); + } - string app = instance.AppId.Split("/")[1]; - Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + string content = File.ReadAllText(instancePath); - string instancePath = GetInstancePath( - app, - instance.Org, - int.Parse(instance.InstanceOwner.PartyId), - instanceGuid + Instance storedInstance = + JsonConvert.DeserializeObject(content) + ?? throw new InvalidDataException( + $"Something went wrong deserializing json for instance {instance.Id} from path {instancePath}" ); - if (!File.Exists(instancePath)) - { - throw new IOException($"Could not find instance {instance.Id} on path {instancePath}"); - } - - string content = File.ReadAllText(instancePath); - - Instance storedInstance = - JsonConvert.DeserializeObject(content) - ?? throw new InvalidDataException( - $"Something went wrong deserializing json for instance {instance.Id} from path {instancePath}" - ); + // Archiving instance if process was ended + if (storedInstance.Process?.Ended == null && process.Ended != null) + { + storedInstance.Status ??= new InstanceStatus(); + storedInstance.Status.Archived = process.Ended; + } - // Archiving instance if process was ended - if (storedInstance.Process?.Ended == null && process.Ended != null) - { - storedInstance.Status ??= new InstanceStatus(); - storedInstance.Status.Archived = process.Ended; - } + storedInstance.Process = process; + storedInstance.LastChanged = DateTime.UtcNow; - storedInstance.Process = process; - storedInstance.LastChanged = DateTime.UtcNow; + File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); - File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); + return Task.FromResult(storedInstance); + } - return Task.FromResult(storedInstance); + private static Instance GetTestInstance(string app, string org, int instanceOwnerId, Guid instanceId) + { + string instancePath = GetInstancePath(app, org, instanceOwnerId, instanceId); + if (!File.Exists(instancePath)) + { + throw new IOException($"Could not find file for instance {instanceId} on specified path {instancePath}."); } - private static Instance GetTestInstance(string app, string org, int instanceOwnerId, Guid instanceId) - { - string instancePath = GetInstancePath(app, org, instanceOwnerId, instanceId); - if (!File.Exists(instancePath)) - { - throw new IOException( - $"Could not find file for instance {instanceId} on specified path {instancePath}." - ); - } + string content = File.ReadAllText(instancePath); + Instance instance = + JsonConvert.DeserializeObject(content) + ?? throw new InvalidDataException( + $"Something went wrong deserializing json for instance from path {instancePath}" + ); - string content = File.ReadAllText(instancePath); - Instance instance = - JsonConvert.DeserializeObject(content) - ?? throw new InvalidDataException( - $"Something went wrong deserializing json for instance from path {instancePath}" - ); + return instance; + } - return instance; + // Finds the path for the instance based on instanceId. Only works if guid is unique. + private static string GetInstancePath(int instanceOwnerPartyId, Guid instanceGuid) + { + string[] paths = Directory.GetFiles( + TestData.GetInstancesDirectory(), + instanceGuid + ".json", + SearchOption.AllDirectories + ); + paths = paths.Where(p => p.Contains($"{instanceOwnerPartyId}")).ToArray(); + if (paths.Length == 1) + { + return paths.First(); } - // Finds the path for the instance based on instanceId. Only works if guid is unique. - private static string GetInstancePath(int instanceOwnerPartyId, Guid instanceGuid) - { - string[] paths = Directory.GetFiles( - TestData.GetInstancesDirectory(), - instanceGuid + ".json", - SearchOption.AllDirectories - ); - paths = paths.Where(p => p.Contains($"{instanceOwnerPartyId}")).ToArray(); - if (paths.Length == 1) - { - return paths.First(); - } + return string.Empty; + } - return string.Empty; - } + private static string GetInstancePath(string app, string org, int instanceOwnerId, Guid instanceId) + { + return Path.Combine( + TestData.GetInstancesDirectory(), + org, + app, + instanceOwnerId.ToString(), + instanceId + ".json" + ); + } - private static string GetInstancePath(string app, string org, int instanceOwnerId, Guid instanceId) - { - return Path.Combine( + private static string GetDataPath(string org, string app, int instanceOwnerId, Guid instanceGuid) + { + return Path.Combine( TestData.GetInstancesDirectory(), org, app, instanceOwnerId.ToString(), - instanceId + ".json" - ); - } + instanceGuid.ToString() + ) + Path.DirectorySeparatorChar; + } - private static string GetDataPath(string org, string app, int instanceOwnerId, Guid instanceGuid) + private List GetDataElements(string org, string app, int instanceOwnerId, Guid instanceId) + { + string path = GetDataPath(org, app, instanceOwnerId, instanceId); + + List dataElements = new(); + + if (!Directory.Exists(path)) { - return Path.Combine( - TestData.GetInstancesDirectory(), - org, - app, - instanceOwnerId.ToString(), - instanceGuid.ToString() - ) + Path.DirectorySeparatorChar; + return dataElements; } - private List GetDataElements(string org, string app, int instanceOwnerId, Guid instanceId) + foreach (string file in Directory.GetFiles(path)) { - string path = GetDataPath(org, app, instanceOwnerId, instanceId); + if (file.Contains(".pretest")) + { + continue; + } - List dataElements = new(); + string content = File.ReadAllText(Path.Combine(path, file)); + DataElement dataElement = + JsonConvert.DeserializeObject(content) + ?? throw new InvalidDataException($"Something went wrong deserializing json for data from path {file}"); - if (!Directory.Exists(path)) + if ( + dataElement.DeleteStatus?.IsHardDeleted == true + && string.IsNullOrEmpty(_httpContextAccessor?.HttpContext?.User?.GetOrg()) + ) { - return dataElements; + continue; } - foreach (string file in Directory.GetFiles(path)) - { - if (file.Contains(".pretest")) - { - continue; - } + dataElements.Add(dataElement); + } - string content = File.ReadAllText(Path.Combine(path, file)); - DataElement dataElement = - JsonConvert.DeserializeObject(content) - ?? throw new InvalidDataException( - $"Something went wrong deserializing json for data from path {file}" - ); - - if ( - dataElement.DeleteStatus?.IsHardDeleted == true - && string.IsNullOrEmpty(_httpContextAccessor?.HttpContext?.User?.GetOrg()) - ) - { - continue; - } + return dataElements; + } - dataElements.Add(dataElement); - } + public Task> GetActiveInstances(int instanceOwnerPartyId) + { + throw new NotImplementedException(); + } - return dataElements; - } + public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid) + { + string org; + string app; + Instance instance; - public Task> GetActiveInstances(int instanceOwnerPartyId) + switch ($"{instanceOwnerPartyId}/{instanceGuid}") { - throw new NotImplementedException(); + case "1337/66233fb5-a9f2-45d4-90b1-f6d93ad40713": + org = "tdd"; + app = "endring-av-navn"; + instance = GetTestInstance(app, org, instanceOwnerPartyId, instanceGuid); + break; + default: + org = string.Empty; + instance = new Instance(); + break; } - public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid) + instance.CompleteConfirmations = new List { - string org; - string app; - Instance instance; - - switch ($"{instanceOwnerPartyId}/{instanceGuid}") - { - case "1337/66233fb5-a9f2-45d4-90b1-f6d93ad40713": - org = "tdd"; - app = "endring-av-navn"; - instance = GetTestInstance(app, org, instanceOwnerPartyId, instanceGuid); - break; - default: - org = string.Empty; - instance = new Instance(); - break; - } + new CompleteConfirmation { StakeholderId = org } + }; - instance.CompleteConfirmations = new List - { - new CompleteConfirmation { StakeholderId = org } - }; + return await Task.FromResult(instance); + } - return await Task.FromResult(instance); + public async Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus) + { + if (!Enum.TryParse(readStatus, true, out ReadStatus newStatus)) + { + throw new ArgumentOutOfRangeException( + nameof(readStatus), + $"Unable to parse argument as a valid ReadStatus enum." + ); } - public async Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus) + string instancePath = GetInstancePath(instanceOwnerPartyId, instanceGuid); + + if (!File.Exists(instancePath)) { - if (!Enum.TryParse(readStatus, true, out ReadStatus newStatus)) - { - throw new ArgumentOutOfRangeException( - nameof(readStatus), - $"Unable to parse argument as a valid ReadStatus enum." - ); - } + throw new IOException($"Could not find file for instance on specified path {instancePath}."); + } - string instancePath = GetInstancePath(instanceOwnerPartyId, instanceGuid); + string content = File.ReadAllText(instancePath); + Instance storedInstance = + JsonConvert.DeserializeObject(content) + ?? throw new InvalidDataException( + $"Something went wrong deserializing json for instance from path {instancePath}" + ); - if (!File.Exists(instancePath)) - { - throw new IOException($"Could not find file for instance on specified path {instancePath}."); - } + storedInstance.Status ??= new InstanceStatus(); + storedInstance.Status.ReadStatus = newStatus; - string content = File.ReadAllText(instancePath); - Instance storedInstance = - JsonConvert.DeserializeObject(content) - ?? throw new InvalidDataException( - $"Something went wrong deserializing json for instance from path {instancePath}" - ); + File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); + return await Task.FromResult(storedInstance); + } - storedInstance.Status ??= new InstanceStatus(); - storedInstance.Status.ReadStatus = newStatus; + public async Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus) + { + DateTime creationTime = DateTime.UtcNow; - File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); - return await Task.FromResult(storedInstance); + if (substatus == null || string.IsNullOrEmpty(substatus.Label)) + { + throw await PlatformHttpException.CreateAsync( + new HttpResponseMessage { StatusCode = System.Net.HttpStatusCode.BadRequest } + ); } - public async Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus) + string instancePath = GetInstancePath(instanceOwnerPartyId, instanceGuid); + + if (!File.Exists(instancePath)) { - DateTime creationTime = DateTime.UtcNow; + throw new IOException($"Could not find file for instance on specified path {instancePath}."); + } - if (substatus == null || string.IsNullOrEmpty(substatus.Label)) - { - throw await PlatformHttpException.CreateAsync( - new HttpResponseMessage { StatusCode = System.Net.HttpStatusCode.BadRequest } - ); - } + string content = File.ReadAllText(instancePath); + Instance storedInstance = + JsonConvert.DeserializeObject(content) + ?? throw new InvalidDataException( + $"Something went wrong deserializing json for instance from path {instancePath}" + ); - string instancePath = GetInstancePath(instanceOwnerPartyId, instanceGuid); + storedInstance.Status ??= new InstanceStatus(); + storedInstance.Status.Substatus = substatus; + storedInstance.LastChanged = creationTime; - if (!File.Exists(instancePath)) - { - throw new IOException($"Could not find file for instance on specified path {instancePath}."); - } + // mock does not set last changed by, but this is set by the platform. + storedInstance.LastChangedBy = string.Empty; - string content = File.ReadAllText(instancePath); - Instance storedInstance = - JsonConvert.DeserializeObject(content) - ?? throw new InvalidDataException( - $"Something went wrong deserializing json for instance from path {instancePath}" - ); + File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); + return await GetInstance(storedInstance); + } - storedInstance.Status ??= new InstanceStatus(); - storedInstance.Status.Substatus = substatus; - storedInstance.LastChanged = creationTime; + public async Task UpdatePresentationTexts( + int instanceOwnerPartyId, + Guid instanceGuid, + PresentationTexts presentationTexts + ) + { + string instancePath = GetInstancePath(instanceOwnerPartyId, instanceGuid); + if (!File.Exists(instancePath)) + { + throw new IOException($"Could not find file for instance on specified path {instancePath}."); + } - // mock does not set last changed by, but this is set by the platform. - storedInstance.LastChangedBy = string.Empty; + string content = File.ReadAllText(instancePath); + Instance storedInstance = + JsonConvert.DeserializeObject(content) + ?? throw new InvalidDataException( + $"Something went wrong deserializing json for instance from path {instancePath}" + ); - File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); - return await GetInstance(storedInstance); - } + storedInstance.PresentationTexts ??= new Dictionary(); - public async Task UpdatePresentationTexts( - int instanceOwnerPartyId, - Guid instanceGuid, - PresentationTexts presentationTexts - ) + foreach (KeyValuePair entry in presentationTexts.Texts) { - string instancePath = GetInstancePath(instanceOwnerPartyId, instanceGuid); - if (!File.Exists(instancePath)) + if (string.IsNullOrEmpty(entry.Value)) { - throw new IOException($"Could not find file for instance on specified path {instancePath}."); + storedInstance.PresentationTexts.Remove(entry.Key); } - - string content = File.ReadAllText(instancePath); - Instance storedInstance = - JsonConvert.DeserializeObject(content) - ?? throw new InvalidDataException( - $"Something went wrong deserializing json for instance from path {instancePath}" - ); - - storedInstance.PresentationTexts ??= new Dictionary(); - - foreach (KeyValuePair entry in presentationTexts.Texts) + else { - if (string.IsNullOrEmpty(entry.Value)) - { - storedInstance.PresentationTexts.Remove(entry.Key); - } - else - { - storedInstance.PresentationTexts[entry.Key] = entry.Value; - } + storedInstance.PresentationTexts[entry.Key] = entry.Value; } + } - // mock does not set last changed by, but this is set by the platform. - storedInstance.LastChangedBy = string.Empty; + // mock does not set last changed by, but this is set by the platform. + storedInstance.LastChangedBy = string.Empty; - File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); + File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); - return await GetInstance(storedInstance); - } + return await GetInstance(storedInstance); + } - public async Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues) + public async Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues) + { + string instancePath = GetInstancePath(instanceOwnerPartyId, instanceGuid); + if (!File.Exists(instancePath)) { - string instancePath = GetInstancePath(instanceOwnerPartyId, instanceGuid); - if (!File.Exists(instancePath)) - { - throw new IOException($"Could not find file for instance on specified path {instancePath}."); - } + throw new IOException($"Could not find file for instance on specified path {instancePath}."); + } - string content = File.ReadAllText(instancePath); - Instance storedInstance = - JsonConvert.DeserializeObject(content) - ?? throw new InvalidDataException( - $"Something went wrong deserializing json for instance from path {instancePath}" - ); + string content = File.ReadAllText(instancePath); + Instance storedInstance = + JsonConvert.DeserializeObject(content) + ?? throw new InvalidDataException( + $"Something went wrong deserializing json for instance from path {instancePath}" + ); - storedInstance.DataValues ??= new Dictionary(); + storedInstance.DataValues ??= new Dictionary(); - foreach (KeyValuePair entry in dataValues.Values) + foreach (KeyValuePair entry in dataValues.Values) + { + if (string.IsNullOrEmpty(entry.Value)) { - if (string.IsNullOrEmpty(entry.Value)) - { - storedInstance.DataValues.Remove(entry.Key); - } - else - { - storedInstance.DataValues[entry.Key] = entry.Value; - } + storedInstance.DataValues.Remove(entry.Key); } + else + { + storedInstance.DataValues[entry.Key] = entry.Value; + } + } - // mock does not set last changed by, but this is set by the platform. - storedInstance.LastChangedBy = string.Empty; + // mock does not set last changed by, but this is set by the platform. + storedInstance.LastChangedBy = string.Empty; - File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); + File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); - return await GetInstance(storedInstance); - } + return await GetInstance(storedInstance); + } - public Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard) + public Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard) + { + string instancePath = GetInstancePath(instanceOwnerPartyId, instanceGuid); + if (!File.Exists(instancePath)) { - string instancePath = GetInstancePath(instanceOwnerPartyId, instanceGuid); - if (!File.Exists(instancePath)) - { - throw new IOException($"Could not find file for instance on specified path {instancePath}."); - } + throw new IOException($"Could not find file for instance on specified path {instancePath}."); + } - string content = File.ReadAllText(instancePath); - Instance storedInstance = - JsonConvert.DeserializeObject(content) - ?? throw new InvalidDataException( - $"Something went wrong deserializing json for instance from path {instancePath}" - ); + string content = File.ReadAllText(instancePath); + Instance storedInstance = + JsonConvert.DeserializeObject(content) + ?? throw new InvalidDataException( + $"Something went wrong deserializing json for instance from path {instancePath}" + ); - storedInstance.Status ??= new InstanceStatus(); + storedInstance.Status ??= new InstanceStatus(); - if (hard) - { - storedInstance.Status.IsHardDeleted = true; - storedInstance.Status.HardDeleted = DateTime.UtcNow; - } + if (hard) + { + storedInstance.Status.IsHardDeleted = true; + storedInstance.Status.HardDeleted = DateTime.UtcNow; + } - storedInstance.Status.IsSoftDeleted = true; - storedInstance.Status.SoftDeleted = DateTime.UtcNow; + storedInstance.Status.IsSoftDeleted = true; + storedInstance.Status.SoftDeleted = DateTime.UtcNow; - // mock does not set last changed by, but this is set by the platform. - storedInstance.LastChangedBy = string.Empty; + // mock does not set last changed by, but this is set by the platform. + storedInstance.LastChangedBy = string.Empty; - File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); + File.WriteAllText(instancePath, JsonConvert.SerializeObject(storedInstance)); - return Task.FromResult(storedInstance); - } + return Task.FromResult(storedInstance); + } - /// - /// Searches through all instance documents (including pretest) - /// - public async Task> GetInstances(Dictionary queryParams) + /// + /// Searches through all instance documents (including pretest) + /// + public async Task> GetInstances(Dictionary queryParams) + { + List validQueryParams = + new() + { + "org", + "appId", + "process.currentTask", + "process.isComplete", + "process.endEvent", + "process.ended", + "instanceOwner.partyId", + "lastChanged", + "created", + "visibleAfter", + "dueBefore", + "excludeConfirmedBy", + "size", + "language", + "status.isSoftDeleted", + "status.isArchived", + "status.isHardDeleted", + "status.isArchivedOrSoftDeleted", + "status.isActiveorSoftDeleted", + "sortBy", + "archiveReference" + }; + + string invalidKey = queryParams.FirstOrDefault(q => !validQueryParams.Contains(q.Key)).Key; + if (!string.IsNullOrEmpty(invalidKey)) { - List validQueryParams = + // platform exceptions. + HttpResponseMessage res = new() { - "org", - "appId", - "process.currentTask", - "process.isComplete", - "process.endEvent", - "process.ended", - "instanceOwner.partyId", - "lastChanged", - "created", - "visibleAfter", - "dueBefore", - "excludeConfirmedBy", - "size", - "language", - "status.isSoftDeleted", - "status.isArchived", - "status.isHardDeleted", - "status.isArchivedOrSoftDeleted", - "status.isActiveorSoftDeleted", - "sortBy", - "archiveReference" + StatusCode = System.Net.HttpStatusCode.BadRequest, + Content = new StringContent($"Unknown query parameter: {invalidKey}") }; - string invalidKey = queryParams.FirstOrDefault(q => !validQueryParams.Contains(q.Key)).Key; - if (!string.IsNullOrEmpty(invalidKey)) - { - // platform exceptions. - HttpResponseMessage res = - new() - { - StatusCode = System.Net.HttpStatusCode.BadRequest, - Content = new StringContent($"Unknown query parameter: {invalidKey}") - }; - - throw await PlatformHttpException.CreateAsync(res); - } + throw await PlatformHttpException.CreateAsync(res); + } + + List instances = new(); - List instances = new(); + string instancesPath = TestData.GetInstancesDirectory(); - string instancesPath = TestData.GetInstancesDirectory(); + int fileDepth = 4; - int fileDepth = 4; + if (queryParams.TryGetValue("appId", out StringValues appIdQueryVal) && appIdQueryVal.Count > 0) + { + instancesPath += + Path.DirectorySeparatorChar + appIdQueryVal.First()?.Replace('/', Path.DirectorySeparatorChar); + fileDepth -= 2; - if (queryParams.TryGetValue("appId", out StringValues appIdQueryVal) && appIdQueryVal.Count > 0) + if ( + queryParams.TryGetValue("instanceOwner.partyId", out StringValues partyIdQueryVal) + && partyIdQueryVal.Count > 0 + ) { - instancesPath += - Path.DirectorySeparatorChar + appIdQueryVal.First()?.Replace('/', Path.DirectorySeparatorChar); - fileDepth -= 2; - - if ( - queryParams.TryGetValue("instanceOwner.partyId", out StringValues partyIdQueryVal) - && partyIdQueryVal.Count > 0 - ) - { - instancesPath += Path.DirectorySeparatorChar + partyIdQueryVal.First(); - fileDepth -= 1; - } + instancesPath += Path.DirectorySeparatorChar + partyIdQueryVal.First(); + fileDepth -= 1; } + } - if (Directory.Exists(instancesPath)) - { - string[] files = Directory.GetFiles(instancesPath, "*.json", SearchOption.AllDirectories); - int instancePathLenght = instancesPath.Split(Path.DirectorySeparatorChar).Length; + if (Directory.Exists(instancesPath)) + { + string[] files = Directory.GetFiles(instancesPath, "*.json", SearchOption.AllDirectories); + int instancePathLenght = instancesPath.Split(Path.DirectorySeparatorChar).Length; - // only parse files at the correct level. Instances are places four levels [org/app/partyId/instance] below instance path. - List instanceFiles = files - .Where(f => f.Split(Path.DirectorySeparatorChar).Length == (instancePathLenght + fileDepth)) - .ToList(); + // only parse files at the correct level. Instances are places four levels [org/app/partyId/instance] below instance path. + List instanceFiles = files + .Where(f => f.Split(Path.DirectorySeparatorChar).Length == (instancePathLenght + fileDepth)) + .ToList(); - foreach (var file in instanceFiles) + foreach (var file in instanceFiles) + { + string content = File.ReadAllText(file); + Instance? instance = JsonConvert.DeserializeObject(content); + if (instance != null && instance.Id != null) { - string content = File.ReadAllText(file); - Instance? instance = JsonConvert.DeserializeObject(content); - if (instance != null && instance.Id != null) - { - instances.Add(instance); - } + instances.Add(instance); } } + } - if (queryParams.ContainsKey("org")) - { - string org = queryParams.GetValueOrDefault("org").ToString(); - instances.RemoveAll(i => !i.Org.Equals(org, StringComparison.OrdinalIgnoreCase)); - } - - if (queryParams.ContainsKey("appId")) - { - string appId = queryParams.GetValueOrDefault("appId").ToString(); - instances.RemoveAll(i => !i.AppId.Equals(appId, StringComparison.OrdinalIgnoreCase)); - } + if (queryParams.ContainsKey("org")) + { + string org = queryParams.GetValueOrDefault("org").ToString(); + instances.RemoveAll(i => !i.Org.Equals(org, StringComparison.OrdinalIgnoreCase)); + } - if (queryParams.ContainsKey("instanceOwner.partyId")) - { - instances.RemoveAll(i => !queryParams["instanceOwner.partyId"].Contains(i.InstanceOwner.PartyId)); - } + if (queryParams.ContainsKey("appId")) + { + string appId = queryParams.GetValueOrDefault("appId").ToString(); + instances.RemoveAll(i => !i.AppId.Equals(appId, StringComparison.OrdinalIgnoreCase)); + } - if (queryParams.ContainsKey("archiveReference")) - { - string archiveRef = queryParams.GetValueOrDefault("archiveReference").ToString(); - instances.RemoveAll(i => !i.Id.EndsWith(archiveRef.ToLower())); - } + if (queryParams.ContainsKey("instanceOwner.partyId")) + { + instances.RemoveAll(i => !queryParams["instanceOwner.partyId"].Contains(i.InstanceOwner.PartyId)); + } - bool match; + if (queryParams.ContainsKey("archiveReference")) + { + string archiveRef = queryParams.GetValueOrDefault("archiveReference").ToString(); + instances.RemoveAll(i => !i.Id.EndsWith(archiveRef.ToLower())); + } - if ( - queryParams.ContainsKey("status.isArchived") - && bool.TryParse(queryParams.GetValueOrDefault("status.isArchived"), out match) - ) - { - instances.RemoveAll(i => i.Status.IsArchived != match); - } + bool match; - if ( - queryParams.ContainsKey("status.isHardDeleted") - && bool.TryParse(queryParams.GetValueOrDefault("status.isHardDeleted"), out match) - ) - { - instances.RemoveAll(i => i.Status.IsHardDeleted != match); - } + if ( + queryParams.ContainsKey("status.isArchived") + && bool.TryParse(queryParams.GetValueOrDefault("status.isArchived"), out match) + ) + { + instances.RemoveAll(i => i.Status.IsArchived != match); + } - if ( - queryParams.ContainsKey("status.isSoftDeleted") - && bool.TryParse(queryParams.GetValueOrDefault("status.isSoftDeleted"), out match) - ) - { - instances.RemoveAll(i => i.Status.IsSoftDeleted != match); - } + if ( + queryParams.ContainsKey("status.isHardDeleted") + && bool.TryParse(queryParams.GetValueOrDefault("status.isHardDeleted"), out match) + ) + { + instances.RemoveAll(i => i.Status.IsHardDeleted != match); + } - instances.RemoveAll(i => i.Status.IsHardDeleted); - return instances; + if ( + queryParams.ContainsKey("status.isSoftDeleted") + && bool.TryParse(queryParams.GetValueOrDefault("status.isSoftDeleted"), out match) + ) + { + instances.RemoveAll(i => i.Status.IsSoftDeleted != match); } - private static (string LastChangedBy, DateTime? LastChanged) FindLastChanged(Instance instance) + instances.RemoveAll(i => i.Status.IsHardDeleted); + return instances; + } + + private static (string LastChangedBy, DateTime? LastChanged) FindLastChanged(Instance instance) + { + string lastChangedBy = instance.LastChangedBy; + DateTime? lastChanged = instance.LastChanged; + if (instance.Data == null || instance.Data.Count == 0) { - string lastChangedBy = instance.LastChangedBy; - DateTime? lastChanged = instance.LastChanged; - if (instance.Data == null || instance.Data.Count == 0) - { - return (lastChangedBy, lastChanged); - } + return (lastChangedBy, lastChanged); + } - List newerDataElements = instance.Data.FindAll(dataElement => - dataElement.LastChanged != null - && dataElement.LastChangedBy != null - && dataElement.LastChanged > instance.LastChanged - ); + List newerDataElements = instance.Data.FindAll(dataElement => + dataElement.LastChanged != null + && dataElement.LastChangedBy != null + && dataElement.LastChanged > instance.LastChanged + ); - if (newerDataElements.Count == 0) - { - return (lastChangedBy, lastChanged); - } + if (newerDataElements.Count == 0) + { + return (lastChangedBy, lastChanged); + } - lastChanged = instance.LastChanged; - newerDataElements.ForEach( - (DataElement dataElement) => + lastChanged = instance.LastChanged; + newerDataElements.ForEach( + (DataElement dataElement) => + { + if (dataElement.LastChanged > lastChanged) { - if (dataElement.LastChanged > lastChanged) - { - lastChangedBy = dataElement.LastChangedBy; - lastChanged = (DateTime)dataElement.LastChanged; - } + lastChangedBy = dataElement.LastChangedBy; + lastChanged = (DateTime)dataElement.LastChanged; } - ); + } + ); - return (lastChangedBy, lastChanged); - } + return (lastChangedBy, lastChanged); } } diff --git a/test/Altinn.App.Api.Tests/Mocks/JwtTokenMock.cs b/test/Altinn.App.Api.Tests/Mocks/JwtTokenMock.cs index 93004cd43..42db2cb83 100644 --- a/test/Altinn.App.Api.Tests/Mocks/JwtTokenMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/JwtTokenMock.cs @@ -1,77 +1,74 @@ -using System; using System.IdentityModel.Tokens.Jwt; -using System.IO; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using Microsoft.IdentityModel.Tokens; -namespace Altinn.App.Api.Tests.Mocks +namespace Altinn.App.Api.Tests.Mocks; + +/// +/// Represents a mechanism for creating JSON Web tokens for use in integration tests. +/// +public static class JwtTokenMock { /// - /// Represents a mechanism for creating JSON Web tokens for use in integration tests. + /// Generates a token with a self signed certificate included in the integration test project. /// - public static class JwtTokenMock + /// The claims principal to include in the token. + /// How long the token should be valid for. + /// A new token. + public static string GenerateToken(ClaimsPrincipal principal, TimeSpan tokenExipry) { - /// - /// Generates a token with a self signed certificate included in the integration test project. - /// - /// The claims principal to include in the token. - /// How long the token should be valid for. - /// A new token. - public static string GenerateToken(ClaimsPrincipal principal, TimeSpan tokenExipry) + JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); + SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor { - JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); - SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor - { - Subject = new ClaimsIdentity(principal.Identity), - Expires = DateTime.UtcNow.AddSeconds(tokenExipry.TotalSeconds), - SigningCredentials = GetSigningCredentials(), - Audience = "altinn.no" - }; + Subject = new ClaimsIdentity(principal.Identity), + Expires = DateTime.UtcNow.AddSeconds(tokenExipry.TotalSeconds), + SigningCredentials = GetSigningCredentials(), + Audience = "altinn.no" + }; - SecurityToken token = tokenHandler.CreateToken(tokenDescriptor); - string tokenstring = tokenHandler.WriteToken(token); + SecurityToken token = tokenHandler.CreateToken(tokenDescriptor); + string tokenstring = tokenHandler.WriteToken(token); - return tokenstring; - } + return tokenstring; + } - /// - /// Validates a token and return the ClaimsPrincipal if successful. The validation key used is from the self signed certificate - /// and is included in the integration test project as a separate file. - /// - /// The token to be validated. - /// ClaimsPrincipal - public static ClaimsPrincipal ValidateToken(string token) - { - string unitTestFolder = Path.GetDirectoryName(new Uri(typeof(JwtTokenMock).Assembly.Location).LocalPath)!; + /// + /// Validates a token and return the ClaimsPrincipal if successful. The validation key used is from the self signed certificate + /// and is included in the integration test project as a separate file. + /// + /// The token to be validated. + /// ClaimsPrincipal + public static ClaimsPrincipal ValidateToken(string token) + { + string unitTestFolder = Path.GetDirectoryName(new Uri(typeof(JwtTokenMock).Assembly.Location).LocalPath)!; - string certPath = Path.Combine(unitTestFolder, "JWTValidationCert.cer"); + string certPath = Path.Combine(unitTestFolder, "JWTValidationCert.cer"); - X509Certificate2 cert = new(certPath); - SecurityKey key = new X509SecurityKey(cert); + X509Certificate2 cert = new(certPath); + SecurityKey key = new X509SecurityKey(cert); - TokenValidationParameters validationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = key, - ValidateIssuer = false, - ValidateAudience = false, - RequireExpirationTime = true, - ValidateLifetime = true, - ClockSkew = TimeSpan.Zero - }; + TokenValidationParameters validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = key, + ValidateIssuer = false, + ValidateAudience = false, + RequireExpirationTime = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; - JwtSecurityTokenHandler validator = new(); - return validator.ValidateToken(token, validationParameters, out SecurityToken validatedToken); - } + JwtSecurityTokenHandler validator = new(); + return validator.ValidateToken(token, validationParameters, out SecurityToken validatedToken); + } - private static SigningCredentials GetSigningCredentials() - { - string unitTestFolder = Path.GetDirectoryName(new Uri(typeof(JwtTokenMock).Assembly.Location).LocalPath)!; + private static SigningCredentials GetSigningCredentials() + { + string unitTestFolder = Path.GetDirectoryName(new Uri(typeof(JwtTokenMock).Assembly.Location).LocalPath)!; - string certPath = Path.Combine(unitTestFolder, "jwtselfsignedcert.pfx"); - X509Certificate2 cert = new X509Certificate2(certPath, "qwer1234"); - return new X509SigningCredentials(cert, SecurityAlgorithms.RsaSha256); - } + string certPath = Path.Combine(unitTestFolder, "jwtselfsignedcert.pfx"); + X509Certificate2 cert = new X509Certificate2(certPath, "qwer1234"); + return new X509SigningCredentials(cert, SecurityAlgorithms.RsaSha256); } } diff --git a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs index f5ec05c64..2dd6e2dd2 100644 --- a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs +++ b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs @@ -15,352 +15,345 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; -namespace Altinn.App.Api.Tests.Mocks +namespace Altinn.App.Api.Tests.Mocks; + +public class PepWithPDPAuthorizationMockSI : Altinn.Common.PEP.Interfaces.IPDP { - public class PepWithPDPAuthorizationMockSI : Altinn.Common.PEP.Interfaces.IPDP - { - private readonly IInstanceClient _instanceClient; + private readonly IInstanceClient _instanceClient; - private readonly PepSettings _pepSettings; + private readonly PepSettings _pepSettings; - private const string OrgAttributeId = "urn:altinn:org"; + private const string OrgAttributeId = "urn:altinn:org"; - private const string AppAttributeId = "urn:altinn:app"; + private const string AppAttributeId = "urn:altinn:app"; - private const string InstanceAttributeId = "urn:altinn:instance-id"; + private const string InstanceAttributeId = "urn:altinn:instance-id"; - private const string TaskAttributeId = "urn:altinn:task"; + private const string TaskAttributeId = "urn:altinn:task"; - private const string EndEventAttributeId = "urn:altinn:end-event"; + private const string EndEventAttributeId = "urn:altinn:end-event"; - private const string PartyAttributeId = "urn:altinn:partyid"; + private const string PartyAttributeId = "urn:altinn:partyid"; - private const string UserAttributeId = "urn:altinn:userid"; + private const string UserAttributeId = "urn:altinn:userid"; - private const string AltinnRoleAttributeId = "urn:altinn:rolecode"; + private const string AltinnRoleAttributeId = "urn:altinn:rolecode"; - public PepWithPDPAuthorizationMockSI(IInstanceClient instanceClient, IOptions pepSettings) - { - this._instanceClient = instanceClient; - _pepSettings = pepSettings.Value; - } + public PepWithPDPAuthorizationMockSI(IInstanceClient instanceClient, IOptions pepSettings) + { + this._instanceClient = instanceClient; + _pepSettings = pepSettings.Value; + } - public async Task GetDecisionForRequest(XacmlJsonRequestRoot xacmlJsonRequest) + public async Task GetDecisionForRequest(XacmlJsonRequestRoot xacmlJsonRequest) + { + try { - try - { - XacmlContextRequest decisionRequest = XacmlJsonXmlConverter.ConvertRequest(xacmlJsonRequest.Request); - decisionRequest = await Enrich(decisionRequest); + XacmlContextRequest decisionRequest = XacmlJsonXmlConverter.ConvertRequest(xacmlJsonRequest.Request); + decisionRequest = await Enrich(decisionRequest); - Authorization.ABAC.PolicyDecisionPoint pdp = new(); + Authorization.ABAC.PolicyDecisionPoint pdp = new(); - XacmlPolicy policy = await GetPolicyAsync(decisionRequest); - XacmlContextResponse contextResponse = pdp.Authorize(decisionRequest, policy); + XacmlPolicy policy = await GetPolicyAsync(decisionRequest); + XacmlContextResponse contextResponse = pdp.Authorize(decisionRequest, policy); - return XacmlJsonXmlConverter.ConvertResponse(contextResponse); - } - catch - { - return null!; - } + return XacmlJsonXmlConverter.ConvertResponse(contextResponse); } + catch + { + return null!; + } + } + + public async Task GetDecisionForUnvalidateRequest(XacmlJsonRequestRoot xacmlJsonRequest, ClaimsPrincipal user) + { + XacmlJsonResponse response = await GetDecisionForRequest(xacmlJsonRequest); + return DecisionHelper.ValidatePdpDecision(response.Response, user); + } + + public async Task Enrich(XacmlContextRequest request) + { + await EnrichResourceAttributes(request); + + return request; + } + + private async Task EnrichResourceAttributes(XacmlContextRequest request) + { + XacmlContextAttributes resourceContextAttributes = request.GetResourceAttributes(); + XacmlResourceAttributes resourceAttributes = GetResourceAttributeValues(resourceContextAttributes); - public async Task GetDecisionForUnvalidateRequest( - XacmlJsonRequestRoot xacmlJsonRequest, - ClaimsPrincipal user + bool resourceAttributeComplete = false; + + if ( + !string.IsNullOrEmpty(resourceAttributes.OrgValue) + && !string.IsNullOrEmpty(resourceAttributes.AppValue) + && !string.IsNullOrEmpty(resourceAttributes.InstanceValue) + && !string.IsNullOrEmpty(resourceAttributes.ResourcePartyValue) + && !string.IsNullOrEmpty(resourceAttributes.TaskValue) ) { - XacmlJsonResponse response = await GetDecisionForRequest(xacmlJsonRequest); - return DecisionHelper.ValidatePdpDecision(response.Response, user); + // The resource attributes are complete + resourceAttributeComplete = true; } - - public async Task Enrich(XacmlContextRequest request) + else if ( + !string.IsNullOrEmpty(resourceAttributes.OrgValue) + && !string.IsNullOrEmpty(resourceAttributes.AppValue) + && string.IsNullOrEmpty(resourceAttributes.InstanceValue) + && !string.IsNullOrEmpty(resourceAttributes.ResourcePartyValue) + && string.IsNullOrEmpty(resourceAttributes.TaskValue) + ) { - await EnrichResourceAttributes(request); - - return request; + // The resource attributes are complete + resourceAttributeComplete = true; + } + else if ( + !string.IsNullOrEmpty(resourceAttributes.OrgValue) + && !string.IsNullOrEmpty(resourceAttributes.AppValue) + && !string.IsNullOrEmpty(resourceAttributes.InstanceValue) + && !string.IsNullOrEmpty(resourceAttributes.ResourcePartyValue) + && !string.IsNullOrEmpty(resourceAttributes.AppResourceValue) + && resourceAttributes.AppResourceValue.Equals("events") + ) + { + // The resource attributes are complete + resourceAttributeComplete = true; } - private async Task EnrichResourceAttributes(XacmlContextRequest request) + if (!resourceAttributeComplete) { - XacmlContextAttributes resourceContextAttributes = request.GetResourceAttributes(); - XacmlResourceAttributes resourceAttributes = GetResourceAttributeValues(resourceContextAttributes); - - bool resourceAttributeComplete = false; - - if ( - !string.IsNullOrEmpty(resourceAttributes.OrgValue) - && !string.IsNullOrEmpty(resourceAttributes.AppValue) - && !string.IsNullOrEmpty(resourceAttributes.InstanceValue) - && !string.IsNullOrEmpty(resourceAttributes.ResourcePartyValue) - && !string.IsNullOrEmpty(resourceAttributes.TaskValue) - ) - { - // The resource attributes are complete - resourceAttributeComplete = true; - } - else if ( - !string.IsNullOrEmpty(resourceAttributes.OrgValue) - && !string.IsNullOrEmpty(resourceAttributes.AppValue) - && string.IsNullOrEmpty(resourceAttributes.InstanceValue) - && !string.IsNullOrEmpty(resourceAttributes.ResourcePartyValue) - && string.IsNullOrEmpty(resourceAttributes.TaskValue) - ) - { - // The resource attributes are complete - resourceAttributeComplete = true; - } - else if ( - !string.IsNullOrEmpty(resourceAttributes.OrgValue) - && !string.IsNullOrEmpty(resourceAttributes.AppValue) - && !string.IsNullOrEmpty(resourceAttributes.InstanceValue) - && !string.IsNullOrEmpty(resourceAttributes.ResourcePartyValue) - && !string.IsNullOrEmpty(resourceAttributes.AppResourceValue) - && resourceAttributes.AppResourceValue.Equals("events") - ) - { - // The resource attributes are complete - resourceAttributeComplete = true; - } + Instance instanceData = await _instanceClient.GetInstance( + resourceAttributes.AppValue, + resourceAttributes.OrgValue, + Convert.ToInt32(resourceAttributes.InstanceValue.Split('/')[0]), + new Guid(resourceAttributes.InstanceValue.Split('/')[1]) + ); - if (!resourceAttributeComplete) + if (instanceData != null) { - Instance instanceData = await _instanceClient.GetInstance( - resourceAttributes.AppValue, + AddIfValueDoesNotExist( + resourceContextAttributes, + XacmlRequestAttribute.OrgAttribute, resourceAttributes.OrgValue, - Convert.ToInt32(resourceAttributes.InstanceValue.Split('/')[0]), - new Guid(resourceAttributes.InstanceValue.Split('/')[1]) + instanceData.Org ); - - if (instanceData != null) + string app = instanceData.AppId.Split("/")[1]; + AddIfValueDoesNotExist( + resourceContextAttributes, + XacmlRequestAttribute.AppAttribute, + resourceAttributes.AppValue, + app + ); + if (instanceData.Process?.CurrentTask != null) { AddIfValueDoesNotExist( resourceContextAttributes, - XacmlRequestAttribute.OrgAttribute, - resourceAttributes.OrgValue, - instanceData.Org + XacmlRequestAttribute.TaskAttribute, + resourceAttributes.TaskValue, + instanceData.Process.CurrentTask.ElementId ); - string app = instanceData.AppId.Split("/")[1]; - AddIfValueDoesNotExist( - resourceContextAttributes, - XacmlRequestAttribute.AppAttribute, - resourceAttributes.AppValue, - app - ); - if (instanceData.Process?.CurrentTask != null) - { - AddIfValueDoesNotExist( - resourceContextAttributes, - XacmlRequestAttribute.TaskAttribute, - resourceAttributes.TaskValue, - instanceData.Process.CurrentTask.ElementId - ); - } - else if (instanceData.Process?.EndEvent != null) - { - AddIfValueDoesNotExist( - resourceContextAttributes, - XacmlRequestAttribute.EndEventAttribute, - string.Empty, - instanceData.Process.EndEvent - ); - } - + } + else if (instanceData.Process?.EndEvent != null) + { AddIfValueDoesNotExist( resourceContextAttributes, - XacmlRequestAttribute.PartyAttribute, - resourceAttributes.ResourcePartyValue, - instanceData.InstanceOwner.PartyId + XacmlRequestAttribute.EndEventAttribute, + string.Empty, + instanceData.Process.EndEvent ); - resourceAttributes.ResourcePartyValue = instanceData.InstanceOwner.PartyId; } - } - await EnrichSubjectAttributes(request, resourceAttributes.ResourcePartyValue); + AddIfValueDoesNotExist( + resourceContextAttributes, + XacmlRequestAttribute.PartyAttribute, + resourceAttributes.ResourcePartyValue, + instanceData.InstanceOwner.PartyId + ); + resourceAttributes.ResourcePartyValue = instanceData.InstanceOwner.PartyId; + } } - private static void AddIfValueDoesNotExist( - XacmlContextAttributes resourceAttributes, - string attributeId, - string attributeValue, - string newAttributeValue - ) + await EnrichSubjectAttributes(request, resourceAttributes.ResourcePartyValue); + } + + private static void AddIfValueDoesNotExist( + XacmlContextAttributes resourceAttributes, + string attributeId, + string attributeValue, + string newAttributeValue + ) + { + if (string.IsNullOrEmpty(attributeValue)) { - if (string.IsNullOrEmpty(attributeValue)) - { - resourceAttributes.Attributes.Add(GetAttribute(attributeId, newAttributeValue)); - } + resourceAttributes.Attributes.Add(GetAttribute(attributeId, newAttributeValue)); } + } - private static XacmlAttribute GetAttribute(string attributeId, string attributeValue) + private static XacmlAttribute GetAttribute(string attributeId, string attributeValue) + { + XacmlAttribute attribute = new(new Uri(attributeId), false); + if (attributeId.Equals(XacmlRequestAttribute.PartyAttribute)) { - XacmlAttribute attribute = new(new Uri(attributeId), false); - if (attributeId.Equals(XacmlRequestAttribute.PartyAttribute)) - { - // When Party attribute is missing from input it is good to return it so PEP can get this information - attribute.IncludeInResult = true; - } - - attribute.AttributeValues.Add( - new XacmlAttributeValue(new Uri(XacmlConstants.DataTypes.XMLString), attributeValue) - ); - return attribute; + // When Party attribute is missing from input it is good to return it so PEP can get this information + attribute.IncludeInResult = true; } - private static async Task EnrichSubjectAttributes(XacmlContextRequest request, string resourceParty) - { - // If there is no resource party then it is impossible to enrich roles - if (string.IsNullOrEmpty(resourceParty)) - { - return; - } + attribute.AttributeValues.Add( + new XacmlAttributeValue(new Uri(XacmlConstants.DataTypes.XMLString), attributeValue) + ); + return attribute; + } - XacmlContextAttributes subjectContextAttributes = request.GetSubjectAttributes(); + private static async Task EnrichSubjectAttributes(XacmlContextRequest request, string resourceParty) + { + // If there is no resource party then it is impossible to enrich roles + if (string.IsNullOrEmpty(resourceParty)) + { + return; + } - int subjectUserId = 0; - int resourcePartyId = Convert.ToInt32(resourceParty); + XacmlContextAttributes subjectContextAttributes = request.GetSubjectAttributes(); - foreach (XacmlAttribute xacmlAttribute in subjectContextAttributes.Attributes) - { - if (xacmlAttribute.AttributeId.OriginalString.Equals(UserAttributeId)) - { - subjectUserId = Convert.ToInt32(xacmlAttribute.AttributeValues.First().Value); - } - } + int subjectUserId = 0; + int resourcePartyId = Convert.ToInt32(resourceParty); - if (subjectUserId == 0) + foreach (XacmlAttribute xacmlAttribute in subjectContextAttributes.Attributes) + { + if (xacmlAttribute.AttributeId.OriginalString.Equals(UserAttributeId)) { - return; + subjectUserId = Convert.ToInt32(xacmlAttribute.AttributeValues.First().Value); } - - List roleList = - await GetDecisionPointRolesForUser(subjectUserId, resourcePartyId) ?? new List(); - - subjectContextAttributes.Attributes.Add(GetRoleAttribute(roleList)); } - private static XacmlResourceAttributes GetResourceAttributeValues( - XacmlContextAttributes resourceContextAttributes - ) + if (subjectUserId == 0) { - XacmlResourceAttributes resourceAttributes = new(); + return; + } - foreach (XacmlAttribute attribute in resourceContextAttributes.Attributes) - { - if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.OrgAttribute)) - { - resourceAttributes.OrgValue = attribute.AttributeValues.First().Value; - } + List roleList = await GetDecisionPointRolesForUser(subjectUserId, resourcePartyId) ?? new List(); - if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.AppAttribute)) - { - resourceAttributes.AppValue = attribute.AttributeValues.First().Value; - } + subjectContextAttributes.Attributes.Add(GetRoleAttribute(roleList)); + } - if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.InstanceAttribute)) - { - resourceAttributes.InstanceValue = attribute.AttributeValues.First().Value; - } + private static XacmlResourceAttributes GetResourceAttributeValues(XacmlContextAttributes resourceContextAttributes) + { + XacmlResourceAttributes resourceAttributes = new(); - if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.PartyAttribute)) - { - resourceAttributes.ResourcePartyValue = attribute.AttributeValues.First().Value; - } + foreach (XacmlAttribute attribute in resourceContextAttributes.Attributes) + { + if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.OrgAttribute)) + { + resourceAttributes.OrgValue = attribute.AttributeValues.First().Value; + } - if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.TaskAttribute)) - { - resourceAttributes.TaskValue = attribute.AttributeValues.First().Value; - } + if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.AppAttribute)) + { + resourceAttributes.AppValue = attribute.AttributeValues.First().Value; + } - if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.AppResourceAttribute)) - { - resourceAttributes.AppResourceValue = attribute.AttributeValues.First().Value; - } + if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.InstanceAttribute)) + { + resourceAttributes.InstanceValue = attribute.AttributeValues.First().Value; } - return resourceAttributes; - } + if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.PartyAttribute)) + { + resourceAttributes.ResourcePartyValue = attribute.AttributeValues.First().Value; + } - private static XacmlAttribute GetRoleAttribute(List roles) - { - XacmlAttribute attribute = new(new Uri(AltinnRoleAttributeId), false); - foreach (Role role in roles) + if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.TaskAttribute)) { - attribute.AttributeValues.Add( - new XacmlAttributeValue(new Uri(XacmlConstants.DataTypes.XMLString), role.Value) - ); + resourceAttributes.TaskValue = attribute.AttributeValues.First().Value; } - return attribute; + if (attribute.AttributeId.OriginalString.Equals(XacmlRequestAttribute.AppResourceAttribute)) + { + resourceAttributes.AppResourceValue = attribute.AttributeValues.First().Value; + } } - public static Task> GetDecisionPointRolesForUser(int coveredByUserId, int offeredByPartyId) + return resourceAttributes; + } + + private static XacmlAttribute GetRoleAttribute(List roles) + { + XacmlAttribute attribute = new(new Uri(AltinnRoleAttributeId), false); + foreach (Role role in roles) { - string rolesPath = TestData.GetTestDataRolesFolder(coveredByUserId, offeredByPartyId); + attribute.AttributeValues.Add( + new XacmlAttributeValue(new Uri(XacmlConstants.DataTypes.XMLString), role.Value) + ); + } - List roles = new(); + return attribute; + } - if (File.Exists(rolesPath)) - { - string content = File.ReadAllText(rolesPath); - roles = - JsonConvert.DeserializeObject>(content) - ?? throw new InvalidDataException( - $"Something went wrong deserializing json for roles from path {rolesPath}" - ); - } + public static Task> GetDecisionPointRolesForUser(int coveredByUserId, int offeredByPartyId) + { + string rolesPath = TestData.GetTestDataRolesFolder(coveredByUserId, offeredByPartyId); - return Task.FromResult(roles); - } + List roles = new(); - private static Task GetPolicyAsync(XacmlContextRequest request) + if (File.Exists(rolesPath)) { - return Task.FromResult(ParsePolicy("policy.xml", GetPolicyPath(request))); + string content = File.ReadAllText(rolesPath); + roles = + JsonConvert.DeserializeObject>(content) + ?? throw new InvalidDataException( + $"Something went wrong deserializing json for roles from path {rolesPath}" + ); } - private static string GetPolicyPath(XacmlContextRequest request) + return Task.FromResult(roles); + } + + private static Task GetPolicyAsync(XacmlContextRequest request) + { + return Task.FromResult(ParsePolicy("policy.xml", GetPolicyPath(request))); + } + + private static string GetPolicyPath(XacmlContextRequest request) + { + string org = string.Empty; + string app = string.Empty; + foreach (XacmlContextAttributes attr in request.Attributes) { - string org = string.Empty; - string app = string.Empty; - foreach (XacmlContextAttributes attr in request.Attributes) + if (attr.Category.OriginalString.Equals(XacmlConstants.MatchAttributeCategory.Resource)) { - if (attr.Category.OriginalString.Equals(XacmlConstants.MatchAttributeCategory.Resource)) + foreach (XacmlAttribute xacmlAttribute in attr.Attributes) { - foreach (XacmlAttribute xacmlAttribute in attr.Attributes) + if ( + xacmlAttribute.AttributeId.OriginalString.Equals(OrgAttributeId) + && xacmlAttribute.AttributeValues.FirstOrDefault() != null + ) + { + org = xacmlAttribute.AttributeValues.First().Value; + } + + if ( + xacmlAttribute.AttributeId.OriginalString.Equals(AppAttributeId) + && xacmlAttribute.AttributeValues.FirstOrDefault() != null + ) { - if ( - xacmlAttribute.AttributeId.OriginalString.Equals(OrgAttributeId) - && xacmlAttribute.AttributeValues.FirstOrDefault() != null - ) - { - org = xacmlAttribute.AttributeValues.First().Value; - } - - if ( - xacmlAttribute.AttributeId.OriginalString.Equals(AppAttributeId) - && xacmlAttribute.AttributeValues.FirstOrDefault() != null - ) - { - app = xacmlAttribute.AttributeValues.First().Value; - } + app = xacmlAttribute.AttributeValues.First().Value; } } } - - return TestData.GetAltinnAppsPolicyPath(org, app); } - public static XacmlPolicy ParsePolicy(string policyDocumentTitle, string policyPath) - { - XmlDocument policyDocument = new(); - policyDocument.Load(Path.Combine(policyPath, policyDocumentTitle)); - XacmlPolicy policy; - using (XmlReader reader = XmlReader.Create(new StringReader(policyDocument.OuterXml))) - { - policy = XacmlParser.ParseXacmlPolicy(reader); - } + return TestData.GetAltinnAppsPolicyPath(org, app); + } - return policy; + public static XacmlPolicy ParsePolicy(string policyDocumentTitle, string policyPath) + { + XmlDocument policyDocument = new(); + policyDocument.Load(Path.Combine(policyPath, policyDocumentTitle)); + XacmlPolicy policy; + using (XmlReader reader = XmlReader.Create(new StringReader(policyDocument.OuterXml))) + { + policy = XacmlParser.ParseXacmlPolicy(reader); } + + return policy; } } diff --git a/test/Altinn.App.Api.Tests/Models/InstanceFileScanResultTests.cs b/test/Altinn.App.Api.Tests/Models/InstanceFileScanResultTests.cs index fb2050370..6bcd6f331 100644 --- a/test/Altinn.App.Api.Tests/Models/InstanceFileScanResultTests.cs +++ b/test/Altinn.App.Api.Tests/Models/InstanceFileScanResultTests.cs @@ -1,166 +1,123 @@ -using System; using Altinn.App.Api.Models; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Enums; using FluentAssertions; -using Xunit; -namespace Altinn.App.Api.Tests.Models +namespace Altinn.App.Api.Tests.Models; + +public class InstanceFileScanResultTests { - public class InstanceFileScanResultTests + //TODO: Add test for empty list and NotApplicable status + [Fact] + public void ZeroDataElement_AggregatedStatusShouldBeNotApplicable() + { + var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); + + instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.NotApplicable); + } + + [Theory] + [InlineData(FileScanResult.Pending)] + [InlineData(FileScanResult.Infected)] + [InlineData(FileScanResult.Clean)] + [InlineData(FileScanResult.NotApplicable)] + public void OneDataElement_AggregatedStatusShouldBeSame(FileScanResult fileScanResult) + { + var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); + + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = fileScanResult } + ); + + instanceFileScanResult.FileScanResult.Should().Be(fileScanResult); + } + + [Theory] + [InlineData(FileScanResult.Pending)] + [InlineData(FileScanResult.Infected)] + [InlineData(FileScanResult.Clean)] + [InlineData(FileScanResult.NotApplicable)] + public void AtLeastOneDataElementInfected_AggregatedStatusShouldBeInfected(FileScanResult fileScanResult) + { + var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); + + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = fileScanResult } + ); + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = FileScanResult.Infected } + ); + + instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.Infected); + } + + [Fact] + public void AllDataElemementsClean_AggregatedStatusShouldBeClean() + { + var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); + + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = FileScanResult.Clean } + ); + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = FileScanResult.Clean } + ); + + instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.Clean); + } + + [Fact] + public void OneCleanOnePending_AggregatedStatusShouldBePending() + { + var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); + + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = FileScanResult.Clean } + ); + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = FileScanResult.Pending } + ); + + instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.Pending); + } + + [Fact] + public void MultiplePending_AggregatedStatusShouldBePending() + { + var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); + + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = FileScanResult.Pending } + ); + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = FileScanResult.Pending } + ); + + instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.Pending); + } + + [Fact] + public void OneOfEach_AggregatedStatusShouldBeInfected() { - //TODO: Add test for empty list and NotApplicable status - [Fact] - public void ZeroDataElement_AggregatedStatusShouldBeNotApplicable() - { - var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); - - instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.NotApplicable); - } - - [Theory] - [InlineData(FileScanResult.Pending)] - [InlineData(FileScanResult.Infected)] - [InlineData(FileScanResult.Clean)] - [InlineData(FileScanResult.NotApplicable)] - public void OneDataElement_AggregatedStatusShouldBeSame(FileScanResult fileScanResult) - { - var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); - - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = fileScanResult } - ); - - instanceFileScanResult.FileScanResult.Should().Be(fileScanResult); - } - - [Theory] - [InlineData(FileScanResult.Pending)] - [InlineData(FileScanResult.Infected)] - [InlineData(FileScanResult.Clean)] - [InlineData(FileScanResult.NotApplicable)] - public void AtLeastOneDataElementInfected_AggregatedStatusShouldBeInfected(FileScanResult fileScanResult) - { - var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); - - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = fileScanResult } - ); - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = FileScanResult.Infected - } - ); - - instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.Infected); - } - - [Fact] - public void AllDataElemementsClean_AggregatedStatusShouldBeClean() - { - var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); - - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = FileScanResult.Clean - } - ); - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = FileScanResult.Clean - } - ); - - instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.Clean); - } - - [Fact] - public void OneCleanOnePending_AggregatedStatusShouldBePending() - { - var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); - - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = FileScanResult.Clean - } - ); - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = FileScanResult.Pending - } - ); - - instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.Pending); - } - - [Fact] - public void MultiplePending_AggregatedStatusShouldBePending() - { - var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); - - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = FileScanResult.Pending - } - ); - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = FileScanResult.Pending - } - ); - - instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.Pending); - } - - [Fact] - public void OneOfEach_AggregatedStatusShouldBeInfected() - { - var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); - - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = FileScanResult.Infected - } - ); - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = FileScanResult.Pending - } - ); - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = FileScanResult.Clean - } - ); - instanceFileScanResult.AddFileScanResult( - new DataElementFileScanResult() - { - Id = Guid.NewGuid().ToString(), - FileScanResult = FileScanResult.NotApplicable - } - ); - - instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.Infected); - } + var instanceFileScanResult = new InstanceFileScanResult(new InstanceIdentifier(12345, Guid.NewGuid())); + + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = FileScanResult.Infected } + ); + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = FileScanResult.Pending } + ); + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() { Id = Guid.NewGuid().ToString(), FileScanResult = FileScanResult.Clean } + ); + instanceFileScanResult.AddFileScanResult( + new DataElementFileScanResult() + { + Id = Guid.NewGuid().ToString(), + FileScanResult = FileScanResult.NotApplicable + } + ); + + instanceFileScanResult.FileScanResult.Should().Be(FileScanResult.Infected); } } diff --git a/test/Altinn.App.Api.Tests/Models/XacmlResourceAttributes.cs b/test/Altinn.App.Api.Tests/Models/XacmlResourceAttributes.cs index 8054255ab..828ec7d76 100644 --- a/test/Altinn.App.Api.Tests/Models/XacmlResourceAttributes.cs +++ b/test/Altinn.App.Api.Tests/Models/XacmlResourceAttributes.cs @@ -1,35 +1,34 @@ -namespace Altinn.App.Api.Tests.Models +namespace Altinn.App.Api.Tests.Models; + +public class XacmlResourceAttributes { - public class XacmlResourceAttributes - { - /// - /// Gets or sets the value for org attribute - /// - public string OrgValue { get; set; } = string.Empty; + /// + /// Gets or sets the value for org attribute + /// + public string OrgValue { get; set; } = string.Empty; - /// - /// Gets or sets the value for app attribute - /// - public string AppValue { get; set; } = string.Empty; + /// + /// Gets or sets the value for app attribute + /// + public string AppValue { get; set; } = string.Empty; - /// - /// Gets or sets the value for instance attribute - /// - public string InstanceValue { get; set; } = string.Empty; + /// + /// Gets or sets the value for instance attribute + /// + public string InstanceValue { get; set; } = string.Empty; - /// - /// Gets or sets the value for resourceparty attribute - /// - public string ResourcePartyValue { get; set; } = string.Empty; + /// + /// Gets or sets the value for resourceparty attribute + /// + public string ResourcePartyValue { get; set; } = string.Empty; - /// - /// Gets or sets the value for task attribute - /// - public string TaskValue { get; set; } = string.Empty; + /// + /// Gets or sets the value for task attribute + /// + public string TaskValue { get; set; } = string.Empty; - /// - /// Gets or sets the value for app resource. - /// - public string AppResourceValue { get; set; } = string.Empty; - } + /// + /// Gets or sets the value for app resource. + /// + public string AppResourceValue { get; set; } = string.Empty; } diff --git a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.cs b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.cs index fc5c4e3ab..187d73a36 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.cs +++ b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.cs @@ -1,7 +1,6 @@ using System.Net.Http.Headers; using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; -using Xunit; using Xunit.Abstractions; namespace Altinn.App.Api.Tests.OpenApi; diff --git a/test/Altinn.App.Api.Tests/Telemetry/TelemetryConfigurationTests.cs b/test/Altinn.App.Api.Tests/Telemetry/TelemetryConfigurationTests.cs index c65beddf1..1a4f4a5a3 100644 --- a/test/Altinn.App.Api.Tests/Telemetry/TelemetryConfigurationTests.cs +++ b/test/Altinn.App.Api.Tests/Telemetry/TelemetryConfigurationTests.cs @@ -13,7 +13,6 @@ using Microsoft.Extensions.Options; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; -using Xunit; namespace Altinn.App.Api.Tests; diff --git a/test/Altinn.App.Api.Tests/TestStubs/SwaggerIncludeXmlCommentsTestDouble.cs b/test/Altinn.App.Api.Tests/TestStubs/SwaggerIncludeXmlCommentsTestDouble.cs index 33ff98274..6f36d8a84 100644 --- a/test/Altinn.App.Api.Tests/TestStubs/SwaggerIncludeXmlCommentsTestDouble.cs +++ b/test/Altinn.App.Api.Tests/TestStubs/SwaggerIncludeXmlCommentsTestDouble.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace Altinn.App.Api.Tests.TestStubs; public class SwaggerIncludeXmlCommentsTestDouble diff --git a/test/Altinn.App.Api.Tests/Utils/JsonUtils.cs b/test/Altinn.App.Api.Tests/Utils/JsonUtils.cs index 27e6894a4..cd8157638 100644 --- a/test/Altinn.App.Api.Tests/Utils/JsonUtils.cs +++ b/test/Altinn.App.Api.Tests/Utils/JsonUtils.cs @@ -2,7 +2,6 @@ namespace Altinn.App.Api.Tests.Utils; using System.Text; using System.Text.Json; -using System.Text.Unicode; public static class JsonUtils { diff --git a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs index c2162fe26..cf3277a96 100644 --- a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs +++ b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs @@ -2,124 +2,119 @@ using Altinn.App.Api.Tests.Mocks; using AltinnCore.Authentication.Constants; -namespace Altinn.App.Api.Tests.Utils +namespace Altinn.App.Api.Tests.Utils; + +public static class PrincipalUtil { - public static class PrincipalUtil + public static string GetToken(int? userId, int? partyId, int authenticationLevel = 2) { - public static string GetToken(int? userId, int? partyId, int authenticationLevel = 2) - { - ClaimsPrincipal principal = GetUserPrincipal(userId, partyId, authenticationLevel); - string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); - return token; - } - - public static ClaimsPrincipal GetUserPrincipal(int? userId, int? partyId, int authenticationLevel = 2) - { - List claims = new List(); - string issuer = "www.altinn.no"; + ClaimsPrincipal principal = GetUserPrincipal(userId, partyId, authenticationLevel); + string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); + return token; + } - claims.Add( - new Claim(ClaimTypes.NameIdentifier, $"user-{userId}-{partyId}", ClaimValueTypes.String, issuer) - ); - if (userId > 0) - { - claims.Add( - new Claim(AltinnCoreClaimTypes.UserId, userId.Value.ToString(), ClaimValueTypes.String, issuer) - ); - } - - if (partyId > 0) - { - claims.Add( - new Claim(AltinnCoreClaimTypes.PartyID, partyId.Value.ToString(), ClaimValueTypes.Integer32, issuer) - ); - } - - claims.Add(new Claim(AltinnCoreClaimTypes.UserName, $"User{userId}", ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); - claims.Add( - new Claim( - AltinnCoreClaimTypes.AuthenticationLevel, - authenticationLevel.ToString(), - ClaimValueTypes.Integer32, - issuer - ) - ); + public static ClaimsPrincipal GetUserPrincipal(int? userId, int? partyId, int authenticationLevel = 2) + { + List claims = new List(); + string issuer = "www.altinn.no"; - ClaimsIdentity identity = new ClaimsIdentity("mock"); - identity.AddClaims(claims); - ClaimsPrincipal principal = new ClaimsPrincipal(identity); - return principal; + claims.Add(new Claim(ClaimTypes.NameIdentifier, $"user-{userId}-{partyId}", ClaimValueTypes.String, issuer)); + if (userId > 0) + { + claims.Add(new Claim(AltinnCoreClaimTypes.UserId, userId.Value.ToString(), ClaimValueTypes.String, issuer)); } - public static ClaimsPrincipal GetOrgPrincipal(string org, int authenticationLevel = 3) + if (partyId > 0) { - List claims = new List(); - string issuer = "www.altinn.no"; - claims.Add(new Claim(AltinnCoreClaimTypes.Org, org, ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); claims.Add( - new Claim( - AltinnCoreClaimTypes.AuthenticationLevel, - authenticationLevel.ToString(), - ClaimValueTypes.Integer32, - issuer - ) + new Claim(AltinnCoreClaimTypes.PartyID, partyId.Value.ToString(), ClaimValueTypes.Integer32, issuer) ); - - ClaimsIdentity identity = new ClaimsIdentity("mock"); - identity.AddClaims(claims); - - return new ClaimsPrincipal(identity); } - public static string GetOrgToken(string org, int authenticationLevel = 3) - { - ClaimsPrincipal principal = GetOrgPrincipal(org, authenticationLevel); - return JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); - } + claims.Add(new Claim(AltinnCoreClaimTypes.UserName, $"User{userId}", ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); + claims.Add( + new Claim( + AltinnCoreClaimTypes.AuthenticationLevel, + authenticationLevel.ToString(), + ClaimValueTypes.Integer32, + issuer + ) + ); + + ClaimsIdentity identity = new ClaimsIdentity("mock"); + identity.AddClaims(claims); + ClaimsPrincipal principal = new ClaimsPrincipal(identity); + return principal; + } - public static string GetSelfIdentifiedUserToken(string username, string partyId, string userId) - { - List claims = new List(); - string issuer = "www.altinn.no"; - claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString(), ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.UserId, userId.ToString(), ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.UserName, username, ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.PartyID, partyId.ToString(), ClaimValueTypes.Integer32, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticationLevel, "0", ClaimValueTypes.Integer32, issuer)); - - ClaimsIdentity identity = new ClaimsIdentity("mock"); - identity.AddClaims(claims); - ClaimsPrincipal principal = new ClaimsPrincipal(identity); - string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); - - return token; - } + public static ClaimsPrincipal GetOrgPrincipal(string org, int authenticationLevel = 3) + { + List claims = new List(); + string issuer = "www.altinn.no"; + claims.Add(new Claim(AltinnCoreClaimTypes.Org, org, ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); + claims.Add( + new Claim( + AltinnCoreClaimTypes.AuthenticationLevel, + authenticationLevel.ToString(), + ClaimValueTypes.Integer32, + issuer + ) + ); + + ClaimsIdentity identity = new ClaimsIdentity("mock"); + identity.AddClaims(claims); + + return new ClaimsPrincipal(identity); + } - public static string GetOrgToken(string org, string orgNo, int authenticationLevel = 4) - { - List claims = new List(); - string issuer = "www.altinn.no"; - claims.Add(new Claim(AltinnCoreClaimTypes.Org, org, ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.OrgNumber, orgNo, ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); - claims.Add( - new Claim( - AltinnCoreClaimTypes.AuthenticationLevel, - authenticationLevel.ToString(), - ClaimValueTypes.Integer32, - issuer - ) - ); + public static string GetOrgToken(string org, int authenticationLevel = 3) + { + ClaimsPrincipal principal = GetOrgPrincipal(org, authenticationLevel); + return JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); + } - ClaimsIdentity identity = new ClaimsIdentity("mock"); - identity.AddClaims(claims); - ClaimsPrincipal principal = new ClaimsPrincipal(identity); - string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); + public static string GetSelfIdentifiedUserToken(string username, string partyId, string userId) + { + List claims = new List(); + string issuer = "www.altinn.no"; + claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString(), ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.UserId, userId.ToString(), ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.UserName, username, ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.PartyID, partyId.ToString(), ClaimValueTypes.Integer32, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticationLevel, "0", ClaimValueTypes.Integer32, issuer)); + + ClaimsIdentity identity = new ClaimsIdentity("mock"); + identity.AddClaims(claims); + ClaimsPrincipal principal = new ClaimsPrincipal(identity); + string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); + + return token; + } - return token; - } + public static string GetOrgToken(string org, string orgNo, int authenticationLevel = 4) + { + List claims = new List(); + string issuer = "www.altinn.no"; + claims.Add(new Claim(AltinnCoreClaimTypes.Org, org, ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.OrgNumber, orgNo, ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); + claims.Add( + new Claim( + AltinnCoreClaimTypes.AuthenticationLevel, + authenticationLevel.ToString(), + ClaimValueTypes.Integer32, + issuer + ) + ); + + ClaimsIdentity identity = new ClaimsIdentity("mock"); + identity.AddClaims(claims); + ClaimsPrincipal principal = new ClaimsPrincipal(identity); + string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); + + return token; } } diff --git a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj index 184bb174a..cbc9a8ec0 100644 --- a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj +++ b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj @@ -41,4 +41,4 @@ - + \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index bb7c20f0b..8d2e0be27 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -101,4 +101,4 @@ - + \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/DataLists/DataListsFactoryTest.cs b/test/Altinn.App.Core.Tests/DataLists/DataListsFactoryTest.cs index 136478c21..956190d2f 100644 --- a/test/Altinn.App.Core.Tests/DataLists/DataListsFactoryTest.cs +++ b/test/Altinn.App.Core.Tests/DataLists/DataListsFactoryTest.cs @@ -3,61 +3,59 @@ using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Models; using FluentAssertions; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.DataLists +namespace Altinn.App.PlatformServices.Tests.DataLists; + +public class DataListsFactoryTest { - public class DataListsFactoryTest + [Fact] + public void GetDataListProvider_CustomDataListProvider_ShouldReturnCustomType() { - [Fact] - public void GetDataListProvider_CustomDataListProvider_ShouldReturnCustomType() - { - var factory = new DataListsFactory(new List() { new CountryDataListProvider() }); + var factory = new DataListsFactory(new List() { new CountryDataListProvider() }); - IDataListProvider dataListProvider = factory.GetDataListProvider("country"); + IDataListProvider dataListProvider = factory.GetDataListProvider("country"); - dataListProvider.Should().BeOfType(); - dataListProvider.Id.Should().Be("country"); - } + dataListProvider.Should().BeOfType(); + dataListProvider.Id.Should().Be("country"); + } - [Fact] - public void GetDataListProvider_NoDataListProvider_ShouldReturnNullDataListProvider() - { - var factory = new DataListsFactory(new List() { }); + [Fact] + public void GetDataListProvider_NoDataListProvider_ShouldReturnNullDataListProvider() + { + var factory = new DataListsFactory(new List() { }); - IDataListProvider dataListProvider = factory.GetDataListProvider("country"); + IDataListProvider dataListProvider = factory.GetDataListProvider("country"); - dataListProvider.Should().BeOfType(); - dataListProvider.Id.Should().Be(string.Empty); - } + dataListProvider.Should().BeOfType(); + dataListProvider.Id.Should().Be(string.Empty); + } - internal class CountryDataListProvider : IDataListProvider - { - public string Id { get; set; } = "country"; + internal class CountryDataListProvider : IDataListProvider + { + public string Id { get; set; } = "country"; - public Task GetDataListAsync(string language, Dictionary keyValuePairs) + public Task GetDataListAsync(string language, Dictionary keyValuePairs) + { + var dataList = new DataList { - var dataList = new DataList + ListItems = new List { - ListItems = new List + new + { + Name = "Norway", + Code = "NO", + Phone = 47 + }, + new { - new - { - Name = "Norway", - Code = "NO", - Phone = 47 - }, - new - { - Name = "Sweden", - Code = "SE", - Phone = 46 - }, - } - }; - - return Task.FromResult(dataList); - } + Name = "Sweden", + Code = "SE", + Phone = 46 + }, + } + }; + + return Task.FromResult(dataList); } } } diff --git a/test/Altinn.App.Core.Tests/DataLists/InstanceDataListsFactoryTest.cs b/test/Altinn.App.Core.Tests/DataLists/InstanceDataListsFactoryTest.cs index d7b27c742..725eeecb9 100644 --- a/test/Altinn.App.Core.Tests/DataLists/InstanceDataListsFactoryTest.cs +++ b/test/Altinn.App.Core.Tests/DataLists/InstanceDataListsFactoryTest.cs @@ -3,67 +3,65 @@ using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Models; using FluentAssertions; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.DataLists +namespace Altinn.App.PlatformServices.Tests.DataLists; + +public class InstanceDataListsFactoryTest { - public class InstanceDataListsFactoryTest + [Fact] + public void GetInstanceDataListProvider_CustomInstanceDataListProvider_ShouldReturnCustomType() { - [Fact] - public void GetInstanceDataListProvider_CustomInstanceDataListProvider_ShouldReturnCustomType() - { - var factory = new InstanceDataListsFactory( - new List() { new CountryDataListProvider() } - ); + var factory = new InstanceDataListsFactory( + new List() { new CountryDataListProvider() } + ); - IInstanceDataListProvider dataListProvider = factory.GetDataListProvider("country"); + IInstanceDataListProvider dataListProvider = factory.GetDataListProvider("country"); - dataListProvider.Should().BeOfType(); - dataListProvider.Id.Should().Be("country"); - } + dataListProvider.Should().BeOfType(); + dataListProvider.Id.Should().Be("country"); + } - [Fact] - public void GetInstanceDataListProvider_NoInstanceDataListProvider_ShouldReturnNullDataListProvider() - { - var factory = new InstanceDataListsFactory(new List() { }); + [Fact] + public void GetInstanceDataListProvider_NoInstanceDataListProvider_ShouldReturnNullDataListProvider() + { + var factory = new InstanceDataListsFactory(new List() { }); - IInstanceDataListProvider dataListProvider = factory.GetDataListProvider("country"); + IInstanceDataListProvider dataListProvider = factory.GetDataListProvider("country"); - dataListProvider.Should().BeOfType(); - dataListProvider.Id.Should().Be(string.Empty); - } + dataListProvider.Should().BeOfType(); + dataListProvider.Id.Should().Be(string.Empty); + } - internal class CountryDataListProvider : IInstanceDataListProvider - { - public string Id { get; set; } = "country"; + internal class CountryDataListProvider : IInstanceDataListProvider + { + public string Id { get; set; } = "country"; - public Task GetInstanceDataListAsync( - InstanceIdentifier instanceId, - string language, - Dictionary keyValuePairs - ) + public Task GetInstanceDataListAsync( + InstanceIdentifier instanceId, + string language, + Dictionary keyValuePairs + ) + { + var dataList = new DataList { - var dataList = new DataList + ListItems = new List { - ListItems = new List + new + { + Name = "Norway", + Code = "NO", + Phone = 47 + }, + new { - new - { - Name = "Norway", - Code = "NO", - Phone = 47 - }, - new - { - Name = "Sweden", - Code = "SE", - Phone = 46 - }, - } - }; + Name = "Sweden", + Code = "SE", + Phone = 46 + }, + } + }; - return Task.FromResult(dataList); - } + return Task.FromResult(dataList); } } } diff --git a/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs b/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs index 0cda3defc..4ef472934 100644 --- a/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs +++ b/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs @@ -1,20 +1,18 @@ #nullable disable using Altinn.App.Core.Features.DataLists; using FluentAssertions; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.DataLists +namespace Altinn.App.PlatformServices.Tests.DataLists; + +public class NullDataListProviderTest { - public class NullDataListProviderTest + [Fact] + public async Task Constructor_InitializedWithEmptyValues() { - [Fact] - public async Task Constructor_InitializedWithEmptyValues() - { - var provider = new NullDataListProvider(); + var provider = new NullDataListProvider(); - provider.Id.Should().Be(string.Empty); - var list = await provider.GetDataListAsync("nb", new Dictionary()); - list.ListItems.Should().BeNull(); - } + provider.Id.Should().Be(string.Empty); + var list = await provider.GetDataListAsync("nb", new Dictionary()); + list.ListItems.Should().BeNull(); } } diff --git a/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs b/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs index 330fabebf..2327c6f62 100644 --- a/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs +++ b/test/Altinn.App.Core.Tests/DataLists/NullInstanceDataListProviderTest.cs @@ -2,24 +2,22 @@ using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Models; using FluentAssertions; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.DataLists +namespace Altinn.App.PlatformServices.Tests.DataLists; + +public class NullInstanceDataListProviderTest { - public class NullInstanceDataListProviderTest + [Fact] + public async Task Constructor_InitializedWithEmptyValues() { - [Fact] - public async Task Constructor_InitializedWithEmptyValues() - { - var provider = new NullInstanceDataListProvider(); + var provider = new NullInstanceDataListProvider(); - provider.Id.Should().Be(string.Empty); - var options = await provider.GetInstanceDataListAsync( - new InstanceIdentifier(12345, Guid.NewGuid()), - "nb", - new Dictionary() - ); - options.ListItems.Should().BeNull(); - } + provider.Id.Should().Be(string.Empty); + var options = await provider.GetInstanceDataListAsync( + new InstanceIdentifier(12345, Guid.NewGuid()), + "nb", + new Dictionary() + ); + options.ListItems.Should().BeNull(); } } diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs index 15434e376..eed41145a 100644 --- a/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs @@ -17,7 +17,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Eformidling.Implementation; diff --git a/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs b/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs index b5b65c7bf..8f92e7dea 100644 --- a/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs +++ b/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs @@ -1,75 +1,73 @@ using Altinn.App.Core.Extensions; using Altinn.App.Core.Models; using Microsoft.AspNetCore.Http; -using Xunit; -namespace Altinn.App.Core.Tests.Extensions +namespace Altinn.App.Core.Tests.Extensions; + +public class DictionaryExtensionsTests { - public class DictionaryExtensionsTests + [Fact] + public void ToNameValueString_OptionParameters_ShouldConvertToHttpHeaderFormat() { - [Fact] - public void ToNameValueString_OptionParameters_ShouldConvertToHttpHeaderFormat() + var options = new AppOptions { - var options = new AppOptions - { - Parameters = new Dictionary { { "lang", "nb" }, { "level", "1" } }, - }; + Parameters = new Dictionary { { "lang", "nb" }, { "level", "1" } }, + }; - IHeaderDictionary headers = new HeaderDictionary - { - { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } - }; + IHeaderDictionary headers = new HeaderDictionary + { + { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } + }; - Assert.Equal("lang=nb,level=1", headers["Altinn-DownstreamParameters"]); - } + Assert.Equal("lang=nb,level=1", headers["Altinn-DownstreamParameters"]); + } + + [Fact] + public void ToNameValueString_OptionParametersWithEmptyValue_ShouldConvertToHttpHeaderFormat() + { + var options = new AppOptions { Parameters = new Dictionary() }; - [Fact] - public void ToNameValueString_OptionParametersWithEmptyValue_ShouldConvertToHttpHeaderFormat() + IHeaderDictionary headers = new HeaderDictionary { - var options = new AppOptions { Parameters = new Dictionary() }; + { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } + }; - IHeaderDictionary headers = new HeaderDictionary - { - { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } - }; + Assert.Equal(string.Empty, headers["Altinn-DownstreamParameters"]); + } - Assert.Equal(string.Empty, headers["Altinn-DownstreamParameters"]); - } + [Fact] + public void ToNameValueString_OptionParametersWithNullValue_ShouldConvertToHttpHeaderFormat() + { + var options = new AppOptions { Parameters = null! }; - [Fact] - public void ToNameValueString_OptionParametersWithNullValue_ShouldConvertToHttpHeaderFormat() + IHeaderDictionary headers = new HeaderDictionary { - var options = new AppOptions { Parameters = null! }; + { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } + }; - IHeaderDictionary headers = new HeaderDictionary - { - { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } - }; - - Assert.Equal(string.Empty, headers["Altinn-DownstreamParameters"]); - } + Assert.Equal(string.Empty, headers["Altinn-DownstreamParameters"]); + } - [Fact] - public void ToNameValueString_OptionParametersWithSpecialCharaters_IsValidAsHeaders() + [Fact] + public void ToNameValueString_OptionParametersWithSpecialCharaters_IsValidAsHeaders() + { + var options = new AppOptions { - var options = new AppOptions + Parameters = new Dictionary { - Parameters = new Dictionary - { - { "lang", "nb" }, - { "level", "1" }, - { "name", "ÆØÅ" }, - { "variant", "Småvilt1" } - }, - }; + { "lang", "nb" }, + { "level", "1" }, + { "name", "ÆØÅ" }, + { "variant", "Småvilt1" } + }, + }; - IHeaderDictionary headers = new HeaderDictionary(); - headers.Append("Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',')); + IHeaderDictionary headers = new HeaderDictionary(); + headers.Append("Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',')); - Assert.Equal( - "lang=nb,level=1,name=%C3%86%C3%98%C3%85,variant=Sm%C3%A5vilt1", - headers["Altinn-DownstreamParameters"] - ); - } + Assert.Equal( + "lang=nb,level=1,name=%C3%86%C3%98%C3%85,variant=Sm%C3%A5vilt1", + headers["Altinn-DownstreamParameters"] + ); } } diff --git a/test/Altinn.App.Core.Tests/Extensions/InstanceEventExtensionsTests.cs b/test/Altinn.App.Core.Tests/Extensions/InstanceEventExtensionsTests.cs index 7e23f586f..c5408fb74 100644 --- a/test/Altinn.App.Core.Tests/Extensions/InstanceEventExtensionsTests.cs +++ b/test/Altinn.App.Core.Tests/Extensions/InstanceEventExtensionsTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Core.Extensions; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Extensions; diff --git a/test/Altinn.App.Core.Tests/Extensions/ProcessStateExtensionTests.cs b/test/Altinn.App.Core.Tests/Extensions/ProcessStateExtensionTests.cs index b06af8f93..a4af1704c 100644 --- a/test/Altinn.App.Core.Tests/Extensions/ProcessStateExtensionTests.cs +++ b/test/Altinn.App.Core.Tests/Extensions/ProcessStateExtensionTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Core.Extensions; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Extensions; diff --git a/test/Altinn.App.Core.Tests/Extensions/ServiceCollectionTests.cs b/test/Altinn.App.Core.Tests/Extensions/ServiceCollectionTests.cs index bad7ed84e..3e9da35fb 100644 --- a/test/Altinn.App.Core.Tests/Extensions/ServiceCollectionTests.cs +++ b/test/Altinn.App.Core.Tests/Extensions/ServiceCollectionTests.cs @@ -4,27 +4,25 @@ using Altinn.App.Core.Internal.Events; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.Extensions +namespace Altinn.App.PlatformServices.Tests.Extensions; + +public class ServiceCollectionTests { - public class ServiceCollectionTests + [Fact] + public void IsAdded_Added_ShouldReturnTrue() { - [Fact] - public void IsAdded_Added_ShouldReturnTrue() - { - IServiceCollection services = new ServiceCollection(); - services.AddHttpClient(); + IServiceCollection services = new ServiceCollection(); + services.AddHttpClient(); - services.IsAdded(typeof(IEventsSubscription)).Should().BeTrue(); - } + services.IsAdded(typeof(IEventsSubscription)).Should().BeTrue(); + } - [Fact] - public void IsAdded_NotAdded_ShouldReturnFalse() - { - IServiceCollection services = new ServiceCollection(); + [Fact] + public void IsAdded_NotAdded_ShouldReturnFalse() + { + IServiceCollection services = new ServiceCollection(); - services.IsAdded(typeof(IEventsSubscription)).Should().BeFalse(); - } + services.IsAdded(typeof(IEventsSubscription)).Should().BeFalse(); } } diff --git a/test/Altinn.App.Core.Tests/Extensions/StringExtensionTests.cs b/test/Altinn.App.Core.Tests/Extensions/StringExtensionTests.cs index 3954cf77a..3d88f7cff 100644 --- a/test/Altinn.App.Core.Tests/Extensions/StringExtensionTests.cs +++ b/test/Altinn.App.Core.Tests/Extensions/StringExtensionTests.cs @@ -1,126 +1,124 @@ #nullable disable using Altinn.App.Core.Extensions; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.Extensions +namespace Altinn.App.PlatformServices.Tests.Extensions; + +public class StringExtensionTests { - public class StringExtensionTests + [Theory] + [InlineData( + "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", + '/' + )] + [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] + [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643/", '/')] + [InlineData("//", '/')] + public void ContainsMoreThanOne_MoreThanOne_ShouldReturnTrue(string s, char c) { - [Theory] - [InlineData( - "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", - '/' - )] - [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] - [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643/", '/')] - [InlineData("//", '/')] - public void ContainsMoreThanOne_MoreThanOne_ShouldReturnTrue(string s, char c) - { - Assert.True(s.ContainsMoreThanOne(c)); - } + Assert.True(s.ContainsMoreThanOne(c)); + } - [Theory] - [InlineData( - "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", - ':' - )] - [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", ':')] - [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] - [InlineData("/", '/')] - public void ContainsMoreThanOne_One_ShouldReturnFalse(string s, char c) - { - Assert.False(s.ContainsMoreThanOne(c)); - } + [Theory] + [InlineData( + "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", + ':' + )] + [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", ':')] + [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] + [InlineData("/", '/')] + public void ContainsMoreThanOne_One_ShouldReturnFalse(string s, char c) + { + Assert.False(s.ContainsMoreThanOne(c)); + } - [Theory] - [InlineData( - "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", - '_' - )] - [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '_')] - [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '\\')] - [InlineData("", '/')] - public void ContainsMoreThanOne_Zero_ShouldReturnFalse(string s, char c) - { - Assert.False(s.ContainsMoreThanOne(c)); - } + [Theory] + [InlineData( + "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", + '_' + )] + [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '_')] + [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '\\')] + [InlineData("", '/')] + public void ContainsMoreThanOne_Zero_ShouldReturnFalse(string s, char c) + { + Assert.False(s.ContainsMoreThanOne(c)); + } - [Theory] - [InlineData( - "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", - '/' - )] - [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] - [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643/", '/')] - [InlineData("//", '/')] - public void ContainsExactlyOne_MoreThanOne_ShouldReturnFalse(string s, char c) - { - Assert.False(s.ContainsExactlyOne(c)); - } + [Theory] + [InlineData( + "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", + '/' + )] + [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] + [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643/", '/')] + [InlineData("//", '/')] + public void ContainsExactlyOne_MoreThanOne_ShouldReturnFalse(string s, char c) + { + Assert.False(s.ContainsExactlyOne(c)); + } - [Theory] - [InlineData( - "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", - ':' - )] - [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] - [InlineData("510002\\4fdbcec8-3e71-43de-862a-0d8098fa0643", '\\')] - [InlineData("/", '/')] - public void ContainsExactlyOne_One_ShouldReturnTrue(string s, char c) - { - Assert.True(s.ContainsExactlyOne(c)); - } + [Theory] + [InlineData( + "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", + ':' + )] + [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] + [InlineData("510002\\4fdbcec8-3e71-43de-862a-0d8098fa0643", '\\')] + [InlineData("/", '/')] + public void ContainsExactlyOne_One_ShouldReturnTrue(string s, char c) + { + Assert.True(s.ContainsExactlyOne(c)); + } - [Theory] - [InlineData( - "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", - '_' - )] - [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '_')] - [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '\\')] - [InlineData("", '/')] - public void ContainsExactlyOne_Zero_ShouldReturnFalse(string s, char c) - { - Assert.False(s.ContainsExactlyOne(c)); - } + [Theory] + [InlineData( + "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", + '_' + )] + [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '_')] + [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '\\')] + [InlineData("", '/')] + public void ContainsExactlyOne_Zero_ShouldReturnFalse(string s, char c) + { + Assert.False(s.ContainsExactlyOne(c)); + } - [Theory] - [InlineData( - "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", - '/' - )] - [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] - [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643/", '/')] - [InlineData("//", '/')] - public void DoesNotContain_MoreThanOne_ShouldReturnFalse(string s, char c) - { - Assert.False(s.DoesNotContain(c)); - } + [Theory] + [InlineData( + "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", + '/' + )] + [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] + [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643/", '/')] + [InlineData("//", '/')] + public void DoesNotContain_MoreThanOne_ShouldReturnFalse(string s, char c) + { + Assert.False(s.DoesNotContain(c)); + } - [Theory] - [InlineData( - "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", - ':' - )] - [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] - [InlineData("510002\\4fdbcec8-3e71-43de-862a-0d8098fa0643", '\\')] - [InlineData("/", '/')] - public void DoesNotContain_One_ShouldReturnFalse(string s, char c) - { - Assert.False(s.DoesNotContain(c)); - } + [Theory] + [InlineData( + "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", + ':' + )] + [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '/')] + [InlineData("510002\\4fdbcec8-3e71-43de-862a-0d8098fa0643", '\\')] + [InlineData("/", '/')] + public void DoesNotContain_One_ShouldReturnFalse(string s, char c) + { + Assert.False(s.DoesNotContain(c)); + } - [Theory] - [InlineData( - "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", - '_' - )] - [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '_')] - [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '\\')] - [InlineData("", '/')] - public void DoesNotContain_Zero_ShouldReturnTrue(string s, char c) - { - Assert.True(s.DoesNotContain(c)); - } + [Theory] + [InlineData( + "http://local.altinn.cloud/dihe/redusert-foreldrebetaling-bhg/#/instance/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", + '_' + )] + [InlineData("/510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '_')] + [InlineData("510002/4fdbcec8-3e71-43de-862a-0d8098fa0643", '\\')] + [InlineData("", '/')] + public void DoesNotContain_Zero_ShouldReturnTrue(string s, char c) + { + Assert.True(s.DoesNotContain(c)); } } diff --git a/test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs index 8090d14f0..74bdb24d7 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/PaymentUserActionTests.cs @@ -2,26 +2,16 @@ using Altinn.App.Core.Features.Action; using Altinn.App.Core.Features.Payment.Models; using Altinn.App.Core.Features.Payment.Services; -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; -using Altinn.App.Core.Internal.Profile; -using Altinn.App.Core.Internal.Sign; -using Altinn.App.Core.Models; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.UserAction; using Altinn.App.Core.Tests.Internal.Process.TestUtils; -using Altinn.Platform.Profile.Models; -using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; -using Json.Patch; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Xunit; -using Signee = Altinn.App.Core.Internal.Sign.Signee; namespace Altinn.App.Core.Tests.Features.Action; diff --git a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs index d2e304a0a..09174a4f3 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs @@ -15,7 +15,6 @@ using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Xunit; using Signee = Altinn.App.Core.Internal.Sign.Signee; namespace Altinn.App.Core.Tests.Features.Action; diff --git a/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs index c3422a6fb..7773e990d 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs @@ -1,7 +1,5 @@ using System.Security.Claims; -using System.Text; using Altinn.App.Core.Features.Action; -using Altinn.App.Core.Infrastructure.Clients.Storage; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Instances; @@ -13,7 +11,6 @@ using AltinnCore.Authentication.Constants; using FluentAssertions; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Features.Action; diff --git a/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs index 7cb48d300..693f56873 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Core.Features.Action; using Altinn.App.Core.Models.UserAction; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Features.Action; diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/GenericDataProcessorTests.cs b/test/Altinn.App.Core.Tests/Features/DataProcessing/GenericDataProcessorTests.cs index 4dadba75a..f71a77e52 100644 --- a/test/Altinn.App.Core.Tests/Features/DataProcessing/GenericDataProcessorTests.cs +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/GenericDataProcessorTests.cs @@ -1,7 +1,6 @@ using Altinn.App.Core.Features.DataProcessing; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Features.DataProcessing; diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/NullInstantiationProcessTests.cs b/test/Altinn.App.Core.Tests/Features/DataProcessing/NullInstantiationProcessTests.cs index 5e7967db7..9e187f161 100644 --- a/test/Altinn.App.Core.Tests/Features/DataProcessing/NullInstantiationProcessTests.cs +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/NullInstantiationProcessTests.cs @@ -3,7 +3,6 @@ using Altinn.App.PlatformServices.Tests.Implementation.TestResources; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.PlatformServices.Tests.Features.DataProcessing; diff --git a/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs index fa16226a1..8ca528d7c 100644 --- a/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Notifications/Sms/SmsNotificationClientTests.cs @@ -20,7 +20,6 @@ namespace Altinn.App.Core.Tests.Features.Notifications.Sms; using Moq; using Moq.Protected; using Xunit; -using static Altinn.App.Core.Features.Telemetry; public class SmsNotificationClientTests { diff --git a/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2MetadataApiClientHttpMessageHandlerMoq.cs b/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2MetadataApiClientHttpMessageHandlerMoq.cs index e3866d59d..4972263c1 100644 --- a/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2MetadataApiClientHttpMessageHandlerMoq.cs +++ b/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2MetadataApiClientHttpMessageHandlerMoq.cs @@ -1,52 +1,51 @@ using System.Net; -namespace Altinn.App.Core.Tests.Features.Options.Altinn2Provider +namespace Altinn.App.Core.Tests.Features.Options.Altinn2Provider; + +public class Altinn2MetadataApiClientHttpMessageHandlerMoq : HttpMessageHandler { - public class Altinn2MetadataApiClientHttpMessageHandlerMoq : HttpMessageHandler - { - // Instrumentation to test that caching works - public int CallCounter { get; private set; } = 0; + // Instrumentation to test that caching works + public int CallCounter { get; private set; } = 0; - protected override Task SendAsync( - HttpRequestMessage httpRequestMessage, - CancellationToken cancellationToken - ) - { - CallCounter++; + protected override Task SendAsync( + HttpRequestMessage httpRequestMessage, + CancellationToken cancellationToken + ) + { + CallCounter++; - var url = httpRequestMessage.RequestUri?.ToString() ?? string.Empty; + var url = httpRequestMessage.RequestUri?.ToString() ?? string.Empty; - if (url.StartsWith("https://www.altinn.no/api/metadata/codelists/serverError")) - { - return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.InternalServerError, }); - } + if (url.StartsWith("https://www.altinn.no/api/metadata/codelists/serverError")) + { + return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.InternalServerError, }); + } - var stringResult = GetStringResult(url); - var status = stringResult != null ? HttpStatusCode.OK : HttpStatusCode.NotFound; - var response = new HttpResponseMessage(status); - response.Content = new ByteArrayContent(System.Text.Encoding.UTF8.GetBytes(stringResult ?? string.Empty)); - response.Content.Headers.Remove("Content-Type"); - response.Content.Headers.Add("Content-Type", "application/json; charset=utf-8"); + var stringResult = GetStringResult(url); + var status = stringResult != null ? HttpStatusCode.OK : HttpStatusCode.NotFound; + var response = new HttpResponseMessage(status); + response.Content = new ByteArrayContent(System.Text.Encoding.UTF8.GetBytes(stringResult ?? string.Empty)); + response.Content.Headers.Remove("Content-Type"); + response.Content.Headers.Add("Content-Type", "application/json; charset=utf-8"); - return Task.FromResult(response); - } + return Task.FromResult(response); + } - private static string? GetStringResult(string url) + private static string? GetStringResult(string url) + { + return url switch { - return url switch - { - "https://www.altinn.no/api/metadata/codelists/ASF_land/2758?language=1033" - => "{\"Name\":\"ASF_Land\",\"Version\":2758,\"Language\":1033,\"Codes\":[{\"Code\":\"\",\"Value1\":\"\",\"Value2\":\"\",\"Value3\":\"\"},{\"Code\":\"AFGHANISTAN\",\"Value1\":\"AFGHANISTAN\",\"Value2\":\"AF\",\"Value3\":\"404\"},{\"Code\":\"ALBANIA\",\"Value1\":\"ALBANIA\",\"Value2\":\"AL\",\"Value3\":\"111\"},{\"Code\":\"ALGERIE\",\"Value1\":\"ALGERIA\",\"Value2\":\"DZ\",\"Value3\":\"203\"},{\"Code\":\"AM. SAMOA\",\"Value1\":\"AMERICAN SAMOA\",\"Value2\":\"AS\",\"Value3\":\"802\"},{\"Code\":\"ANDORRA\",\"Value1\":\"ANDORRA\",\"Value2\":\"AD\",\"Value3\":\"114\"},{\"Code\":\"ANGOLA\",\"Value1\":\"ANGOLA\",\"Value2\":\"AO\",\"Value3\":\"204\"},{\"Code\":\"ANGUILLA\",\"Value1\":\"ANGUILLA\",\"Value2\":\"AI\",\"Value3\":\"660\"},{\"Code\":\"ANTARKTIS\",\"Value1\":\"ANTARCTICA\",\"Value2\":\"AQ\",\"Value3\":\"901\"},{\"Code\":\"ANTIGUA OG BARBUDA\",\"Value1\":\"ANTIGUA AND BARBUDA\",\"Value2\":\"AG\",\"Value3\":\"603\"},{\"Code\":\"ARGENTINA\",\"Value1\":\"ARGENTINA\",\"Value2\":\"AR\",\"Value3\":\"705\"},{\"Code\":\"ARMENIA\",\"Value1\":\"ARMENIA\",\"Value2\":\"AM\",\"Value3\":\"406\"},{\"Code\":\"ARUBA\",\"Value1\":\"ARUBA\",\"Value2\":\"AW\",\"Value3\":\"657\"},{\"Code\":\"AUSTRALIA\",\"Value1\":\"AUSTRALIA\",\"Value2\":\"AU\",\"Value3\":\"805\"},{\"Code\":\"ØSTERRIKE\",\"Value1\":\"AUSTRIA\",\"Value2\":\"AT\",\"Value3\":\"153\"},{\"Code\":\"AZERBAJDZJAN\",\"Value1\":\"AZERBAIJAN\",\"Value2\":\"AZ\",\"Value3\":\"407\"},{\"Code\":\"BAHAMAS\",\"Value1\":\"BAHAMAS\",\"Value2\":\"BS\",\"Value3\":\"605\"},{\"Code\":\"BAHRAIN\",\"Value1\":\"BAHRAIN\",\"Value2\":\"BH\",\"Value3\":\"409\"},{\"Code\":\"BANGLADESH\",\"Value1\":\"BANGLADESH\",\"Value2\":\"BD\",\"Value3\":\"410\"},{\"Code\":\"BARBADOS\",\"Value1\":\"BARBADOS\",\"Value2\":\"BB\",\"Value3\":\"602\"},{\"Code\":\"HVITERUSSLAND\",\"Value1\":\"BELARUS\",\"Value2\":\"BY\",\"Value3\":\"120\"},{\"Code\":\"BELGIA\",\"Value1\":\"BELGIUM\",\"Value2\":\"BE\",\"Value3\":\"112\"},{\"Code\":\"BELIZE\",\"Value1\":\"BELIZE\",\"Value2\":\"BZ\",\"Value3\":\"604\"},{\"Code\":\"BENIN\",\"Value1\":\"BENIN\",\"Value2\":\"BJ\",\"Value3\":\"229\"},{\"Code\":\"BERMUDA\",\"Value1\":\"BERMUDA\",\"Value2\":\"BM\",\"Value3\":\"606\"},{\"Code\":\"BHUTAN\",\"Value1\":\"BHUTAN\",\"Value2\":\"BT\",\"Value3\":\"412\"},{\"Code\":\"BOLIVIA\",\"Value1\":\"BOLIVIA\",\"Value2\":\"BO\",\"Value3\":\"710\"},{\"Code\":\"BONAIRE, SINT EUSTATIUS OG SABA\",\"Value1\":\"BONAIRE, SINT EUSTATIUS AND SABA\",\"Value2\":\"BQ\",\"Value3\":\"659\"},{\"Code\":\"BOSNIA-HERCEGOVINA\",\"Value1\":\"BOSNIA AND HERZEGOVINA\",\"Value2\":\"BA\",\"Value3\":\"155\"},{\"Code\":\"BOTSWANA\",\"Value1\":\"BOTSWANA\",\"Value2\":\"BW\",\"Value3\":\"205\"},{\"Code\":\"BOUVETØYA\",\"Value1\":\"BOUVET ISLAND\",\"Value2\":\"BV\",\"Value3\":\"904\"},{\"Code\":\"BRASIL\",\"Value1\":\"BRAZIL\",\"Value2\":\"BR\",\"Value3\":\"715\"},{\"Code\":\"BRITISK-INDISKE HAV\",\"Value1\":\"BRITISH INDIAN OCEAN TERRITORY\",\"Value2\":\"IO\",\"Value3\":\"213\"},{\"Code\":\"BRUNEI\",\"Value1\":\"BRUNEI\",\"Value2\":\"BN\",\"Value3\":\"416\"},{\"Code\":\"BULGARIA\",\"Value1\":\"BULGARIA\",\"Value2\":\"BG\",\"Value3\":\"113\"},{\"Code\":\"BURKINA FASO\",\"Value1\":\"BURKINA FASO\",\"Value2\":\"BF\",\"Value3\":\"393\"},{\"Code\":\"BURUNDI\",\"Value1\":\"BURUNDI\",\"Value2\":\"BI\",\"Value3\":\"216\"},{\"Code\":\"KAMBODSJA\",\"Value1\":\"CAMBODIA\",\"Value2\":\"KH\",\"Value3\":\"478\"},{\"Code\":\"KAMERUN\",\"Value1\":\"CAMEROON\",\"Value2\":\"CM\",\"Value3\":\"270\"},{\"Code\":\"CANADA\",\"Value1\":\"CANADA\",\"Value2\":\"CA\",\"Value3\":\"612\"},{\"Code\":\"KAPP VERDE\",\"Value1\":\"CAPE VERDE\",\"Value2\":\"CV\",\"Value3\":\"273\"},{\"Code\":\"CAYMANØYENE\",\"Value1\":\"CAYMAN ISLANDS\",\"Value2\":\"KY\",\"Value3\":\"613\"},{\"Code\":\"SENTRALAFRIKANSKE REPUBLIKK\",\"Value1\":\"CENTRAL AFRICAN REPUBLIC\",\"Value2\":\"CF\",\"Value3\":\"337\"},{\"Code\":\"TCHAD\",\"Value1\":\"CHAD\",\"Value2\":\"TD\",\"Value3\":\"373\"},{\"Code\":\"CHILE\",\"Value1\":\"CHILE\",\"Value2\":\"CL\",\"Value3\":\"725\"},{\"Code\":\"KINA\",\"Value1\":\"CHINA\",\"Value2\":\"CN\",\"Value3\":\"484\"},{\"Code\":\"CHRISTMASØYA\",\"Value1\":\"CHRISTMAS ISLAND\",\"Value2\":\"CX\",\"Value3\":\"807\"},{\"Code\":\"KOKOSØYENE\",\"Value1\":\"COCOS ISLANDS (KEELING)\",\"Value2\":\"CC\",\"Value3\":\"808\"},{\"Code\":\"COLOMBIA\",\"Value1\":\"COLOMBIA\",\"Value2\":\"CO\",\"Value3\":\"730\"},{\"Code\":\"KOMORENE\",\"Value1\":\"COMOROS\",\"Value2\":\"KM\",\"Value3\":\"220\"},{\"Code\":\"KONGO, REPUBLIKKEN\",\"Value1\":\"CONGO\",\"Value2\":\"CG\",\"Value3\":\"278\"},{\"Code\":\"KONGO, DEN DEMOKR. REPUBL\",\"Value1\":\"CONGO, THE DEMOCRATIC REPUBLIC OF THE\",\"Value2\":\"CD\",\"Value3\":\"279\"},{\"Code\":\"COOKØYENE\",\"Value1\":\"COOK ISLANDS\",\"Value2\":\"CK\",\"Value3\":\"809\"},{\"Code\":\"COSTA RICA\",\"Value1\":\"COSTA RICA\",\"Value2\":\"CR\",\"Value3\":\"616\"},{\"Code\":\"ELFENBEINSKYSTEN\",\"Value1\":\"CÔTE D IVOIRE\",\"Value2\":\"CI\",\"Value3\":\"239\"},{\"Code\":\"KROATIA\",\"Value1\":\"CROATIA\",\"Value2\":\"HR\",\"Value3\":\"122\"},{\"Code\":\"CUBA\",\"Value1\":\"CUBA\",\"Value2\":\"CU\",\"Value3\":\"620\"},{\"Code\":\"CURACAO\",\"Value1\":\"CURACAO\",\"Value2\":\"CW\",\"Value3\":\"661\"},{\"Code\":\"KYPROS\",\"Value1\":\"CYPRUS\",\"Value2\":\"CY\",\"Value3\":\"500\"},{\"Code\":\"DEN TSJEKKISKE REP.\",\"Value1\":\"CZECHIA\",\"Value2\":\"CZ\",\"Value3\":\"158\"},{\"Code\":\"DANMARK\",\"Value1\":\"DENMARK\",\"Value2\":\"DK\",\"Value3\":\"101\"},{\"Code\":\"DJIBOUTI\",\"Value1\":\"DJIBOUTI\",\"Value2\":\"DJ\",\"Value3\":\"250\"},{\"Code\":\"DOMINICA\",\"Value1\":\"DOMINICA\",\"Value2\":\"DM\",\"Value3\":\"622\"},{\"Code\":\"DEN DOMINIKANSKE REP\",\"Value1\":\"DOMINICAN REPUBLIC\",\"Value2\":\"DO\",\"Value3\":\"624\"},{\"Code\":\"ECUADOR\",\"Value1\":\"ECUADOR\",\"Value2\":\"EC\",\"Value3\":\"735\"},{\"Code\":\"EGYPT\",\"Value1\":\"EGYPT\",\"Value2\":\"EG\",\"Value3\":\"249\"},{\"Code\":\"EL SALVADOR\",\"Value1\":\"EL SALVADOR\",\"Value2\":\"SV\",\"Value3\":\"672\"},{\"Code\":\"EKVATORIAL-GUINEA\",\"Value1\":\"EQUATORIAL GUINEA\",\"Value2\":\"GQ\",\"Value3\":\"235\"},{\"Code\":\"ERITREA\",\"Value1\":\"ERITREA\",\"Value2\":\"ER\",\"Value3\":\"241\"},{\"Code\":\"ESTLAND\",\"Value1\":\"ESTONIA\",\"Value2\":\"EE\",\"Value3\":\"115\"},{\"Code\":\"ETIOPIA\",\"Value1\":\"ETHIOPIA\",\"Value2\":\"ET\",\"Value3\":\"246\"},{\"Code\":\"FALKLANDSØYENE\",\"Value1\":\"FALKLAND ISLANDS\",\"Value2\":\"FK\",\"Value3\":\"740\"},{\"Code\":\"FÆRØYENE\",\"Value1\":\"FAROE ISLANDS\",\"Value2\":\"FO\",\"Value3\":\"104\"},{\"Code\":\"FIJII\",\"Value1\":\"FIJI\",\"Value2\":\"FJ\",\"Value3\":\"811\"},{\"Code\":\"FINLAND\",\"Value1\":\"FINLAND\",\"Value2\":\"FI\",\"Value3\":\"103\"},{\"Code\":\"FRANKRIKE\",\"Value1\":\"FRANCE\",\"Value2\":\"FR\",\"Value3\":\"117\"},{\"Code\":\"FRANSK GUYANA\",\"Value1\":\"FRENCH GUIANA\",\"Value2\":\"GF\",\"Value3\":\"745\"},{\"Code\":\"FRANSK POLYNESIA\",\"Value1\":\"FRENCH POLYNESIA\",\"Value2\":\"PF\",\"Value3\":\"814\"},{\"Code\":\"DE FRANSKE TERRITORIENE I SØR\",\"Value1\":\"FRENCH SOUTHERN TEERITORIES\",\"Value2\":\"TF\",\"Value3\":\"913\"},{\"Code\":\"GABON\",\"Value1\":\"GABON\",\"Value2\":\"GA\",\"Value3\":\"254\"},{\"Code\":\"GAMBIA\",\"Value1\":\"GAMBIA\",\"Value2\":\"GM\",\"Value3\":\"256\"},{\"Code\":\"GEORGIA\",\"Value1\":\"GEORGIA\",\"Value2\":\"GE\",\"Value3\":\"430\"},{\"Code\":\"TYSKLAND\",\"Value1\":\"GERMANY\",\"Value2\":\"DE\",\"Value3\":\"144\"},{\"Code\":\"GHANA\",\"Value1\":\"GHANA\",\"Value2\":\"GH\",\"Value3\":\"260\"},{\"Code\":\"GIBRALTAR\",\"Value1\":\"GIBRALTAR\",\"Value2\":\"GI\",\"Value3\":\"118\"},{\"Code\":\"STORBRITANNIA\",\"Value1\":\"GREAT BRITAIN\",\"Value2\":\"GB\",\"Value3\":\"139\"},{\"Code\":\"HELLAS\",\"Value1\":\"GREECE\",\"Value2\":\"GR\",\"Value3\":\"119\"},{\"Code\":\"GRØNLAND\",\"Value1\":\"GREENLAND\",\"Value2\":\"GL\",\"Value3\":\"102\"},{\"Code\":\"GRENADA\",\"Value1\":\"GRENADA\",\"Value2\":\"GD\",\"Value3\":\"629\"},{\"Code\":\"GUADELOUPE\",\"Value1\":\"GUADELOUPE\",\"Value2\":\"GP\",\"Value3\":\"631\"},{\"Code\":\"GUAM\",\"Value1\":\"GUAM\",\"Value2\":\"GU\",\"Value3\":\"817\"},{\"Code\":\"GUATEMALA\",\"Value1\":\"GUATEMALA\",\"Value2\":\"GT\",\"Value3\":\"632\"},{\"Code\":\"GUERNSEY\",\"Value1\":\"GUERNSEY\",\"Value2\":\"GG\",\"Value3\":\"162\"},{\"Code\":\"GUINEA\",\"Value1\":\"GUINEA\",\"Value2\":\"GN\",\"Value3\":\"264\"},{\"Code\":\"GUINEA-BISSAU\",\"Value1\":\"GUINEA-BISSAU\",\"Value2\":\"GW\",\"Value3\":\"266\"},{\"Code\":\"GUYANA\",\"Value1\":\"GUYANA\",\"Value2\":\"GY\",\"Value3\":\"720\"},{\"Code\":\"HAITI\",\"Value1\":\"HAITI\",\"Value2\":\"HT\",\"Value3\":\"636\"},{\"Code\":\"HEARD- OG MCDONALDØYENE\",\"Value1\":\"HEARD ISLAND AND MCDONALD ISLANDS\",\"Value2\":\"HM\",\"Value3\":\"908\"},{\"Code\":\"HONDURAS\",\"Value1\":\"HONDURAS\",\"Value2\":\"HN\",\"Value3\":\"644\"},{\"Code\":\"HONGKONG\",\"Value1\":\"HONG KONG\",\"Value2\":\"HK\",\"Value3\":\"436\"},{\"Code\":\"UNGARN\",\"Value1\":\"HUNGARY\",\"Value2\":\"HU\",\"Value3\":\"152\"},{\"Code\":\"ISLAND\",\"Value1\":\"ICELAND\",\"Value2\":\"IS\",\"Value3\":\"105\"},{\"Code\":\"INDIA\",\"Value1\":\"INDIA\",\"Value2\":\"IN\",\"Value3\":\"444\"},{\"Code\":\"INDONESIA\",\"Value1\":\"INDONESIA\",\"Value2\":\"ID\",\"Value3\":\"448\"},{\"Code\":\"IRAN\",\"Value1\":\"IRAN\",\"Value2\":\"IR\",\"Value3\":\"456\"},{\"Code\":\"IRAK\",\"Value1\":\"IRAQ\",\"Value2\":\"IQ\",\"Value3\":\"452\"},{\"Code\":\"IRLAND\",\"Value1\":\"IRELAND\",\"Value2\":\"IE\",\"Value3\":\"121\"},{\"Code\":\"MAN\",\"Value1\":\"ISLE OF MAN\",\"Value2\":\"IM\",\"Value3\":\"164\"},{\"Code\":\"ISRAEL\",\"Value1\":\"ISRAEL\",\"Value2\":\"IL\",\"Value3\":\"460\"},{\"Code\":\"ITALIA\",\"Value1\":\"ITALY\",\"Value2\":\"IT\",\"Value3\":\"123\"},{\"Code\":\"JAMAICA\",\"Value1\":\"JAMAICA\",\"Value2\":\"JM\",\"Value3\":\"648\"},{\"Code\":\"JAPAN\",\"Value1\":\"JAPAN\",\"Value2\":\"JP\",\"Value3\":\"464\"},{\"Code\":\"JERSEY\",\"Value1\":\"JERSEY\",\"Value2\":\"JE\",\"Value3\":\"163\"},{\"Code\":\"JORDAN\",\"Value1\":\"JORDAN\",\"Value2\":\"JO\",\"Value3\":\"476\"},{\"Code\":\"KAZAKHSTAN\",\"Value1\":\"KAZAKHSTAN\",\"Value2\":\"KZ\",\"Value3\":\"480\"},{\"Code\":\"KENYA\",\"Value1\":\"KENYA\",\"Value2\":\"KE\",\"Value3\":\"276\"},{\"Code\":\"KIRIBATI\",\"Value1\":\"KIRIBATI\",\"Value2\":\"KI\",\"Value3\":\"815\"},{\"Code\":\"KOSOVO\",\"Value1\":\"KOSOVO\",\"Value2\":\"XK\",\"Value3\":\"161\"},{\"Code\":\"KUWAIT\",\"Value1\":\"KUWAIT\",\"Value2\":\"KW\",\"Value3\":\"496\"},{\"Code\":\"KYRGYZSTAN\",\"Value1\":\"KYRGYZSTAN\",\"Value2\":\"KG\",\"Value3\":\"502\"},{\"Code\":\"LAOS\",\"Value1\":\"LAOS\",\"Value2\":\"LA\",\"Value3\":\"504\"},{\"Code\":\"LATVIA\",\"Value1\":\"LATVIA\",\"Value2\":\"LV\",\"Value3\":\"124\"},{\"Code\":\"LIBANON\",\"Value1\":\"LEBANON\",\"Value2\":\"LB\",\"Value3\":\"508\"},{\"Code\":\"LESOTHO\",\"Value1\":\"LESOTHO\",\"Value2\":\"LS\",\"Value3\":\"281\"},{\"Code\":\"LIBERIA\",\"Value1\":\"LIBERIA\",\"Value2\":\"LR\",\"Value3\":\"283\"},{\"Code\":\"LIBYA\",\"Value1\":\"LIBYA\",\"Value2\":\"LY\",\"Value3\":\"286\"},{\"Code\":\"LIECHTENSTEIN\",\"Value1\":\"LIECHTENSTEIN\",\"Value2\":\"LI\",\"Value3\":\"128\"},{\"Code\":\"LITAUEN\",\"Value1\":\"LITHUANIA\",\"Value2\":\"LT\",\"Value3\":\"136\"},{\"Code\":\"LUXEMBOURG\",\"Value1\":\"LUXEMBOURG\",\"Value2\":\"LU\",\"Value3\":\"129\"},{\"Code\":\"MACAO\",\"Value1\":\"MACAU\",\"Value2\":\"MO\",\"Value3\":\"510\"},{\"Code\":\"MADAGASKAR\",\"Value1\":\"MADAGASCAR\",\"Value2\":\"MG\",\"Value3\":\"289\"},{\"Code\":\"MALAWI\",\"Value1\":\"MALAWI\",\"Value2\":\"MW\",\"Value3\":\"296\"},{\"Code\":\"MALAYSIA\",\"Value1\":\"MALAYSIA\",\"Value2\":\"MY\",\"Value3\":\"512\"},{\"Code\":\"MALDIVENE\",\"Value1\":\"MALDIVES\",\"Value2\":\"MV\",\"Value3\":\"513\"},{\"Code\":\"MALI\",\"Value1\":\"MALI\",\"Value2\":\"ML\",\"Value3\":\"299\"},{\"Code\":\"MALTA\",\"Value1\":\"MALTA\",\"Value2\":\"MT\",\"Value3\":\"126\"},{\"Code\":\"MARSHALLØYENE\",\"Value1\":\"MARSHALL ISLANDS\",\"Value2\":\"MH\",\"Value3\":\"835\"},{\"Code\":\"MARTINIQUE\",\"Value1\":\"MARTINIQUE\",\"Value2\":\"MQ\",\"Value3\":\"650\"},{\"Code\":\"MAURITANIA\",\"Value1\":\"MAURITANIA\",\"Value2\":\"MR\",\"Value3\":\"306\"},{\"Code\":\"MAURITIUS\",\"Value1\":\"MAURITIUS\",\"Value2\":\"MU\",\"Value3\":\"307\"},{\"Code\":\"MAYOTTE\",\"Value1\":\"MAYOTTE\",\"Value2\":\"YT\",\"Value3\":\"322\"},{\"Code\":\"MEXICO\",\"Value1\":\"MEXICO\",\"Value2\":\"MX\",\"Value3\":\"652\"},{\"Code\":\"MIKRONESIAFØDERASJONEN\",\"Value1\":\"MICRONESIA, FEDERATED STATES OF\",\"Value2\":\"FM\",\"Value3\":\"826\"},{\"Code\":\"DIVERSE\",\"Value1\":\"MISCELLANEOUS\",\"Value2\":\"ZZ\",\"Value3\":\"990\"},{\"Code\":\"MOLDOVA\",\"Value1\":\"MOLDOVA\",\"Value2\":\"MD\",\"Value3\":\"138\"},{\"Code\":\"MONACO\",\"Value1\":\"MONACO\",\"Value2\":\"MC\",\"Value3\":\"130\"},{\"Code\":\"MONGOLIA\",\"Value1\":\"MONGOLIA\",\"Value2\":\"MN\",\"Value3\":\"516\"},{\"Code\":\"MONTENEGRO\",\"Value1\":\"MONTENEGRO\",\"Value2\":\"ME\",\"Value3\":\"160\"},{\"Code\":\"MONTSERRAT\",\"Value1\":\"MONTSERRAT\",\"Value2\":\"MS\",\"Value3\":\"654\"},{\"Code\":\"MAROKKO\",\"Value1\":\"MOROCCO\",\"Value2\":\"MA\",\"Value3\":\"303\"},{\"Code\":\"MOSAMBIK\",\"Value1\":\"MOZAMBIQUE\",\"Value2\":\"MZ\",\"Value3\":\"319\"},{\"Code\":\"MYANMAR (BURMA)\",\"Value1\":\"MYANMAR\",\"Value2\":\"MM\",\"Value3\":\"420\"},{\"Code\":\"NAMIBIA\",\"Value1\":\"NAMIBIA\",\"Value2\":\"NA\",\"Value3\":\"308\"},{\"Code\":\"NAURU\",\"Value1\":\"NAURU\",\"Value2\":\"NR\",\"Value3\":\"818\"},{\"Code\":\"NEPAL\",\"Value1\":\"NEPAL\",\"Value2\":\"NP\",\"Value3\":\"528\"},{\"Code\":\"NEDERLAND\",\"Value1\":\"NETHERLANDS\",\"Value2\":\"NL\",\"Value3\":\"127\"},{\"Code\":\"DE NEDERLANDSKE ANTILLENE\",\"Value1\":\"NETHERLANDS ANTILLES\",\"Value2\":\"AN\",\"Value3\":\"656\"},{\"Code\":\"NY-KALEDONIA\",\"Value1\":\"NEW CALEDONIA\",\"Value2\":\"NC\",\"Value3\":\"833\"},{\"Code\":\"NEW ZEALAND\",\"Value1\":\"NEW ZEALAND\",\"Value2\":\"NZ\",\"Value3\":\"820\"},{\"Code\":\"NICARAGUA\",\"Value1\":\"NICARAGUA\",\"Value2\":\"NI\",\"Value3\":\"664\"},{\"Code\":\"NIGER\",\"Value1\":\"NIGER\",\"Value2\":\"NE\",\"Value3\":\"309\"},{\"Code\":\"NIGERIA\",\"Value1\":\"NIGERIA\",\"Value2\":\"NG\",\"Value3\":\"313\"},{\"Code\":\"NIUE\",\"Value1\":\"NIUE\",\"Value2\":\"NU\",\"Value3\":\"821\"},{\"Code\":\"NORFOLKØYA\",\"Value1\":\"NORFOLK ISLAND\",\"Value2\":\"NF\",\"Value3\":\"822\"},{\"Code\":\"NORD-KOREA\",\"Value1\":\"NORTH KOREA\",\"Value2\":\"KP\",\"Value3\":\"488\"},{\"Code\":\"NORD-MAKEDONIA\",\"Value1\":\"NORTH MACEDONIA\",\"Value2\":\"MK\",\"Value3\":\"156\"},{\"Code\":\"NORD-MARIANENE\",\"Value1\":\"NORTHERN MARIANA ISLANDS\",\"Value2\":\"MP\",\"Value3\":\"840\"},{\"Code\":\"NORGE\",\"Value1\":\"NORWAY\",\"Value2\":\"NO\",\"Value3\":\"000\"},{\"Code\":\"OMAN\",\"Value1\":\"OMAN\",\"Value2\":\"OM\",\"Value3\":\"520\"},{\"Code\":\"PAKISTAN\",\"Value1\":\"PAKISTAN\",\"Value2\":\"PK\",\"Value3\":\"534\"},{\"Code\":\"PALAU\",\"Value1\":\"PALAU\",\"Value2\":\"PW\",\"Value3\":\"839\"},{\"Code\":\"DET PALESTINSKE OMRÅDET\",\"Value1\":\"PALESTINE\",\"Value2\":\"PS\",\"Value3\":\"524\"},{\"Code\":\"PANAMA\",\"Value1\":\"PANAMA\",\"Value2\":\"PA\",\"Value3\":\"668\"},{\"Code\":\"PAPUA NY-GUINEA\",\"Value1\":\"PAPUA NEW GUINEA\",\"Value2\":\"PG\",\"Value3\":\"827\"},{\"Code\":\"PARAGUAY\",\"Value1\":\"PARAGUAY\",\"Value2\":\"PY\",\"Value3\":\"755\"},{\"Code\":\"PERU\",\"Value1\":\"PERU\",\"Value2\":\"PE\",\"Value3\":\"760\"},{\"Code\":\"FILIPPINENE\",\"Value1\":\"PHILIPPINES\",\"Value2\":\"PH\",\"Value3\":\"428\"},{\"Code\":\"PITCAIRN\",\"Value1\":\"PITCAIRN\",\"Value2\":\"PN\",\"Value3\":\"828\"},{\"Code\":\"POLEN\",\"Value1\":\"POLAND\",\"Value2\":\"PL\",\"Value3\":\"131\"},{\"Code\":\"PORTUGAL\",\"Value1\":\"PORTUGAL\",\"Value2\":\"PT\",\"Value3\":\"132\"},{\"Code\":\"PUERTO RICO\",\"Value1\":\"PUERTO RICO\",\"Value2\":\"PR\",\"Value3\":\"685\"},{\"Code\":\"QATAR\",\"Value1\":\"QATAR\",\"Value2\":\"QA\",\"Value3\":\"540\"},{\"Code\":\"RÉUNION\",\"Value1\":\"REUNION\",\"Value2\":\"RE\",\"Value3\":\"323\"},{\"Code\":\"ROMANIA\",\"Value1\":\"ROMANIA\",\"Value2\":\"RO\",\"Value3\":\"133\"},{\"Code\":\"RUSSLAND\",\"Value1\":\"RUSSIA\",\"Value2\":\"RU\",\"Value3\":\"140\"},{\"Code\":\"RWANDA\",\"Value1\":\"RWANDA\",\"Value2\":\"RW\",\"Value3\":\"329\"},{\"Code\":\"ST.HELENA\",\"Value1\":\"SAINT HELENA\",\"Value2\":\"SH\",\"Value3\":\"209\"},{\"Code\":\"ST.KITTS OG NEVIS\",\"Value1\":\"SAINT KITTS AND NEVIS\",\"Value2\":\"KN\",\"Value3\":\"677\"},{\"Code\":\"ST. LUCIA\",\"Value1\":\"SAINT LUCIA\",\"Value2\":\"LC\",\"Value3\":\"678\"},{\"Code\":\"SAINT MARTIN\",\"Value1\":\"SAINT MARTIN (FRENCH PART)\",\"Value2\":\"MF\",\"Value3\":\"686\"},{\"Code\":\"ST.PIERRE OG MIQUELON\",\"Value1\":\"SAINT PIERRE AND MIQUELON\",\"Value2\":\"PM\",\"Value3\":\"676\"},{\"Code\":\"ST. VINCENT OG GRENADINENE\",\"Value1\":\"SAINT VINCENT AND THE GRENADINES\",\"Value2\":\"VC\",\"Value3\":\"679\"},{\"Code\":\"VEST-SAMOA\",\"Value1\":\"SAMOA\",\"Value2\":\"WS\",\"Value3\":\"830\"},{\"Code\":\"SAN MARINO\",\"Value1\":\"SAN MARINO\",\"Value2\":\"SM\",\"Value3\":\"134\"},{\"Code\":\"SAO TOME OG PRINCIPE\",\"Value1\":\"SAO TOME AND PRINCIPE\",\"Value2\":\"ST\",\"Value3\":\"333\"},{\"Code\":\"SAUDI-ARABIA\",\"Value1\":\"SAUDI ARABIA\",\"Value2\":\"SA\",\"Value3\":\"544\"},{\"Code\":\"SENEGAL\",\"Value1\":\"SENEGAL\",\"Value2\":\"SN\",\"Value3\":\"336\"},{\"Code\":\"SERBIA\",\"Value1\":\"SERBIA\",\"Value2\":\"RS\",\"Value3\":\"159\"},{\"Code\":\"SEYCHELLENE\",\"Value1\":\"SEYCHELLES\",\"Value2\":\"SC\",\"Value3\":\"338\"},{\"Code\":\"SIERRA LEONE\",\"Value1\":\"SIERRA LEONE\",\"Value2\":\"SL\",\"Value3\":\"339\"},{\"Code\":\"SINGAPORE\",\"Value1\":\"SINGAPORE\",\"Value2\":\"SG\",\"Value3\":\"548\"},{\"Code\":\"SINT MAARTEN\",\"Value1\":\"SINT MAARTEN (DUTCH PART)\",\"Value2\":\"SX\",\"Value3\":\"658\"},{\"Code\":\"SLOVAKIA\",\"Value1\":\"SLOVAKIA\",\"Value2\":\"SK\",\"Value3\":\"157\"},{\"Code\":\"SLOVENIA\",\"Value1\":\"SLOVENIA\",\"Value2\":\"SI\",\"Value3\":\"146\"},{\"Code\":\"SALOMONØYENE\",\"Value1\":\"SOLOMON ISLANDS\",\"Value2\":\"SB\",\"Value3\":\"806\"},{\"Code\":\"SOMALIA\",\"Value1\":\"SOMALIA\",\"Value2\":\"SO\",\"Value3\":\"346\"},{\"Code\":\"SØR-AFRIKA\",\"Value1\":\"SOUTH AFRICA\",\"Value2\":\"ZA\",\"Value3\":\"359\"},{\"Code\":\"SØR-GEORGIA OG DE SØNDRE SANDWICHØYENE\",\"Value1\":\"SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS\",\"Value2\":\"GS\",\"Value3\":\"907\"},{\"Code\":\"SØR-KOREA\",\"Value1\":\"SOUTH KOREA\",\"Value2\":\"KR\",\"Value3\":\"492\"},{\"Code\":\"SØR-SUDAN\",\"Value1\":\"SOUTH SUDAN\",\"Value2\":\"SS\",\"Value3\":\"355\"},{\"Code\":\"SPANIA\",\"Value1\":\"SPAIN\",\"Value2\":\"ES\",\"Value3\":\"137\"},{\"Code\":\"SRI LANKA\",\"Value1\":\"SRI LANKA\",\"Value2\":\"LK\",\"Value3\":\"424\"},{\"Code\":\"ST BARTHELEMY\",\"Value1\":\"ST BARTHELEMY\",\"Value2\":\"BL\",\"Value3\":\"687\"},{\"Code\":\"SUDAN\",\"Value1\":\"SUDAN\",\"Value2\":\"SD\",\"Value3\":\"356\"},{\"Code\":\"SURINAME\",\"Value1\":\"SURINAME\",\"Value2\":\"SR\",\"Value3\":\"765\"},{\"Code\":\"SVALBARD OG JAN MAYEN\",\"Value1\":\"SVALBARD AND JAN MAYEN\",\"Value2\":\"SJ\",\"Value3\":\"911\"},{\"Code\":\"SWAZILAND\",\"Value1\":\"SWAZILAND\",\"Value2\":\"SZ\",\"Value3\":\"357\"},{\"Code\":\"SVERIGE\",\"Value1\":\"SWEDEN\",\"Value2\":\"SE\",\"Value3\":\"106\"},{\"Code\":\"SVEITS\",\"Value1\":\"SWITZERLAND\",\"Value2\":\"CH\",\"Value3\":\"141\"},{\"Code\":\"SYRIA\",\"Value1\":\"SYRIA\",\"Value2\":\"SY\",\"Value3\":\"564\"},{\"Code\":\"TAIWAN\",\"Value1\":\"TAIWAN\",\"Value2\":\"TW\",\"Value3\":\"432\"},{\"Code\":\"TADZJIKISTAN\",\"Value1\":\"TAJIKISTAN\",\"Value2\":\"TJ\",\"Value3\":\"550\"},{\"Code\":\"TANZANIA\",\"Value1\":\"TANZANIA\",\"Value2\":\"TZ\",\"Value3\":\"369\"},{\"Code\":\"THAILAND\",\"Value1\":\"THAILAND\",\"Value2\":\"TH\",\"Value3\":\"568\"},{\"Code\":\"ØST-TIMOR\",\"Value1\":\"TIMOR-LESTE (EAST TIMOR)\",\"Value2\":\"TL\",\"Value3\":\"537\"},{\"Code\":\"TOGO\",\"Value1\":\"TOGO\",\"Value2\":\"TG\",\"Value3\":\"376\"},{\"Code\":\"TOKELAU\",\"Value1\":\"TOKELAU\",\"Value2\":\"TK\",\"Value3\":\"829\"},{\"Code\":\"TONGA\",\"Value1\":\"TONGA\",\"Value2\":\"TO\",\"Value3\":\"813\"},{\"Code\":\"TRINIDAD OG TOBAGO\",\"Value1\":\"TRINIDAD AND TOBAGO\",\"Value2\":\"TT\",\"Value3\":\"680\"},{\"Code\":\"TUNISIA\",\"Value1\":\"TUNISIA\",\"Value2\":\"TN\",\"Value3\":\"379\"},{\"Code\":\"TYRKIA\",\"Value1\":\"TURKEY\",\"Value2\":\"TR\",\"Value3\":\"143\"},{\"Code\":\"TURKMENISTAN\",\"Value1\":\"TURKMENISTAN\",\"Value2\":\"TM\",\"Value3\":\"552\"},{\"Code\":\"TURKS/CAICOSØYENE\",\"Value1\":\"TURKS AND CAICOS ISLANDS\",\"Value2\":\"TC\",\"Value3\":\"681\"},{\"Code\":\"TUVALU\",\"Value1\":\"TUVALU\",\"Value2\":\"TV\",\"Value3\":\"816\"},{\"Code\":\"UGANDA\",\"Value1\":\"UGANDA\",\"Value2\":\"UG\",\"Value3\":\"386\"},{\"Code\":\"UKRAINA\",\"Value1\":\"UKRAINE\",\"Value2\":\"UA\",\"Value3\":\"148\"},{\"Code\":\"DE ARABISKE EMIRATER\",\"Value1\":\"UNITED ARAB EMIRATES\",\"Value2\":\"AE\",\"Value3\":\"426\"},{\"Code\":\"USA\",\"Value1\":\"UNITED STATES\",\"Value2\":\"US\",\"Value3\":\"684\"},{\"Code\":\"MINDRE STILLEHAVSØYER\",\"Value1\":\"UNITED STATES MINOR OUTLYING ISLANDS\",\"Value2\":\"UM\",\"Value3\":\"819\"},{\"Code\":\"URUGUAY\",\"Value1\":\"URUGUAY\",\"Value2\":\"UY\",\"Value3\":\"770\"},{\"Code\":\"UZBEKISTAN\",\"Value1\":\"UZBEKISTAN\",\"Value2\":\"UZ\",\"Value3\":\"554\"},{\"Code\":\"VANUATU\",\"Value1\":\"VANUATU\",\"Value2\":\"VU\",\"Value3\":\"812\"},{\"Code\":\"VATIKANSTATEN\",\"Value1\":\"VATICAN (OR HOLY SEE)\",\"Value2\":\"VA\",\"Value3\":\"154\"},{\"Code\":\"VENEZUELA\",\"Value1\":\"VENEZUELA\",\"Value2\":\"VE\",\"Value3\":\"775\"},{\"Code\":\"VIETNAM\",\"Value1\":\"VIETNAM\",\"Value2\":\"VN\",\"Value3\":\"575\"},{\"Code\":\"JOMFRUØYENE BRIT.\",\"Value1\":\"VIRGIN ISLANDS, BRITISH\",\"Value2\":\"VG\",\"Value3\":\"608\"},{\"Code\":\"JOMFRUØYENE AM.\",\"Value1\":\"VIRGIN ISLANDS, U.S.\",\"Value2\":\"VI\",\"Value3\":\"601\"},{\"Code\":\"WALLIS/FUTUNAØYENE\",\"Value1\":\"WALLIS AND FUTUNA\",\"Value2\":\"WF\",\"Value3\":\"832\"},{\"Code\":\"VEST-SAHARA\",\"Value1\":\"WESTERN SAHARA\",\"Value2\":\"EH\",\"Value3\":\"304\"},{\"Code\":\"JEMEN\",\"Value1\":\"YEMEN\",\"Value2\":\"YE\",\"Value3\":\"578\"},{\"Code\":\"ZAMBIA\",\"Value1\":\"ZAMBIA\",\"Value2\":\"ZM\",\"Value3\":\"389\"},{\"Code\":\"ZIMBABWE\",\"Value1\":\"ZIMBABWE\",\"Value2\":\"ZW\",\"Value3\":\"326\"},{\"Code\":\"ÅLAND\",\"Value1\":\"ÅLAND ISLANDS\",\"Value2\":\"AX\",\"Value3\":\"902\"}],\"_links\":{\"self\":{\"href\":\"https://www.altinn.no/api/metadata/codelists/ASF_Land/2758?language=1033\"}}}", - "https://www.altinn.no/api/metadata/codelists/ASF_land/2758?language=1044" - => "{\"Name\": \"ASF_Land\",\"Version\": 2758,\"Language\": 1044,\"Codes\": [{ \"Code\": \"\", \"Value1\": \"\", \"Value2\": \"\", \"Value3\": \"\" },{\"Code\": \"MONTENEGRO\",\"Value1\": \"MONTENEGRO\",\"Value2\": \"ME\",\"Value3\": \"160\"},{ \"Code\": \"NORGE\", \"Value1\": \"NORGE\", \"Value2\": \"NO\", \"Value3\": \"000\" },{\"Code\": \"NY-KALEDONIA\",\"Value1\": \"NY-KALEDONIA\",\"Value2\": \"NC\",\"Value3\": \"833\"},{ \"Code\": \"OMAN\", \"Value1\": \"OMAN\", \"Value2\": \"OM\", \"Value3\": \"520\" },{\"Code\": \"PAKISTAN\",\"Value1\": \"PAKISTAN\",\"Value2\": \"PK\",\"Value3\": \"534\"},{ \"Code\": \"PALAU\", \"Value1\": \"PALAU\", \"Value2\": \"PW\", \"Value3\": \"839\" },{ \"Code\": \"PANAMA\", \"Value1\": \"PANAMA\", \"Value2\": \"PA\", \"Value3\": \"668\" },{\"Code\": \"PAPUA NY-GUINEA\",\"Value1\": \"PAPUA NY-GUINEA\",\"Value2\": \"PG\",\"Value3\": \"827\"},{\"Code\": \"PARAGUAY\",\"Value1\": \"PARAGUAY\",\"Value2\": \"PY\",\"Value3\": \"755\"},{ \"Code\": \"PERU\", \"Value1\": \"PERU\", \"Value2\": \"PE\", \"Value3\": \"760\" },{ \"Code\": \"POLEN\", \"Value1\": \"POLEN\", \"Value2\": \"PL\", \"Value3\": \"131\" },{\"Code\": \"PORTUGAL\",\"Value1\": \"PORTUGAL\",\"Value2\": \"PT\",\"Value3\": \"132\"},{\"Code\": \"SLOVAKIA\",\"Value1\": \"SLOVAKIA\",\"Value2\": \"SK\",\"Value3\": \"157\"},{\"Code\": \"SLOVENIA\",\"Value1\": \"SLOVENIA\",\"Value2\": \"SI\",\"Value3\": \"146\"},{ \"Code\": \"VIETNAM\", \"Value1\": \"VIETNAM\", \"Value2\": \"VN\", \"Value3\": \"575\" },{\"Code\": \"WALLIS/FUTUNAØYENE\",\"Value1\": \"WALLIS/FUTUNAØYENE\",\"Value2\": \"WF\",\"Value3\": \"832\"},{ \"Code\": \"ZAMBIA\", \"Value1\": \"ZAMBIA\", \"Value2\": \"ZM\", \"Value3\": \"389\" },{\"Code\": \"ZIMBABWE\",\"Value1\": \"ZIMBABWE\",\"Value2\": \"ZW\",\"Value3\": \"326\"},{\"Code\": \"ØSTERRIKE\",\"Value1\": \"ØSTERRIKE\",\"Value2\": \"AT\",\"Value3\": \"153\"},],\"_links\": {\"self\": {\"href\": \"https://www.altinn.no/api/metadata/codelists/ASF_Land/2758?language=1044\"}}}", - "https://www.altinn.no/api/metadata/codelists/ASF_land/?language=1044" - => "{\"Name\": \"ASF_Land\",\"Version\": 2758,\"Language\": 1044,\"Codes\": [{ \"Code\": \"\", \"Value1\": \"\", \"Value2\": \"\", \"Value3\": \"\" },{\"Code\": \"MONTENEGRO\",\"Value1\": \"MONTENEGRO\",\"Value2\": \"ME\",\"Value3\": \"160\"},{ \"Code\": \"NORGE\", \"Value1\": \"NORGE\", \"Value2\": \"NO\", \"Value3\": \"000\" },{\"Code\": \"NY-KALEDONIA\",\"Value1\": \"NY-KALEDONIA\",\"Value2\": \"NC\",\"Value3\": \"833\"},{ \"Code\": \"OMAN\", \"Value1\": \"OMAN\", \"Value2\": \"OM\", \"Value3\": \"520\" },{\"Code\": \"PAKISTAN\",\"Value1\": \"PAKISTAN\",\"Value2\": \"PK\",\"Value3\": \"534\"},{ \"Code\": \"PALAU\", \"Value1\": \"PALAU\", \"Value2\": \"PW\", \"Value3\": \"839\" },{ \"Code\": \"PANAMA\", \"Value1\": \"PANAMA\", \"Value2\": \"PA\", \"Value3\": \"668\" },{\"Code\": \"PAPUA NY-GUINEA\",\"Value1\": \"PAPUA NY-GUINEA\",\"Value2\": \"PG\",\"Value3\": \"827\"},{\"Code\": \"PARAGUAY\",\"Value1\": \"PARAGUAY\",\"Value2\": \"PY\",\"Value3\": \"755\"},{ \"Code\": \"PERU\", \"Value1\": \"PERU\", \"Value2\": \"PE\", \"Value3\": \"760\" },{ \"Code\": \"POLEN\", \"Value1\": \"POLEN\", \"Value2\": \"PL\", \"Value3\": \"131\" },{\"Code\": \"PORTUGAL\",\"Value1\": \"PORTUGAL\",\"Value2\": \"PT\",\"Value3\": \"132\"},{\"Code\": \"SLOVAKIA\",\"Value1\": \"SLOVAKIA\",\"Value2\": \"SK\",\"Value3\": \"157\"},{\"Code\": \"SLOVENIA\",\"Value1\": \"SLOVENIA\",\"Value2\": \"SI\",\"Value3\": \"146\"},{ \"Code\": \"VIETNAM\", \"Value1\": \"VIETNAM\", \"Value2\": \"VN\", \"Value3\": \"575\" },{\"Code\": \"WALLIS/FUTUNAØYENE\",\"Value1\": \"WALLIS/FUTUNAØYENE\",\"Value2\": \"WF\",\"Value3\": \"832\"},{ \"Code\": \"ZAMBIA\", \"Value1\": \"ZAMBIA\", \"Value2\": \"ZM\", \"Value3\": \"389\" },{\"Code\": \"ZIMBABWE\",\"Value1\": \"ZIMBABWE\",\"Value2\": \"ZW\",\"Value3\": \"326\"},{\"Code\": \"ØSTERRIKE\",\"Value1\": \"ØSTERRIKE\",\"Value2\": \"AT\",\"Value3\": \"153\"},],\"_links\": {\"self\": {\"href\": \"https://www.altinn.no/api/metadata/codelists/ASF_Land/2758?language=1044\"}}}", - "https://www.altinn.no/api/metadata/codelists/ASF_Fylker/3063?language=1044" - => "{\"Name\":\"ASF_Fylker\",\"Version\":3063,\"Language\":1044,\"Codes\":[{\"Code\":\"\",\"Value1\":\"\",\"Value2\":\"\",\"Value3\":\"\"},{\"Code\":\"Agder\",\"Value1\":\"Agder\",\"Value2\":\"4200\",\"Value3\":\"\"},{\"Code\":\"Akershus - UTGÅTT\",\"Value1\":\"Akershus - UTGÅTT\",\"Value2\":\"0200\",\"Value3\":\"\"},{\"Code\":\"Aust-Agder - UTGÅTT\",\"Value1\":\"Aust-Agder - UTGÅTT\",\"Value2\":\"0900\",\"Value3\":\"\"},{\"Code\":\"Buskerud - UTGÅTT\",\"Value1\":\"Buskerud -UTGÅTT\",\"Value2\":\"0600\",\"Value3\":\"\"},{\"Code\":\"Finnmark - UTGÅTT\",\"Value1\":\"Finnmark - UTGÅTT\",\"Value2\":\"2000\",\"Value3\":\"\"},{\"Code\":\"Hedmark - UTGÅTT\",\"Value1\":\"Hedmark - UTGÅTT\",\"Value2\":\"0400\",\"Value3\":\"\"},{\"Code\":\"Hordaland - UTGÅTT\",\"Value1\":\"Hordaland - UTGÅTT\",\"Value2\":\"1200\",\"Value3\":\"\"},{\"Code\":\"Innlandet\",\"Value1\":\"Innlandet\",\"Value2\":\"3400\",\"Value3\":\"\"},{\"Code\":\"Møre og Romsdal\",\"Value1\":\"Møre og Romsdal\",\"Value2\":\"1500\",\"Value3\":\"\"},{\"Code\":\"Nordland\",\"Value1\":\"Nordland\",\"Value2\":\"1800\",\"Value3\":\"\"},{\"Code\":\"Oppland - UTGÅTT\",\"Value1\":\"Oppland -UTGÅTT\",\"Value2\":\"0500\",\"Value3\":\"\"},{\"Code\":\"Oslo\",\"Value1\":\"Oslo\",\"Value2\":\"0300\",\"Value3\":\"\"},{\"Code\":\"Rogaland\",\"Value1\":\"Rogaland\",\"Value2\":\"1100\",\"Value3\":\"\"},{\"Code\":\"Sogn og Fjordane - UTGÅTT\",\"Value1\":\"Sogn og Fjordane - UTGÅTT\",\"Value2\":\"1400\",\"Value3\":\"\"},{\"Code\":\"Telemark - UTGÅTT\",\"Value1\":\"Telemark - UTGÅTT\",\"Value2\":\"0800\",\"Value3\":\"\"},{\"Code\":\"Troms - UTGÅTT\",\"Value1\":\"Troms - UTGÅTT\",\"Value2\":\"1900\",\"Value3\":\"\"},{\"Code\":\"Troms og Finnmark\",\"Value1\":\"Troms og Finnmark\",\"Value2\":\"5400\",\"Value3\":\"\"},{\"Code\":\"Trøndelag\",\"Value1\":\"Trøndelag\",\"Value2\":\"5000\",\"Value3\":\"\"},{\"Code\":\"Vest-Agder - UTGÅTT\",\"Value1\":\"Vest-Agder - UTGÅTT\",\"Value2\":\"1000\",\"Value3\":\"\"},{\"Code\":\"Vestfold og Telemark\",\"Value1\":\"Vestfold og Telemark\",\"Value2\":\"3800\",\"Value3\":\"\"},{\"Code\":\"Vestfold - UTGÅTT\",\"Value1\":\"Vestfold -UTGÅTT\",\"Value2\":\"0700\",\"Value3\":\"\"},{\"Code\":\"Vestland\",\"Value1\":\"Vestland\",\"Value2\":\"4600\",\"Value3\":\"\"},{\"Code\":\"Viken\",\"Value1\":\"Viken\",\"Value2\":\"3000\",\"Value3\":\"\"},{\"Code\":\"Østfold - UTGÅTT\",\"Value1\":\"Østfold - UTGÅTT\",\"Value2\":\"0100\",\"Value3\":\"\"}],\"_links\":{\"self\":{\"href\":\"https://www.altinn.no/api/metadata/codelists/ASF_Fylker/3063?language=1044\"}}}", - "https://www.altinn.no/api/metadata/codelists/ASF_Fylker/3063?language=2068" - => "{\"Name\":\"ASF_Fylker\",\"Version\":3063,\"Language\":1044,\"Codes\":[{\"Code\":\"\",\"Value1\":\"\",\"Value2\":\"\",\"Value3\":\"\"},{\"Code\":\"Agder\",\"Value1\":\"Agder\",\"Value2\":\"4200\",\"Value3\":\"\"},{\"Code\":\"Akershus - UTGÅTT\",\"Value1\":\"Akershus - UTGÅTT\",\"Value2\":\"0200\",\"Value3\":\"\"},{\"Code\":\"Aust-Agder - UTGÅTT\",\"Value1\":\"Aust-Agder - UTGÅTT\",\"Value2\":\"0900\",\"Value3\":\"\"},{\"Code\":\"Buskerud - UTGÅTT\",\"Value1\":\"Buskerud -UTGÅTT\",\"Value2\":\"0600\",\"Value3\":\"\"},{\"Code\":\"Finnmark - UTGÅTT\",\"Value1\":\"Finnmark - UTGÅTT\",\"Value2\":\"2000\",\"Value3\":\"\"},{\"Code\":\"Hedmark - UTGÅTT\",\"Value1\":\"Hedmark - UTGÅTT\",\"Value2\":\"0400\",\"Value3\":\"\"},{\"Code\":\"Hordaland - UTGÅTT\",\"Value1\":\"Hordaland - UTGÅTT\",\"Value2\":\"1200\",\"Value3\":\"\"},{\"Code\":\"Innlandet\",\"Value1\":\"Innlandet\",\"Value2\":\"3400\",\"Value3\":\"\"},{\"Code\":\"Møre og Romsdal\",\"Value1\":\"Møre og Romsdal\",\"Value2\":\"1500\",\"Value3\":\"\"},{\"Code\":\"Nordland\",\"Value1\":\"Nordland\",\"Value2\":\"1800\",\"Value3\":\"\"},{\"Code\":\"Oppland - UTGÅTT\",\"Value1\":\"Oppland -UTGÅTT\",\"Value2\":\"0500\",\"Value3\":\"\"},{\"Code\":\"Oslo\",\"Value1\":\"Oslo\",\"Value2\":\"0300\",\"Value3\":\"\"},{\"Code\":\"Rogaland\",\"Value1\":\"Rogaland\",\"Value2\":\"1100\",\"Value3\":\"\"},{\"Code\":\"Sogn og Fjordane - UTGÅTT\",\"Value1\":\"Sogn og Fjordane - UTGÅTT\",\"Value2\":\"1400\",\"Value3\":\"\"},{\"Code\":\"Telemark - UTGÅTT\",\"Value1\":\"Telemark - UTGÅTT\",\"Value2\":\"0800\",\"Value3\":\"\"},{\"Code\":\"Troms - UTGÅTT\",\"Value1\":\"Troms - UTGÅTT\",\"Value2\":\"1900\",\"Value3\":\"\"},{\"Code\":\"Troms og Finnmark\",\"Value1\":\"Troms og Finnmark\",\"Value2\":\"5400\",\"Value3\":\"\"},{\"Code\":\"Trøndelag\",\"Value1\":\"Trøndelag\",\"Value2\":\"5000\",\"Value3\":\"\"},{\"Code\":\"Vest-Agder - UTGÅTT\",\"Value1\":\"Vest-Agder - UTGÅTT\",\"Value2\":\"1000\",\"Value3\":\"\"},{\"Code\":\"Vestfold og Telemark\",\"Value1\":\"Vestfold og Telemark\",\"Value2\":\"3800\",\"Value3\":\"\"},{\"Code\":\"Vestfold - UTGÅTT\",\"Value1\":\"Vestfold -UTGÅTT\",\"Value2\":\"0700\",\"Value3\":\"\"},{\"Code\":\"Vestland\",\"Value1\":\"Vestland\",\"Value2\":\"4600\",\"Value3\":\"\"},{\"Code\":\"Viken\",\"Value1\":\"Viken\",\"Value2\":\"3000\",\"Value3\":\"\"},{\"Code\":\"Østfold - UTGÅTT\",\"Value1\":\"Østfold - UTGÅTT\",\"Value2\":\"0100\",\"Value3\":\"\"}],\"_links\":{\"self\":{\"href\":\"https://www.altinn.no/api/metadata/codelists/ASF_Fylker/3063?language=1044\"}}}", - _ => null, - }; - } + "https://www.altinn.no/api/metadata/codelists/ASF_land/2758?language=1033" + => "{\"Name\":\"ASF_Land\",\"Version\":2758,\"Language\":1033,\"Codes\":[{\"Code\":\"\",\"Value1\":\"\",\"Value2\":\"\",\"Value3\":\"\"},{\"Code\":\"AFGHANISTAN\",\"Value1\":\"AFGHANISTAN\",\"Value2\":\"AF\",\"Value3\":\"404\"},{\"Code\":\"ALBANIA\",\"Value1\":\"ALBANIA\",\"Value2\":\"AL\",\"Value3\":\"111\"},{\"Code\":\"ALGERIE\",\"Value1\":\"ALGERIA\",\"Value2\":\"DZ\",\"Value3\":\"203\"},{\"Code\":\"AM. SAMOA\",\"Value1\":\"AMERICAN SAMOA\",\"Value2\":\"AS\",\"Value3\":\"802\"},{\"Code\":\"ANDORRA\",\"Value1\":\"ANDORRA\",\"Value2\":\"AD\",\"Value3\":\"114\"},{\"Code\":\"ANGOLA\",\"Value1\":\"ANGOLA\",\"Value2\":\"AO\",\"Value3\":\"204\"},{\"Code\":\"ANGUILLA\",\"Value1\":\"ANGUILLA\",\"Value2\":\"AI\",\"Value3\":\"660\"},{\"Code\":\"ANTARKTIS\",\"Value1\":\"ANTARCTICA\",\"Value2\":\"AQ\",\"Value3\":\"901\"},{\"Code\":\"ANTIGUA OG BARBUDA\",\"Value1\":\"ANTIGUA AND BARBUDA\",\"Value2\":\"AG\",\"Value3\":\"603\"},{\"Code\":\"ARGENTINA\",\"Value1\":\"ARGENTINA\",\"Value2\":\"AR\",\"Value3\":\"705\"},{\"Code\":\"ARMENIA\",\"Value1\":\"ARMENIA\",\"Value2\":\"AM\",\"Value3\":\"406\"},{\"Code\":\"ARUBA\",\"Value1\":\"ARUBA\",\"Value2\":\"AW\",\"Value3\":\"657\"},{\"Code\":\"AUSTRALIA\",\"Value1\":\"AUSTRALIA\",\"Value2\":\"AU\",\"Value3\":\"805\"},{\"Code\":\"ØSTERRIKE\",\"Value1\":\"AUSTRIA\",\"Value2\":\"AT\",\"Value3\":\"153\"},{\"Code\":\"AZERBAJDZJAN\",\"Value1\":\"AZERBAIJAN\",\"Value2\":\"AZ\",\"Value3\":\"407\"},{\"Code\":\"BAHAMAS\",\"Value1\":\"BAHAMAS\",\"Value2\":\"BS\",\"Value3\":\"605\"},{\"Code\":\"BAHRAIN\",\"Value1\":\"BAHRAIN\",\"Value2\":\"BH\",\"Value3\":\"409\"},{\"Code\":\"BANGLADESH\",\"Value1\":\"BANGLADESH\",\"Value2\":\"BD\",\"Value3\":\"410\"},{\"Code\":\"BARBADOS\",\"Value1\":\"BARBADOS\",\"Value2\":\"BB\",\"Value3\":\"602\"},{\"Code\":\"HVITERUSSLAND\",\"Value1\":\"BELARUS\",\"Value2\":\"BY\",\"Value3\":\"120\"},{\"Code\":\"BELGIA\",\"Value1\":\"BELGIUM\",\"Value2\":\"BE\",\"Value3\":\"112\"},{\"Code\":\"BELIZE\",\"Value1\":\"BELIZE\",\"Value2\":\"BZ\",\"Value3\":\"604\"},{\"Code\":\"BENIN\",\"Value1\":\"BENIN\",\"Value2\":\"BJ\",\"Value3\":\"229\"},{\"Code\":\"BERMUDA\",\"Value1\":\"BERMUDA\",\"Value2\":\"BM\",\"Value3\":\"606\"},{\"Code\":\"BHUTAN\",\"Value1\":\"BHUTAN\",\"Value2\":\"BT\",\"Value3\":\"412\"},{\"Code\":\"BOLIVIA\",\"Value1\":\"BOLIVIA\",\"Value2\":\"BO\",\"Value3\":\"710\"},{\"Code\":\"BONAIRE, SINT EUSTATIUS OG SABA\",\"Value1\":\"BONAIRE, SINT EUSTATIUS AND SABA\",\"Value2\":\"BQ\",\"Value3\":\"659\"},{\"Code\":\"BOSNIA-HERCEGOVINA\",\"Value1\":\"BOSNIA AND HERZEGOVINA\",\"Value2\":\"BA\",\"Value3\":\"155\"},{\"Code\":\"BOTSWANA\",\"Value1\":\"BOTSWANA\",\"Value2\":\"BW\",\"Value3\":\"205\"},{\"Code\":\"BOUVETØYA\",\"Value1\":\"BOUVET ISLAND\",\"Value2\":\"BV\",\"Value3\":\"904\"},{\"Code\":\"BRASIL\",\"Value1\":\"BRAZIL\",\"Value2\":\"BR\",\"Value3\":\"715\"},{\"Code\":\"BRITISK-INDISKE HAV\",\"Value1\":\"BRITISH INDIAN OCEAN TERRITORY\",\"Value2\":\"IO\",\"Value3\":\"213\"},{\"Code\":\"BRUNEI\",\"Value1\":\"BRUNEI\",\"Value2\":\"BN\",\"Value3\":\"416\"},{\"Code\":\"BULGARIA\",\"Value1\":\"BULGARIA\",\"Value2\":\"BG\",\"Value3\":\"113\"},{\"Code\":\"BURKINA FASO\",\"Value1\":\"BURKINA FASO\",\"Value2\":\"BF\",\"Value3\":\"393\"},{\"Code\":\"BURUNDI\",\"Value1\":\"BURUNDI\",\"Value2\":\"BI\",\"Value3\":\"216\"},{\"Code\":\"KAMBODSJA\",\"Value1\":\"CAMBODIA\",\"Value2\":\"KH\",\"Value3\":\"478\"},{\"Code\":\"KAMERUN\",\"Value1\":\"CAMEROON\",\"Value2\":\"CM\",\"Value3\":\"270\"},{\"Code\":\"CANADA\",\"Value1\":\"CANADA\",\"Value2\":\"CA\",\"Value3\":\"612\"},{\"Code\":\"KAPP VERDE\",\"Value1\":\"CAPE VERDE\",\"Value2\":\"CV\",\"Value3\":\"273\"},{\"Code\":\"CAYMANØYENE\",\"Value1\":\"CAYMAN ISLANDS\",\"Value2\":\"KY\",\"Value3\":\"613\"},{\"Code\":\"SENTRALAFRIKANSKE REPUBLIKK\",\"Value1\":\"CENTRAL AFRICAN REPUBLIC\",\"Value2\":\"CF\",\"Value3\":\"337\"},{\"Code\":\"TCHAD\",\"Value1\":\"CHAD\",\"Value2\":\"TD\",\"Value3\":\"373\"},{\"Code\":\"CHILE\",\"Value1\":\"CHILE\",\"Value2\":\"CL\",\"Value3\":\"725\"},{\"Code\":\"KINA\",\"Value1\":\"CHINA\",\"Value2\":\"CN\",\"Value3\":\"484\"},{\"Code\":\"CHRISTMASØYA\",\"Value1\":\"CHRISTMAS ISLAND\",\"Value2\":\"CX\",\"Value3\":\"807\"},{\"Code\":\"KOKOSØYENE\",\"Value1\":\"COCOS ISLANDS (KEELING)\",\"Value2\":\"CC\",\"Value3\":\"808\"},{\"Code\":\"COLOMBIA\",\"Value1\":\"COLOMBIA\",\"Value2\":\"CO\",\"Value3\":\"730\"},{\"Code\":\"KOMORENE\",\"Value1\":\"COMOROS\",\"Value2\":\"KM\",\"Value3\":\"220\"},{\"Code\":\"KONGO, REPUBLIKKEN\",\"Value1\":\"CONGO\",\"Value2\":\"CG\",\"Value3\":\"278\"},{\"Code\":\"KONGO, DEN DEMOKR. REPUBL\",\"Value1\":\"CONGO, THE DEMOCRATIC REPUBLIC OF THE\",\"Value2\":\"CD\",\"Value3\":\"279\"},{\"Code\":\"COOKØYENE\",\"Value1\":\"COOK ISLANDS\",\"Value2\":\"CK\",\"Value3\":\"809\"},{\"Code\":\"COSTA RICA\",\"Value1\":\"COSTA RICA\",\"Value2\":\"CR\",\"Value3\":\"616\"},{\"Code\":\"ELFENBEINSKYSTEN\",\"Value1\":\"CÔTE D IVOIRE\",\"Value2\":\"CI\",\"Value3\":\"239\"},{\"Code\":\"KROATIA\",\"Value1\":\"CROATIA\",\"Value2\":\"HR\",\"Value3\":\"122\"},{\"Code\":\"CUBA\",\"Value1\":\"CUBA\",\"Value2\":\"CU\",\"Value3\":\"620\"},{\"Code\":\"CURACAO\",\"Value1\":\"CURACAO\",\"Value2\":\"CW\",\"Value3\":\"661\"},{\"Code\":\"KYPROS\",\"Value1\":\"CYPRUS\",\"Value2\":\"CY\",\"Value3\":\"500\"},{\"Code\":\"DEN TSJEKKISKE REP.\",\"Value1\":\"CZECHIA\",\"Value2\":\"CZ\",\"Value3\":\"158\"},{\"Code\":\"DANMARK\",\"Value1\":\"DENMARK\",\"Value2\":\"DK\",\"Value3\":\"101\"},{\"Code\":\"DJIBOUTI\",\"Value1\":\"DJIBOUTI\",\"Value2\":\"DJ\",\"Value3\":\"250\"},{\"Code\":\"DOMINICA\",\"Value1\":\"DOMINICA\",\"Value2\":\"DM\",\"Value3\":\"622\"},{\"Code\":\"DEN DOMINIKANSKE REP\",\"Value1\":\"DOMINICAN REPUBLIC\",\"Value2\":\"DO\",\"Value3\":\"624\"},{\"Code\":\"ECUADOR\",\"Value1\":\"ECUADOR\",\"Value2\":\"EC\",\"Value3\":\"735\"},{\"Code\":\"EGYPT\",\"Value1\":\"EGYPT\",\"Value2\":\"EG\",\"Value3\":\"249\"},{\"Code\":\"EL SALVADOR\",\"Value1\":\"EL SALVADOR\",\"Value2\":\"SV\",\"Value3\":\"672\"},{\"Code\":\"EKVATORIAL-GUINEA\",\"Value1\":\"EQUATORIAL GUINEA\",\"Value2\":\"GQ\",\"Value3\":\"235\"},{\"Code\":\"ERITREA\",\"Value1\":\"ERITREA\",\"Value2\":\"ER\",\"Value3\":\"241\"},{\"Code\":\"ESTLAND\",\"Value1\":\"ESTONIA\",\"Value2\":\"EE\",\"Value3\":\"115\"},{\"Code\":\"ETIOPIA\",\"Value1\":\"ETHIOPIA\",\"Value2\":\"ET\",\"Value3\":\"246\"},{\"Code\":\"FALKLANDSØYENE\",\"Value1\":\"FALKLAND ISLANDS\",\"Value2\":\"FK\",\"Value3\":\"740\"},{\"Code\":\"FÆRØYENE\",\"Value1\":\"FAROE ISLANDS\",\"Value2\":\"FO\",\"Value3\":\"104\"},{\"Code\":\"FIJII\",\"Value1\":\"FIJI\",\"Value2\":\"FJ\",\"Value3\":\"811\"},{\"Code\":\"FINLAND\",\"Value1\":\"FINLAND\",\"Value2\":\"FI\",\"Value3\":\"103\"},{\"Code\":\"FRANKRIKE\",\"Value1\":\"FRANCE\",\"Value2\":\"FR\",\"Value3\":\"117\"},{\"Code\":\"FRANSK GUYANA\",\"Value1\":\"FRENCH GUIANA\",\"Value2\":\"GF\",\"Value3\":\"745\"},{\"Code\":\"FRANSK POLYNESIA\",\"Value1\":\"FRENCH POLYNESIA\",\"Value2\":\"PF\",\"Value3\":\"814\"},{\"Code\":\"DE FRANSKE TERRITORIENE I SØR\",\"Value1\":\"FRENCH SOUTHERN TEERITORIES\",\"Value2\":\"TF\",\"Value3\":\"913\"},{\"Code\":\"GABON\",\"Value1\":\"GABON\",\"Value2\":\"GA\",\"Value3\":\"254\"},{\"Code\":\"GAMBIA\",\"Value1\":\"GAMBIA\",\"Value2\":\"GM\",\"Value3\":\"256\"},{\"Code\":\"GEORGIA\",\"Value1\":\"GEORGIA\",\"Value2\":\"GE\",\"Value3\":\"430\"},{\"Code\":\"TYSKLAND\",\"Value1\":\"GERMANY\",\"Value2\":\"DE\",\"Value3\":\"144\"},{\"Code\":\"GHANA\",\"Value1\":\"GHANA\",\"Value2\":\"GH\",\"Value3\":\"260\"},{\"Code\":\"GIBRALTAR\",\"Value1\":\"GIBRALTAR\",\"Value2\":\"GI\",\"Value3\":\"118\"},{\"Code\":\"STORBRITANNIA\",\"Value1\":\"GREAT BRITAIN\",\"Value2\":\"GB\",\"Value3\":\"139\"},{\"Code\":\"HELLAS\",\"Value1\":\"GREECE\",\"Value2\":\"GR\",\"Value3\":\"119\"},{\"Code\":\"GRØNLAND\",\"Value1\":\"GREENLAND\",\"Value2\":\"GL\",\"Value3\":\"102\"},{\"Code\":\"GRENADA\",\"Value1\":\"GRENADA\",\"Value2\":\"GD\",\"Value3\":\"629\"},{\"Code\":\"GUADELOUPE\",\"Value1\":\"GUADELOUPE\",\"Value2\":\"GP\",\"Value3\":\"631\"},{\"Code\":\"GUAM\",\"Value1\":\"GUAM\",\"Value2\":\"GU\",\"Value3\":\"817\"},{\"Code\":\"GUATEMALA\",\"Value1\":\"GUATEMALA\",\"Value2\":\"GT\",\"Value3\":\"632\"},{\"Code\":\"GUERNSEY\",\"Value1\":\"GUERNSEY\",\"Value2\":\"GG\",\"Value3\":\"162\"},{\"Code\":\"GUINEA\",\"Value1\":\"GUINEA\",\"Value2\":\"GN\",\"Value3\":\"264\"},{\"Code\":\"GUINEA-BISSAU\",\"Value1\":\"GUINEA-BISSAU\",\"Value2\":\"GW\",\"Value3\":\"266\"},{\"Code\":\"GUYANA\",\"Value1\":\"GUYANA\",\"Value2\":\"GY\",\"Value3\":\"720\"},{\"Code\":\"HAITI\",\"Value1\":\"HAITI\",\"Value2\":\"HT\",\"Value3\":\"636\"},{\"Code\":\"HEARD- OG MCDONALDØYENE\",\"Value1\":\"HEARD ISLAND AND MCDONALD ISLANDS\",\"Value2\":\"HM\",\"Value3\":\"908\"},{\"Code\":\"HONDURAS\",\"Value1\":\"HONDURAS\",\"Value2\":\"HN\",\"Value3\":\"644\"},{\"Code\":\"HONGKONG\",\"Value1\":\"HONG KONG\",\"Value2\":\"HK\",\"Value3\":\"436\"},{\"Code\":\"UNGARN\",\"Value1\":\"HUNGARY\",\"Value2\":\"HU\",\"Value3\":\"152\"},{\"Code\":\"ISLAND\",\"Value1\":\"ICELAND\",\"Value2\":\"IS\",\"Value3\":\"105\"},{\"Code\":\"INDIA\",\"Value1\":\"INDIA\",\"Value2\":\"IN\",\"Value3\":\"444\"},{\"Code\":\"INDONESIA\",\"Value1\":\"INDONESIA\",\"Value2\":\"ID\",\"Value3\":\"448\"},{\"Code\":\"IRAN\",\"Value1\":\"IRAN\",\"Value2\":\"IR\",\"Value3\":\"456\"},{\"Code\":\"IRAK\",\"Value1\":\"IRAQ\",\"Value2\":\"IQ\",\"Value3\":\"452\"},{\"Code\":\"IRLAND\",\"Value1\":\"IRELAND\",\"Value2\":\"IE\",\"Value3\":\"121\"},{\"Code\":\"MAN\",\"Value1\":\"ISLE OF MAN\",\"Value2\":\"IM\",\"Value3\":\"164\"},{\"Code\":\"ISRAEL\",\"Value1\":\"ISRAEL\",\"Value2\":\"IL\",\"Value3\":\"460\"},{\"Code\":\"ITALIA\",\"Value1\":\"ITALY\",\"Value2\":\"IT\",\"Value3\":\"123\"},{\"Code\":\"JAMAICA\",\"Value1\":\"JAMAICA\",\"Value2\":\"JM\",\"Value3\":\"648\"},{\"Code\":\"JAPAN\",\"Value1\":\"JAPAN\",\"Value2\":\"JP\",\"Value3\":\"464\"},{\"Code\":\"JERSEY\",\"Value1\":\"JERSEY\",\"Value2\":\"JE\",\"Value3\":\"163\"},{\"Code\":\"JORDAN\",\"Value1\":\"JORDAN\",\"Value2\":\"JO\",\"Value3\":\"476\"},{\"Code\":\"KAZAKHSTAN\",\"Value1\":\"KAZAKHSTAN\",\"Value2\":\"KZ\",\"Value3\":\"480\"},{\"Code\":\"KENYA\",\"Value1\":\"KENYA\",\"Value2\":\"KE\",\"Value3\":\"276\"},{\"Code\":\"KIRIBATI\",\"Value1\":\"KIRIBATI\",\"Value2\":\"KI\",\"Value3\":\"815\"},{\"Code\":\"KOSOVO\",\"Value1\":\"KOSOVO\",\"Value2\":\"XK\",\"Value3\":\"161\"},{\"Code\":\"KUWAIT\",\"Value1\":\"KUWAIT\",\"Value2\":\"KW\",\"Value3\":\"496\"},{\"Code\":\"KYRGYZSTAN\",\"Value1\":\"KYRGYZSTAN\",\"Value2\":\"KG\",\"Value3\":\"502\"},{\"Code\":\"LAOS\",\"Value1\":\"LAOS\",\"Value2\":\"LA\",\"Value3\":\"504\"},{\"Code\":\"LATVIA\",\"Value1\":\"LATVIA\",\"Value2\":\"LV\",\"Value3\":\"124\"},{\"Code\":\"LIBANON\",\"Value1\":\"LEBANON\",\"Value2\":\"LB\",\"Value3\":\"508\"},{\"Code\":\"LESOTHO\",\"Value1\":\"LESOTHO\",\"Value2\":\"LS\",\"Value3\":\"281\"},{\"Code\":\"LIBERIA\",\"Value1\":\"LIBERIA\",\"Value2\":\"LR\",\"Value3\":\"283\"},{\"Code\":\"LIBYA\",\"Value1\":\"LIBYA\",\"Value2\":\"LY\",\"Value3\":\"286\"},{\"Code\":\"LIECHTENSTEIN\",\"Value1\":\"LIECHTENSTEIN\",\"Value2\":\"LI\",\"Value3\":\"128\"},{\"Code\":\"LITAUEN\",\"Value1\":\"LITHUANIA\",\"Value2\":\"LT\",\"Value3\":\"136\"},{\"Code\":\"LUXEMBOURG\",\"Value1\":\"LUXEMBOURG\",\"Value2\":\"LU\",\"Value3\":\"129\"},{\"Code\":\"MACAO\",\"Value1\":\"MACAU\",\"Value2\":\"MO\",\"Value3\":\"510\"},{\"Code\":\"MADAGASKAR\",\"Value1\":\"MADAGASCAR\",\"Value2\":\"MG\",\"Value3\":\"289\"},{\"Code\":\"MALAWI\",\"Value1\":\"MALAWI\",\"Value2\":\"MW\",\"Value3\":\"296\"},{\"Code\":\"MALAYSIA\",\"Value1\":\"MALAYSIA\",\"Value2\":\"MY\",\"Value3\":\"512\"},{\"Code\":\"MALDIVENE\",\"Value1\":\"MALDIVES\",\"Value2\":\"MV\",\"Value3\":\"513\"},{\"Code\":\"MALI\",\"Value1\":\"MALI\",\"Value2\":\"ML\",\"Value3\":\"299\"},{\"Code\":\"MALTA\",\"Value1\":\"MALTA\",\"Value2\":\"MT\",\"Value3\":\"126\"},{\"Code\":\"MARSHALLØYENE\",\"Value1\":\"MARSHALL ISLANDS\",\"Value2\":\"MH\",\"Value3\":\"835\"},{\"Code\":\"MARTINIQUE\",\"Value1\":\"MARTINIQUE\",\"Value2\":\"MQ\",\"Value3\":\"650\"},{\"Code\":\"MAURITANIA\",\"Value1\":\"MAURITANIA\",\"Value2\":\"MR\",\"Value3\":\"306\"},{\"Code\":\"MAURITIUS\",\"Value1\":\"MAURITIUS\",\"Value2\":\"MU\",\"Value3\":\"307\"},{\"Code\":\"MAYOTTE\",\"Value1\":\"MAYOTTE\",\"Value2\":\"YT\",\"Value3\":\"322\"},{\"Code\":\"MEXICO\",\"Value1\":\"MEXICO\",\"Value2\":\"MX\",\"Value3\":\"652\"},{\"Code\":\"MIKRONESIAFØDERASJONEN\",\"Value1\":\"MICRONESIA, FEDERATED STATES OF\",\"Value2\":\"FM\",\"Value3\":\"826\"},{\"Code\":\"DIVERSE\",\"Value1\":\"MISCELLANEOUS\",\"Value2\":\"ZZ\",\"Value3\":\"990\"},{\"Code\":\"MOLDOVA\",\"Value1\":\"MOLDOVA\",\"Value2\":\"MD\",\"Value3\":\"138\"},{\"Code\":\"MONACO\",\"Value1\":\"MONACO\",\"Value2\":\"MC\",\"Value3\":\"130\"},{\"Code\":\"MONGOLIA\",\"Value1\":\"MONGOLIA\",\"Value2\":\"MN\",\"Value3\":\"516\"},{\"Code\":\"MONTENEGRO\",\"Value1\":\"MONTENEGRO\",\"Value2\":\"ME\",\"Value3\":\"160\"},{\"Code\":\"MONTSERRAT\",\"Value1\":\"MONTSERRAT\",\"Value2\":\"MS\",\"Value3\":\"654\"},{\"Code\":\"MAROKKO\",\"Value1\":\"MOROCCO\",\"Value2\":\"MA\",\"Value3\":\"303\"},{\"Code\":\"MOSAMBIK\",\"Value1\":\"MOZAMBIQUE\",\"Value2\":\"MZ\",\"Value3\":\"319\"},{\"Code\":\"MYANMAR (BURMA)\",\"Value1\":\"MYANMAR\",\"Value2\":\"MM\",\"Value3\":\"420\"},{\"Code\":\"NAMIBIA\",\"Value1\":\"NAMIBIA\",\"Value2\":\"NA\",\"Value3\":\"308\"},{\"Code\":\"NAURU\",\"Value1\":\"NAURU\",\"Value2\":\"NR\",\"Value3\":\"818\"},{\"Code\":\"NEPAL\",\"Value1\":\"NEPAL\",\"Value2\":\"NP\",\"Value3\":\"528\"},{\"Code\":\"NEDERLAND\",\"Value1\":\"NETHERLANDS\",\"Value2\":\"NL\",\"Value3\":\"127\"},{\"Code\":\"DE NEDERLANDSKE ANTILLENE\",\"Value1\":\"NETHERLANDS ANTILLES\",\"Value2\":\"AN\",\"Value3\":\"656\"},{\"Code\":\"NY-KALEDONIA\",\"Value1\":\"NEW CALEDONIA\",\"Value2\":\"NC\",\"Value3\":\"833\"},{\"Code\":\"NEW ZEALAND\",\"Value1\":\"NEW ZEALAND\",\"Value2\":\"NZ\",\"Value3\":\"820\"},{\"Code\":\"NICARAGUA\",\"Value1\":\"NICARAGUA\",\"Value2\":\"NI\",\"Value3\":\"664\"},{\"Code\":\"NIGER\",\"Value1\":\"NIGER\",\"Value2\":\"NE\",\"Value3\":\"309\"},{\"Code\":\"NIGERIA\",\"Value1\":\"NIGERIA\",\"Value2\":\"NG\",\"Value3\":\"313\"},{\"Code\":\"NIUE\",\"Value1\":\"NIUE\",\"Value2\":\"NU\",\"Value3\":\"821\"},{\"Code\":\"NORFOLKØYA\",\"Value1\":\"NORFOLK ISLAND\",\"Value2\":\"NF\",\"Value3\":\"822\"},{\"Code\":\"NORD-KOREA\",\"Value1\":\"NORTH KOREA\",\"Value2\":\"KP\",\"Value3\":\"488\"},{\"Code\":\"NORD-MAKEDONIA\",\"Value1\":\"NORTH MACEDONIA\",\"Value2\":\"MK\",\"Value3\":\"156\"},{\"Code\":\"NORD-MARIANENE\",\"Value1\":\"NORTHERN MARIANA ISLANDS\",\"Value2\":\"MP\",\"Value3\":\"840\"},{\"Code\":\"NORGE\",\"Value1\":\"NORWAY\",\"Value2\":\"NO\",\"Value3\":\"000\"},{\"Code\":\"OMAN\",\"Value1\":\"OMAN\",\"Value2\":\"OM\",\"Value3\":\"520\"},{\"Code\":\"PAKISTAN\",\"Value1\":\"PAKISTAN\",\"Value2\":\"PK\",\"Value3\":\"534\"},{\"Code\":\"PALAU\",\"Value1\":\"PALAU\",\"Value2\":\"PW\",\"Value3\":\"839\"},{\"Code\":\"DET PALESTINSKE OMRÅDET\",\"Value1\":\"PALESTINE\",\"Value2\":\"PS\",\"Value3\":\"524\"},{\"Code\":\"PANAMA\",\"Value1\":\"PANAMA\",\"Value2\":\"PA\",\"Value3\":\"668\"},{\"Code\":\"PAPUA NY-GUINEA\",\"Value1\":\"PAPUA NEW GUINEA\",\"Value2\":\"PG\",\"Value3\":\"827\"},{\"Code\":\"PARAGUAY\",\"Value1\":\"PARAGUAY\",\"Value2\":\"PY\",\"Value3\":\"755\"},{\"Code\":\"PERU\",\"Value1\":\"PERU\",\"Value2\":\"PE\",\"Value3\":\"760\"},{\"Code\":\"FILIPPINENE\",\"Value1\":\"PHILIPPINES\",\"Value2\":\"PH\",\"Value3\":\"428\"},{\"Code\":\"PITCAIRN\",\"Value1\":\"PITCAIRN\",\"Value2\":\"PN\",\"Value3\":\"828\"},{\"Code\":\"POLEN\",\"Value1\":\"POLAND\",\"Value2\":\"PL\",\"Value3\":\"131\"},{\"Code\":\"PORTUGAL\",\"Value1\":\"PORTUGAL\",\"Value2\":\"PT\",\"Value3\":\"132\"},{\"Code\":\"PUERTO RICO\",\"Value1\":\"PUERTO RICO\",\"Value2\":\"PR\",\"Value3\":\"685\"},{\"Code\":\"QATAR\",\"Value1\":\"QATAR\",\"Value2\":\"QA\",\"Value3\":\"540\"},{\"Code\":\"RÉUNION\",\"Value1\":\"REUNION\",\"Value2\":\"RE\",\"Value3\":\"323\"},{\"Code\":\"ROMANIA\",\"Value1\":\"ROMANIA\",\"Value2\":\"RO\",\"Value3\":\"133\"},{\"Code\":\"RUSSLAND\",\"Value1\":\"RUSSIA\",\"Value2\":\"RU\",\"Value3\":\"140\"},{\"Code\":\"RWANDA\",\"Value1\":\"RWANDA\",\"Value2\":\"RW\",\"Value3\":\"329\"},{\"Code\":\"ST.HELENA\",\"Value1\":\"SAINT HELENA\",\"Value2\":\"SH\",\"Value3\":\"209\"},{\"Code\":\"ST.KITTS OG NEVIS\",\"Value1\":\"SAINT KITTS AND NEVIS\",\"Value2\":\"KN\",\"Value3\":\"677\"},{\"Code\":\"ST. LUCIA\",\"Value1\":\"SAINT LUCIA\",\"Value2\":\"LC\",\"Value3\":\"678\"},{\"Code\":\"SAINT MARTIN\",\"Value1\":\"SAINT MARTIN (FRENCH PART)\",\"Value2\":\"MF\",\"Value3\":\"686\"},{\"Code\":\"ST.PIERRE OG MIQUELON\",\"Value1\":\"SAINT PIERRE AND MIQUELON\",\"Value2\":\"PM\",\"Value3\":\"676\"},{\"Code\":\"ST. VINCENT OG GRENADINENE\",\"Value1\":\"SAINT VINCENT AND THE GRENADINES\",\"Value2\":\"VC\",\"Value3\":\"679\"},{\"Code\":\"VEST-SAMOA\",\"Value1\":\"SAMOA\",\"Value2\":\"WS\",\"Value3\":\"830\"},{\"Code\":\"SAN MARINO\",\"Value1\":\"SAN MARINO\",\"Value2\":\"SM\",\"Value3\":\"134\"},{\"Code\":\"SAO TOME OG PRINCIPE\",\"Value1\":\"SAO TOME AND PRINCIPE\",\"Value2\":\"ST\",\"Value3\":\"333\"},{\"Code\":\"SAUDI-ARABIA\",\"Value1\":\"SAUDI ARABIA\",\"Value2\":\"SA\",\"Value3\":\"544\"},{\"Code\":\"SENEGAL\",\"Value1\":\"SENEGAL\",\"Value2\":\"SN\",\"Value3\":\"336\"},{\"Code\":\"SERBIA\",\"Value1\":\"SERBIA\",\"Value2\":\"RS\",\"Value3\":\"159\"},{\"Code\":\"SEYCHELLENE\",\"Value1\":\"SEYCHELLES\",\"Value2\":\"SC\",\"Value3\":\"338\"},{\"Code\":\"SIERRA LEONE\",\"Value1\":\"SIERRA LEONE\",\"Value2\":\"SL\",\"Value3\":\"339\"},{\"Code\":\"SINGAPORE\",\"Value1\":\"SINGAPORE\",\"Value2\":\"SG\",\"Value3\":\"548\"},{\"Code\":\"SINT MAARTEN\",\"Value1\":\"SINT MAARTEN (DUTCH PART)\",\"Value2\":\"SX\",\"Value3\":\"658\"},{\"Code\":\"SLOVAKIA\",\"Value1\":\"SLOVAKIA\",\"Value2\":\"SK\",\"Value3\":\"157\"},{\"Code\":\"SLOVENIA\",\"Value1\":\"SLOVENIA\",\"Value2\":\"SI\",\"Value3\":\"146\"},{\"Code\":\"SALOMONØYENE\",\"Value1\":\"SOLOMON ISLANDS\",\"Value2\":\"SB\",\"Value3\":\"806\"},{\"Code\":\"SOMALIA\",\"Value1\":\"SOMALIA\",\"Value2\":\"SO\",\"Value3\":\"346\"},{\"Code\":\"SØR-AFRIKA\",\"Value1\":\"SOUTH AFRICA\",\"Value2\":\"ZA\",\"Value3\":\"359\"},{\"Code\":\"SØR-GEORGIA OG DE SØNDRE SANDWICHØYENE\",\"Value1\":\"SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS\",\"Value2\":\"GS\",\"Value3\":\"907\"},{\"Code\":\"SØR-KOREA\",\"Value1\":\"SOUTH KOREA\",\"Value2\":\"KR\",\"Value3\":\"492\"},{\"Code\":\"SØR-SUDAN\",\"Value1\":\"SOUTH SUDAN\",\"Value2\":\"SS\",\"Value3\":\"355\"},{\"Code\":\"SPANIA\",\"Value1\":\"SPAIN\",\"Value2\":\"ES\",\"Value3\":\"137\"},{\"Code\":\"SRI LANKA\",\"Value1\":\"SRI LANKA\",\"Value2\":\"LK\",\"Value3\":\"424\"},{\"Code\":\"ST BARTHELEMY\",\"Value1\":\"ST BARTHELEMY\",\"Value2\":\"BL\",\"Value3\":\"687\"},{\"Code\":\"SUDAN\",\"Value1\":\"SUDAN\",\"Value2\":\"SD\",\"Value3\":\"356\"},{\"Code\":\"SURINAME\",\"Value1\":\"SURINAME\",\"Value2\":\"SR\",\"Value3\":\"765\"},{\"Code\":\"SVALBARD OG JAN MAYEN\",\"Value1\":\"SVALBARD AND JAN MAYEN\",\"Value2\":\"SJ\",\"Value3\":\"911\"},{\"Code\":\"SWAZILAND\",\"Value1\":\"SWAZILAND\",\"Value2\":\"SZ\",\"Value3\":\"357\"},{\"Code\":\"SVERIGE\",\"Value1\":\"SWEDEN\",\"Value2\":\"SE\",\"Value3\":\"106\"},{\"Code\":\"SVEITS\",\"Value1\":\"SWITZERLAND\",\"Value2\":\"CH\",\"Value3\":\"141\"},{\"Code\":\"SYRIA\",\"Value1\":\"SYRIA\",\"Value2\":\"SY\",\"Value3\":\"564\"},{\"Code\":\"TAIWAN\",\"Value1\":\"TAIWAN\",\"Value2\":\"TW\",\"Value3\":\"432\"},{\"Code\":\"TADZJIKISTAN\",\"Value1\":\"TAJIKISTAN\",\"Value2\":\"TJ\",\"Value3\":\"550\"},{\"Code\":\"TANZANIA\",\"Value1\":\"TANZANIA\",\"Value2\":\"TZ\",\"Value3\":\"369\"},{\"Code\":\"THAILAND\",\"Value1\":\"THAILAND\",\"Value2\":\"TH\",\"Value3\":\"568\"},{\"Code\":\"ØST-TIMOR\",\"Value1\":\"TIMOR-LESTE (EAST TIMOR)\",\"Value2\":\"TL\",\"Value3\":\"537\"},{\"Code\":\"TOGO\",\"Value1\":\"TOGO\",\"Value2\":\"TG\",\"Value3\":\"376\"},{\"Code\":\"TOKELAU\",\"Value1\":\"TOKELAU\",\"Value2\":\"TK\",\"Value3\":\"829\"},{\"Code\":\"TONGA\",\"Value1\":\"TONGA\",\"Value2\":\"TO\",\"Value3\":\"813\"},{\"Code\":\"TRINIDAD OG TOBAGO\",\"Value1\":\"TRINIDAD AND TOBAGO\",\"Value2\":\"TT\",\"Value3\":\"680\"},{\"Code\":\"TUNISIA\",\"Value1\":\"TUNISIA\",\"Value2\":\"TN\",\"Value3\":\"379\"},{\"Code\":\"TYRKIA\",\"Value1\":\"TURKEY\",\"Value2\":\"TR\",\"Value3\":\"143\"},{\"Code\":\"TURKMENISTAN\",\"Value1\":\"TURKMENISTAN\",\"Value2\":\"TM\",\"Value3\":\"552\"},{\"Code\":\"TURKS/CAICOSØYENE\",\"Value1\":\"TURKS AND CAICOS ISLANDS\",\"Value2\":\"TC\",\"Value3\":\"681\"},{\"Code\":\"TUVALU\",\"Value1\":\"TUVALU\",\"Value2\":\"TV\",\"Value3\":\"816\"},{\"Code\":\"UGANDA\",\"Value1\":\"UGANDA\",\"Value2\":\"UG\",\"Value3\":\"386\"},{\"Code\":\"UKRAINA\",\"Value1\":\"UKRAINE\",\"Value2\":\"UA\",\"Value3\":\"148\"},{\"Code\":\"DE ARABISKE EMIRATER\",\"Value1\":\"UNITED ARAB EMIRATES\",\"Value2\":\"AE\",\"Value3\":\"426\"},{\"Code\":\"USA\",\"Value1\":\"UNITED STATES\",\"Value2\":\"US\",\"Value3\":\"684\"},{\"Code\":\"MINDRE STILLEHAVSØYER\",\"Value1\":\"UNITED STATES MINOR OUTLYING ISLANDS\",\"Value2\":\"UM\",\"Value3\":\"819\"},{\"Code\":\"URUGUAY\",\"Value1\":\"URUGUAY\",\"Value2\":\"UY\",\"Value3\":\"770\"},{\"Code\":\"UZBEKISTAN\",\"Value1\":\"UZBEKISTAN\",\"Value2\":\"UZ\",\"Value3\":\"554\"},{\"Code\":\"VANUATU\",\"Value1\":\"VANUATU\",\"Value2\":\"VU\",\"Value3\":\"812\"},{\"Code\":\"VATIKANSTATEN\",\"Value1\":\"VATICAN (OR HOLY SEE)\",\"Value2\":\"VA\",\"Value3\":\"154\"},{\"Code\":\"VENEZUELA\",\"Value1\":\"VENEZUELA\",\"Value2\":\"VE\",\"Value3\":\"775\"},{\"Code\":\"VIETNAM\",\"Value1\":\"VIETNAM\",\"Value2\":\"VN\",\"Value3\":\"575\"},{\"Code\":\"JOMFRUØYENE BRIT.\",\"Value1\":\"VIRGIN ISLANDS, BRITISH\",\"Value2\":\"VG\",\"Value3\":\"608\"},{\"Code\":\"JOMFRUØYENE AM.\",\"Value1\":\"VIRGIN ISLANDS, U.S.\",\"Value2\":\"VI\",\"Value3\":\"601\"},{\"Code\":\"WALLIS/FUTUNAØYENE\",\"Value1\":\"WALLIS AND FUTUNA\",\"Value2\":\"WF\",\"Value3\":\"832\"},{\"Code\":\"VEST-SAHARA\",\"Value1\":\"WESTERN SAHARA\",\"Value2\":\"EH\",\"Value3\":\"304\"},{\"Code\":\"JEMEN\",\"Value1\":\"YEMEN\",\"Value2\":\"YE\",\"Value3\":\"578\"},{\"Code\":\"ZAMBIA\",\"Value1\":\"ZAMBIA\",\"Value2\":\"ZM\",\"Value3\":\"389\"},{\"Code\":\"ZIMBABWE\",\"Value1\":\"ZIMBABWE\",\"Value2\":\"ZW\",\"Value3\":\"326\"},{\"Code\":\"ÅLAND\",\"Value1\":\"ÅLAND ISLANDS\",\"Value2\":\"AX\",\"Value3\":\"902\"}],\"_links\":{\"self\":{\"href\":\"https://www.altinn.no/api/metadata/codelists/ASF_Land/2758?language=1033\"}}}", + "https://www.altinn.no/api/metadata/codelists/ASF_land/2758?language=1044" + => "{\"Name\": \"ASF_Land\",\"Version\": 2758,\"Language\": 1044,\"Codes\": [{ \"Code\": \"\", \"Value1\": \"\", \"Value2\": \"\", \"Value3\": \"\" },{\"Code\": \"MONTENEGRO\",\"Value1\": \"MONTENEGRO\",\"Value2\": \"ME\",\"Value3\": \"160\"},{ \"Code\": \"NORGE\", \"Value1\": \"NORGE\", \"Value2\": \"NO\", \"Value3\": \"000\" },{\"Code\": \"NY-KALEDONIA\",\"Value1\": \"NY-KALEDONIA\",\"Value2\": \"NC\",\"Value3\": \"833\"},{ \"Code\": \"OMAN\", \"Value1\": \"OMAN\", \"Value2\": \"OM\", \"Value3\": \"520\" },{\"Code\": \"PAKISTAN\",\"Value1\": \"PAKISTAN\",\"Value2\": \"PK\",\"Value3\": \"534\"},{ \"Code\": \"PALAU\", \"Value1\": \"PALAU\", \"Value2\": \"PW\", \"Value3\": \"839\" },{ \"Code\": \"PANAMA\", \"Value1\": \"PANAMA\", \"Value2\": \"PA\", \"Value3\": \"668\" },{\"Code\": \"PAPUA NY-GUINEA\",\"Value1\": \"PAPUA NY-GUINEA\",\"Value2\": \"PG\",\"Value3\": \"827\"},{\"Code\": \"PARAGUAY\",\"Value1\": \"PARAGUAY\",\"Value2\": \"PY\",\"Value3\": \"755\"},{ \"Code\": \"PERU\", \"Value1\": \"PERU\", \"Value2\": \"PE\", \"Value3\": \"760\" },{ \"Code\": \"POLEN\", \"Value1\": \"POLEN\", \"Value2\": \"PL\", \"Value3\": \"131\" },{\"Code\": \"PORTUGAL\",\"Value1\": \"PORTUGAL\",\"Value2\": \"PT\",\"Value3\": \"132\"},{\"Code\": \"SLOVAKIA\",\"Value1\": \"SLOVAKIA\",\"Value2\": \"SK\",\"Value3\": \"157\"},{\"Code\": \"SLOVENIA\",\"Value1\": \"SLOVENIA\",\"Value2\": \"SI\",\"Value3\": \"146\"},{ \"Code\": \"VIETNAM\", \"Value1\": \"VIETNAM\", \"Value2\": \"VN\", \"Value3\": \"575\" },{\"Code\": \"WALLIS/FUTUNAØYENE\",\"Value1\": \"WALLIS/FUTUNAØYENE\",\"Value2\": \"WF\",\"Value3\": \"832\"},{ \"Code\": \"ZAMBIA\", \"Value1\": \"ZAMBIA\", \"Value2\": \"ZM\", \"Value3\": \"389\" },{\"Code\": \"ZIMBABWE\",\"Value1\": \"ZIMBABWE\",\"Value2\": \"ZW\",\"Value3\": \"326\"},{\"Code\": \"ØSTERRIKE\",\"Value1\": \"ØSTERRIKE\",\"Value2\": \"AT\",\"Value3\": \"153\"},],\"_links\": {\"self\": {\"href\": \"https://www.altinn.no/api/metadata/codelists/ASF_Land/2758?language=1044\"}}}", + "https://www.altinn.no/api/metadata/codelists/ASF_land/?language=1044" + => "{\"Name\": \"ASF_Land\",\"Version\": 2758,\"Language\": 1044,\"Codes\": [{ \"Code\": \"\", \"Value1\": \"\", \"Value2\": \"\", \"Value3\": \"\" },{\"Code\": \"MONTENEGRO\",\"Value1\": \"MONTENEGRO\",\"Value2\": \"ME\",\"Value3\": \"160\"},{ \"Code\": \"NORGE\", \"Value1\": \"NORGE\", \"Value2\": \"NO\", \"Value3\": \"000\" },{\"Code\": \"NY-KALEDONIA\",\"Value1\": \"NY-KALEDONIA\",\"Value2\": \"NC\",\"Value3\": \"833\"},{ \"Code\": \"OMAN\", \"Value1\": \"OMAN\", \"Value2\": \"OM\", \"Value3\": \"520\" },{\"Code\": \"PAKISTAN\",\"Value1\": \"PAKISTAN\",\"Value2\": \"PK\",\"Value3\": \"534\"},{ \"Code\": \"PALAU\", \"Value1\": \"PALAU\", \"Value2\": \"PW\", \"Value3\": \"839\" },{ \"Code\": \"PANAMA\", \"Value1\": \"PANAMA\", \"Value2\": \"PA\", \"Value3\": \"668\" },{\"Code\": \"PAPUA NY-GUINEA\",\"Value1\": \"PAPUA NY-GUINEA\",\"Value2\": \"PG\",\"Value3\": \"827\"},{\"Code\": \"PARAGUAY\",\"Value1\": \"PARAGUAY\",\"Value2\": \"PY\",\"Value3\": \"755\"},{ \"Code\": \"PERU\", \"Value1\": \"PERU\", \"Value2\": \"PE\", \"Value3\": \"760\" },{ \"Code\": \"POLEN\", \"Value1\": \"POLEN\", \"Value2\": \"PL\", \"Value3\": \"131\" },{\"Code\": \"PORTUGAL\",\"Value1\": \"PORTUGAL\",\"Value2\": \"PT\",\"Value3\": \"132\"},{\"Code\": \"SLOVAKIA\",\"Value1\": \"SLOVAKIA\",\"Value2\": \"SK\",\"Value3\": \"157\"},{\"Code\": \"SLOVENIA\",\"Value1\": \"SLOVENIA\",\"Value2\": \"SI\",\"Value3\": \"146\"},{ \"Code\": \"VIETNAM\", \"Value1\": \"VIETNAM\", \"Value2\": \"VN\", \"Value3\": \"575\" },{\"Code\": \"WALLIS/FUTUNAØYENE\",\"Value1\": \"WALLIS/FUTUNAØYENE\",\"Value2\": \"WF\",\"Value3\": \"832\"},{ \"Code\": \"ZAMBIA\", \"Value1\": \"ZAMBIA\", \"Value2\": \"ZM\", \"Value3\": \"389\" },{\"Code\": \"ZIMBABWE\",\"Value1\": \"ZIMBABWE\",\"Value2\": \"ZW\",\"Value3\": \"326\"},{\"Code\": \"ØSTERRIKE\",\"Value1\": \"ØSTERRIKE\",\"Value2\": \"AT\",\"Value3\": \"153\"},],\"_links\": {\"self\": {\"href\": \"https://www.altinn.no/api/metadata/codelists/ASF_Land/2758?language=1044\"}}}", + "https://www.altinn.no/api/metadata/codelists/ASF_Fylker/3063?language=1044" + => "{\"Name\":\"ASF_Fylker\",\"Version\":3063,\"Language\":1044,\"Codes\":[{\"Code\":\"\",\"Value1\":\"\",\"Value2\":\"\",\"Value3\":\"\"},{\"Code\":\"Agder\",\"Value1\":\"Agder\",\"Value2\":\"4200\",\"Value3\":\"\"},{\"Code\":\"Akershus - UTGÅTT\",\"Value1\":\"Akershus - UTGÅTT\",\"Value2\":\"0200\",\"Value3\":\"\"},{\"Code\":\"Aust-Agder - UTGÅTT\",\"Value1\":\"Aust-Agder - UTGÅTT\",\"Value2\":\"0900\",\"Value3\":\"\"},{\"Code\":\"Buskerud - UTGÅTT\",\"Value1\":\"Buskerud -UTGÅTT\",\"Value2\":\"0600\",\"Value3\":\"\"},{\"Code\":\"Finnmark - UTGÅTT\",\"Value1\":\"Finnmark - UTGÅTT\",\"Value2\":\"2000\",\"Value3\":\"\"},{\"Code\":\"Hedmark - UTGÅTT\",\"Value1\":\"Hedmark - UTGÅTT\",\"Value2\":\"0400\",\"Value3\":\"\"},{\"Code\":\"Hordaland - UTGÅTT\",\"Value1\":\"Hordaland - UTGÅTT\",\"Value2\":\"1200\",\"Value3\":\"\"},{\"Code\":\"Innlandet\",\"Value1\":\"Innlandet\",\"Value2\":\"3400\",\"Value3\":\"\"},{\"Code\":\"Møre og Romsdal\",\"Value1\":\"Møre og Romsdal\",\"Value2\":\"1500\",\"Value3\":\"\"},{\"Code\":\"Nordland\",\"Value1\":\"Nordland\",\"Value2\":\"1800\",\"Value3\":\"\"},{\"Code\":\"Oppland - UTGÅTT\",\"Value1\":\"Oppland -UTGÅTT\",\"Value2\":\"0500\",\"Value3\":\"\"},{\"Code\":\"Oslo\",\"Value1\":\"Oslo\",\"Value2\":\"0300\",\"Value3\":\"\"},{\"Code\":\"Rogaland\",\"Value1\":\"Rogaland\",\"Value2\":\"1100\",\"Value3\":\"\"},{\"Code\":\"Sogn og Fjordane - UTGÅTT\",\"Value1\":\"Sogn og Fjordane - UTGÅTT\",\"Value2\":\"1400\",\"Value3\":\"\"},{\"Code\":\"Telemark - UTGÅTT\",\"Value1\":\"Telemark - UTGÅTT\",\"Value2\":\"0800\",\"Value3\":\"\"},{\"Code\":\"Troms - UTGÅTT\",\"Value1\":\"Troms - UTGÅTT\",\"Value2\":\"1900\",\"Value3\":\"\"},{\"Code\":\"Troms og Finnmark\",\"Value1\":\"Troms og Finnmark\",\"Value2\":\"5400\",\"Value3\":\"\"},{\"Code\":\"Trøndelag\",\"Value1\":\"Trøndelag\",\"Value2\":\"5000\",\"Value3\":\"\"},{\"Code\":\"Vest-Agder - UTGÅTT\",\"Value1\":\"Vest-Agder - UTGÅTT\",\"Value2\":\"1000\",\"Value3\":\"\"},{\"Code\":\"Vestfold og Telemark\",\"Value1\":\"Vestfold og Telemark\",\"Value2\":\"3800\",\"Value3\":\"\"},{\"Code\":\"Vestfold - UTGÅTT\",\"Value1\":\"Vestfold -UTGÅTT\",\"Value2\":\"0700\",\"Value3\":\"\"},{\"Code\":\"Vestland\",\"Value1\":\"Vestland\",\"Value2\":\"4600\",\"Value3\":\"\"},{\"Code\":\"Viken\",\"Value1\":\"Viken\",\"Value2\":\"3000\",\"Value3\":\"\"},{\"Code\":\"Østfold - UTGÅTT\",\"Value1\":\"Østfold - UTGÅTT\",\"Value2\":\"0100\",\"Value3\":\"\"}],\"_links\":{\"self\":{\"href\":\"https://www.altinn.no/api/metadata/codelists/ASF_Fylker/3063?language=1044\"}}}", + "https://www.altinn.no/api/metadata/codelists/ASF_Fylker/3063?language=2068" + => "{\"Name\":\"ASF_Fylker\",\"Version\":3063,\"Language\":1044,\"Codes\":[{\"Code\":\"\",\"Value1\":\"\",\"Value2\":\"\",\"Value3\":\"\"},{\"Code\":\"Agder\",\"Value1\":\"Agder\",\"Value2\":\"4200\",\"Value3\":\"\"},{\"Code\":\"Akershus - UTGÅTT\",\"Value1\":\"Akershus - UTGÅTT\",\"Value2\":\"0200\",\"Value3\":\"\"},{\"Code\":\"Aust-Agder - UTGÅTT\",\"Value1\":\"Aust-Agder - UTGÅTT\",\"Value2\":\"0900\",\"Value3\":\"\"},{\"Code\":\"Buskerud - UTGÅTT\",\"Value1\":\"Buskerud -UTGÅTT\",\"Value2\":\"0600\",\"Value3\":\"\"},{\"Code\":\"Finnmark - UTGÅTT\",\"Value1\":\"Finnmark - UTGÅTT\",\"Value2\":\"2000\",\"Value3\":\"\"},{\"Code\":\"Hedmark - UTGÅTT\",\"Value1\":\"Hedmark - UTGÅTT\",\"Value2\":\"0400\",\"Value3\":\"\"},{\"Code\":\"Hordaland - UTGÅTT\",\"Value1\":\"Hordaland - UTGÅTT\",\"Value2\":\"1200\",\"Value3\":\"\"},{\"Code\":\"Innlandet\",\"Value1\":\"Innlandet\",\"Value2\":\"3400\",\"Value3\":\"\"},{\"Code\":\"Møre og Romsdal\",\"Value1\":\"Møre og Romsdal\",\"Value2\":\"1500\",\"Value3\":\"\"},{\"Code\":\"Nordland\",\"Value1\":\"Nordland\",\"Value2\":\"1800\",\"Value3\":\"\"},{\"Code\":\"Oppland - UTGÅTT\",\"Value1\":\"Oppland -UTGÅTT\",\"Value2\":\"0500\",\"Value3\":\"\"},{\"Code\":\"Oslo\",\"Value1\":\"Oslo\",\"Value2\":\"0300\",\"Value3\":\"\"},{\"Code\":\"Rogaland\",\"Value1\":\"Rogaland\",\"Value2\":\"1100\",\"Value3\":\"\"},{\"Code\":\"Sogn og Fjordane - UTGÅTT\",\"Value1\":\"Sogn og Fjordane - UTGÅTT\",\"Value2\":\"1400\",\"Value3\":\"\"},{\"Code\":\"Telemark - UTGÅTT\",\"Value1\":\"Telemark - UTGÅTT\",\"Value2\":\"0800\",\"Value3\":\"\"},{\"Code\":\"Troms - UTGÅTT\",\"Value1\":\"Troms - UTGÅTT\",\"Value2\":\"1900\",\"Value3\":\"\"},{\"Code\":\"Troms og Finnmark\",\"Value1\":\"Troms og Finnmark\",\"Value2\":\"5400\",\"Value3\":\"\"},{\"Code\":\"Trøndelag\",\"Value1\":\"Trøndelag\",\"Value2\":\"5000\",\"Value3\":\"\"},{\"Code\":\"Vest-Agder - UTGÅTT\",\"Value1\":\"Vest-Agder - UTGÅTT\",\"Value2\":\"1000\",\"Value3\":\"\"},{\"Code\":\"Vestfold og Telemark\",\"Value1\":\"Vestfold og Telemark\",\"Value2\":\"3800\",\"Value3\":\"\"},{\"Code\":\"Vestfold - UTGÅTT\",\"Value1\":\"Vestfold -UTGÅTT\",\"Value2\":\"0700\",\"Value3\":\"\"},{\"Code\":\"Vestland\",\"Value1\":\"Vestland\",\"Value2\":\"4600\",\"Value3\":\"\"},{\"Code\":\"Viken\",\"Value1\":\"Viken\",\"Value2\":\"3000\",\"Value3\":\"\"},{\"Code\":\"Østfold - UTGÅTT\",\"Value1\":\"Østfold - UTGÅTT\",\"Value2\":\"0100\",\"Value3\":\"\"}],\"_links\":{\"self\":{\"href\":\"https://www.altinn.no/api/metadata/codelists/ASF_Fylker/3063?language=1044\"}}}", + _ => null, + }; } } diff --git a/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2OptionsCacheTests.cs b/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2OptionsCacheTests.cs index b31f7b133..c11953937 100644 --- a/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2OptionsCacheTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2OptionsCacheTests.cs @@ -3,103 +3,101 @@ using Altinn.App.Core.Features.Options.Altinn2Provider; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; -using Xunit; -namespace Altinn.App.Core.Tests.Features.Options.Altinn2Provider +namespace Altinn.App.Core.Tests.Features.Options.Altinn2Provider; + +public class Altinn2OptionsCacheTests { - public class Altinn2OptionsCacheTests + public static ServiceCollection GetServiceCollection() { - public static ServiceCollection GetServiceCollection() - { - var services = new ServiceCollection(); + var services = new ServiceCollection(); - services.AddMemoryCache(); + services.AddMemoryCache(); - var httpClient = services.AddHttpClient(); + var httpClient = services.AddHttpClient(); - // Registrer the instrumented fake message handler as singleton, so we can count - // how many http calls gets through the cache. - services.AddSingleton(); - httpClient.ConfigurePrimaryHttpMessageHandler(); + // Registrer the instrumented fake message handler as singleton, so we can count + // how many http calls gets through the cache. + services.AddSingleton(); + httpClient.ConfigurePrimaryHttpMessageHandler(); - return services; - } + return services; + } - [Fact] - public async Task Altinn2OptionsTests_TestCache() - { - var services = GetServiceCollection(); - services.AddAltinn2CodeList( - id: "ASF_Land3", - transform: (code) => new() { Value = code.Code, Label = code.Value1 }, - codeListVersion: 2758, - metadataApiId: "ASF_land" - ); - services.AddAltinn2CodeList( - id: "ASF_Land4", - transform: (code) => new() { Value = code.Code, Label = code.Value1 }, - codeListVersion: 2758, - metadataApiId: "ASF_land" - ); - services.AddAltinn2CodeList( - id: "ASF_Fylker", - transform: (code) => new() { Value = code.Code, Label = code.Value1 }, - codeListVersion: 3063, - metadataApiId: "ASF_Fylker" - ); + [Fact] + public async Task Altinn2OptionsTests_TestCache() + { + var services = GetServiceCollection(); + services.AddAltinn2CodeList( + id: "ASF_Land3", + transform: (code) => new() { Value = code.Code, Label = code.Value1 }, + codeListVersion: 2758, + metadataApiId: "ASF_land" + ); + services.AddAltinn2CodeList( + id: "ASF_Land4", + transform: (code) => new() { Value = code.Code, Label = code.Value1 }, + codeListVersion: 2758, + metadataApiId: "ASF_land" + ); + services.AddAltinn2CodeList( + id: "ASF_Fylker", + transform: (code) => new() { Value = code.Code, Label = code.Value1 }, + codeListVersion: 3063, + metadataApiId: "ASF_Fylker" + ); - var sp = services.BuildServiceProvider(validateScopes: true); + var sp = services.BuildServiceProvider(validateScopes: true); - // Do two fetches of ASF_Land and see that only one call gets passed to the messageHandler - using (var scope = sp.CreateScope()) - { - var providers = scope.ServiceProvider.GetRequiredService>(); - var messageHandler = - scope.ServiceProvider.GetRequiredService(); - messageHandler.CallCounter.Should().Be(0); - providers.Count().Should().Be(3); - var optionsProvider = providers.Single(p => p.Id == "ASF_Land3"); - await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); - await Task.Delay(5); - messageHandler.CallCounter.Should().Be(1); + // Do two fetches of ASF_Land and see that only one call gets passed to the messageHandler + using (var scope = sp.CreateScope()) + { + var providers = scope.ServiceProvider.GetRequiredService>(); + var messageHandler = + scope.ServiceProvider.GetRequiredService(); + messageHandler.CallCounter.Should().Be(0); + providers.Count().Should().Be(3); + var optionsProvider = providers.Single(p => p.Id == "ASF_Land3"); + await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); + await Task.Delay(5); + messageHandler.CallCounter.Should().Be(1); - await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); - await Task.Delay(5); - messageHandler.CallCounter.Should().Be(1); - } + await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); + await Task.Delay(5); + messageHandler.CallCounter.Should().Be(1); + } - // Repeat the process in another scope - using (var scope = sp.CreateScope()) - { - var providers = scope.ServiceProvider.GetRequiredService>(); - var messageHandler = - scope.ServiceProvider.GetRequiredService(); - var optionsProvider = providers.Single(p => p.Id == "ASF_Land3"); - await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); - await Task.Delay(5); - messageHandler.CallCounter.Should().Be(1); + // Repeat the process in another scope + using (var scope = sp.CreateScope()) + { + var providers = scope.ServiceProvider.GetRequiredService>(); + var messageHandler = + scope.ServiceProvider.GetRequiredService(); + var optionsProvider = providers.Single(p => p.Id == "ASF_Land3"); + await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); + await Task.Delay(5); + messageHandler.CallCounter.Should().Be(1); - await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); - await Task.Delay(5); - messageHandler.CallCounter.Should().Be(1); - } + await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); + await Task.Delay(5); + messageHandler.CallCounter.Should().Be(1); + } - // Try another request that uses ASF_Fylker instead, and see that another call is made - using (var scope = sp.CreateScope()) - { - var providers = scope.ServiceProvider.GetRequiredService>(); - var messageHandler = - scope.ServiceProvider.GetRequiredService(); - var optionsProvider = providers.Single(p => p.Id == "ASF_Fylker"); - await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); - await Task.Delay(5); - messageHandler.CallCounter.Should().Be(2); + // Try another request that uses ASF_Fylker instead, and see that another call is made + using (var scope = sp.CreateScope()) + { + var providers = scope.ServiceProvider.GetRequiredService>(); + var messageHandler = + scope.ServiceProvider.GetRequiredService(); + var optionsProvider = providers.Single(p => p.Id == "ASF_Fylker"); + await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); + await Task.Delay(5); + messageHandler.CallCounter.Should().Be(2); - // Fetch the list in nynorsk and see that yeat another call is made - await optionsProvider.GetAppOptionsAsync("nn", new Dictionary()); - await Task.Delay(5); - messageHandler.CallCounter.Should().Be(3); - } + // Fetch the list in nynorsk and see that yeat another call is made + await optionsProvider.GetAppOptionsAsync("nn", new Dictionary()); + await Task.Delay(5); + messageHandler.CallCounter.Should().Be(3); } } } diff --git a/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2OptionsTests.cs b/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2OptionsTests.cs index 3dbde837b..66c175cd2 100644 --- a/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2OptionsTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2OptionsTests.cs @@ -3,159 +3,157 @@ using Altinn.App.Core.Features.Options.Altinn2Provider; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; -using Xunit; -namespace Altinn.App.Core.Tests.Features.Options.Altinn2Provider +namespace Altinn.App.Core.Tests.Features.Options.Altinn2Provider; + +public class Altinn2OptionsTests { - public class Altinn2OptionsTests + /// + /// Change this to false to test with real https://www.altinn.no/api/metadata/codelists instead of + /// the moq in + /// + private readonly bool _shouldMoqAltinn2Api = true; + + public ServiceCollection GetServiceCollection() { - /// - /// Change this to false to test with real https://www.altinn.no/api/metadata/codelists instead of - /// the moq in - /// - private readonly bool _shouldMoqAltinn2Api = true; + var services = new ServiceCollection(); - public ServiceCollection GetServiceCollection() - { - var services = new ServiceCollection(); + services.AddMemoryCache(); - services.AddMemoryCache(); + var httpClient = services.AddHttpClient(); - var httpClient = services.AddHttpClient(); + if (_shouldMoqAltinn2Api) + { + services.AddTransient(); + httpClient.ConfigurePrimaryHttpMessageHandler(); + } - if (_shouldMoqAltinn2Api) - { - services.AddTransient(); - httpClient.ConfigurePrimaryHttpMessageHandler(); - } + return services; + } - return services; - } + [Fact] + public void Altinn2OptionsTests_NoCustomOptionsProvider_NotReturnProviders() + { + var services = GetServiceCollection(); - [Fact] - public void Altinn2OptionsTests_NoCustomOptionsProvider_NotReturnProviders() + // no custom api registrerd here + var sp = services.BuildServiceProvider(validateScopes: true); + using (var scope = sp.CreateScope()) { - var services = GetServiceCollection(); - - // no custom api registrerd here - var sp = services.BuildServiceProvider(validateScopes: true); - using (var scope = sp.CreateScope()) - { - var providers = scope.ServiceProvider.GetRequiredService>(); - providers.Should().BeEmpty(); - } + var providers = scope.ServiceProvider.GetRequiredService>(); + providers.Should().BeEmpty(); } + } - [Fact] - public async Task Altinn2OptionsTests_MoreThan4AndNorwayIncluded() + [Fact] + public async Task Altinn2OptionsTests_MoreThan4AndNorwayIncluded() + { + var services = GetServiceCollection(); + services.AddAltinn2CodeList( + id: "ASF_Land1", + transform: (code) => new() { Value = code.Code, Label = code.Value1 }, + codeListVersion: 2758, + metadataApiId: "ASF_land" + ); + + var sp = services.BuildServiceProvider(validateScopes: true); + using (var scope = sp.CreateScope()) { - var services = GetServiceCollection(); - services.AddAltinn2CodeList( - id: "ASF_Land1", - transform: (code) => new() { Value = code.Code, Label = code.Value1 }, - codeListVersion: 2758, - metadataApiId: "ASF_land" - ); - - var sp = services.BuildServiceProvider(validateScopes: true); - using (var scope = sp.CreateScope()) - { - var providers = scope.ServiceProvider.GetRequiredService>(); - providers.Count().Should().Be(1); - var optionsProvider = providers.Single(p => p.Id == "ASF_Land1"); - var landOptions = await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); - landOptions.Options.Should().HaveCountGreaterThan(4, "ASF_Land needs to have more than 4 countries"); - landOptions.Options.Should().Match(options => options.Any(o => o.Value == "NORGE")); - } + var providers = scope.ServiceProvider.GetRequiredService>(); + providers.Count().Should().Be(1); + var optionsProvider = providers.Single(p => p.Id == "ASF_Land1"); + var landOptions = await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); + landOptions.Options.Should().HaveCountGreaterThan(4, "ASF_Land needs to have more than 4 countries"); + landOptions.Options.Should().Match(options => options.Any(o => o.Value == "NORGE")); } + } - [Fact] - public async Task Altinn2OptionsTests_EnglishLanguage() + [Fact] + public async Task Altinn2OptionsTests_EnglishLanguage() + { + var services = GetServiceCollection(); + services.AddAltinn2CodeList( + id: "ASF_Land1", + transform: (code) => new() { Value = code.Code, Label = code.Value1 }, + codeListVersion: 2758, + metadataApiId: "ASF_land" + ); + + var sp = services.BuildServiceProvider(validateScopes: true); + using (var scope = sp.CreateScope()) { - var services = GetServiceCollection(); - services.AddAltinn2CodeList( - id: "ASF_Land1", - transform: (code) => new() { Value = code.Code, Label = code.Value1 }, - codeListVersion: 2758, - metadataApiId: "ASF_land" - ); - - var sp = services.BuildServiceProvider(validateScopes: true); - using (var scope = sp.CreateScope()) - { - var providers = scope.ServiceProvider.GetRequiredService>(); - providers.Count().Should().Be(1); - var optionsProvider = providers.Single(p => p.Id == "ASF_Land1"); - var landOptions = await optionsProvider.GetAppOptionsAsync("en", new Dictionary()); - landOptions.Options.Should().HaveCountGreaterThan(4, "ASF_Land needs to have more than 4 countries"); - landOptions.Options.Should().Match(options => options.Any(o => o.Label == "NORWAY")); - } + var providers = scope.ServiceProvider.GetRequiredService>(); + providers.Count().Should().Be(1); + var optionsProvider = providers.Single(p => p.Id == "ASF_Land1"); + var landOptions = await optionsProvider.GetAppOptionsAsync("en", new Dictionary()); + landOptions.Options.Should().HaveCountGreaterThan(4, "ASF_Land needs to have more than 4 countries"); + landOptions.Options.Should().Match(options => options.Any(o => o.Label == "NORWAY")); } + } - [Fact] - public async Task Altinn2OptionsTests_FilterOnlyNorway() + [Fact] + public async Task Altinn2OptionsTests_FilterOnlyNorway() + { + var services = GetServiceCollection(); + services.AddAltinn2CodeList( + id: "OnlyNorway", + transform: (code) => new() { Value = code.Code, Label = code.Value1 }, + filter: (code) => code.Value2 == "NO", + codeListVersion: 2758, + metadataApiId: "ASF_land" + ); + + var sp = services.BuildServiceProvider(validateScopes: true); + using (var scope = sp.CreateScope()) { - var services = GetServiceCollection(); - services.AddAltinn2CodeList( - id: "OnlyNorway", - transform: (code) => new() { Value = code.Code, Label = code.Value1 }, - filter: (code) => code.Value2 == "NO", - codeListVersion: 2758, - metadataApiId: "ASF_land" - ); - - var sp = services.BuildServiceProvider(validateScopes: true); - using (var scope = sp.CreateScope()) - { - var providers = scope.ServiceProvider.GetRequiredService>(); - providers.Count().Should().Be(1); - var optionsProvider = providers.Single(p => p.Id == "OnlyNorway"); - var landOptions = await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); - landOptions.Options.Should().HaveCount(1, "We filter out only norway"); - landOptions.Options.Should().Match(options => options.Any(o => o.Value == "NORGE")); - } + var providers = scope.ServiceProvider.GetRequiredService>(); + providers.Count().Should().Be(1); + var optionsProvider = providers.Single(p => p.Id == "OnlyNorway"); + var landOptions = await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); + landOptions.Options.Should().HaveCount(1, "We filter out only norway"); + landOptions.Options.Should().Match(options => options.Any(o => o.Value == "NORGE")); } + } - [Fact] - public async Task Altinn2OptionsTests_NoCodeListVersionProvided() + [Fact] + public async Task Altinn2OptionsTests_NoCodeListVersionProvided() + { + var services = GetServiceCollection(); + services.AddAltinn2CodeList( + id: "OnlyNorway", + transform: (code) => new() { Value = code.Code, Label = code.Value1 }, + filter: (code) => code.Value2 == "NO", + codeListVersion: null, + metadataApiId: "ASF_land" + ); + + var sp = services.BuildServiceProvider(validateScopes: true); + using (var scope = sp.CreateScope()) { - var services = GetServiceCollection(); - services.AddAltinn2CodeList( - id: "OnlyNorway", - transform: (code) => new() { Value = code.Code, Label = code.Value1 }, - filter: (code) => code.Value2 == "NO", - codeListVersion: null, - metadataApiId: "ASF_land" - ); - - var sp = services.BuildServiceProvider(validateScopes: true); - using (var scope = sp.CreateScope()) - { - var providers = scope.ServiceProvider.GetRequiredService>(); - providers.Count().Should().Be(1); - var optionsProvider = providers.Single(p => p.Id == "OnlyNorway"); - var landOptions = await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); - landOptions.Options.Should().HaveCount(1, "We filter out only norway"); - landOptions.Options.Should().Match(options => options.Any(o => o.Value == "NORGE")); - } + var providers = scope.ServiceProvider.GetRequiredService>(); + providers.Count().Should().Be(1); + var optionsProvider = providers.Single(p => p.Id == "OnlyNorway"); + var landOptions = await optionsProvider.GetAppOptionsAsync("nb", new Dictionary()); + landOptions.Options.Should().HaveCount(1, "We filter out only norway"); + landOptions.Options.Should().Match(options => options.Any(o => o.Value == "NORGE")); } + } - [Fact] - public void Altinn2OptionsTests_Altinn2MetadataClientNotRegistered() - { - var services = new ServiceCollection(); - - services.AddAltinn2CodeList( - id: "OnlyNorway", - transform: (code) => new() { Value = code.Code, Label = code.Value1 }, - filter: (code) => code.Value2 == "NO", - codeListVersion: 2758, - metadataApiId: "ASF_land" - ); - - services - .Should() - .Contain(serviceDescriptor => serviceDescriptor.ServiceType == typeof(Altinn2MetadataApiClient)); - } + [Fact] + public void Altinn2OptionsTests_Altinn2MetadataClientNotRegistered() + { + var services = new ServiceCollection(); + + services.AddAltinn2CodeList( + id: "OnlyNorway", + transform: (code) => new() { Value = code.Code, Label = code.Value1 }, + filter: (code) => code.Value2 == "NO", + codeListVersion: 2758, + metadataApiId: "ASF_land" + ); + + services + .Should() + .Contain(serviceDescriptor => serviceDescriptor.ServiceType == typeof(Altinn2MetadataApiClient)); } } diff --git a/test/Altinn.App.Core.Tests/Features/Options/AppOptionsFactoryTests.cs b/test/Altinn.App.Core.Tests/Features/Options/AppOptionsFactoryTests.cs index d3ee2b593..6a8512ed2 100644 --- a/test/Altinn.App.Core.Tests/Features/Options/AppOptionsFactoryTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Options/AppOptionsFactoryTests.cs @@ -3,127 +3,125 @@ using Altinn.App.Core.Models; using FluentAssertions; using Moq; -using Xunit; -namespace Altinn.App.Core.Tests.Features.Options +namespace Altinn.App.Core.Tests.Features.Options; + +public class AppOptionsFactoryTests { - public class AppOptionsFactoryTests + [Fact] + public void GetOptionsProvider_NoCustomOptionsProvider_ShouldReturnDefault() { - [Fact] - public void GetOptionsProvider_NoCustomOptionsProvider_ShouldReturnDefault() - { - var appOptionsFileHandler = new Mock(); - var factory = new AppOptionsFactory( - new List() { new DefaultAppOptionsProvider(appOptionsFileHandler.Object) } - ); + var appOptionsFileHandler = new Mock(); + var factory = new AppOptionsFactory( + new List() { new DefaultAppOptionsProvider(appOptionsFileHandler.Object) } + ); - IAppOptionsProvider optionsProvider = factory.GetOptionsProvider("country"); + IAppOptionsProvider optionsProvider = factory.GetOptionsProvider("country"); - optionsProvider.Should().BeOfType(); - optionsProvider.Id.Should().Be("country"); - } + optionsProvider.Should().BeOfType(); + optionsProvider.Id.Should().Be("country"); + } - [Fact] - public void GetOptionsProvider_NoCustomOptionsProvider_ShouldReturnDefaultTwice() - { - var appOptionsFileHandler = new Mock(); - var factory = new AppOptionsFactory( - new List() { new DefaultAppOptionsProvider(appOptionsFileHandler.Object) } - ); + [Fact] + public void GetOptionsProvider_NoCustomOptionsProvider_ShouldReturnDefaultTwice() + { + var appOptionsFileHandler = new Mock(); + var factory = new AppOptionsFactory( + new List() { new DefaultAppOptionsProvider(appOptionsFileHandler.Object) } + ); - IAppOptionsProvider optionsProvider1 = factory.GetOptionsProvider("fylke"); - IAppOptionsProvider optionsProvider2 = factory.GetOptionsProvider("kommune"); + IAppOptionsProvider optionsProvider1 = factory.GetOptionsProvider("fylke"); + IAppOptionsProvider optionsProvider2 = factory.GetOptionsProvider("kommune"); - optionsProvider1.Id.Should().Be("fylke"); - optionsProvider2.Id.Should().Be("kommune"); - } + optionsProvider1.Id.Should().Be("fylke"); + optionsProvider2.Id.Should().Be("kommune"); + } - [Fact] - public void GetOptionsProvider_NoDefaultProvider_ShouldThrowException() - { - var factory = new AppOptionsFactory(new List()); + [Fact] + public void GetOptionsProvider_NoDefaultProvider_ShouldThrowException() + { + var factory = new AppOptionsFactory(new List()); - System.Action action = () => factory.GetOptionsProvider("country"); + System.Action action = () => factory.GetOptionsProvider("country"); - action.Should().Throw(); - } + action.Should().Throw(); + } - [Fact] - public void GetOptionsProvider_CustomOptionsProvider_ShouldReturnCustomType() - { - var appOptionsFileHandler = new Mock(); - var factory = new AppOptionsFactory( - new List() - { - new DefaultAppOptionsProvider(appOptionsFileHandler.Object), - new CountryAppOptionsProvider() - } - ); + [Fact] + public void GetOptionsProvider_CustomOptionsProvider_ShouldReturnCustomType() + { + var appOptionsFileHandler = new Mock(); + var factory = new AppOptionsFactory( + new List() + { + new DefaultAppOptionsProvider(appOptionsFileHandler.Object), + new CountryAppOptionsProvider() + } + ); - IAppOptionsProvider optionsProvider = factory.GetOptionsProvider("country"); + IAppOptionsProvider optionsProvider = factory.GetOptionsProvider("country"); - optionsProvider.Should().BeOfType(); - optionsProvider.Id.Should().Be("country"); - } + optionsProvider.Should().BeOfType(); + optionsProvider.Id.Should().Be("country"); + } - [Fact] - public void GetOptionsProvider_CustomOptionsProviderWithUpperCase_ShouldReturnCustomType() - { - var appOptionsFileHandler = new Mock(); - var factory = new AppOptionsFactory( - new List() - { - new DefaultAppOptionsProvider(appOptionsFileHandler.Object), - new CountryAppOptionsProvider() - } - ); + [Fact] + public void GetOptionsProvider_CustomOptionsProviderWithUpperCase_ShouldReturnCustomType() + { + var appOptionsFileHandler = new Mock(); + var factory = new AppOptionsFactory( + new List() + { + new DefaultAppOptionsProvider(appOptionsFileHandler.Object), + new CountryAppOptionsProvider() + } + ); - IAppOptionsProvider optionsProvider = factory.GetOptionsProvider("Country"); + IAppOptionsProvider optionsProvider = factory.GetOptionsProvider("Country"); - optionsProvider.Should().BeOfType(); - optionsProvider.Id.Should().Be("country"); - } + optionsProvider.Should().BeOfType(); + optionsProvider.Id.Should().Be("country"); + } - [Fact] - public async Task GetParameters_CustomOptionsProviderWithUpperCase_ShouldReturnCustomType() - { - var appOptionsFileHandler = new Mock(); - var factory = new AppOptionsFactory( - new List() - { - new DefaultAppOptionsProvider(appOptionsFileHandler.Object), - new CountryAppOptionsProvider() - } - ); - - IAppOptionsProvider optionsProvider = factory.GetOptionsProvider("Country"); - - AppOptions options = await optionsProvider.GetAppOptionsAsync( - "nb", - new Dictionary() { { "key", "value" } } - ); - options.Parameters.First(x => x.Key == "key").Value.Should().Be("value"); - } + [Fact] + public async Task GetParameters_CustomOptionsProviderWithUpperCase_ShouldReturnCustomType() + { + var appOptionsFileHandler = new Mock(); + var factory = new AppOptionsFactory( + new List() + { + new DefaultAppOptionsProvider(appOptionsFileHandler.Object), + new CountryAppOptionsProvider() + } + ); - private class CountryAppOptionsProvider : IAppOptionsProvider - { - public string Id { get; set; } = "country"; + IAppOptionsProvider optionsProvider = factory.GetOptionsProvider("Country"); + + AppOptions options = await optionsProvider.GetAppOptionsAsync( + "nb", + new Dictionary() { { "key", "value" } } + ); + options.Parameters.First(x => x.Key == "key").Value.Should().Be("value"); + } - public Task GetAppOptionsAsync(string? language, Dictionary keyValuePairs) + private class CountryAppOptionsProvider : IAppOptionsProvider + { + public string Id { get; set; } = "country"; + + public Task GetAppOptionsAsync(string? language, Dictionary keyValuePairs) + { + var options = new AppOptions { - var options = new AppOptions + Options = new List { - Options = new List - { - new AppOption { Label = "Norge", Value = "47" }, - new AppOption { Label = "Sverige", Value = "46" } - }, + new AppOption { Label = "Norge", Value = "47" }, + new AppOption { Label = "Sverige", Value = "46" } + }, - Parameters = keyValuePairs! - }; + Parameters = keyValuePairs! + }; - return Task.FromResult(options); - } + return Task.FromResult(options); } } } diff --git a/test/Altinn.App.Core.Tests/Features/Options/InstanceAppOptionsFactoryTests.cs b/test/Altinn.App.Core.Tests/Features/Options/InstanceAppOptionsFactoryTests.cs index 96fe3cc54..7b936306a 100644 --- a/test/Altinn.App.Core.Tests/Features/Options/InstanceAppOptionsFactoryTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Options/InstanceAppOptionsFactoryTests.cs @@ -2,59 +2,57 @@ using Altinn.App.Core.Features.Options; using Altinn.App.Core.Models; using FluentAssertions; -using Xunit; -namespace Altinn.App.Core.Tests.Features.Options +namespace Altinn.App.Core.Tests.Features.Options; + +public class InstanceAppOptionsFactoryTests { - public class InstanceAppOptionsFactoryTests + [Fact] + public void GetOptionsProvider_NoCustomOptionsProvider_ShouldReturnDefault() { - [Fact] - public void GetOptionsProvider_NoCustomOptionsProvider_ShouldReturnDefault() - { - var factory = new InstanceAppOptionsFactory( - new List() { new VehiclesInstanceAppOptionsProvider() } - ); + var factory = new InstanceAppOptionsFactory( + new List() { new VehiclesInstanceAppOptionsProvider() } + ); - IInstanceAppOptionsProvider optionsProvider = factory.GetOptionsProvider("not-vehicles"); + IInstanceAppOptionsProvider optionsProvider = factory.GetOptionsProvider("not-vehicles"); - optionsProvider.Should().BeOfType(); - } + optionsProvider.Should().BeOfType(); + } - [Fact] - public void GetOptionsProvider_CustomOptionsProvider_ShouldReturnCustomType() - { - var factory = new InstanceAppOptionsFactory( - new List() { new VehiclesInstanceAppOptionsProvider() } - ); + [Fact] + public void GetOptionsProvider_CustomOptionsProvider_ShouldReturnCustomType() + { + var factory = new InstanceAppOptionsFactory( + new List() { new VehiclesInstanceAppOptionsProvider() } + ); - IInstanceAppOptionsProvider optionsProvider = factory.GetOptionsProvider("vehicles"); + IInstanceAppOptionsProvider optionsProvider = factory.GetOptionsProvider("vehicles"); - optionsProvider.Should().BeOfType(); - optionsProvider.Id.Should().Be("vehicles"); - } + optionsProvider.Should().BeOfType(); + optionsProvider.Id.Should().Be("vehicles"); + } - public class VehiclesInstanceAppOptionsProvider : IInstanceAppOptionsProvider - { - public string Id => "vehicles"; + public class VehiclesInstanceAppOptionsProvider : IInstanceAppOptionsProvider + { + public string Id => "vehicles"; - public Task GetInstanceAppOptionsAsync( - InstanceIdentifier instanceIdentifier, - string? language, - Dictionary keyValuePairs - ) + public Task GetInstanceAppOptionsAsync( + InstanceIdentifier instanceIdentifier, + string? language, + Dictionary keyValuePairs + ) + { + var options = new AppOptions { - var options = new AppOptions + Options = new List { - Options = new List - { - new AppOption { Label = "Skoda Octavia 1.6", Value = "DN49525" }, - new AppOption { Label = "e-Golf", Value = "EK38470" }, - new AppOption { Label = "Tilhenger", Value = "JT5817" } - } - }; - - return Task.FromResult(options); - } + new AppOption { Label = "Skoda Octavia 1.6", Value = "DN49525" }, + new AppOption { Label = "e-Golf", Value = "EK38470" }, + new AppOption { Label = "Tilhenger", Value = "JT5817" } + } + }; + + return Task.FromResult(options); } } } diff --git a/test/Altinn.App.Core.Tests/Features/Options/JoinedAppOptionsTests.cs b/test/Altinn.App.Core.Tests/Features/Options/JoinedAppOptionsTests.cs index 18dff9be8..a082bd9c1 100644 --- a/test/Altinn.App.Core.Tests/Features/Options/JoinedAppOptionsTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Options/JoinedAppOptionsTests.cs @@ -4,7 +4,6 @@ using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Features.Options; diff --git a/test/Altinn.App.Core.Tests/Features/Options/NullInstanceAppOptionsProviderTests.cs b/test/Altinn.App.Core.Tests/Features/Options/NullInstanceAppOptionsProviderTests.cs index 9f255728b..270788bd9 100644 --- a/test/Altinn.App.Core.Tests/Features/Options/NullInstanceAppOptionsProviderTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Options/NullInstanceAppOptionsProviderTests.cs @@ -1,24 +1,22 @@ using Altinn.App.Core.Features.Options; using Altinn.App.Core.Models; using FluentAssertions; -using Xunit; -namespace Altinn.App.Core.Tests.Features.Options +namespace Altinn.App.Core.Tests.Features.Options; + +public class NullInstanceAppOptionsProviderTests { - public class NullInstanceAppOptionsProviderTests + [Fact] + public async Task Constructor_InitializedWithEmptyValues() { - [Fact] - public async Task Constructor_InitializedWithEmptyValues() - { - var provider = new NullInstanceAppOptionsProvider(); + var provider = new NullInstanceAppOptionsProvider(); - provider.Id.Should().Be(string.Empty); - var options = await provider.GetInstanceAppOptionsAsync( - new InstanceIdentifier(12345, Guid.NewGuid()), - "nb", - new Dictionary() - ); - options.Options.Should().BeNull(); - } + provider.Id.Should().Be(string.Empty); + var options = await provider.GetInstanceAppOptionsAsync( + new InstanceIdentifier(12345, Guid.NewGuid()), + "nb", + new Dictionary() + ); + options.Options.Should().BeNull(); } } diff --git a/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/HttpApiResultTests.cs b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/HttpApiResultTests.cs index 679a26095..2c09a11b6 100644 --- a/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/HttpApiResultTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/HttpApiResultTests.cs @@ -1,6 +1,5 @@ using System.Net; using Altinn.App.Core.Features.Payment.Processors.Nets.Models; -using Xunit; namespace Altinn.App.Core.Tests.Features.Payment.Providers.Nets; diff --git a/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs index 6e5f752a8..01b96af36 100644 --- a/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsMapperTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Core.Features.Payment.Processors.Nets; using Altinn.App.Core.Features.Payment.Processors.Nets.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Features.Payment.Providers.Nets; diff --git a/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsPaymentProcessorTests.cs b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsPaymentProcessorTests.cs index a9a18ea90..2756f1406 100644 --- a/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsPaymentProcessorTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Payment/Providers/Nets/NetsPaymentProcessorTests.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Features.Payment; using Altinn.App.Core.Features.Payment.Exceptions; using Altinn.App.Core.Features.Payment.Models; using Altinn.App.Core.Features.Payment.Processors.Nets; @@ -9,7 +7,6 @@ using FluentAssertions; using Microsoft.Extensions.Options; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Features.Payment.Providers.Nets; diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/DataAnnotationValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/DataAnnotationValidatorTests.cs index cb9805e44..fa44e4ca3 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/Default/DataAnnotationValidatorTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/DataAnnotationValidatorTests.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Features.Validators.Default; diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/DefaultTaskValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/DefaultTaskValidatorTests.cs index f24585be1..d972b537b 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/Default/DefaultTaskValidatorTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/DefaultTaskValidatorTests.cs @@ -6,7 +6,6 @@ using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Features.Validators.Default; diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/ExpressionValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/ExpressionValidatorTests.cs index ebf8e3638..1a02044a0 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/Default/ExpressionValidatorTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/ExpressionValidatorTests.cs @@ -5,7 +5,6 @@ using System.Text.Json.Serialization; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features.Validation.Default; -using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Models; @@ -18,7 +17,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using Xunit; using Xunit.Sdk; namespace Altinn.App.Core.Tests.Features.Validators.Default; diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/LegacyIValidationFormDataTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/LegacyIValidationFormDataTests.cs index c96f14914..93da0336a 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/Default/LegacyIValidationFormDataTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/LegacyIValidationFormDataTests.cs @@ -9,148 +9,146 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Moq; -using Xunit; -namespace Altinn.App.Core.Tests.Features.Validators.Default +namespace Altinn.App.Core.Tests.Features.Validators.Default; + +public class LegacyIValidationFormDataTests { - public class LegacyIValidationFormDataTests + private readonly LegacyIInstanceValidatorFormDataValidator _validator; + private readonly Mock _instanceValidator = new(); + + public LegacyIValidationFormDataTests() + { + var generalSettings = new GeneralSettings(); + _validator = new LegacyIInstanceValidatorFormDataValidator( + Microsoft.Extensions.Options.Options.Create(generalSettings), + _instanceValidator.Object + ); + } + + [Fact] + public async Task ValidateFormData_NoErrors() + { + // Arrange + var data = new object(); + + var validator = new LegacyIInstanceValidatorFormDataValidator( + Microsoft.Extensions.Options.Options.Create(new GeneralSettings()), + null + ); + validator.HasRelevantChanges(data, data).Should().BeFalse(); + + // Act + var result = await validator.ValidateFormData(new Instance(), new DataElement(), data, null); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task ValidateFormData_WithErrors() { - private readonly LegacyIInstanceValidatorFormDataValidator _validator; - private readonly Mock _instanceValidator = new(); - - public LegacyIValidationFormDataTests() - { - var generalSettings = new GeneralSettings(); - _validator = new LegacyIInstanceValidatorFormDataValidator( - Microsoft.Extensions.Options.Options.Create(generalSettings), - _instanceValidator.Object + // Arrange + var data = new object(); + + _instanceValidator + .Setup(iv => iv.ValidateData(It.IsAny(), It.IsAny())) + .Callback( + (object _, ModelStateDictionary modelState) => + { + modelState.AddModelError("test", "test"); + modelState.AddModelError("ddd", "*FIXED*test"); + } ); - } - [Fact] - public async Task ValidateFormData_NoErrors() - { - // Arrange - var data = new object(); + // Act + var result = await _validator.ValidateFormData(new Instance(), new DataElement(), data, null); + + // Assert + result + .Should() + .BeEquivalentTo( + JsonSerializer.Deserialize>( + """ + [ + { + "severity": 4, + "instanceId": null, + "dataElementId": null, + "field": "ddd", + "code": "test", + "description": "test", + "source": "Custom", + "customTextKey": null + }, + { + "severity": 1, + "instanceId": null, + "dataElementId": null, + "field": "test", + "code": "test", + "description": "test", + "source": "Custom", + "customTextKey": null + } + ] + """ + ) + ); + } + + private class TestModel + { + [JsonPropertyName("test")] + public string Test { get; set; } - var validator = new LegacyIInstanceValidatorFormDataValidator( - Microsoft.Extensions.Options.Options.Create(new GeneralSettings()), - null + public int IntegerWithout { get; set; } + + [JsonPropertyName("child")] + public TestModel Child { get; set; } + + [JsonPropertyName("children")] + public List TestList { get; set; } + } + + [Theory] + [InlineData("test", "test", "test with small case")] + [InlineData("Test", "test", "test with capital case gets rewritten")] + [InlineData("NotModelMatch", "NotModelMatch", "Error that does not mach model is kept as is")] + [InlineData( + "Child.TestList[2].child", + "child.children[2].child", + "TestList is renamed to children because of JsonPropertyName" + )] + [InlineData("test.children.child", "test.children.child", "valid JsonPropertyName based path is kept as is")] + public async Task ValidateErrorAndMappingWithCustomModel(string errorKey, string field, string errorMessage) + { + // Arrange + var data = new TestModel(); + + _instanceValidator + .Setup(iv => iv.ValidateData(It.IsAny(), It.IsAny())) + .Callback( + (object _, ModelStateDictionary modelState) => + { + modelState.AddModelError(errorKey, errorMessage); + modelState.AddModelError(errorKey, "*FIXED*" + errorMessage + " Fixed"); + } ); - validator.HasRelevantChanges(data, data).Should().BeFalse(); - - // Act - var result = await validator.ValidateFormData(new Instance(), new DataElement(), data, null); - - // Assert - Assert.Empty(result); - } - - [Fact] - public async Task ValidateFormData_WithErrors() - { - // Arrange - var data = new object(); - - _instanceValidator - .Setup(iv => iv.ValidateData(It.IsAny(), It.IsAny())) - .Callback( - (object _, ModelStateDictionary modelState) => - { - modelState.AddModelError("test", "test"); - modelState.AddModelError("ddd", "*FIXED*test"); - } - ); - - // Act - var result = await _validator.ValidateFormData(new Instance(), new DataElement(), data, null); - - // Assert - result - .Should() - .BeEquivalentTo( - JsonSerializer.Deserialize>( - """ - [ - { - "severity": 4, - "instanceId": null, - "dataElementId": null, - "field": "ddd", - "code": "test", - "description": "test", - "source": "Custom", - "customTextKey": null - }, - { - "severity": 1, - "instanceId": null, - "dataElementId": null, - "field": "test", - "code": "test", - "description": "test", - "source": "Custom", - "customTextKey": null - } - ] - """ - ) - ); - } - - private class TestModel - { - [JsonPropertyName("test")] - public string Test { get; set; } - - public int IntegerWithout { get; set; } - - [JsonPropertyName("child")] - public TestModel Child { get; set; } - - [JsonPropertyName("children")] - public List TestList { get; set; } - } - - [Theory] - [InlineData("test", "test", "test with small case")] - [InlineData("Test", "test", "test with capital case gets rewritten")] - [InlineData("NotModelMatch", "NotModelMatch", "Error that does not mach model is kept as is")] - [InlineData( - "Child.TestList[2].child", - "child.children[2].child", - "TestList is renamed to children because of JsonPropertyName" - )] - [InlineData("test.children.child", "test.children.child", "valid JsonPropertyName based path is kept as is")] - public async Task ValidateErrorAndMappingWithCustomModel(string errorKey, string field, string errorMessage) - { - // Arrange - var data = new TestModel(); - - _instanceValidator - .Setup(iv => iv.ValidateData(It.IsAny(), It.IsAny())) - .Callback( - (object _, ModelStateDictionary modelState) => - { - modelState.AddModelError(errorKey, errorMessage); - modelState.AddModelError(errorKey, "*FIXED*" + errorMessage + " Fixed"); - } - ); - - // Act - var result = await _validator.ValidateFormData(new Instance(), new DataElement(), data, null); - - // Assert - result.Should().HaveCount(2); - var errorIssue = result.Should().ContainSingle(i => i.Severity == ValidationIssueSeverity.Error).Which; - errorIssue.Field.Should().Be(field); - errorIssue.Severity.Should().Be(ValidationIssueSeverity.Error); - errorIssue.Description.Should().Be(errorMessage); - - var fixedIssue = result.Should().ContainSingle(i => i.Severity == ValidationIssueSeverity.Fixed).Which; - fixedIssue.Field.Should().Be(field); - fixedIssue.Severity.Should().Be(ValidationIssueSeverity.Fixed); - fixedIssue.Description.Should().Be(errorMessage + " Fixed"); - } + + // Act + var result = await _validator.ValidateFormData(new Instance(), new DataElement(), data, null); + + // Assert + result.Should().HaveCount(2); + var errorIssue = result.Should().ContainSingle(i => i.Severity == ValidationIssueSeverity.Error).Which; + errorIssue.Field.Should().Be(field); + errorIssue.Severity.Should().Be(ValidationIssueSeverity.Error); + errorIssue.Description.Should().Be(errorMessage); + + var fixedIssue = result.Should().ContainSingle(i => i.Severity == ValidationIssueSeverity.Fixed).Which; + fixedIssue.Field.Should().Be(field); + fixedIssue.Severity.Should().Be(ValidationIssueSeverity.Fixed); + fixedIssue.Description.Should().Be(errorMessage + " Fixed"); } } diff --git a/test/Altinn.App.Core.Tests/Features/Validators/GenericValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/GenericValidatorTests.cs index eae71e8bb..8642adf93 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/GenericValidatorTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/GenericValidatorTests.cs @@ -3,7 +3,6 @@ using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Features.Validators; diff --git a/test/Altinn.App.Core.Tests/Features/Validators/NullInstantiationValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/NullInstantiationValidatorTests.cs index 3d66fb247..ea5fdf3a9 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/NullInstantiationValidatorTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/NullInstantiationValidatorTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Core.Features.Validation; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.PlatformServices.Tests.Features.Validators; diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceOldTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceOldTests.cs index 0a83b8722..dcf31c318 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceOldTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceOldTests.cs @@ -14,7 +14,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Features.Validators; diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs index 8d9899e4d..a9a67f3d3 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Features.Validators; diff --git a/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs index 3910eb663..36090d78c 100644 --- a/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs @@ -5,7 +5,6 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Helpers/JsonSerializerIgnorePrefixTests.cs b/test/Altinn.App.Core.Tests/Helpers/JsonSerializerIgnorePrefixTests.cs index 83ac5ee7f..4645df69c 100644 --- a/test/Altinn.App.Core.Tests/Helpers/JsonSerializerIgnorePrefixTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/JsonSerializerIgnorePrefixTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Core.Helpers; using Altinn.App.PlatformServices.Tests.Helpers; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Helpers/LinqExpressionHelpersTests.cs b/test/Altinn.App.Core.Tests/Helpers/LinqExpressionHelpersTests.cs index 4aa156cbb..7206b744e 100644 --- a/test/Altinn.App.Core.Tests/Helpers/LinqExpressionHelpersTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/LinqExpressionHelpersTests.cs @@ -1,7 +1,6 @@ using System.Text.Json.Serialization; using Altinn.App.Core.Helpers; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Helpers/MimeTypeMapTests.cs b/test/Altinn.App.Core.Tests/Helpers/MimeTypeMapTests.cs index f908e8334..7cc89fbae 100644 --- a/test/Altinn.App.Core.Tests/Helpers/MimeTypeMapTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/MimeTypeMapTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Core.Helpers; using Altinn.App.Core.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs index 7dfc40fa0..54fcbe144 100644 --- a/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs @@ -4,7 +4,6 @@ using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; -using Xunit; using Xunit.Abstractions; namespace Altinn.App.Core.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs b/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs index 52310d3cd..70b711339 100644 --- a/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs @@ -1,6 +1,5 @@ using Altinn.App.Core.Helpers; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs b/test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs index 696540bf5..55a682ac4 100644 --- a/test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/ObjectUtils_XmlSerializationTests.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; using Moq; -using Xunit; using Xunit.Abstractions; // ReSharper disable InconsistentNaming diff --git a/test/Altinn.App.Core.Tests/Helpers/PathHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/PathHelperTests.cs index e01359618..ab003d752 100644 --- a/test/Altinn.App.Core.Tests/Helpers/PathHelperTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/PathHelperTests.cs @@ -1,42 +1,40 @@ #nullable disable using Altinn.App.Core.Helpers; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.Helpers +namespace Altinn.App.PlatformServices.Tests.Helpers; + +public class PathHelperTests { - public class PathHelperTests + [Theory] + [InlineData("/this/is/legal/path", "/this/is/legal/path/test")] + [InlineData("/this/is/legal/path", "/this/is/legal/path/test/test2/../../test2")] + public void AbsoluteLegalPathsReturnsTrueForValidPaths(string legalPath, string filePath) { - [Theory] - [InlineData("/this/is/legal/path", "/this/is/legal/path/test")] - [InlineData("/this/is/legal/path", "/this/is/legal/path/test/test2/../../test2")] - public void AbsoluteLegalPathsReturnsTrueForValidPaths(string legalPath, string filePath) - { - Assert.True(PathHelper.ValidateLegalFilePath(legalPath, filePath)); - } + Assert.True(PathHelper.ValidateLegalFilePath(legalPath, filePath)); + } - [Theory] - [InlineData("/this/is/legal/path", "/this/is/not/legal/path/test")] - [InlineData("/this/is/legal/path", "/this/is/legal/path/test/../../../../test2")] - public void AbsoluteLegalPathsReturnsFalseForInvalidPaths(string legalPath, string filePath) - { - Assert.False(PathHelper.ValidateLegalFilePath(legalPath, filePath)); - } + [Theory] + [InlineData("/this/is/legal/path", "/this/is/not/legal/path/test")] + [InlineData("/this/is/legal/path", "/this/is/legal/path/test/../../../../test2")] + public void AbsoluteLegalPathsReturnsFalseForInvalidPaths(string legalPath, string filePath) + { + Assert.False(PathHelper.ValidateLegalFilePath(legalPath, filePath)); + } - [Theory] - [InlineData("this/is/legal/path", "this/is/legal/path/test")] - [InlineData("this/is/legal/path", "this/is/legal/path/test/test2/../../test2")] - public void RelativeLegalPathsReturnsTrueForValidPaths(string legalPath, string filePath) - { - Assert.True(PathHelper.ValidateLegalFilePath(legalPath, filePath)); - } + [Theory] + [InlineData("this/is/legal/path", "this/is/legal/path/test")] + [InlineData("this/is/legal/path", "this/is/legal/path/test/test2/../../test2")] + public void RelativeLegalPathsReturnsTrueForValidPaths(string legalPath, string filePath) + { + Assert.True(PathHelper.ValidateLegalFilePath(legalPath, filePath)); + } - [Theory] - [InlineData("this/is/legal/path", "this/is/not/legal/path/test")] - [InlineData("this/is/legal/path", "this/is/legal/path/test/../../../../test2")] - [InlineData("this\\is\\legal\\path", "this\\is\\not\\legal\\path\\test")] - public void RelativeLegalPathsReturnsFalseForInvalidPaths(string legalPath, string filePath) - { - Assert.False(PathHelper.ValidateLegalFilePath(legalPath, filePath)); - } + [Theory] + [InlineData("this/is/legal/path", "this/is/not/legal/path/test")] + [InlineData("this/is/legal/path", "this/is/legal/path/test/../../../../test2")] + [InlineData("this\\is\\legal\\path", "this\\is\\not\\legal\\path\\test")] + public void RelativeLegalPathsReturnsFalseForInvalidPaths(string legalPath, string filePath) + { + Assert.False(PathHelper.ValidateLegalFilePath(legalPath, filePath)); } } diff --git a/test/Altinn.App.Core.Tests/Helpers/ProcessHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/ProcessHelperTests.cs index 84bedbae8..93a200d9b 100644 --- a/test/Altinn.App.Core.Tests/Helpers/ProcessHelperTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/ProcessHelperTests.cs @@ -1,11 +1,7 @@ -using System.Collections.Generic; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.PlatformServices.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Helpers/RemoveBomExtentionsTests.cs b/test/Altinn.App.Core.Tests/Helpers/RemoveBomExtentionsTests.cs index 1a6668f12..19f1c2183 100644 --- a/test/Altinn.App.Core.Tests/Helpers/RemoveBomExtentionsTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/RemoveBomExtentionsTests.cs @@ -1,7 +1,6 @@ #nullable disable using Altinn.App.Core.Helpers; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Helpers/SelfLinkHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/SelfLinkHelperTests.cs index 01dd4a6e5..7076f9d1d 100644 --- a/test/Altinn.App.Core.Tests/Helpers/SelfLinkHelperTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/SelfLinkHelperTests.cs @@ -2,33 +2,31 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; using Moq; -using Xunit; -namespace Altinn.App.Core.Tests.Helpers +namespace Altinn.App.Core.Tests.Helpers; + +public class SelfLinkHelperTests { - public class SelfLinkHelperTests + [Theory] + [InlineData("ttd.apps.altinn.no", "copy-me", "1337")] + [InlineData("skd.apps.altinn.no", "copy-this", "7474")] + public void BuildFrontendSelfLink(string host, string appId, string partyId) { - [Theory] - [InlineData("ttd.apps.altinn.no", "copy-me", "1337")] - [InlineData("skd.apps.altinn.no", "copy-this", "7474")] - public void BuildFrontendSelfLink(string host, string appId, string partyId) - { - // Arrange - Guid instanceGuid = Guid.NewGuid(); - Instance instance = new() { Id = $"{partyId}/{instanceGuid}", AppId = appId }; + // Arrange + Guid instanceGuid = Guid.NewGuid(); + Instance instance = new() { Id = $"{partyId}/{instanceGuid}", AppId = appId }; - Mock requestMock = new(); - requestMock.Setup(r => r.Host).Returns(new HostString(host)); + Mock requestMock = new(); + requestMock.Setup(r => r.Host).Returns(new HostString(host)); - // Act - string url = SelfLinkHelper.BuildFrontendSelfLink(instance, requestMock.Object); + // Act + string url = SelfLinkHelper.BuildFrontendSelfLink(instance, requestMock.Object); - // Assert - Assert.Contains(host, url); - Assert.Contains("/#/instance/", url); - Assert.Contains(partyId, url); - Assert.Contains(instanceGuid.ToString(), url); - Assert.Contains(appId, url); - } + // Assert + Assert.Contains(host, url); + Assert.Contains("/#/instance/", url); + Assert.Contains(partyId, url); + Assert.Contains(instanceGuid.ToString(), url); + Assert.Contains(appId, url); } } diff --git a/test/Altinn.App.Core.Tests/Helpers/ShadowFieldsConverterTests.cs b/test/Altinn.App.Core.Tests/Helpers/ShadowFieldsConverterTests.cs index 420e41584..27e725631 100644 --- a/test/Altinn.App.Core.Tests/Helpers/ShadowFieldsConverterTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/ShadowFieldsConverterTests.cs @@ -3,7 +3,6 @@ using System.Text.Json.Serialization.Metadata; using Altinn.App.Core.Helpers; using Altinn.App.Core.Tests.Implementation.TestData.AppDataModel; -using Xunit; namespace Altinn.App.PlatformServices.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Helpers/Utf8JsonReaderExtensionsTests.cs b/test/Altinn.App.Core.Tests/Helpers/Utf8JsonReaderExtensionsTests.cs index ebac925b0..dc525b333 100644 --- a/test/Altinn.App.Core.Tests/Helpers/Utf8JsonReaderExtensionsTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/Utf8JsonReaderExtensionsTests.cs @@ -4,7 +4,6 @@ using System.Text.RegularExpressions; using Altinn.App.Core.Helpers.Extensions; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Helpers; diff --git a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs index 0d650e99c..72c0c9575 100644 --- a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs @@ -10,7 +10,6 @@ using Microsoft.Extensions.Options; using Microsoft.FeatureManagement; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Implementation; diff --git a/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs b/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs index 80195a8bf..5093ad4db 100644 --- a/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs +++ b/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs @@ -3,87 +3,85 @@ using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Moq; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.Implementation +namespace Altinn.App.PlatformServices.Tests.Implementation; + +public class DefaultPageOrderTest { - public class DefaultPageOrderTest - { - private readonly Mock appResourcesMock; + private readonly Mock appResourcesMock; - public DefaultPageOrderTest() - { - appResourcesMock = new Mock(); - } + public DefaultPageOrderTest() + { + appResourcesMock = new Mock(); + } - [Fact] - public async Task GetPageOrder_Returns_LayoutSettingsForSet_when_layoutSetId_is_defined() - { - // Arrange - AppIdentifier appIdentifier = new AppIdentifier("ttd", "best-app"); - Guid guid = Guid.NewGuid(); - InstanceIdentifier instanceIdentifier = InstanceIdentifier.NoInstance; - string layoutSetId = "layoutSetId"; - string currentPage = "page1"; - string dataTypeId = "dataTypeId"; - object formData = new object(); + [Fact] + public async Task GetPageOrder_Returns_LayoutSettingsForSet_when_layoutSetId_is_defined() + { + // Arrange + AppIdentifier appIdentifier = new AppIdentifier("ttd", "best-app"); + Guid guid = Guid.NewGuid(); + InstanceIdentifier instanceIdentifier = InstanceIdentifier.NoInstance; + string layoutSetId = "layoutSetId"; + string currentPage = "page1"; + string dataTypeId = "dataTypeId"; + object formData = new object(); - List expected = new List { "page2", "page3" }; - appResourcesMock - .Setup(ar => ar.GetLayoutSettingsForSet(layoutSetId)) - .Returns(new LayoutSettings { Pages = new Pages { Order = expected } }); + List expected = new List { "page2", "page3" }; + appResourcesMock + .Setup(ar => ar.GetLayoutSettingsForSet(layoutSetId)) + .Returns(new LayoutSettings { Pages = new Pages { Order = expected } }); - // Act - DefaultPageOrder target = new DefaultPageOrder(appResourcesMock.Object); + // Act + DefaultPageOrder target = new DefaultPageOrder(appResourcesMock.Object); - List actual = await target.GetPageOrder( - appIdentifier, - instanceIdentifier, - layoutSetId, - currentPage, - dataTypeId, - formData - ); + List actual = await target.GetPageOrder( + appIdentifier, + instanceIdentifier, + layoutSetId, + currentPage, + dataTypeId, + formData + ); - // Assert - Assert.Equal(expected, actual); - appResourcesMock.Verify(ar => ar.GetLayoutSettingsForSet(layoutSetId), Times.Once); - appResourcesMock.VerifyNoOtherCalls(); - } + // Assert + Assert.Equal(expected, actual); + appResourcesMock.Verify(ar => ar.GetLayoutSettingsForSet(layoutSetId), Times.Once); + appResourcesMock.VerifyNoOtherCalls(); + } - [Fact] - public async Task GetPageOrder_Returns_LayoutSettings_layoutSetId_is_null() - { - // Arrange - AppIdentifier appIdentifier = new AppIdentifier("ttd", "best-app"); - Guid guid = Guid.NewGuid(); - InstanceIdentifier instanceIdentifier = InstanceIdentifier.NoInstance; - string layoutSetId = null; - string currentPage = "page1"; - string dataTypeId = "dataTypeId"; - object formData = new object(); + [Fact] + public async Task GetPageOrder_Returns_LayoutSettings_layoutSetId_is_null() + { + // Arrange + AppIdentifier appIdentifier = new AppIdentifier("ttd", "best-app"); + Guid guid = Guid.NewGuid(); + InstanceIdentifier instanceIdentifier = InstanceIdentifier.NoInstance; + string layoutSetId = null; + string currentPage = "page1"; + string dataTypeId = "dataTypeId"; + object formData = new object(); - List expected = new List { "page2", "page3" }; - appResourcesMock - .Setup(ar => ar.GetLayoutSettings()) - .Returns(new LayoutSettings { Pages = new Pages { Order = expected } }); + List expected = new List { "page2", "page3" }; + appResourcesMock + .Setup(ar => ar.GetLayoutSettings()) + .Returns(new LayoutSettings { Pages = new Pages { Order = expected } }); - // Act - DefaultPageOrder target = new DefaultPageOrder(appResourcesMock.Object); + // Act + DefaultPageOrder target = new DefaultPageOrder(appResourcesMock.Object); - List actual = await target.GetPageOrder( - appIdentifier, - instanceIdentifier, - layoutSetId, - currentPage, - dataTypeId, - formData - ); + List actual = await target.GetPageOrder( + appIdentifier, + instanceIdentifier, + layoutSetId, + currentPage, + dataTypeId, + formData + ); - // Assert - Assert.Equal(expected, actual); - appResourcesMock.Verify(ar => ar.GetLayoutSettings(), Times.Once); - appResourcesMock.VerifyNoOtherCalls(); - } + // Assert + Assert.Equal(expected, actual); + appResourcesMock.Verify(ar => ar.GetLayoutSettings(), Times.Once); + appResourcesMock.VerifyNoOtherCalls(); } } diff --git a/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs b/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs index 84e49f365..75a6234e2 100644 --- a/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs +++ b/test/Altinn.App.Core.Tests/Implementation/EventsClientTest.cs @@ -14,221 +14,220 @@ using Moq; using Moq.Protected; -namespace Altinn.App.Core.Tests.Implementation +namespace Altinn.App.Core.Tests.Implementation; + +public class EventsClientTest { - public class EventsClientTest + private readonly IOptions platformSettingsOptions; + private readonly Mock> appSettingsOptions; + private readonly IOptions generalSettingsOptions; + private readonly Mock handlerMock; + private readonly Mock contextAccessor; + private readonly Mock accessTokenGeneratorMock; + private readonly Mock _appMetadataMock; + + public EventsClientTest() { - private readonly IOptions platformSettingsOptions; - private readonly Mock> appSettingsOptions; - private readonly IOptions generalSettingsOptions; - private readonly Mock handlerMock; - private readonly Mock contextAccessor; - private readonly Mock accessTokenGeneratorMock; - private readonly Mock _appMetadataMock; - - public EventsClientTest() - { - platformSettingsOptions = Microsoft.Extensions.Options.Options.Create(new()); - appSettingsOptions = new Mock>(); - generalSettingsOptions = Microsoft.Extensions.Options.Options.Create( - new() { ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}/" } - ); - handlerMock = new Mock(MockBehavior.Strict); - contextAccessor = new Mock(); - accessTokenGeneratorMock = new Mock(); - _appMetadataMock = new Mock(); - } - - [Fact] - public async Task AddEvent_RegisterEventWithInstanceOwnerOrganisation_CloudEventInRequestContainOrganisationNumber() - { - TelemetrySink telemetrySink = new(); - // Arrange - Instance instance = - new() - { - AppId = "ttd/best-app", - Org = "ttd", - InstanceOwner = new InstanceOwner { OrganisationNumber = "org", PartyId = 123.ToString() } - }; - - HttpResponseMessage httpResponseMessage = - new() { StatusCode = HttpStatusCode.OK, Content = new StringContent(Guid.NewGuid().ToString()) }; - - HttpRequestMessage actualRequest = null; - void SetRequest(HttpRequestMessage request) => actualRequest = request; - InitializeMocks(httpResponseMessage, SetRequest); - - HttpClient httpClient = new(handlerMock.Object); - - EventsClient target = - new( - platformSettingsOptions, - contextAccessor.Object, - httpClient, - accessTokenGeneratorMock.Object, - _appMetadataMock.Object, - appSettingsOptions.Object, - generalSettingsOptions, - telemetrySink.Object - ); - - // Act - await target.AddEvent("created", instance); - - // Assert - Assert.NotNull(actualRequest); - Assert.Equal(HttpMethod.Post, actualRequest.Method); - Assert.EndsWith("app", actualRequest.RequestUri.OriginalString); - - string requestContent = await actualRequest.Content.ReadAsStringAsync(); - CloudEvent actualEvent = JsonSerializer.Deserialize(requestContent); - - Assert.NotNull(actualEvent); - Assert.Equal("/party/123", actualEvent.Subject); - Assert.Equal("/org/org", actualEvent.AlternativeSubject); - Assert.Contains("ttd.apps.at22.altinn.cloud/ttd/best-app/instances", actualEvent.Source.OriginalString); - - handlerMock.VerifyAll(); - - await Verify(telemetrySink.GetSnapshot()); - } + platformSettingsOptions = Microsoft.Extensions.Options.Options.Create(new()); + appSettingsOptions = new Mock>(); + generalSettingsOptions = Microsoft.Extensions.Options.Options.Create( + new() { ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}/" } + ); + handlerMock = new Mock(MockBehavior.Strict); + contextAccessor = new Mock(); + accessTokenGeneratorMock = new Mock(); + _appMetadataMock = new Mock(); + } - [Fact] - public async Task AddEvent_RegisterEventWithInstanceOwnerPerson_CloudEventInRequestContainPersonNumber() - { - // Arrange - Instance instance = new Instance + [Fact] + public async Task AddEvent_RegisterEventWithInstanceOwnerOrganisation_CloudEventInRequestContainOrganisationNumber() + { + TelemetrySink telemetrySink = new(); + // Arrange + Instance instance = + new() { AppId = "ttd/best-app", Org = "ttd", - InstanceOwner = new InstanceOwner { PersonNumber = "43234123", PartyId = 321.ToString() } + InstanceOwner = new InstanceOwner { OrganisationNumber = "org", PartyId = 123.ToString() } }; - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(Guid.NewGuid().ToString()) - }; + HttpResponseMessage httpResponseMessage = + new() { StatusCode = HttpStatusCode.OK, Content = new StringContent(Guid.NewGuid().ToString()) }; - HttpRequestMessage actualRequest = null; - void SetRequest(HttpRequestMessage request) => actualRequest = request; - InitializeMocks(httpResponseMessage, SetRequest); + HttpRequestMessage actualRequest = null; + void SetRequest(HttpRequestMessage request) => actualRequest = request; + InitializeMocks(httpResponseMessage, SetRequest); - HttpClient httpClient = new HttpClient(handlerMock.Object); + HttpClient httpClient = new(handlerMock.Object); - EventsClient target = new EventsClient( + EventsClient target = + new( platformSettingsOptions, contextAccessor.Object, httpClient, accessTokenGeneratorMock.Object, _appMetadataMock.Object, appSettingsOptions.Object, - generalSettingsOptions + generalSettingsOptions, + telemetrySink.Object ); - // Act - await target.AddEvent("created", instance); - - // Assert - Assert.NotNull(actualRequest); - Assert.Equal(HttpMethod.Post, actualRequest.Method); - Assert.EndsWith("app", actualRequest.RequestUri.OriginalString); - - string requestContent = await actualRequest.Content.ReadAsStringAsync(); - CloudEvent actualEvent = JsonSerializer.Deserialize(requestContent); + // Act + await target.AddEvent("created", instance); - Assert.NotNull(actualEvent); - Assert.Equal("/party/321", actualEvent.Subject); - Assert.Equal("/person/43234123", actualEvent.AlternativeSubject); - Assert.Contains("ttd.apps.at22.altinn.cloud/ttd/best-app/instances", actualEvent.Source.OriginalString); + // Assert + Assert.NotNull(actualRequest); + Assert.Equal(HttpMethod.Post, actualRequest.Method); + Assert.EndsWith("app", actualRequest.RequestUri.OriginalString); - handlerMock.VerifyAll(); - } + string requestContent = await actualRequest.Content.ReadAsStringAsync(); + CloudEvent actualEvent = JsonSerializer.Deserialize(requestContent); - [Fact] - public async Task AddEvent_TheServiceResponseIndicateFailure_ThrowsPlatformHttpException() - { - // Arrange - Instance instance = new Instance - { - Org = "ttd", - AppId = "tdd/test", - InstanceOwner = new InstanceOwner { OrganisationNumber = "org" } - }; + Assert.NotNull(actualEvent); + Assert.Equal("/party/123", actualEvent.Subject); + Assert.Equal("/org/org", actualEvent.AlternativeSubject); + Assert.Contains("ttd.apps.at22.altinn.cloud/ttd/best-app/instances", actualEvent.Source.OriginalString); - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.BadRequest, - Content = new StringContent(Guid.NewGuid().ToString()), - }; + handlerMock.VerifyAll(); - HttpRequestMessage actualRequest = null; - void SetRequest(HttpRequestMessage request) => actualRequest = request; - InitializeMocks(httpResponseMessage, SetRequest); + await Verify(telemetrySink.GetSnapshot()); + } - HttpClient httpClient = new HttpClient(handlerMock.Object); + [Fact] + public async Task AddEvent_RegisterEventWithInstanceOwnerPerson_CloudEventInRequestContainPersonNumber() + { + // Arrange + Instance instance = new Instance + { + AppId = "ttd/best-app", + Org = "ttd", + InstanceOwner = new InstanceOwner { PersonNumber = "43234123", PartyId = 321.ToString() } + }; - EventsClient target = new EventsClient( - platformSettingsOptions, - contextAccessor.Object, - httpClient, - accessTokenGeneratorMock.Object, - _appMetadataMock.Object, - appSettingsOptions.Object, - generalSettingsOptions - ); + HttpResponseMessage httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Guid.NewGuid().ToString()) + }; + + HttpRequestMessage actualRequest = null; + void SetRequest(HttpRequestMessage request) => actualRequest = request; + InitializeMocks(httpResponseMessage, SetRequest); + + HttpClient httpClient = new HttpClient(handlerMock.Object); + + EventsClient target = new EventsClient( + platformSettingsOptions, + contextAccessor.Object, + httpClient, + accessTokenGeneratorMock.Object, + _appMetadataMock.Object, + appSettingsOptions.Object, + generalSettingsOptions + ); + + // Act + await target.AddEvent("created", instance); + + // Assert + Assert.NotNull(actualRequest); + Assert.Equal(HttpMethod.Post, actualRequest.Method); + Assert.EndsWith("app", actualRequest.RequestUri.OriginalString); + + string requestContent = await actualRequest.Content.ReadAsStringAsync(); + CloudEvent actualEvent = JsonSerializer.Deserialize(requestContent); + + Assert.NotNull(actualEvent); + Assert.Equal("/party/321", actualEvent.Subject); + Assert.Equal("/person/43234123", actualEvent.AlternativeSubject); + Assert.Contains("ttd.apps.at22.altinn.cloud/ttd/best-app/instances", actualEvent.Source.OriginalString); + + handlerMock.VerifyAll(); + } - PlatformHttpException actual = null; + [Fact] + public async Task AddEvent_TheServiceResponseIndicateFailure_ThrowsPlatformHttpException() + { + // Arrange + Instance instance = new Instance + { + Org = "ttd", + AppId = "tdd/test", + InstanceOwner = new InstanceOwner { OrganisationNumber = "org" } + }; - // Act - try - { - await target.AddEvent("created", instance); - } - catch (PlatformHttpException platformHttpException) - { - actual = platformHttpException; - } + HttpResponseMessage httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.BadRequest, + Content = new StringContent(Guid.NewGuid().ToString()), + }; + + HttpRequestMessage actualRequest = null; + void SetRequest(HttpRequestMessage request) => actualRequest = request; + InitializeMocks(httpResponseMessage, SetRequest); + + HttpClient httpClient = new HttpClient(handlerMock.Object); + + EventsClient target = new EventsClient( + platformSettingsOptions, + contextAccessor.Object, + httpClient, + accessTokenGeneratorMock.Object, + _appMetadataMock.Object, + appSettingsOptions.Object, + generalSettingsOptions + ); + + PlatformHttpException actual = null; + + // Act + try + { + await target.AddEvent("created", instance); + } + catch (PlatformHttpException platformHttpException) + { + actual = platformHttpException; + } - // Assert - Assert.NotNull(actual); - Assert.Equal(HttpStatusCode.BadRequest, actual.Response.StatusCode); + // Assert + Assert.NotNull(actual); + Assert.Equal(HttpStatusCode.BadRequest, actual.Response.StatusCode); - Assert.NotNull(actualRequest); + Assert.NotNull(actualRequest); - handlerMock.VerifyAll(); - } + handlerMock.VerifyAll(); + } - private void InitializeMocks(HttpResponseMessage httpResponseMessage, Action callback) - { - platformSettingsOptions.Value.ApiEventsEndpoint = "http://localhost:5101/events/api/v1/"; - platformSettingsOptions.Value.SubscriptionKey = "key"; + private void InitializeMocks(HttpResponseMessage httpResponseMessage, Action callback) + { + platformSettingsOptions.Value.ApiEventsEndpoint = "http://localhost:5101/events/api/v1/"; + platformSettingsOptions.Value.SubscriptionKey = "key"; - generalSettingsOptions.Value.HostName = "at22.altinn.cloud"; + generalSettingsOptions.Value.HostName = "at22.altinn.cloud"; - AppSettings appSettings = new AppSettings { RuntimeCookieName = "AltinnStudioRuntime" }; - appSettingsOptions.Setup(s => s.CurrentValue).Returns(appSettings); + AppSettings appSettings = new AppSettings { RuntimeCookieName = "AltinnStudioRuntime" }; + appSettingsOptions.Setup(s => s.CurrentValue).Returns(appSettings); - contextAccessor.Setup(s => s.HttpContext).Returns(new DefaultHttpContext()); + contextAccessor.Setup(s => s.HttpContext).Returns(new DefaultHttpContext()); - accessTokenGeneratorMock - .Setup(at => at.GenerateAccessToken(It.IsAny(), It.IsAny())) - .Returns("dummy access token"); + accessTokenGeneratorMock + .Setup(at => at.GenerateAccessToken(It.IsAny(), It.IsAny())) + .Returns("dummy access token"); - ApplicationMetadata app = new ApplicationMetadata("ttd/best-app") { Id = "ttd/best-app", Org = "ttd" }; - _appMetadataMock.Setup(ar => ar.GetApplicationMetadata()).ReturnsAsync(app); + ApplicationMetadata app = new ApplicationMetadata("ttd/best-app") { Id = "ttd/best-app", Org = "ttd" }; + _appMetadataMock.Setup(ar => ar.GetApplicationMetadata()).ReturnsAsync(app); - handlerMock - .Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny() - ) - .Callback((request, _) => callback(request)) - .ReturnsAsync(httpResponseMessage) - .Verifiable(); - } + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .Callback((request, _) => callback(request)) + .ReturnsAsync(httpResponseMessage) + .Verifiable(); } } diff --git a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs index 2a5c332d5..1c4a9ac1a 100644 --- a/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/InstanceClientTests.cs @@ -15,552 +15,543 @@ using Moq.Protected; using Newtonsoft.Json; -namespace Altinn.App.PlatformServices.Tests.Implementation +namespace Altinn.App.PlatformServices.Tests.Implementation; + +public class InstanceClientTests : IDisposable { - public class InstanceClientTests : IDisposable + private readonly Mock> platformSettingsOptions; + private readonly Mock> appSettingsOptions; + private readonly Mock handlerMock; + private readonly Mock contextAccessor; + private readonly Mock> logger; + private readonly TelemetrySink telemetry; + + public InstanceClientTests() { - private readonly Mock> platformSettingsOptions; - private readonly Mock> appSettingsOptions; - private readonly Mock handlerMock; - private readonly Mock contextAccessor; - private readonly Mock> logger; - private readonly TelemetrySink telemetry; - - public InstanceClientTests() - { - platformSettingsOptions = new Mock>(); - appSettingsOptions = new Mock>(); - handlerMock = new Mock(MockBehavior.Strict); - contextAccessor = new Mock(); - logger = new Mock>(); - telemetry = new TelemetrySink(); - } + platformSettingsOptions = new Mock>(); + appSettingsOptions = new Mock>(); + handlerMock = new Mock(MockBehavior.Strict); + contextAccessor = new Mock(); + logger = new Mock>(); + telemetry = new TelemetrySink(); + } - [Fact] - public async Task AddCompleteConfirmation_SuccessfulCallToStorage() + [Fact] + public async Task AddCompleteConfirmation_SuccessfulCallToStorage() + { + // Arrange + Instance instance = new Instance { - // Arrange - Instance instance = new Instance + CompleteConfirmations = new List { - CompleteConfirmations = new List - { - new CompleteConfirmation { StakeholderId = "test" } - } - }; + new CompleteConfirmation { StakeholderId = "test" } + } + }; - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(instance), Encoding.UTF8, "application/json"), - }; + HttpResponseMessage httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(instance), Encoding.UTF8, "application/json"), + }; - InitializeMocks([httpResponseMessage], ["complete"]); + InitializeMocks([httpResponseMessage], ["complete"]); - HttpClient httpClient = new HttpClient(handlerMock.Object); + HttpClient httpClient = new HttpClient(handlerMock.Object); - InstanceClient target = new InstanceClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - appSettingsOptions.Object, - telemetry.Object - ); + InstanceClient target = new InstanceClient( + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + appSettingsOptions.Object, + telemetry.Object + ); - // Act - await target.AddCompleteConfirmation(1337, Guid.NewGuid()); + // Act + await target.AddCompleteConfirmation(1337, Guid.NewGuid()); - // Assert - handlerMock.VerifyAll(); + // Assert + handlerMock.VerifyAll(); - await Verify(telemetry.GetSnapshot()); - } + await Verify(telemetry.GetSnapshot()); + } - [Fact] - public async Task AddCompleteConfirmation_StorageReturnsNonSuccess_ThrowsPlatformHttpException() + [Fact] + public async Task AddCompleteConfirmation_StorageReturnsNonSuccess_ThrowsPlatformHttpException() + { + // Arrange + HttpResponseMessage httpResponseMessage = new HttpResponseMessage { - // Arrange - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.Forbidden, - Content = new StringContent("Error message", Encoding.UTF8, "application/json"), - }; - - InitializeMocks([httpResponseMessage], ["complete"]); + StatusCode = HttpStatusCode.Forbidden, + Content = new StringContent("Error message", Encoding.UTF8, "application/json"), + }; - HttpClient httpClient = new HttpClient(handlerMock.Object); + InitializeMocks([httpResponseMessage], ["complete"]); - InstanceClient target = new InstanceClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - appSettingsOptions.Object, - telemetry.Object - ); + HttpClient httpClient = new HttpClient(handlerMock.Object); - PlatformHttpException actualException = null; + InstanceClient target = new InstanceClient( + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + appSettingsOptions.Object, + telemetry.Object + ); - // Act - try - { - await target.AddCompleteConfirmation(1337, Guid.NewGuid()); - } - catch (PlatformHttpException e) - { - actualException = e; - } - - // Assert - handlerMock.VerifyAll(); + PlatformHttpException actualException = null; - Assert.NotNull(actualException); + // Act + try + { + await target.AddCompleteConfirmation(1337, Guid.NewGuid()); } - - [Fact] - public async Task UpdateReadStatus_StorageReturnsNonSuccess_LogsErrorAppContinues() + catch (PlatformHttpException e) { - // Arrange - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.Forbidden, - Content = new StringContent("Error message", Encoding.UTF8, "application/json"), - }; + actualException = e; + } - InitializeMocks([httpResponseMessage], ["read"]); + // Assert + handlerMock.VerifyAll(); - HttpClient httpClient = new HttpClient(handlerMock.Object); + Assert.NotNull(actualException); + } - InstanceClient target = new InstanceClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - appSettingsOptions.Object, - telemetry.Object - ); + [Fact] + public async Task UpdateReadStatus_StorageReturnsNonSuccess_LogsErrorAppContinues() + { + // Arrange + HttpResponseMessage httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.Forbidden, + Content = new StringContent("Error message", Encoding.UTF8, "application/json"), + }; - PlatformHttpException actualException = null; + InitializeMocks([httpResponseMessage], ["read"]); - // Act - try - { - await target.UpdateReadStatus(1337, Guid.NewGuid(), "read"); - } - catch (PlatformHttpException e) - { - actualException = e; - } + HttpClient httpClient = new HttpClient(handlerMock.Object); - // Assert - handlerMock.VerifyAll(); + InstanceClient target = new InstanceClient( + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + appSettingsOptions.Object, + telemetry.Object + ); - Assert.Null(actualException); - } + PlatformHttpException actualException = null; - [Fact] - public async Task UpdateReadStatus_StorageReturnsSuccess() + // Act + try { - // Arrange - Instance expected = new Instance { Status = new InstanceStatus { ReadStatus = ReadStatus.Read } }; + await target.UpdateReadStatus(1337, Guid.NewGuid(), "read"); + } + catch (PlatformHttpException e) + { + actualException = e; + } - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), - }; + // Assert + handlerMock.VerifyAll(); - InitializeMocks([httpResponseMessage], ["read"]); + Assert.Null(actualException); + } - HttpClient httpClient = new HttpClient(handlerMock.Object); + [Fact] + public async Task UpdateReadStatus_StorageReturnsSuccess() + { + // Arrange + Instance expected = new Instance { Status = new InstanceStatus { ReadStatus = ReadStatus.Read } }; - InstanceClient target = new InstanceClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - appSettingsOptions.Object, - telemetry.Object - ); + HttpResponseMessage httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), + }; - // Act - Instance actual = await target.UpdateReadStatus(1337, Guid.NewGuid(), "read"); + InitializeMocks([httpResponseMessage], ["read"]); - // Assert - Assert.Equal(expected.Status.ReadStatus, actual.Status.ReadStatus); - handlerMock.VerifyAll(); - } + HttpClient httpClient = new HttpClient(handlerMock.Object); - [Fact] - public async Task UpdateSubtatus_StorageReturnsSuccess() - { - // Arrange - Instance expected = new Instance - { - Status = new InstanceStatus - { - Substatus = new Substatus { Label = "Substatus.Label", Description = "Substatus.Description" } - } - }; + InstanceClient target = new InstanceClient( + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + appSettingsOptions.Object, + telemetry.Object + ); - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), - }; + // Act + Instance actual = await target.UpdateReadStatus(1337, Guid.NewGuid(), "read"); - InitializeMocks([httpResponseMessage], ["substatus"]); - - HttpClient httpClient = new HttpClient(handlerMock.Object); - - InstanceClient target = new InstanceClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - appSettingsOptions.Object, - telemetry.Object - ); - - // Act - Instance actual = await target.UpdateSubstatus( - 1337, - Guid.NewGuid(), - new Substatus { Label = "Substatus.Label", Description = "Substatus.Description" } - ); - - // Assert - Assert.Equal(expected.Status.Substatus.Label, actual.Status.Substatus.Label); - Assert.Equal(expected.Status.Substatus.Description, actual.Status.Substatus.Description); - handlerMock.VerifyAll(); - } + // Assert + Assert.Equal(expected.Status.ReadStatus, actual.Status.ReadStatus); + handlerMock.VerifyAll(); + } - [Fact] - public async Task UpdateSubtatus_StorageReturnsNonSuccess_ThrowsPlatformHttpException() + [Fact] + public async Task UpdateSubtatus_StorageReturnsSuccess() + { + // Arrange + Instance expected = new Instance { - // Arrange - HttpResponseMessage httpResponseMessage = new HttpResponseMessage + Status = new InstanceStatus { - StatusCode = HttpStatusCode.Forbidden, - Content = new StringContent("Error message", Encoding.UTF8, "application/json"), - }; + Substatus = new Substatus { Label = "Substatus.Label", Description = "Substatus.Description" } + } + }; - InitializeMocks([httpResponseMessage], ["substatus"]); + HttpResponseMessage httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), + }; + + InitializeMocks([httpResponseMessage], ["substatus"]); + + HttpClient httpClient = new HttpClient(handlerMock.Object); + + InstanceClient target = new InstanceClient( + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + appSettingsOptions.Object, + telemetry.Object + ); + + // Act + Instance actual = await target.UpdateSubstatus( + 1337, + Guid.NewGuid(), + new Substatus { Label = "Substatus.Label", Description = "Substatus.Description" } + ); + + // Assert + Assert.Equal(expected.Status.Substatus.Label, actual.Status.Substatus.Label); + Assert.Equal(expected.Status.Substatus.Description, actual.Status.Substatus.Description); + handlerMock.VerifyAll(); + } - HttpClient httpClient = new HttpClient(handlerMock.Object); + [Fact] + public async Task UpdateSubtatus_StorageReturnsNonSuccess_ThrowsPlatformHttpException() + { + // Arrange + HttpResponseMessage httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.Forbidden, + Content = new StringContent("Error message", Encoding.UTF8, "application/json"), + }; - InstanceClient target = new InstanceClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - appSettingsOptions.Object, - telemetry.Object - ); + InitializeMocks([httpResponseMessage], ["substatus"]); - PlatformHttpException actualException = null; + HttpClient httpClient = new HttpClient(handlerMock.Object); - // Act - try - { - await target.UpdateSubstatus(1337, Guid.NewGuid(), new Substatus()); - } - catch (PlatformHttpException e) - { - actualException = e; - } + InstanceClient target = new InstanceClient( + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + appSettingsOptions.Object, + telemetry.Object + ); - // Assert - handlerMock.VerifyAll(); + PlatformHttpException actualException = null; - Assert.NotNull(actualException); + // Act + try + { + await target.UpdateSubstatus(1337, Guid.NewGuid(), new Substatus()); + } + catch (PlatformHttpException e) + { + actualException = e; } - [Fact] - public async Task DeleteInstance_StorageReturnsSuccess() + // Assert + handlerMock.VerifyAll(); + + Assert.NotNull(actualException); + } + + [Fact] + public async Task DeleteInstance_StorageReturnsSuccess() + { + // Arrange + Guid instanceGuid = Guid.NewGuid(); + string instanceOwnerId = "1337"; + + Instance expected = new Instance { - // Arrange - Guid instanceGuid = Guid.NewGuid(); - string instanceOwnerId = "1337"; + InstanceOwner = new InstanceOwner { PartyId = instanceOwnerId }, + Id = $"{instanceOwnerId}/{instanceGuid}" + }; - Instance expected = new Instance - { - InstanceOwner = new InstanceOwner { PartyId = instanceOwnerId }, - Id = $"{instanceOwnerId}/{instanceGuid}" - }; + HttpResponseMessage httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), + }; - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), - }; + InitializeMocks([httpResponseMessage], ["1337"]); - InitializeMocks([httpResponseMessage], ["1337"]); + HttpClient httpClient = new HttpClient(handlerMock.Object); - HttpClient httpClient = new HttpClient(handlerMock.Object); + InstanceClient target = new InstanceClient( + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + appSettingsOptions.Object, + telemetry.Object + ); - InstanceClient target = new InstanceClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - appSettingsOptions.Object, - telemetry.Object - ); + // Act + Instance actual = await target.DeleteInstance(1337, Guid.NewGuid(), false); - // Act - Instance actual = await target.DeleteInstance(1337, Guid.NewGuid(), false); + // Assert + Assert.Equal("1337", actual.InstanceOwner.PartyId); + handlerMock.VerifyAll(); + } - // Assert - Assert.Equal("1337", actual.InstanceOwner.PartyId); - handlerMock.VerifyAll(); - } + [Fact] + public async Task DeleteInstance_StorageReturnsNonSuccess_ThrowsPlatformHttpException() + { + // Arrange + Guid instanceGuid = Guid.NewGuid(); - [Fact] - public async Task DeleteInstance_StorageReturnsNonSuccess_ThrowsPlatformHttpException() + HttpResponseMessage httpResponseMessage = new HttpResponseMessage { - // Arrange - Guid instanceGuid = Guid.NewGuid(); + StatusCode = HttpStatusCode.Forbidden, + Content = new StringContent("Error message", Encoding.UTF8, "application/json"), + }; - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.Forbidden, - Content = new StringContent("Error message", Encoding.UTF8, "application/json"), - }; + InitializeMocks([httpResponseMessage], ["1337"]); - InitializeMocks([httpResponseMessage], ["1337"]); + HttpClient httpClient = new HttpClient(handlerMock.Object); - HttpClient httpClient = new HttpClient(handlerMock.Object); + InstanceClient target = new InstanceClient( + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + appSettingsOptions.Object, + telemetry.Object + ); - InstanceClient target = new InstanceClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - appSettingsOptions.Object, - telemetry.Object - ); + PlatformHttpException actualException = null; - PlatformHttpException actualException = null; + // Act + try + { + await target.DeleteInstance(1337, instanceGuid, false); + } + catch (PlatformHttpException e) + { + actualException = e; + } - // Act - try - { - await target.DeleteInstance(1337, instanceGuid, false); - } - catch (PlatformHttpException e) - { - actualException = e; - } + // Assert + handlerMock.VerifyAll(); - // Assert - handlerMock.VerifyAll(); + Assert.NotNull(actualException); + } - Assert.NotNull(actualException); - } + [Fact] + public async Task UpdatePresentationTexts_StorageReturnsNonSuccess_ThrowsPlatformHttpException() + { + // Arrange + Guid instanceGuid = Guid.NewGuid(); - [Fact] - public async Task UpdatePresentationTexts_StorageReturnsNonSuccess_ThrowsPlatformHttpException() + HttpResponseMessage httpResponseMessage = new HttpResponseMessage { - // Arrange - Guid instanceGuid = Guid.NewGuid(); + StatusCode = HttpStatusCode.Forbidden, + Content = new StringContent("Error message", Encoding.UTF8, "application/json"), + }; - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.Forbidden, - Content = new StringContent("Error message", Encoding.UTF8, "application/json"), - }; + InitializeMocks([httpResponseMessage], ["1337"]); - InitializeMocks([httpResponseMessage], ["1337"]); + HttpClient httpClient = new HttpClient(handlerMock.Object); - HttpClient httpClient = new HttpClient(handlerMock.Object); + InstanceClient target = new InstanceClient( + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + appSettingsOptions.Object, + telemetry.Object + ); - InstanceClient target = new InstanceClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - appSettingsOptions.Object, - telemetry.Object - ); + PlatformHttpException actualException = null; - PlatformHttpException actualException = null; + // Act + try + { + await target.UpdatePresentationTexts(1337, instanceGuid, new PresentationTexts()); + } + catch (PlatformHttpException e) + { + actualException = e; + } - // Act - try - { - await target.UpdatePresentationTexts(1337, instanceGuid, new PresentationTexts()); - } - catch (PlatformHttpException e) - { - actualException = e; - } + // Assert + handlerMock.VerifyAll(); - // Assert - handlerMock.VerifyAll(); + Assert.NotNull(actualException); + } - Assert.NotNull(actualException); - } + [Fact] + public async Task UpdatePresentationTexts_SuccessfulCallToStorage() + { + // Arrange + Guid instanceGuid = Guid.NewGuid(); + int instanceOwnerId = 1337; + + Instance expected = new Instance + { + InstanceOwner = new InstanceOwner { PartyId = instanceOwnerId.ToString() }, + Id = $"{instanceOwnerId}/{instanceGuid}" + }; - [Fact] - public async Task UpdatePresentationTexts_SuccessfulCallToStorage() + HttpResponseMessage httpResponseMessage = new HttpResponseMessage { - // Arrange - Guid instanceGuid = Guid.NewGuid(); - int instanceOwnerId = 1337; + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), + }; + + InitializeMocks([httpResponseMessage], ["presentationtexts"]); + + HttpClient httpClient = new HttpClient(handlerMock.Object); + + InstanceClient target = new InstanceClient( + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + appSettingsOptions.Object, + telemetry.Object + ); + + // Act + await target.UpdatePresentationTexts(instanceOwnerId, instanceGuid, new PresentationTexts()); - Instance expected = new Instance + // Assert + handlerMock.VerifyAll(); + } + + [Fact] + public async Task QueryInstances_QueryResponseContainsNext() + { + // Arrange + QueryResponse queryResponse1 = + new() { - InstanceOwner = new InstanceOwner { PartyId = instanceOwnerId.ToString() }, - Id = $"{instanceOwnerId}/{instanceGuid}" + Count = 1, + Instances = new List { new Instance { Id = $"{1337}/{Guid.NewGuid()}" } }, + Next = + "https://platform.altinn.no/storage/api/instances?appId=ttd%2Fapps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false&continuationtoken=abcd" }; - HttpResponseMessage httpResponseMessage = new HttpResponseMessage + QueryResponse queryResponse2 = + new() { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(expected), Encoding.UTF8, "application/json"), + Count = 1, + Instances = new List { new Instance { Id = $"{1337}/{Guid.NewGuid()}" } } }; - InitializeMocks([httpResponseMessage], ["presentationtexts"]); + string urlPart1 = + "instances?appId=ttd%2Fapps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false"; + string urlPart2 = + "https://platform.altinn.no/storage/api/instances?appId=ttd%2Fapps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false&continuationtoken=abcd"; - HttpClient httpClient = new HttpClient(handlerMock.Object); + HttpResponseMessage httpResponseMessage1 = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(queryResponse1), Encoding.UTF8, "application/json"), + }; - InstanceClient target = new InstanceClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - appSettingsOptions.Object, - telemetry.Object - ); + HttpResponseMessage httpResponseMessage2 = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(queryResponse2), Encoding.UTF8, "application/json"), + }; - // Act - await target.UpdatePresentationTexts(instanceOwnerId, instanceGuid, new PresentationTexts()); + InitializeMocks([httpResponseMessage1, httpResponseMessage2], [urlPart1, urlPart2]); - // Assert - handlerMock.VerifyAll(); - } + HttpClient httpClient = new HttpClient(handlerMock.Object); - [Fact] - public async Task QueryInstances_QueryResponseContainsNext() - { - // Arrange - QueryResponse queryResponse1 = - new() - { - Count = 1, - Instances = new List { new Instance { Id = $"{1337}/{Guid.NewGuid()}" } }, - Next = - "https://platform.altinn.no/storage/api/instances?appId=ttd%2Fapps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false&continuationtoken=abcd" - }; - - QueryResponse queryResponse2 = - new() - { - Count = 1, - Instances = new List { new Instance { Id = $"{1337}/{Guid.NewGuid()}" } } - }; - - string urlPart1 = - "instances?appId=ttd%2Fapps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false"; - string urlPart2 = - "https://platform.altinn.no/storage/api/instances?appId=ttd%2Fapps-test&instanceOwner.partyId=1337&status.isArchived=false&status.isSoftDeleted=false&continuationtoken=abcd"; - - HttpResponseMessage httpResponseMessage1 = new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent( - JsonConvert.SerializeObject(queryResponse1), - Encoding.UTF8, - "application/json" - ), - }; + InstanceClient target = new InstanceClient( + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + appSettingsOptions.Object, + telemetry.Object + ); - HttpResponseMessage httpResponseMessage2 = new HttpResponseMessage + Dictionary queryParams = + new() { - StatusCode = HttpStatusCode.OK, - Content = new StringContent( - JsonConvert.SerializeObject(queryResponse2), - Encoding.UTF8, - "application/json" - ), + { "appId", $"ttd/apps-test" }, + { "instanceOwner.partyId", "1337" }, + { "status.isArchived", "false" }, + { "status.isSoftDeleted", "false" } }; - InitializeMocks([httpResponseMessage1, httpResponseMessage2], [urlPart1, urlPart2]); - - HttpClient httpClient = new HttpClient(handlerMock.Object); - - InstanceClient target = new InstanceClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - appSettingsOptions.Object, - telemetry.Object - ); - - Dictionary queryParams = - new() - { - { "appId", $"ttd/apps-test" }, - { "instanceOwner.partyId", "1337" }, - { "status.isArchived", "false" }, - { "status.isSoftDeleted", "false" } - }; - - // Act - List instances = await target.GetInstances(queryParams); - - // Assert - Assert.Equal(2, instances.Count); - handlerMock.VerifyAll(); - } + // Act + List instances = await target.GetInstances(queryParams); + + // Assert + Assert.Equal(2, instances.Count); + handlerMock.VerifyAll(); + } - private void InitializeMocks(HttpResponseMessage[] httpResponseMessages, string[] urlPart) + private void InitializeMocks(HttpResponseMessage[] httpResponseMessages, string[] urlPart) + { + PlatformSettings platformSettings = new PlatformSettings { - PlatformSettings platformSettings = new PlatformSettings - { - ApiStorageEndpoint = "http://localhost", - SubscriptionKey = "key" - }; - platformSettingsOptions.Setup(s => s.Value).Returns(platformSettings); + ApiStorageEndpoint = "http://localhost", + SubscriptionKey = "key" + }; + platformSettingsOptions.Setup(s => s.Value).Returns(platformSettings); - AppSettings appSettings = new AppSettings { RuntimeCookieName = "AltinnStudioRuntime" }; - appSettingsOptions.Setup(s => s.CurrentValue).Returns(appSettings); + AppSettings appSettings = new AppSettings { RuntimeCookieName = "AltinnStudioRuntime" }; + appSettingsOptions.Setup(s => s.CurrentValue).Returns(appSettings); - contextAccessor.Setup(s => s.HttpContext).Returns(new DefaultHttpContext()); + contextAccessor.Setup(s => s.HttpContext).Returns(new DefaultHttpContext()); - if (httpResponseMessages.Length == 2) - { - handlerMock - .Protected() - .SetupSequence>( - "SendAsync", - ItExpr.Is(p => - p.RequestUri.ToString().Contains(urlPart[0]) || p.RequestUri.ToString().Contains(urlPart[1]) - ), - ItExpr.IsAny() - ) - .ReturnsAsync(httpResponseMessages[0]) - .ReturnsAsync(httpResponseMessages[1]); - } - else - { - handlerMock - .Protected() - .Setup>( - "SendAsync", - ItExpr.Is(p => p.RequestUri.ToString().Contains(urlPart[0])), - ItExpr.IsAny() - ) - .ReturnsAsync(httpResponseMessages[0]) - .Verifiable(); - } + if (httpResponseMessages.Length == 2) + { + handlerMock + .Protected() + .SetupSequence>( + "SendAsync", + ItExpr.Is(p => + p.RequestUri.ToString().Contains(urlPart[0]) || p.RequestUri.ToString().Contains(urlPart[1]) + ), + ItExpr.IsAny() + ) + .ReturnsAsync(httpResponseMessages[0]) + .ReturnsAsync(httpResponseMessages[1]); } - - public void Dispose() + else { - telemetry.Dispose(); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.Is(p => p.RequestUri.ToString().Contains(urlPart[0])), + ItExpr.IsAny() + ) + .ReturnsAsync(httpResponseMessages[0]) + .Verifiable(); } } + + public void Dispose() + { + telemetry.Dispose(); + } } diff --git a/test/Altinn.App.Core.Tests/Implementation/NullPdfFormatterTests.cs b/test/Altinn.App.Core.Tests/Implementation/NullPdfFormatterTests.cs index a1f108ace..8194d726d 100644 --- a/test/Altinn.App.Core.Tests/Implementation/NullPdfFormatterTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/NullPdfFormatterTests.cs @@ -3,7 +3,6 @@ using Altinn.App.Core.Models; using Altinn.App.PlatformServices.Tests.Implementation.TestResources; using FluentAssertions; -using Xunit; namespace Altinn.App.PlatformServices.Tests.Implementation; diff --git a/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs b/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs index f463a683e..0adea14cb 100644 --- a/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs @@ -12,170 +12,164 @@ using Altinn.Platform.Register.Models; using Microsoft.Extensions.Options; using Moq; -using Xunit; -namespace Altinn.App.Core.Tests.Implementation +namespace Altinn.App.Core.Tests.Implementation; + +public class PersonClientTests { - public class PersonClientTests - { - private readonly Mock> _platformSettingsOptions; - private readonly Mock _appMetadata; - private readonly Mock _accessTokenGenerator; - private readonly Mock _userTokenProvider; + private readonly Mock> _platformSettingsOptions; + private readonly Mock _appMetadata; + private readonly Mock _accessTokenGenerator; + private readonly Mock _userTokenProvider; - public PersonClientTests() - { - PlatformSettings platformSettings = new() { ApiRegisterEndpoint = "http://real.domain.com" }; - _platformSettingsOptions = new Mock>(); - _platformSettingsOptions.Setup(s => s.Value).Returns(platformSettings); - - _appMetadata = new Mock(); - _appMetadata - .Setup(s => s.GetApplicationMetadata()) - .ReturnsAsync(new ApplicationMetadata("ttd/app") { Org = "ttd", Id = "ttd/app" }); - - _accessTokenGenerator = new Mock(); - _accessTokenGenerator - .Setup(s => - s.GenerateAccessToken(It.Is(org => org == "ttd"), It.Is(app => app == "app")) - ) - .Returns("accesstoken"); - - _userTokenProvider = new Mock(); - _userTokenProvider.Setup(s => s.GetUserToken()).Returns("usertoken"); - } + public PersonClientTests() + { + PlatformSettings platformSettings = new() { ApiRegisterEndpoint = "http://real.domain.com" }; + _platformSettingsOptions = new Mock>(); + _platformSettingsOptions.Setup(s => s.Value).Returns(platformSettings); + + _appMetadata = new Mock(); + _appMetadata + .Setup(s => s.GetApplicationMetadata()) + .ReturnsAsync(new ApplicationMetadata("ttd/app") { Org = "ttd", Id = "ttd/app" }); + + _accessTokenGenerator = new Mock(); + _accessTokenGenerator + .Setup(s => s.GenerateAccessToken(It.Is(org => org == "ttd"), It.Is(app => app == "app"))) + .Returns("accesstoken"); + + _userTokenProvider = new Mock(); + _userTokenProvider.Setup(s => s.GetUserToken()).Returns("usertoken"); + } - [Fact] - public async Task GetPerson_PlatformResponseOk_OutcomeSuccessful() - { - // Arrange - HttpRequestMessage? platformRequest = null; - DelegatingHandlerStub messageHandler = - new( - async (HttpRequestMessage request, CancellationToken token) => - { - platformRequest = request; - Person person = new Person { LastName = "Lastname" }; - return await CreateHttpResponseMessage(person); - } - ); - - var target = new PersonClient( - new HttpClient(messageHandler), - _platformSettingsOptions.Object, - _appMetadata.Object, - _accessTokenGenerator.Object, - _userTokenProvider.Object + [Fact] + public async Task GetPerson_PlatformResponseOk_OutcomeSuccessful() + { + // Arrange + HttpRequestMessage? platformRequest = null; + DelegatingHandlerStub messageHandler = + new( + async (HttpRequestMessage request, CancellationToken token) => + { + platformRequest = request; + Person person = new Person { LastName = "Lastname" }; + return await CreateHttpResponseMessage(person); + } ); - // Act - var actual = await target.GetPerson("personnummer", "lastname", CancellationToken.None); + var target = new PersonClient( + new HttpClient(messageHandler), + _platformSettingsOptions.Object, + _appMetadata.Object, + _accessTokenGenerator.Object, + _userTokenProvider.Object + ); + + // Act + var actual = await target.GetPerson("personnummer", "lastname", CancellationToken.None); + + // Assert + _appMetadata.VerifyAll(); + _accessTokenGenerator.VerifyAll(); + _userTokenProvider.VerifyAll(); + + Assert.NotNull(platformRequest); + + Assert.Equal(HttpMethod.Get, platformRequest!.Method); + Assert.Equal("Bearer usertoken", platformRequest!.Headers.Authorization!.ToString()); + Assert.Equal("accesstoken", platformRequest!.Headers.GetValues("PlatformAccessToken").First()); + Assert.StartsWith("http://real.domain.com", platformRequest!.RequestUri!.ToString()); + Assert.EndsWith("persons", platformRequest!.RequestUri!.ToString()); + Assert.Equal("personnummer", platformRequest!.Headers.GetValues("X-Ai-NationalIdentityNumber").First()); + Assert.Equal(ConvertToBase64("lastname"), platformRequest!.Headers.GetValues("X-Ai-LastName").First()); + + Assert.NotNull(actual); + } - // Assert - _appMetadata.VerifyAll(); - _accessTokenGenerator.VerifyAll(); - _userTokenProvider.VerifyAll(); + [Fact] + public async Task GetPerson_PlatformResponseNotFound_ReturnsNull() + { + // Arrange + HttpRequestMessage? platformRequest = null; + DelegatingHandlerStub messageHandler = + new( + async (HttpRequestMessage request, CancellationToken token) => + { + platformRequest = request; + return await Task.FromResult(new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }); + } + ); - Assert.NotNull(platformRequest); + var target = new PersonClient( + new HttpClient(messageHandler), + _platformSettingsOptions.Object, + _appMetadata.Object, + _accessTokenGenerator.Object, + _userTokenProvider.Object + ); - Assert.Equal(HttpMethod.Get, platformRequest!.Method); - Assert.Equal("Bearer usertoken", platformRequest!.Headers.Authorization!.ToString()); - Assert.Equal("accesstoken", platformRequest!.Headers.GetValues("PlatformAccessToken").First()); - Assert.StartsWith("http://real.domain.com", platformRequest!.RequestUri!.ToString()); - Assert.EndsWith("persons", platformRequest!.RequestUri!.ToString()); - Assert.Equal("personnummer", platformRequest!.Headers.GetValues("X-Ai-NationalIdentityNumber").First()); - Assert.Equal(ConvertToBase64("lastname"), platformRequest!.Headers.GetValues("X-Ai-LastName").First()); + // Act + var actual = await target.GetPerson("personnummer", "lastname", CancellationToken.None); - Assert.NotNull(actual); - } + // Assert + Assert.NotNull(platformRequest); + Assert.Null(actual); + } - [Fact] - public async Task GetPerson_PlatformResponseNotFound_ReturnsNull() - { - // Arrange - HttpRequestMessage? platformRequest = null; - DelegatingHandlerStub messageHandler = - new( - async (HttpRequestMessage request, CancellationToken token) => + [Fact] + public async Task GetPerson_PlatformResponseTooManyRequests_ThrowsPlatformHttpException() + { + // Arrange + HttpRequestMessage? platformRequest = null; + DelegatingHandlerStub messageHandler = + new( + async (HttpRequestMessage request, CancellationToken token) => + { + platformRequest = request; + HttpResponseMessage responseMessage = new HttpResponseMessage { - platformRequest = request; - return await Task.FromResult( - new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound } - ); - } - ); - - var target = new PersonClient( - new HttpClient(messageHandler), - _platformSettingsOptions.Object, - _appMetadata.Object, - _accessTokenGenerator.Object, - _userTokenProvider.Object + StatusCode = HttpStatusCode.TooManyRequests + }; + return await Task.FromResult(responseMessage); + } ); - // Act - var actual = await target.GetPerson("personnummer", "lastname", CancellationToken.None); + var target = new PersonClient( + new HttpClient(messageHandler), + _platformSettingsOptions.Object, + _appMetadata.Object, + _accessTokenGenerator.Object, + _userTokenProvider.Object + ); - // Assert - Assert.NotNull(platformRequest); - Assert.Null(actual); - } + PlatformHttpException? actual = null; - [Fact] - public async Task GetPerson_PlatformResponseTooManyRequests_ThrowsPlatformHttpException() + // Act + try { - // Arrange - HttpRequestMessage? platformRequest = null; - DelegatingHandlerStub messageHandler = - new( - async (HttpRequestMessage request, CancellationToken token) => - { - platformRequest = request; - HttpResponseMessage responseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.TooManyRequests - }; - return await Task.FromResult(responseMessage); - } - ); - - var target = new PersonClient( - new HttpClient(messageHandler), - _platformSettingsOptions.Object, - _appMetadata.Object, - _accessTokenGenerator.Object, - _userTokenProvider.Object - ); - - PlatformHttpException? actual = null; - - // Act - try - { - _ = await target.GetPerson("personnummer", "lastname", CancellationToken.None); - } - catch (PlatformHttpException phe) - { - actual = phe; - } - - // Assert - Assert.NotNull(platformRequest); - Assert.NotNull(actual); - Assert.Equal(HttpStatusCode.TooManyRequests, actual!.Response.StatusCode); + _ = await target.GetPerson("personnummer", "lastname", CancellationToken.None); } - - private static async Task CreateHttpResponseMessage(object obj) + catch (PlatformHttpException phe) { - string content = JsonSerializer.Serialize(obj); - StringContent stringContent = new StringContent(content, Encoding.UTF8, "application/json"); - return await Task.FromResult(new HttpResponseMessage { Content = stringContent }); + actual = phe; } - private static string ConvertToBase64(string text) - { - var bytes = Encoding.UTF8.GetBytes(text); - return Convert.ToBase64String(bytes); - } + // Assert + Assert.NotNull(platformRequest); + Assert.NotNull(actual); + Assert.Equal(HttpStatusCode.TooManyRequests, actual!.Response.StatusCode); + } + + private static async Task CreateHttpResponseMessage(object obj) + { + string content = JsonSerializer.Serialize(obj); + StringContent stringContent = new StringContent(content, Encoding.UTF8, "application/json"); + return await Task.FromResult(new HttpResponseMessage { Content = stringContent }); + } + + private static string ConvertToBase64(string text) + { + var bytes = Encoding.UTF8.GetBytes(text); + return Convert.ToBase64String(bytes); } } diff --git a/test/Altinn.App.Core.Tests/Implementation/TestData/AppDataModel/ModelWithShadowFields.cs b/test/Altinn.App.Core.Tests/Implementation/TestData/AppDataModel/ModelWithShadowFields.cs index 5ff44476f..dfb847723 100644 --- a/test/Altinn.App.Core.Tests/Implementation/TestData/AppDataModel/ModelWithShadowFields.cs +++ b/test/Altinn.App.Core.Tests/Implementation/TestData/AppDataModel/ModelWithShadowFields.cs @@ -4,74 +4,73 @@ using System.Xml.Serialization; using Newtonsoft.Json; -namespace Altinn.App.Core.Tests.Implementation.TestData.AppDataModel +namespace Altinn.App.Core.Tests.Implementation.TestData.AppDataModel; + +[XmlRoot(ElementName = "model")] +public class ModelWithShadowFields { - [XmlRoot(ElementName = "model")] - public class ModelWithShadowFields - { - [Range(double.MinValue, double.MaxValue)] - [XmlElement("property1", Order = 1)] - [JsonProperty("property1")] - [JsonPropertyName("property1")] - [Required] - public decimal? Property1 { get; set; } + [Range(double.MinValue, double.MaxValue)] + [XmlElement("property1", Order = 1)] + [JsonProperty("property1")] + [JsonPropertyName("property1")] + [Required] + public decimal? Property1 { get; set; } - [Range(double.MinValue, double.MaxValue)] - [XmlElement("property2", Order = 2)] - [JsonProperty("property2")] - [JsonPropertyName("property2")] - [Required] - public decimal? Property2 { get; set; } + [Range(double.MinValue, double.MaxValue)] + [XmlElement("property2", Order = 2)] + [JsonProperty("property2")] + [JsonPropertyName("property2")] + [Required] + public decimal? Property2 { get; set; } - [XmlElement("property3", Order = 3)] - [JsonProperty("property3")] - [JsonPropertyName("property3")] - public string Property3 { get; set; } + [XmlElement("property3", Order = 3)] + [JsonProperty("property3")] + [JsonPropertyName("property3")] + public string Property3 { get; set; } - [XmlElement("AltinnSF_hello", Order = 4)] - [JsonProperty("AltinnSF_hello")] - [JsonPropertyName("AltinnSF_hello")] - public string AltinnSF_hello { get; set; } + [XmlElement("AltinnSF_hello", Order = 4)] + [JsonProperty("AltinnSF_hello")] + [JsonPropertyName("AltinnSF_hello")] + public string AltinnSF_hello { get; set; } - [XmlElement("AltinnSF_test", Order = 5)] - [JsonProperty("AltinnSF_test")] - [JsonPropertyName("AltinnSF_test")] - public string AltinnSF_test { get; set; } + [XmlElement("AltinnSF_test", Order = 5)] + [JsonProperty("AltinnSF_test")] + [JsonPropertyName("AltinnSF_test")] + public string AltinnSF_test { get; set; } - [XmlElement("AltinnSF_gruppeish", Order = 6)] - [JsonProperty("AltinnSF_gruppeish")] - [JsonPropertyName("AltinnSF_gruppeish")] - public AltinnSF_gruppeish AltinnSF_gruppeish { get; set; } + [XmlElement("AltinnSF_gruppeish", Order = 6)] + [JsonProperty("AltinnSF_gruppeish")] + [JsonPropertyName("AltinnSF_gruppeish")] + public AltinnSF_gruppeish AltinnSF_gruppeish { get; set; } - [XmlElement("gruppe", Order = 7)] - [JsonProperty("gruppe")] - [JsonPropertyName("gruppe")] - public List Gruppe { get; set; } - } + [XmlElement("gruppe", Order = 7)] + [JsonProperty("gruppe")] + [JsonPropertyName("gruppe")] + public List Gruppe { get; set; } +} - public class AltinnSF_gruppeish - { - [XmlElement("f1", Order = 1)] - [JsonProperty("f1")] - [JsonPropertyName("f1")] - public string F1 { get; set; } +public class AltinnSF_gruppeish +{ + [XmlElement("f1", Order = 1)] + [JsonProperty("f1")] + [JsonPropertyName("f1")] + public string F1 { get; set; } - [XmlElement("f2", Order = 2)] - [JsonProperty("f2")] - [JsonPropertyName("f2")] - public string F2 { get; set; } - } + [XmlElement("f2", Order = 2)] + [JsonProperty("f2")] + [JsonPropertyName("f2")] + public string F2 { get; set; } +} - public class Gruppe - { - [XmlElement("gf1", Order = 1)] - [JsonProperty("gf1")] - [JsonPropertyName("gf1")] - public string Gf1 { get; set; } +public class Gruppe +{ + [XmlElement("gf1", Order = 1)] + [JsonProperty("gf1")] + [JsonPropertyName("gf1")] + public string Gf1 { get; set; } - [XmlElement("AltinnSF_gf-hjelpefelt", Order = 2)] - [JsonProperty("AltinnSF_gf-hjelpefelt")] - [JsonPropertyName("AltinnSF_gf-hjelpefelt")] - public string AltinnSF_gfhjelpefelt { get; set; } - } + [XmlElement("AltinnSF_gf-hjelpefelt", Order = 2)] + [JsonProperty("AltinnSF_gf-hjelpefelt")] + [JsonPropertyName("AltinnSF_gf-hjelpefelt")] + public string AltinnSF_gfhjelpefelt { get; set; } } diff --git a/test/Altinn.App.Core.Tests/Implementation/TextClientTest.cs b/test/Altinn.App.Core.Tests/Implementation/TextClientTest.cs index 73d49e263..c3d995ae6 100644 --- a/test/Altinn.App.Core.Tests/Implementation/TextClientTest.cs +++ b/test/Altinn.App.Core.Tests/Implementation/TextClientTest.cs @@ -12,144 +12,142 @@ using Moq; using Moq.Protected; using Newtonsoft.Json; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.Implementation +namespace Altinn.App.PlatformServices.Tests.Implementation; + +public class TextClientTest { - public class TextClientTest + private readonly Mock> platformSettingsOptions; + private readonly Mock> appSettingsOptions; + private readonly Mock handlerMock; + private readonly Mock contextAccessor; + private readonly Mock> logger; + private readonly IMemoryCache memoryCache; + + public TextClientTest() { - private readonly Mock> platformSettingsOptions; - private readonly Mock> appSettingsOptions; - private readonly Mock handlerMock; - private readonly Mock contextAccessor; - private readonly Mock> logger; - private readonly IMemoryCache memoryCache; - - public TextClientTest() - { - platformSettingsOptions = new Mock>(); - appSettingsOptions = new Mock>(); - handlerMock = new Mock(MockBehavior.Strict); - contextAccessor = new Mock(); - logger = new Mock>(); + platformSettingsOptions = new Mock>(); + appSettingsOptions = new Mock>(); + handlerMock = new Mock(MockBehavior.Strict); + contextAccessor = new Mock(); + logger = new Mock>(); - var services = new ServiceCollection(); - services.AddMemoryCache(); - var serviceProvider = services.BuildServiceProvider(); + var services = new ServiceCollection(); + services.AddMemoryCache(); + var serviceProvider = services.BuildServiceProvider(); - memoryCache = serviceProvider.GetService(); - } + memoryCache = serviceProvider.GetService(); + } - [Fact] - public async Task GetAppTextNb_SuccessfulCallToStorage() - { - // Arrange - memoryCache.Remove("org-app-nb"); - TextResource texts = new TextResource { Language = "nb" }; - - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonConvert.SerializeObject(texts), Encoding.UTF8, "application/json"), - }; - - InitializeMocks(httpResponseMessage, "texts"); - HttpClient httpClient = new HttpClient(handlerMock.Object); - TextClient target = new TextClient( - appSettingsOptions.Object, - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - memoryCache - ); - - // Act - await target.GetText("org", "app", "nb"); - - // Assert - handlerMock.VerifyAll(); - } - - [Fact] - public async Task GetAppTextNb_TextSuccessfullyRetrievedFromCache() + [Fact] + public async Task GetAppTextNb_SuccessfulCallToStorage() + { + // Arrange + memoryCache.Remove("org-app-nb"); + TextResource texts = new TextResource { Language = "nb" }; + + HttpResponseMessage httpResponseMessage = new HttpResponseMessage { - // Arrange - memoryCache.Remove("org-app-nb"); - TextResource texts = new TextResource { Language = "nb" }; - memoryCache.Set("org-app-nb", texts); - - InitializeMocks(new HttpResponseMessage(), "texts"); - - HttpClient httpClient = new HttpClient(handlerMock.Object); - TextClient target = new TextClient( - appSettingsOptions.Object, - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - memoryCache - ); - - // Act - TextResource actual = await target.GetText("org", "app", "nb"); - - // Assert - Assert.Equal(texts.Language, actual.Language); - } - - [Fact] - public async Task GetAppTextNb_StorageReturnsError() + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(texts), Encoding.UTF8, "application/json"), + }; + + InitializeMocks(httpResponseMessage, "texts"); + HttpClient httpClient = new HttpClient(handlerMock.Object); + TextClient target = new TextClient( + appSettingsOptions.Object, + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + memoryCache + ); + + // Act + await target.GetText("org", "app", "nb"); + + // Assert + handlerMock.VerifyAll(); + } + + [Fact] + public async Task GetAppTextNb_TextSuccessfullyRetrievedFromCache() + { + // Arrange + memoryCache.Remove("org-app-nb"); + TextResource texts = new TextResource { Language = "nb" }; + memoryCache.Set("org-app-nb", texts); + + InitializeMocks(new HttpResponseMessage(), "texts"); + + HttpClient httpClient = new HttpClient(handlerMock.Object); + TextClient target = new TextClient( + appSettingsOptions.Object, + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + memoryCache + ); + + // Act + TextResource actual = await target.GetText("org", "app", "nb"); + + // Assert + Assert.Equal(texts.Language, actual.Language); + } + + [Fact] + public async Task GetAppTextNb_StorageReturnsError() + { + // Arrange + memoryCache.Remove("org-app-nb"); + + HttpResponseMessage httpResponseMessage = new HttpResponseMessage { - // Arrange - memoryCache.Remove("org-app-nb"); - - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.InternalServerError - }; - - InitializeMocks(httpResponseMessage, "texts"); - HttpClient httpClient = new HttpClient(handlerMock.Object); - TextClient target = new TextClient( - appSettingsOptions.Object, - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - httpClient, - memoryCache - ); - - // Act - TextResource actual = await target.GetText("org", "app", "nb"); - - // Assert - Assert.Null(actual); - } - - private void InitializeMocks(HttpResponseMessage httpResponseMessage, string urlPart) + StatusCode = HttpStatusCode.InternalServerError + }; + + InitializeMocks(httpResponseMessage, "texts"); + HttpClient httpClient = new HttpClient(handlerMock.Object); + TextClient target = new TextClient( + appSettingsOptions.Object, + platformSettingsOptions.Object, + logger.Object, + contextAccessor.Object, + httpClient, + memoryCache + ); + + // Act + TextResource actual = await target.GetText("org", "app", "nb"); + + // Assert + Assert.Null(actual); + } + + private void InitializeMocks(HttpResponseMessage httpResponseMessage, string urlPart) + { + PlatformSettings platformSettings = new PlatformSettings { - PlatformSettings platformSettings = new PlatformSettings - { - ApiStorageEndpoint = "http://localhost", - SubscriptionKey = "key" - }; - platformSettingsOptions.Setup(s => s.Value).Returns(platformSettings); - - AppSettings appSettings = new AppSettings { RuntimeCookieName = "AltinnStudioRuntime" }; - appSettingsOptions.Setup(s => s.Value).Returns(appSettings); - - contextAccessor.Setup(s => s.HttpContext).Returns(new DefaultHttpContext()); - - handlerMock - .Protected() - .Setup>( - "SendAsync", - ItExpr.Is(p => p.RequestUri.ToString().Contains(urlPart)), - ItExpr.IsAny() - ) - .ReturnsAsync(httpResponseMessage) - .Verifiable(); - } + ApiStorageEndpoint = "http://localhost", + SubscriptionKey = "key" + }; + platformSettingsOptions.Setup(s => s.Value).Returns(platformSettings); + + AppSettings appSettings = new AppSettings { RuntimeCookieName = "AltinnStudioRuntime" }; + appSettingsOptions.Setup(s => s.Value).Returns(appSettings); + + contextAccessor.Setup(s => s.HttpContext).Returns(new DefaultHttpContext()); + + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.Is(p => p.RequestUri.ToString().Contains(urlPart)), + ItExpr.IsAny() + ) + .ReturnsAsync(httpResponseMessage) + .Verifiable(); } } diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs index be95297be..1c9768b54 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs @@ -9,84 +9,82 @@ using Microsoft.Extensions.Options; using Moq; using Moq.Protected; -using Xunit; using Xunit.Abstractions; -namespace Altinn.App.PlatformServices.Tests.Infrastructure.Clients +namespace Altinn.App.PlatformServices.Tests.Infrastructure.Clients; + +public class EventsSubscriptionClientTests { - public class EventsSubscriptionClientTests + private readonly ITestOutputHelper _testOutputHelper; + + public EventsSubscriptionClientTests(ITestOutputHelper testOutputHelper) { - private readonly ITestOutputHelper _testOutputHelper; + _testOutputHelper = testOutputHelper; + } - public EventsSubscriptionClientTests(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } + [Fact] + public async Task AddSubscription_ShouldReturnOk() + { + EventsSubscriptionClient client = GetEventSubscriptonClient(); - [Fact] - public async Task AddSubscription_ShouldReturnOk() - { - EventsSubscriptionClient client = GetEventSubscriptonClient(); + Subscription subscription = await client.AddSubscription("ttd", "test-app", "app.events.type"); - Subscription subscription = await client.AddSubscription("ttd", "test-app", "app.events.type"); + subscription.Should().NotBeNull(); + } - subscription.Should().NotBeNull(); - } + private static EventsSubscriptionClient GetEventSubscriptonClient() + { + IEventSecretCodeProvider secretCodeProvider = new TestSecretCodeProvider(); + Mock> loggerMock = new(); - private static EventsSubscriptionClient GetEventSubscriptonClient() + IOptions platformSettings = Microsoft.Extensions.Options.Options.Create( + new PlatformSettings() + { + ApiEventsEndpoint = "http://localhost:5101/events/api/v1/", + SubscriptionKey = "key" + } + ); + + IOptions generalSettings = Microsoft.Extensions.Options.Options.Create( + new GeneralSettings { HostName = "at22.altinn.cloud" } + ); + + Subscription subscriptionContent = new() { Id = 123 }; + + HttpResponseMessage httpResponseMessage = new HttpResponseMessage { - IEventSecretCodeProvider secretCodeProvider = new TestSecretCodeProvider(); - Mock> loggerMock = new(); + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(subscriptionContent)) + }; - IOptions platformSettings = Microsoft.Extensions.Options.Options.Create( - new PlatformSettings() - { - ApiEventsEndpoint = "http://localhost:5101/events/api/v1/", - SubscriptionKey = "key" - } - ); + Mock handlerMock = new(); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync(httpResponseMessage); - IOptions generalSettings = Microsoft.Extensions.Options.Options.Create( - new GeneralSettings { HostName = "at22.altinn.cloud" } - ); + HttpClient httpClient = new HttpClient(handlerMock.Object); - Subscription subscriptionContent = new() { Id = 123 }; + var client = new EventsSubscriptionClient( + platformSettings, + httpClient, + generalSettings, + secretCodeProvider, + loggerMock.Object + ); - HttpResponseMessage httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonSerializer.Serialize(subscriptionContent)) - }; - - Mock handlerMock = new(); - handlerMock - .Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny() - ) - .ReturnsAsync(httpResponseMessage); - - HttpClient httpClient = new HttpClient(handlerMock.Object); - - var client = new EventsSubscriptionClient( - platformSettings, - httpClient, - generalSettings, - secretCodeProvider, - loggerMock.Object - ); - - return client; - } + return client; + } - public class TestSecretCodeProvider : IEventSecretCodeProvider + public class TestSecretCodeProvider : IEventSecretCodeProvider + { + public Task GetSecretCode() { - public Task GetSecretCode() - { - return Task.FromResult("42"); - } + return Task.FromResult("42"); } } } diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs index 1b204e9ef..b79015b0b 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs @@ -18,882 +18,875 @@ using Microsoft.Extensions.Options; using Moq; -namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage; + +public class DataClientTests { - public class DataClientTests + private readonly Mock> platformSettingsOptions; + private readonly Mock userTokenProvide; + private readonly ILogger logger; + private readonly string apiStorageEndpoint = "https://local.platform.altinn.no/api/storage/"; + + public DataClientTests() { - private readonly Mock> platformSettingsOptions; - private readonly Mock userTokenProvide; - private readonly ILogger logger; - private readonly string apiStorageEndpoint = "https://local.platform.altinn.no/api/storage/"; + platformSettingsOptions = new Mock>(); + PlatformSettings platformSettings = new() { ApiStorageEndpoint = apiStorageEndpoint }; + platformSettingsOptions.Setup(s => s.Value).Returns(platformSettings); + userTokenProvide = new Mock(); + userTokenProvide.Setup(u => u.GetUserToken()).Returns("dummytesttoken"); - public DataClientTests() - { - platformSettingsOptions = new Mock>(); - PlatformSettings platformSettings = new() { ApiStorageEndpoint = apiStorageEndpoint }; - platformSettingsOptions.Setup(s => s.Value).Returns(platformSettings); - userTokenProvide = new Mock(); - userTokenProvide.Setup(u => u.GetUserToken()).Returns("dummytesttoken"); + logger = new NullLogger(); + } - logger = new NullLogger(); - } + [Fact] + public async Task InsertBinaryData_MethodProduceValidPlatformRequest() + { + // Arrange + HttpRequestMessage? platformRequest = null; + TelemetrySink telemetrySink = new(); - [Fact] - public async Task InsertBinaryData_MethodProduceValidPlatformRequest() - { - // Arrange - HttpRequestMessage? platformRequest = null; - TelemetrySink telemetrySink = new(); + var target = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + platformRequest = request; + + DataElement dataElement = new DataElement { Id = "DataElement.Id", InstanceGuid = "InstanceGuid" }; + await Task.CompletedTask; + return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; + }, + telemetrySink + ); + + var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); + var instanceIdentifier = new InstanceIdentifier(323413, Guid.NewGuid()); + Uri expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data?dataType=catstories", + UriKind.RelativeOrAbsolute + ); + + // Act + DataElement actual = await target.InsertBinaryData( + instanceIdentifier.ToString(), + "catstories", + "application/pdf", + "a cats story.pdf", + stream + ); + + // Assert + Assert.NotNull(actual); + + Assert.NotNull(platformRequest); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Post, "\"a cats story.pdf\"", "application/pdf"); + + await Verify(telemetrySink.GetSnapshot()); + } - var target = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - platformRequest = request; + [Fact] + public async Task InsertBinaryData_MethodProduceValidPlatformRequest_with_generatedFrom_query_params() + { + // Arrange + HttpRequestMessage? platformRequest = null; - DataElement dataElement = new DataElement { Id = "DataElement.Id", InstanceGuid = "InstanceGuid" }; - await Task.CompletedTask; - return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; - }, - telemetrySink - ); - - var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); - var instanceIdentifier = new InstanceIdentifier(323413, Guid.NewGuid()); - Uri expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data?dataType=catstories", - UriKind.RelativeOrAbsolute - ); - - // Act - DataElement actual = await target.InsertBinaryData( - instanceIdentifier.ToString(), - "catstories", - "application/pdf", - "a cats story.pdf", - stream - ); - - // Assert - Assert.NotNull(actual); - - Assert.NotNull(platformRequest); - AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Post, "\"a cats story.pdf\"", "application/pdf"); - - await Verify(telemetrySink.GetSnapshot()); - } + var target = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + platformRequest = request; - [Fact] - public async Task InsertBinaryData_MethodProduceValidPlatformRequest_with_generatedFrom_query_params() - { - // Arrange - HttpRequestMessage? platformRequest = null; + DataElement dataElement = new DataElement { Id = "DataElement.Id", InstanceGuid = "InstanceGuid" }; + await Task.CompletedTask; + return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; + } + ); + + var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); + var instanceIdentifier = new InstanceIdentifier(323413, Guid.NewGuid()); + Uri expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data?dataType=catstories&generatedFromTask=Task_1", + UriKind.RelativeOrAbsolute + ); + + // Act + DataElement actual = await target.InsertBinaryData( + instanceIdentifier.ToString(), + "catstories", + "application/pdf", + "a cats story.pdf", + stream, + "Task_1" + ); + + // Assert + Assert.NotNull(actual); + + Assert.NotNull(platformRequest); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Post, "\"a cats story.pdf\"", "application/pdf"); + } + + [Fact] + public async Task GetFormData_MethodProduceValidPlatformRequest_ReturnedFormIsValid() + { + // Arrange + HttpRequestMessage? platformRequest = null; - var target = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => + var target = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + platformRequest = request; + + string serializedModel = + string.Empty + + @"" + + @"" + + @" " + + @" Test Test 123" + + @" " + + @""; + await Task.CompletedTask; + + HttpResponseMessage response = new HttpResponseMessage() { - platformRequest = request; - - DataElement dataElement = new DataElement { Id = "DataElement.Id", InstanceGuid = "InstanceGuid" }; - await Task.CompletedTask; - return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; - } - ); - - var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); - var instanceIdentifier = new InstanceIdentifier(323413, Guid.NewGuid()); - Uri expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data?dataType=catstories&generatedFromTask=Task_1", - UriKind.RelativeOrAbsolute - ); - - // Act - DataElement actual = await target.InsertBinaryData( - instanceIdentifier.ToString(), - "catstories", - "application/pdf", - "a cats story.pdf", - stream, - "Task_1" - ); - - // Assert - Assert.NotNull(actual); - - Assert.NotNull(platformRequest); - AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Post, "\"a cats story.pdf\"", "application/pdf"); - } + Content = new StringContent(serializedModel) + }; + response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/xml"); + return response; + } + ); + + Guid dataElementGuid = Guid.NewGuid(); + var instanceIdentifier = new InstanceIdentifier(323413, Guid.NewGuid()); + Uri expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataElementGuid}", + UriKind.RelativeOrAbsolute + ); + + // Act + object response = await target.GetFormData( + instanceIdentifier.InstanceGuid, + typeof(SkjemaWithNamespace), + "org", + "app", + 323413, + dataElementGuid + ); + + // Assert + var actual = response as SkjemaWithNamespace; + Assert.NotNull(actual); + Assert.NotNull(actual!.Foretakgrp8820); + Assert.NotNull(actual!.Foretakgrp8820.EnhetNavnEndringdatadef31); + + Assert.NotNull(platformRequest); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Get, null, "application/xml"); + } - [Fact] - public async Task GetFormData_MethodProduceValidPlatformRequest_ReturnedFormIsValid() - { - // Arrange - HttpRequestMessage? platformRequest = null; + [Fact] + public async Task InsertBinaryData_PlatformRespondNotOk_ThrowsPlatformException() + { + // Arrange + var target = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.BadRequest }; + } + ); - var target = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - platformRequest = request; - - string serializedModel = - string.Empty - + @"" - + @"" - + @" " - + @" Test Test 123" - + @" " - + @""; - await Task.CompletedTask; - - HttpResponseMessage response = new HttpResponseMessage() - { - Content = new StringContent(serializedModel) - }; - response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/xml"); - return response; - } - ); - - Guid dataElementGuid = Guid.NewGuid(); - var instanceIdentifier = new InstanceIdentifier(323413, Guid.NewGuid()); - Uri expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataElementGuid}", - UriKind.RelativeOrAbsolute - ); - - // Act - object response = await target.GetFormData( - instanceIdentifier.InstanceGuid, - typeof(SkjemaWithNamespace), - "org", - "app", - 323413, - dataElementGuid - ); - - // Assert - var actual = response as SkjemaWithNamespace; - Assert.NotNull(actual); - Assert.NotNull(actual!.Foretakgrp8820); - Assert.NotNull(actual!.Foretakgrp8820.EnhetNavnEndringdatadef31); - - Assert.NotNull(platformRequest); - AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Get, null, "application/xml"); - } + var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); - [Fact] - public async Task InsertBinaryData_PlatformRespondNotOk_ThrowsPlatformException() - { - // Arrange - var target = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.BadRequest }; - } - ); - - var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); - - // Act - var actual = await Assert.ThrowsAsync( - async () => - await target.InsertBinaryData( - "instanceId", - "catstories", - "application/pdf", - "a cats story.pdf", - stream - ) - ); + // Act + var actual = await Assert.ThrowsAsync( + async () => + await target.InsertBinaryData("instanceId", "catstories", "application/pdf", "a cats story.pdf", stream) + ); - // Assert - Assert.NotNull(actual); - Assert.Equal(HttpStatusCode.BadRequest, actual.Response.StatusCode); - } + // Assert + Assert.NotNull(actual); + Assert.Equal(HttpStatusCode.BadRequest, actual.Response.StatusCode); + } - [Fact] - public async Task UpdateBinaryData_put_updated_data_and_Return_DataElement() + [Fact] + public async Task UpdateBinaryData_put_updated_data_and_Return_DataElement() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + DataElement expectedDataelement = new DataElement { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - HttpRequestMessage? platformRequest = null; - int invocations = 0; - DataElement expectedDataelement = new DataElement + Id = instanceIdentifier.ToString(), + InstanceGuid = instanceIdentifier.InstanceGuid.ToString() + }; + var dataClient = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => { - Id = instanceIdentifier.ToString(), - InstanceGuid = instanceIdentifier.InstanceGuid.ToString() - }; - var dataClient = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - invocations++; - platformRequest = request; - - DataElement dataElement = new DataElement - { - Id = instanceIdentifier.ToString(), - InstanceGuid = instanceIdentifier.InstanceGuid.ToString() - }; - await Task.CompletedTask; - return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; - } - ); - Uri expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", - UriKind.RelativeOrAbsolute - ); - var restult = await dataClient.UpdateBinaryData( - instanceIdentifier, - "application/json", - "test.json", - dataGuid, - new MemoryStream() - ); - invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, "test.json", "application/json"); - restult.Should().BeEquivalentTo(expectedDataelement); - } + invocations++; + platformRequest = request; - [Fact] - public async Task UpdateBinaryData_returns_exception_when_put_to_storage_result_in_servererror() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - int invocations = 0; - var dataClient = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => + DataElement dataElement = new DataElement { - invocations++; - - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; - } - ); - var actual = await Assert.ThrowsAsync( - async () => - await dataClient.UpdateBinaryData( - instanceIdentifier, - "application/json", - "test.json", - dataGuid, - new MemoryStream() - ) - ); - invocations.Should().Be(1); - actual.Should().NotBeNull(); - actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); - } + Id = instanceIdentifier.ToString(), + InstanceGuid = instanceIdentifier.InstanceGuid.ToString() + }; + await Task.CompletedTask; + return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; + } + ); + Uri expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", + UriKind.RelativeOrAbsolute + ); + var restult = await dataClient.UpdateBinaryData( + instanceIdentifier, + "application/json", + "test.json", + dataGuid, + new MemoryStream() + ); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, "test.json", "application/json"); + restult.Should().BeEquivalentTo(expectedDataelement); + } - [Fact] - public async Task UpdateBinaryData_returns_exception_when_put_to_storage_result_in_conflict() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - int invocations = 0; - var dataClient = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - invocations++; - - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.Conflict }; - } - ); - var actual = await Assert.ThrowsAsync( - async () => - await dataClient.UpdateBinaryData( - instanceIdentifier, - "application/json", - "test.json", - dataGuid, - new MemoryStream() - ) - ); - invocations.Should().Be(1); - actual.Should().NotBeNull(); - actual.Response.StatusCode.Should().Be(HttpStatusCode.Conflict); - } + [Fact] + public async Task UpdateBinaryData_returns_exception_when_put_to_storage_result_in_servererror() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + var dataClient = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; - [Fact] - public async Task GetBinaryData_returns_stream_of_binary_data() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - HttpRequestMessage? platformRequest = null; - int invocations = 0; - var dataClient = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - invocations++; - platformRequest = request; - - await Task.CompletedTask; - return new HttpResponseMessage() { Content = new StringContent("hello worlds") }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", - UriKind.RelativeOrAbsolute - ); - var response = await dataClient.GetBinaryData( - "ttd", - "app", - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - dataGuid - ); - invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, null, null); - using StreamReader streamReader = new StreamReader(response); - var responseString = await streamReader.ReadToEndAsync(); - responseString.Should().BeEquivalentTo("hello worlds"); - } + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + } + ); + var actual = await Assert.ThrowsAsync( + async () => + await dataClient.UpdateBinaryData( + instanceIdentifier, + "application/json", + "test.json", + dataGuid, + new MemoryStream() + ) + ); + invocations.Should().Be(1); + actual.Should().NotBeNull(); + actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } - [Fact] - public async Task GetBinaryData_returns_empty_stream_when_storage_returns_notfound() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - HttpRequestMessage? platformRequest = null; - int invocations = 0; - var dataClient = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - invocations++; - platformRequest = request; - - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", - UriKind.RelativeOrAbsolute - ); - var response = await dataClient.GetBinaryData( - "ttd", - "app", - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - dataGuid - ); - response.Should().BeNull(); - invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, null, null); - } + [Fact] + public async Task UpdateBinaryData_returns_exception_when_put_to_storage_result_in_conflict() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + var dataClient = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; - [Fact] - public async Task GetBinaryData_throws_PlatformHttpException_when_server_error_returned_from_storage() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - int invocations = 0; - var dataClient = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.Conflict }; + } + ); + var actual = await Assert.ThrowsAsync( + async () => + await dataClient.UpdateBinaryData( + instanceIdentifier, + "application/json", + "test.json", + dataGuid, + new MemoryStream() + ) + ); + invocations.Should().Be(1); + actual.Should().NotBeNull(); + actual.Response.StatusCode.Should().Be(HttpStatusCode.Conflict); + } + + [Fact] + public async Task GetBinaryData_returns_stream_of_binary_data() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; + + await Task.CompletedTask; + return new HttpResponseMessage() { Content = new StringContent("hello worlds") }; + } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", + UriKind.RelativeOrAbsolute + ); + var response = await dataClient.GetBinaryData( + "ttd", + "app", + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + dataGuid + ); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, null, null); + using StreamReader streamReader = new StreamReader(response); + var responseString = await streamReader.ReadToEndAsync(); + responseString.Should().BeEquivalentTo("hello worlds"); + } + + [Fact] + public async Task GetBinaryData_returns_empty_stream_when_storage_returns_notfound() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; + + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }; + } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", + UriKind.RelativeOrAbsolute + ); + var response = await dataClient.GetBinaryData( + "ttd", + "app", + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + dataGuid + ); + response.Should().BeNull(); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, null, null); + } + + [Fact] + public async Task GetBinaryData_throws_PlatformHttpException_when_server_error_returned_from_storage() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + var dataClient = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + } + ); + var actual = await Assert.ThrowsAsync( + async () => + await dataClient.GetBinaryData( + "ttd", + "app", + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + dataGuid + ) + ); + invocations.Should().Be(1); + actual.Should().NotBeNull(); + actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + + [Fact] + public async Task GetBinaryDataList_returns_AttachemtList_when_DataElements_found() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; + + await Task.CompletedTask; + return new HttpResponseMessage() { - invocations++; - - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; - } - ); - var actual = await Assert.ThrowsAsync( - async () => - await dataClient.GetBinaryData( - "ttd", - "app", - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - dataGuid + Content = new StringContent( + "{\"dataElements\":[{\"Id\":\"aaaa-bbbb-cccc-dddd\",\"Size\":10,\"DataType\":\"cats\"},{\"Id\":\"eeee-ffff-gggg-hhhh\", \"Size\":20,\"DataType\":\"dogs\"}]}" ) - ); - invocations.Should().Be(1); - actual.Should().NotBeNull(); - actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); - } - - [Fact] - public async Task GetBinaryDataList_returns_AttachemtList_when_DataElements_found() + }; + } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/dataelements", + UriKind.RelativeOrAbsolute + ); + var response = await dataClient.GetBinaryDataList( + "ttd", + "app", + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid + ); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, null, null); + + var expectedList = new List() { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - HttpRequestMessage? platformRequest = null; - int invocations = 0; - var dataClient = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - invocations++; - platformRequest = request; - - await Task.CompletedTask; - return new HttpResponseMessage() - { - Content = new StringContent( - "{\"dataElements\":[{\"Id\":\"aaaa-bbbb-cccc-dddd\",\"Size\":10,\"DataType\":\"cats\"},{\"Id\":\"eeee-ffff-gggg-hhhh\", \"Size\":20,\"DataType\":\"dogs\"}]}" - ) - }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/dataelements", - UriKind.RelativeOrAbsolute - ); - var response = await dataClient.GetBinaryDataList( - "ttd", - "app", - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid - ); - invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, null, null); - - var expectedList = new List() + new AttachmentList() { - new AttachmentList() + Attachments = new List() { - Attachments = new List() - { - new Attachment() { Id = "aaaa-bbbb-cccc-dddd", Size = 10 } - }, - Type = "cats" + new Attachment() { Id = "aaaa-bbbb-cccc-dddd", Size = 10 } }, - new AttachmentList() + Type = "cats" + }, + new AttachmentList() + { + Attachments = new List() { - Attachments = new List() - { - new Attachment() { Id = "eeee-ffff-gggg-hhhh", Size = 20 } - }, - Type = "dogs" + new Attachment() { Id = "eeee-ffff-gggg-hhhh", Size = 20 } }, - new AttachmentList() + Type = "dogs" + }, + new AttachmentList() + { + Attachments = new List() { - Attachments = new List() - { - new Attachment() { Id = "eeee-ffff-gggg-hhhh", Size = 20 } - }, - Type = "attachments" + new Attachment() { Id = "eeee-ffff-gggg-hhhh", Size = 20 } }, - }; - response.Should().BeEquivalentTo(expectedList); - } + Type = "attachments" + }, + }; + response.Should().BeEquivalentTo(expectedList); + } - [Fact] - public async Task GetBinaryDataList_throws_PlatformHttpException_if_non_ok_response() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - int invocations = 0; - var dataClient = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - invocations++; - - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; - } - ); - var actual = await Assert.ThrowsAsync( - async () => - await dataClient.GetBinaryDataList( - "ttd", - "app", - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid - ) - ); - invocations.Should().Be(1); - actual.Should().NotBeNull(); - actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); - } + [Fact] + public async Task GetBinaryDataList_throws_PlatformHttpException_if_non_ok_response() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + int invocations = 0; + var dataClient = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; - [Fact] - public async Task DeleteBinaryData_returns_true_when_data_was_deleted() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - HttpRequestMessage? platformRequest = null; - int invocations = 0; - var dataClient = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - invocations++; - platformRequest = request; - - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}?delay=False", - UriKind.RelativeOrAbsolute - ); - var result = await dataClient.DeleteBinaryData( - "ttd", - "app", - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - dataGuid - ); - invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); - result.Should().BeTrue(); - } + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + } + ); + var actual = await Assert.ThrowsAsync( + async () => + await dataClient.GetBinaryDataList( + "ttd", + "app", + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid + ) + ); + invocations.Should().Be(1); + actual.Should().NotBeNull(); + actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } - [Fact] - public async Task DeleteBinaryData_throws_PlatformHttpException_when_dataelement_not_found() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - HttpRequestMessage? platformRequest = null; - int invocations = 0; - var dataClient = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - invocations++; - platformRequest = request; - - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}?delay=False", - UriKind.RelativeOrAbsolute - ); - var actual = await Assert.ThrowsAsync( - async () => - await dataClient.DeleteBinaryData( - "ttd", - "app", - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - dataGuid - ) - ); - invocations.Should().Be(1); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); - actual.Response.StatusCode.Should().Be(HttpStatusCode.NotFound); - } + [Fact] + public async Task DeleteBinaryData_returns_true_when_data_was_deleted() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; - [Fact] - public async Task DeleteData_returns_true_when_data_was_deleted_with_delay_true() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - HttpRequestMessage? platformRequest = null; - int invocations = 0; - var dataClient = GetDataClient( - async (HttpRequestMessage request, CancellationToken token) => - { - invocations++; - platformRequest = request; - - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}?delay=True", - UriKind.RelativeOrAbsolute - ); - var result = await dataClient.DeleteData( - "ttd", - "app", - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - dataGuid, - true - ); - invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); - result.Should().BeTrue(); - } + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; + } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}?delay=False", + UriKind.RelativeOrAbsolute + ); + var result = await dataClient.DeleteBinaryData( + "ttd", + "app", + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + dataGuid + ); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + result.Should().BeTrue(); + } - [Fact] - public async Task UpdateData_serializes_and_updates_formdata() - { - ExampleModel exampleModel = new ExampleModel() { Name = "Test", Age = 22 }; - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - HttpRequestMessage? platformRequest = null; - int invocations = 0; - var dataClient = GetDataClient( - async (request, token) => - { - invocations++; - platformRequest = request; - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", - UriKind.RelativeOrAbsolute - ); - await dataClient.UpdateData( - exampleModel, - instanceIdentifier.InstanceGuid, - exampleModel.GetType(), - "ttd", - "app", - instanceIdentifier.InstanceOwnerPartyId, - dataGuid - ); - invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, null, "application/xml"); - } + [Fact] + public async Task DeleteBinaryData_throws_PlatformHttpException_when_dataelement_not_found() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; - [Fact] - public async Task UpdateData_throws_error_if_serilization_fails() - { - object exampleModel = new ExampleModel() { Name = "Test", Age = 22 }; - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - int invocations = 0; - var dataClient = GetDataClient( - async (request, token) => - { - invocations++; - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; - } - ); - await Assert.ThrowsAsync( - async () => - await dataClient.UpdateData( - exampleModel, - instanceIdentifier.InstanceGuid, - typeof(DataElement), - "ttd", - "app", - instanceIdentifier.InstanceOwnerPartyId, - dataGuid - ) - ); - invocations.Should().Be(0); - } + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }; + } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}?delay=False", + UriKind.RelativeOrAbsolute + ); + var actual = await Assert.ThrowsAsync( + async () => + await dataClient.DeleteBinaryData( + "ttd", + "app", + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + dataGuid + ) + ); + invocations.Should().Be(1); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + actual.Response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } - [Fact] - public async Task UpdateData_throws_platformhttpexception_if_platform_request_fails() - { - object exampleModel = new ExampleModel() { Name = "Test", Age = 22 }; - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - HttpRequestMessage? platformRequest = null; - int invocations = 0; - var dataClient = GetDataClient( - async (request, token) => - { - invocations++; - platformRequest = request; - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", - UriKind.RelativeOrAbsolute - ); - var result = await Assert.ThrowsAsync( - async () => - await dataClient.UpdateData( - exampleModel, - instanceIdentifier.InstanceGuid, - typeof(ExampleModel), - "ttd", - "app", - instanceIdentifier.InstanceOwnerPartyId, - dataGuid - ) - ); - invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, null, "application/xml"); - result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); - } + [Fact] + public async Task DeleteData_returns_true_when_data_was_deleted_with_delay_true() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient( + async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; - [Fact] - public async Task LockDataElement_calls_lock_endpoint_in_storage_and_returns_updated_DataElement() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - HttpRequestMessage? platformRequest = null; - int invocations = 0; - DataElement dataElement = new() { Id = "67a5ef12-6e38-41f8-8b42-f91249ebcec0", Locked = true }; - var dataClient = GetDataClient( - async (request, token) => - { - invocations++; - platformRequest = request; - await Task.CompletedTask; - return new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent("{\"id\":\"67a5ef12-6e38-41f8-8b42-f91249ebcec0\",\"locked\":true}") - }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", - UriKind.RelativeOrAbsolute - ); - var response = await dataClient.LockDataElement(instanceIdentifier, dataGuid); - invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - response.Should().BeEquivalentTo(dataElement); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put); - } + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; + } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}?delay=True", + UriKind.RelativeOrAbsolute + ); + var result = await dataClient.DeleteData( + "ttd", + "app", + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + dataGuid, + true + ); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + result.Should().BeTrue(); + } - [Fact] - public async Task LockDataElement_throws_platformhttpexception_if_platform_request_fails() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - int invocations = 0; - HttpRequestMessage? platformRequest = null; - var dataClient = GetDataClient( - async (request, token) => - { - invocations++; - platformRequest = request; - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", - UriKind.RelativeOrAbsolute - ); - var result = await Assert.ThrowsAsync( - async () => await dataClient.LockDataElement(instanceIdentifier, dataGuid) - ); - invocations.Should().Be(1); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put); - result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); - } + [Fact] + public async Task UpdateData_serializes_and_updates_formdata() + { + ExampleModel exampleModel = new ExampleModel() { Name = "Test", Age = 22 }; + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient( + async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; + } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", + UriKind.RelativeOrAbsolute + ); + await dataClient.UpdateData( + exampleModel, + instanceIdentifier.InstanceGuid, + exampleModel.GetType(), + "ttd", + "app", + instanceIdentifier.InstanceOwnerPartyId, + dataGuid + ); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, null, "application/xml"); + } - [Fact] - public async Task UnlockDataElement_calls_lock_endpoint_in_storage_and_returns_updated_DataElement() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - HttpRequestMessage? platformRequest = null; - int invocations = 0; - DataElement dataElement = new() { Id = "67a5ef12-6e38-41f8-8b42-f91249ebcec0", Locked = true }; - var dataClient = GetDataClient( - async (request, token) => - { - invocations++; - platformRequest = request; - await Task.CompletedTask; - return new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent("{\"id\":\"67a5ef12-6e38-41f8-8b42-f91249ebcec0\",\"locked\":true}") - }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", - UriKind.RelativeOrAbsolute - ); - var response = await dataClient.UnlockDataElement(instanceIdentifier, dataGuid); - invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - response.Should().BeEquivalentTo(dataElement); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); - } + [Fact] + public async Task UpdateData_throws_error_if_serilization_fails() + { + object exampleModel = new ExampleModel() { Name = "Test", Age = 22 }; + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + var dataClient = GetDataClient( + async (request, token) => + { + invocations++; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; + } + ); + await Assert.ThrowsAsync( + async () => + await dataClient.UpdateData( + exampleModel, + instanceIdentifier.InstanceGuid, + typeof(DataElement), + "ttd", + "app", + instanceIdentifier.InstanceOwnerPartyId, + dataGuid + ) + ); + invocations.Should().Be(0); + } - [Fact] - public async Task UnlockDataElement_throws_platformhttpexception_if_platform_request_fails() - { - var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); - var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); - int invocations = 0; - HttpRequestMessage? platformRequest = null; - var dataClient = GetDataClient( - async (request, token) => + [Fact] + public async Task UpdateData_throws_platformhttpexception_if_platform_request_fails() + { + object exampleModel = new ExampleModel() { Name = "Test", Age = 22 }; + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient( + async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", + UriKind.RelativeOrAbsolute + ); + var result = await Assert.ThrowsAsync( + async () => + await dataClient.UpdateData( + exampleModel, + instanceIdentifier.InstanceGuid, + typeof(ExampleModel), + "ttd", + "app", + instanceIdentifier.InstanceOwnerPartyId, + dataGuid + ) + ); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, null, "application/xml"); + result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + + [Fact] + public async Task LockDataElement_calls_lock_endpoint_in_storage_and_returns_updated_DataElement() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + DataElement dataElement = new() { Id = "67a5ef12-6e38-41f8-8b42-f91249ebcec0", Locked = true }; + var dataClient = GetDataClient( + async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { - invocations++; - platformRequest = request; - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; - } - ); - var expectedUri = new Uri( - $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", - UriKind.RelativeOrAbsolute - ); - var result = await Assert.ThrowsAsync( - async () => await dataClient.UnlockDataElement(instanceIdentifier, dataGuid) - ); - invocations.Should().Be(1); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); - result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); - } + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"id\":\"67a5ef12-6e38-41f8-8b42-f91249ebcec0\",\"locked\":true}") + }; + } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", + UriKind.RelativeOrAbsolute + ); + var response = await dataClient.LockDataElement(instanceIdentifier, dataGuid); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + response.Should().BeEquivalentTo(dataElement); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put); + } - private DataClient GetDataClient( - Func> handlerFunc, - TelemetrySink? telemetrySink = null - ) - { - DelegatingHandlerStub delegatingHandlerStub = new(handlerFunc); - return new DataClient( - platformSettingsOptions.Object, - logger, - new HttpClient(delegatingHandlerStub), - userTokenProvide.Object, - telemetrySink?.Object - ); - } + [Fact] + public async Task LockDataElement_throws_platformhttpexception_if_platform_request_fails() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + HttpRequestMessage? platformRequest = null; + var dataClient = GetDataClient( + async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", + UriKind.RelativeOrAbsolute + ); + var result = await Assert.ThrowsAsync( + async () => await dataClient.LockDataElement(instanceIdentifier, dataGuid) + ); + invocations.Should().Be(1); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put); + result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } - private void AssertHttpRequest( - HttpRequestMessage actual, - Uri expectedUri, - HttpMethod method, - string? expectedFilename = null, - string? expectedContentType = null - ) - { - IEnumerable? actualContentType = null; - IEnumerable? actualContentDisposition = null; - actual.Content?.Headers.TryGetValues("Content-Type", out actualContentType); - actual.Content?.Headers.TryGetValues("Content-Disposition", out actualContentDisposition); - var authHeader = actual.Headers.Authorization; - actual.RequestUri.Should().BeEquivalentTo(expectedUri); - actual.Method.Should().BeEquivalentTo(method); - Uri.Compare( - actual.RequestUri, - expectedUri, - UriComponents.HttpRequestUrl, - UriFormat.SafeUnescaped, - StringComparison.OrdinalIgnoreCase - ) - .Should() - .Be(0, "Actual request Uri did not match expected Uri"); - if (expectedContentType is not null) + [Fact] + public async Task UnlockDataElement_calls_lock_endpoint_in_storage_and_returns_updated_DataElement() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + DataElement dataElement = new() { Id = "67a5ef12-6e38-41f8-8b42-f91249ebcec0", Locked = true }; + var dataClient = GetDataClient( + async (request, token) => { - actualContentType?.FirstOrDefault().Should().BeEquivalentTo(expectedContentType); + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"id\":\"67a5ef12-6e38-41f8-8b42-f91249ebcec0\",\"locked\":true}") + }; } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", + UriKind.RelativeOrAbsolute + ); + var response = await dataClient.UnlockDataElement(instanceIdentifier, dataGuid); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + response.Should().BeEquivalentTo(dataElement); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + } - if (expectedFilename is not null) + [Fact] + public async Task UnlockDataElement_throws_platformhttpexception_if_platform_request_fails() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + HttpRequestMessage? platformRequest = null; + var dataClient = GetDataClient( + async (request, token) => { - Assert.NotNull(actualContentDisposition); - var actualContentDispositionValue = actualContentDisposition.FirstOrDefault(); - Assert.NotNull(actualContentDispositionValue); - ContentDispositionHeaderValue - .Parse(actualContentDispositionValue) - .FileName?.Should() - .BeEquivalentTo(expectedFilename); + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; } + ); + var expectedUri = new Uri( + $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", + UriKind.RelativeOrAbsolute + ); + var result = await Assert.ThrowsAsync( + async () => await dataClient.UnlockDataElement(instanceIdentifier, dataGuid) + ); + invocations.Should().Be(1); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + + private DataClient GetDataClient( + Func> handlerFunc, + TelemetrySink? telemetrySink = null + ) + { + DelegatingHandlerStub delegatingHandlerStub = new(handlerFunc); + return new DataClient( + platformSettingsOptions.Object, + logger, + new HttpClient(delegatingHandlerStub), + userTokenProvide.Object, + telemetrySink?.Object + ); + } - authHeader?.Parameter.Should().BeEquivalentTo("dummytesttoken"); + private void AssertHttpRequest( + HttpRequestMessage actual, + Uri expectedUri, + HttpMethod method, + string? expectedFilename = null, + string? expectedContentType = null + ) + { + IEnumerable? actualContentType = null; + IEnumerable? actualContentDisposition = null; + actual.Content?.Headers.TryGetValues("Content-Type", out actualContentType); + actual.Content?.Headers.TryGetValues("Content-Disposition", out actualContentDisposition); + var authHeader = actual.Headers.Authorization; + actual.RequestUri.Should().BeEquivalentTo(expectedUri); + actual.Method.Should().BeEquivalentTo(method); + Uri.Compare( + actual.RequestUri, + expectedUri, + UriComponents.HttpRequestUrl, + UriFormat.SafeUnescaped, + StringComparison.OrdinalIgnoreCase + ) + .Should() + .Be(0, "Actual request Uri did not match expected Uri"); + if (expectedContentType is not null) + { + actualContentType?.FirstOrDefault().Should().BeEquivalentTo(expectedContentType); } + + if (expectedFilename is not null) + { + Assert.NotNull(actualContentDisposition); + var actualContentDispositionValue = actualContentDisposition.FirstOrDefault(); + Assert.NotNull(actualContentDispositionValue); + ContentDispositionHeaderValue + .Parse(actualContentDispositionValue) + .FileName?.Should() + .BeEquivalentTo(expectedFilename); + } + + authHeader?.Parameter.Should().BeEquivalentTo("dummytesttoken"); } } diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs index 218ff9f3a..879aa90f3 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs @@ -10,7 +10,6 @@ using FluentAssertions; using Microsoft.Extensions.Options; using Moq; -using Xunit; using Signee = Altinn.App.Core.Internal.Sign.Signee; namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage; diff --git a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs index bf618b42a..74a0af033 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs @@ -3,7 +3,6 @@ using System.Text.Json; using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Features; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; @@ -11,571 +10,552 @@ using Microsoft.Extensions.Options; using Microsoft.FeatureManagement; using Moq; -using Xunit; using JsonSerializer = System.Text.Json.JsonSerializer; -namespace Altinn.App.Core.Tests.Internal.App +namespace Altinn.App.Core.Tests.Internal.App; + +public class AppMedataTest { - public class AppMedataTest - { - private static readonly JsonSerializerOptions _jsonSerializerOptions = - new() { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; - private readonly string appBasePath = Path.Combine("Internal", "App", "TestData") + Path.DirectorySeparatorChar; + private readonly string appBasePath = Path.Combine("Internal", "App", "TestData") + Path.DirectorySeparatorChar; - [Fact] - public async Task GetApplicationMetadata_desrializes_file_from_disk() - { - var featureManagerMock = new Mock(); - FrontendFeatures frontendFeatures = new(featureManagerMock.Object); - Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); - TelemetrySink telemetrySink = new(); + [Fact] + public async Task GetApplicationMetadata_desrializes_file_from_disk() + { + var featureManagerMock = new Mock(); + FrontendFeatures frontendFeatures = new(featureManagerMock.Object); + Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + TelemetrySink telemetrySink = new(); - AppSettings appSettings = GetAppSettings("AppMetadata", "default.applicationmetadata.json"); - IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings), null, telemetrySink); - ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + AppSettings appSettings = GetAppSettings("AppMetadata", "default.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings), null, telemetrySink); + ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + { + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, + DataTypes = new List() { - Id = "tdd/bestilling", - Org = "tdd", - Created = DateTime.Parse("2019-09-16T22:22:22"), - CreatedBy = "username", - Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, - DataTypes = new List() + new() { - new() - { - Id = "vedlegg", - AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, - MinCount = 0, - TaskId = "Task_1" - }, - new() - { - Id = "ref-data-as-pdf", - AllowedContentTypes = new List() { "application/pdf" }, - MinCount = 1, - TaskId = "Task_1" - } + Id = "vedlegg", + AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, + MinCount = 0, + TaskId = "Task_1" }, - PartyTypesAllowed = new PartyTypesAllowed() + new() { - BankruptcyEstate = true, - Organisation = true, - Person = true, - SubUnit = true - }, - OnEntry = new OnEntry() { Show = "select-instance" }, - Features = enabledFrontendFeatures - }; - var actual = await appMetadata.GetApplicationMetadata(); - actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(expected); + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + }, + OnEntry = new OnEntry() { Show = "select-instance" }, + Features = enabledFrontendFeatures + }; + var actual = await appMetadata.GetApplicationMetadata(); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); - await Verify(telemetrySink.GetSnapshot()); - } + await Verify(telemetrySink.GetSnapshot()); + } - [Fact] - public async Task GetApplicationMetadata_eformidling_desrializes_file_from_disk() - { - var featureManagerMock = new Mock(); - IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); - Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + [Fact] + public async Task GetApplicationMetadata_eformidling_desrializes_file_from_disk() + { + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); - AppSettings appSettings = GetAppSettings("AppMetadata", "eformid.applicationmetadata.json"); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + AppSettings appSettings = GetAppSettings("AppMetadata", "eformid.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + { + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, + DataTypes = new List() { - Id = "tdd/bestilling", - Org = "tdd", - Created = DateTime.Parse("2019-09-16T22:22:22"), - CreatedBy = "username", - Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, - DataTypes = new List() - { - new() - { - Id = "vedlegg", - AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, - MinCount = 0, - TaskId = "Task_1" - }, - new() - { - Id = "ref-data-as-pdf", - AllowedContentTypes = new List() { "application/pdf" }, - MinCount = 1, - TaskId = "Task_1" - } - }, - PartyTypesAllowed = new PartyTypesAllowed() + new() { - BankruptcyEstate = true, - Organisation = true, - Person = true, - SubUnit = true + Id = "vedlegg", + AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, + MinCount = 0, + TaskId = "Task_1" }, - EFormidling = new EFormidlingContract() + new() { - ServiceId = "DPF", - DPFShipmentType = "altinn3.skjema", - Receiver = "910123456", - SendAfterTaskId = "Task_1", - Process = "urn:no:difi:profile:arkivmelding:administrasjon:ver1.0", - Standard = "urn:no:difi:arkivmelding:xsd::arkivmelding", - TypeVersion = "2.0", - Type = "arkivmelding", - SecurityLevel = 3, - DataTypes = new List() { "372c7af5-71e1-4e99-8e05-4716711a8b53", } - }, - OnEntry = new OnEntry() { Show = "select-instance" }, - Features = enabledFrontendFeatures - }; - var actual = await appMetadata.GetApplicationMetadata(); - actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(expected); - } + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + }, + EFormidling = new EFormidlingContract() + { + ServiceId = "DPF", + DPFShipmentType = "altinn3.skjema", + Receiver = "910123456", + SendAfterTaskId = "Task_1", + Process = "urn:no:difi:profile:arkivmelding:administrasjon:ver1.0", + Standard = "urn:no:difi:arkivmelding:xsd::arkivmelding", + TypeVersion = "2.0", + Type = "arkivmelding", + SecurityLevel = 3, + DataTypes = new List() { "372c7af5-71e1-4e99-8e05-4716711a8b53", } + }, + OnEntry = new OnEntry() { Show = "select-instance" }, + Features = enabledFrontendFeatures + }; + var actual = await appMetadata.GetApplicationMetadata(); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + } - [Fact] - public async Task GetApplicationMetadata_second_read_from_cache() + [Fact] + public async Task GetApplicationMetadata_second_read_from_cache() + { + AppSettings appSettings = GetAppSettings("AppMetadata", "default.applicationmetadata.json"); + Mock appFeaturesMock = new Mock(); + appFeaturesMock + .Setup(af => af.GetFrontendFeatures()) + .ReturnsAsync(new Dictionary() { { "footer", true } }); + IAppMetadata appMetadata = SetupAppMedata( + Microsoft.Extensions.Options.Options.Create(appSettings), + appFeaturesMock.Object + ); + ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") { - AppSettings appSettings = GetAppSettings("AppMetadata", "default.applicationmetadata.json"); - Mock appFeaturesMock = new Mock(); - appFeaturesMock - .Setup(af => af.GetFrontendFeatures()) - .ReturnsAsync(new Dictionary() { { "footer", true } }); - IAppMetadata appMetadata = SetupAppMedata( - Microsoft.Extensions.Options.Options.Create(appSettings), - appFeaturesMock.Object - ); - ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, + DataTypes = new List() { - Id = "tdd/bestilling", - Org = "tdd", - Created = DateTime.Parse("2019-09-16T22:22:22"), - CreatedBy = "username", - Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, - DataTypes = new List() + new() { - new() - { - Id = "vedlegg", - AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, - MinCount = 0, - TaskId = "Task_1" - }, - new() - { - Id = "ref-data-as-pdf", - AllowedContentTypes = new List() { "application/pdf" }, - MinCount = 1, - TaskId = "Task_1" - } + Id = "vedlegg", + AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, + MinCount = 0, + TaskId = "Task_1" }, - PartyTypesAllowed = new PartyTypesAllowed() + new() { - BankruptcyEstate = true, - Organisation = true, - Person = true, - SubUnit = true - }, - OnEntry = new OnEntry() { Show = "select-instance" }, - Features = new Dictionary() { { "footer", true } } - }; - var actual = await appMetadata.GetApplicationMetadata(); - var actual2 = await appMetadata.GetApplicationMetadata(); - appFeaturesMock.Verify(af => af.GetFrontendFeatures()); - appFeaturesMock.VerifyAll(); - actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(expected); - actual2.Should().BeEquivalentTo(expected); - } + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + }, + OnEntry = new OnEntry() { Show = "select-instance" }, + Features = new Dictionary() { { "footer", true } } + }; + var actual = await appMetadata.GetApplicationMetadata(); + var actual2 = await appMetadata.GetApplicationMetadata(); + appFeaturesMock.Verify(af => af.GetFrontendFeatures()); + appFeaturesMock.VerifyAll(); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + actual2.Should().BeEquivalentTo(expected); + } - [Fact] - public async Task GetApplicationMetadata_onEntry_InstanceSelection_DefaultSelectedOption_read_legacy_value_if_new_not_set() - { - var featureManagerMock = new Mock(); - IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); - Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + [Fact] + public async Task GetApplicationMetadata_onEntry_InstanceSelection_DefaultSelectedOption_read_legacy_value_if_new_not_set() + { + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); - AppSettings appSettings = GetAppSettings( - "AppMetadata", - "onentry-legacy-selectoptions.applicationmetadata.json" - ); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + AppSettings appSettings = GetAppSettings( + "AppMetadata", + "onentry-legacy-selectoptions.applicationmetadata.json" + ); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + { + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, + DataTypes = new List() { - Id = "tdd/bestilling", - Org = "tdd", - Created = DateTime.Parse("2019-09-16T22:22:22"), - CreatedBy = "username", - Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, - DataTypes = new List() + new() { - new() - { - Id = "vedlegg", - AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, - MinCount = 0, - TaskId = "Task_1" - }, - new() - { - Id = "ref-data-as-pdf", - AllowedContentTypes = new List() { "application/pdf" }, - MinCount = 1, - TaskId = "Task_1" - } + Id = "vedlegg", + AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, + MinCount = 0, + TaskId = "Task_1" }, - PartyTypesAllowed = new PartyTypesAllowed() + new() { - BankruptcyEstate = true, - Organisation = true, - Person = true, - SubUnit = true - }, - OnEntry = new OnEntry() + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + }, + OnEntry = new OnEntry() + { + Show = "select-instance", + InstanceSelection = new() { - Show = "select-instance", - InstanceSelection = new() - { - SortDirection = "desc", - RowsPerPageOptions = new List() { 5, 3, 10, 25, 50, 100 }, - DefaultRowsPerPage = 1, - DefaultSelectedOption = 1 - } - }, - Features = enabledFrontendFeatures - }; - var actual = await appMetadata.GetApplicationMetadata(); - actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(expected); - actual.OnEntry?.InstanceSelection?.DefaultSelectedOption.Should().Be(1); - } + SortDirection = "desc", + RowsPerPageOptions = new List() { 5, 3, 10, 25, 50, 100 }, + DefaultRowsPerPage = 1, + DefaultSelectedOption = 1 + } + }, + Features = enabledFrontendFeatures + }; + var actual = await appMetadata.GetApplicationMetadata(); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + actual.OnEntry?.InstanceSelection?.DefaultSelectedOption.Should().Be(1); + } - [Fact] - public async Task GetApplicationMetadata_onEntry_supports_new_option() - { - var featureManagerMock = new Mock(); - IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); - Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + [Fact] + public async Task GetApplicationMetadata_onEntry_supports_new_option() + { + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); - AppSettings appSettings = GetAppSettings( - "AppMetadata", - "onentry-new-selectoptions.applicationmetadata.json" - ); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + AppSettings appSettings = GetAppSettings("AppMetadata", "onentry-new-selectoptions.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + { + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, + DataTypes = new List() { - Id = "tdd/bestilling", - Org = "tdd", - Created = DateTime.Parse("2019-09-16T22:22:22"), - CreatedBy = "username", - Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, - DataTypes = new List() + new() { - new() - { - Id = "vedlegg", - AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, - MinCount = 0, - TaskId = "Task_1" - }, - new() - { - Id = "ref-data-as-pdf", - AllowedContentTypes = new List() { "application/pdf" }, - MinCount = 1, - TaskId = "Task_1" - } + Id = "vedlegg", + AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, + MinCount = 0, + TaskId = "Task_1" }, - PartyTypesAllowed = new PartyTypesAllowed() + new() { - BankruptcyEstate = true, - Organisation = true, - Person = true, - SubUnit = true - }, - OnEntry = new OnEntry() + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + }, + OnEntry = new OnEntry() + { + Show = "select-instance", + InstanceSelection = new() { - Show = "select-instance", - InstanceSelection = new() - { - SortDirection = "desc", - RowsPerPageOptions = new List() { 5, 3, 10, 25, 50, 100 }, - DefaultSelectedOption = 2 - } - }, - Features = enabledFrontendFeatures - }; - var actual = await appMetadata.GetApplicationMetadata(); - actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(expected); - actual.OnEntry?.InstanceSelection?.DefaultSelectedOption.Should().Be(2); - } + SortDirection = "desc", + RowsPerPageOptions = new List() { 5, 3, 10, 25, 50, 100 }, + DefaultSelectedOption = 2 + } + }, + Features = enabledFrontendFeatures + }; + var actual = await appMetadata.GetApplicationMetadata(); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + actual.OnEntry?.InstanceSelection?.DefaultSelectedOption.Should().Be(2); + } - [Fact] - public async Task GetApplicationMetadata_onEntry_prefer_new_option() - { - var featureManagerMock = new Mock(); - IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); - Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + [Fact] + public async Task GetApplicationMetadata_onEntry_prefer_new_option() + { + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); - AppSettings appSettings = GetAppSettings( - "AppMetadata", - "onentry-prefer-new-selectoptions.applicationmetadata.json" - ); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + AppSettings appSettings = GetAppSettings( + "AppMetadata", + "onentry-prefer-new-selectoptions.applicationmetadata.json" + ); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + { + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, + DataTypes = new List() { - Id = "tdd/bestilling", - Org = "tdd", - Created = DateTime.Parse("2019-09-16T22:22:22"), - CreatedBy = "username", - Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, - DataTypes = new List() + new() { - new() - { - Id = "vedlegg", - AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, - MinCount = 0, - TaskId = "Task_1" - }, - new() - { - Id = "ref-data-as-pdf", - AllowedContentTypes = new List() { "application/pdf" }, - MinCount = 1, - TaskId = "Task_1" - } + Id = "vedlegg", + AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, + MinCount = 0, + TaskId = "Task_1" }, - PartyTypesAllowed = new PartyTypesAllowed() + new() { - BankruptcyEstate = true, - Organisation = true, - Person = true, - SubUnit = true - }, - OnEntry = new OnEntry() + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + }, + OnEntry = new OnEntry() + { + Show = "select-instance", + InstanceSelection = new() { - Show = "select-instance", - InstanceSelection = new() - { - SortDirection = "desc", - RowsPerPageOptions = new List() { 5, 3, 10, 25, 50, 100 }, - DefaultRowsPerPage = 1, - DefaultSelectedOption = 3 - } - }, - Features = enabledFrontendFeatures - }; - var actual = await appMetadata.GetApplicationMetadata(); - actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(expected); - actual.OnEntry?.InstanceSelection?.DefaultSelectedOption.Should().Be(3); - } + SortDirection = "desc", + RowsPerPageOptions = new List() { 5, 3, 10, 25, 50, 100 }, + DefaultRowsPerPage = 1, + DefaultSelectedOption = 3 + } + }, + Features = enabledFrontendFeatures + }; + var actual = await appMetadata.GetApplicationMetadata(); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + actual.OnEntry?.InstanceSelection?.DefaultSelectedOption.Should().Be(3); + } - [Fact] - public async Task GetApplicationMetadata_logo_can_intstantiate_with_source_and_DisplayAppOwnerNameInHeader() - { - var featureManagerMock = new Mock(); - IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); - Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + [Fact] + public async Task GetApplicationMetadata_logo_can_intstantiate_with_source_and_DisplayAppOwnerNameInHeader() + { + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); - AppSettings appSettings = GetAppSettings("AppMetadata", "logo-org-source.applicationmetadata.json"); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + AppSettings appSettings = GetAppSettings("AppMetadata", "logo-org-source.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + { + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, + DataTypes = new List() { - Id = "tdd/bestilling", - Org = "tdd", - Created = DateTime.Parse("2019-09-16T22:22:22"), - CreatedBy = "username", - Title = new Dictionary() { { "nb", "Bestillingseksempelapp" } }, - DataTypes = new List() + new() { - new() - { - Id = "vedlegg", - AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, - MinCount = 0, - TaskId = "Task_1" - }, - new() - { - Id = "ref-data-as-pdf", - AllowedContentTypes = new List() { "application/pdf" }, - MinCount = 1, - TaskId = "Task_1" - } + Id = "vedlegg", + AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, + MinCount = 0, + TaskId = "Task_1" }, - PartyTypesAllowed = new PartyTypesAllowed() + new() { - BankruptcyEstate = true, - Organisation = true, - Person = true, - SubUnit = true - }, - OnEntry = new OnEntry() - { - Show = "select-instance", - InstanceSelection = new() - { - SortDirection = "desc", - RowsPerPageOptions = new List() { 5, 3, 10, 25, 50, 100 }, - DefaultRowsPerPage = 1, - DefaultSelectedOption = 3 - } - }, - Logo = new Logo + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + }, + OnEntry = new OnEntry() + { + Show = "select-instance", + InstanceSelection = new() { - Source = "org", - DisplayAppOwnerNameInHeader = true, - Size = "medium" - }, - Features = enabledFrontendFeatures - }; - var actual = await appMetadata.GetApplicationMetadata(); - actual.Should().NotBeNull(); - actual.Should().BeEquivalentTo(expected); - } + SortDirection = "desc", + RowsPerPageOptions = new List() { 5, 3, 10, 25, 50, 100 }, + DefaultRowsPerPage = 1, + DefaultSelectedOption = 3 + } + }, + Logo = new Logo + { + Source = "org", + DisplayAppOwnerNameInHeader = true, + Size = "medium" + }, + Features = enabledFrontendFeatures + }; + var actual = await appMetadata.GetApplicationMetadata(); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + } - [Fact] - public async Task GetApplicationMetadata_deserializes_unmapped_properties() - { - AppSettings appSettings = GetAppSettings("AppMetadata", "unmapped-properties.applicationmetadata.json"); - IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); - var actual = await appMetadata.GetApplicationMetadata(); - actual.Should().NotBeNull(); - actual.UnmappedProperties.Should().NotBeNull(); - actual.UnmappedProperties!["foo"].Should().BeOfType(); - ((JsonElement)actual.UnmappedProperties["foo"]).GetProperty("bar").GetString().Should().Be("baz"); - } + [Fact] + public async Task GetApplicationMetadata_deserializes_unmapped_properties() + { + AppSettings appSettings = GetAppSettings("AppMetadata", "unmapped-properties.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); + var actual = await appMetadata.GetApplicationMetadata(); + actual.Should().NotBeNull(); + actual.UnmappedProperties.Should().NotBeNull(); + actual.UnmappedProperties!["foo"].Should().BeOfType(); + ((JsonElement)actual.UnmappedProperties["foo"]).GetProperty("bar").GetString().Should().Be("baz"); + } - [Fact] - public async Task GetApplicationMetadata_deserialize_serialize_unmapped_properties() - { - AppSettings appSettings = GetAppSettings("AppMetadata", "unmapped-properties.applicationmetadata.json"); - IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); - var appMetadataObj = await appMetadata.GetApplicationMetadata(); - string serialized = JsonSerializer.Serialize(appMetadataObj, _jsonSerializerOptions); - string expected = File.ReadAllText( - Path.Join(appBasePath, "AppMetadata", "unmapped-properties.applicationmetadata.expected.json") - ); - expected = expected.Replace( - "--AltinnNugetVersion--", - typeof(ApplicationMetadata).Assembly!.GetName().Version!.ToString() - ); - serialized.Should().Be(expected); - } + [Fact] + public async Task GetApplicationMetadata_deserialize_serialize_unmapped_properties() + { + AppSettings appSettings = GetAppSettings("AppMetadata", "unmapped-properties.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); + var appMetadataObj = await appMetadata.GetApplicationMetadata(); + string serialized = JsonSerializer.Serialize(appMetadataObj, _jsonSerializerOptions); + string expected = File.ReadAllText( + Path.Join(appBasePath, "AppMetadata", "unmapped-properties.applicationmetadata.expected.json") + ); + expected = expected.Replace( + "--AltinnNugetVersion--", + typeof(ApplicationMetadata).Assembly!.GetName().Version!.ToString() + ); + serialized.Should().Be(expected); + } - [Fact] - public async Task GetApplicationMetadata_throws_ApplicationConfigException_if_file_not_found() - { - AppSettings appSettings = GetAppSettings("AppMetadata", "notfound.applicationmetadata.json"); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - await Assert.ThrowsAsync( - async () => await appMetadata.GetApplicationMetadata() - ); - } + [Fact] + public async Task GetApplicationMetadata_throws_ApplicationConfigException_if_file_not_found() + { + AppSettings appSettings = GetAppSettings("AppMetadata", "notfound.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + await Assert.ThrowsAsync(async () => await appMetadata.GetApplicationMetadata()); + } - [Fact] - public async Task GetApplicationMetadata_throw_ApplicationConfigException_if_deserialization_fails() - { - AppSettings appSettings = GetAppSettings("AppMetadata", "invalid.applicationmetadata.json"); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - await Assert.ThrowsAsync( - async () => await appMetadata.GetApplicationMetadata() - ); - } + [Fact] + public async Task GetApplicationMetadata_throw_ApplicationConfigException_if_deserialization_fails() + { + AppSettings appSettings = GetAppSettings("AppMetadata", "invalid.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + await Assert.ThrowsAsync(async () => await appMetadata.GetApplicationMetadata()); + } - [Fact] - public async Task GetApplicationMetadata_throws_ApplicationConfigException_if_deserialization_fails_due_to_string_in_int() - { - AppSettings appSettings = GetAppSettings("AppMetadata", "invalid-int.applicationmetadata.json"); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - await Assert.ThrowsAsync( - async () => await appMetadata.GetApplicationMetadata() - ); - } + [Fact] + public async Task GetApplicationMetadata_throws_ApplicationConfigException_if_deserialization_fails_due_to_string_in_int() + { + AppSettings appSettings = GetAppSettings("AppMetadata", "invalid-int.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + await Assert.ThrowsAsync(async () => await appMetadata.GetApplicationMetadata()); + } - [Fact] - public async Task GetApplicationXACMLPolicy_return_policyfile_as_string() - { - AppSettings appSettings = GetAppSettings(subfolder: "AppPolicy", policyFilename: "policy.xml"); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - string expected = - "" + Environment.NewLine + "policy"; - var actual = await appMetadata.GetApplicationXACMLPolicy(); - actual.Should().BeEquivalentTo(expected); - } + [Fact] + public async Task GetApplicationXACMLPolicy_return_policyfile_as_string() + { + AppSettings appSettings = GetAppSettings(subfolder: "AppPolicy", policyFilename: "policy.xml"); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + string expected = "" + Environment.NewLine + "policy"; + var actual = await appMetadata.GetApplicationXACMLPolicy(); + actual.Should().BeEquivalentTo(expected); + } - [Fact] - public async Task GetApplicationXACMLPolicy_throws_FileNotFoundException_if_file_not_found() - { - AppSettings appSettings = GetAppSettings(subfolder: "AppPolicy", policyFilename: "notfound.xml"); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - await Assert.ThrowsAsync(async () => await appMetadata.GetApplicationXACMLPolicy()); - } + [Fact] + public async Task GetApplicationXACMLPolicy_throws_FileNotFoundException_if_file_not_found() + { + AppSettings appSettings = GetAppSettings(subfolder: "AppPolicy", policyFilename: "notfound.xml"); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + await Assert.ThrowsAsync(async () => await appMetadata.GetApplicationXACMLPolicy()); + } - [Fact] - public async Task GetApplicationBPMNProcess_return_process_as_string() - { - AppSettings appSettings = GetAppSettings(subfolder: "AppProcess", bpmnFilename: "process.bpmn"); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - string expected = - "" + Environment.NewLine + "process"; - var actual = await appMetadata.GetApplicationBPMNProcess(); - actual.Should().BeEquivalentTo(expected); - } + [Fact] + public async Task GetApplicationBPMNProcess_return_process_as_string() + { + AppSettings appSettings = GetAppSettings(subfolder: "AppProcess", bpmnFilename: "process.bpmn"); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + string expected = "" + Environment.NewLine + "process"; + var actual = await appMetadata.GetApplicationBPMNProcess(); + actual.Should().BeEquivalentTo(expected); + } - [Fact] - public async Task GetApplicationBPMNProcess_throws_ApplicationConfigException_if_file_not_found() - { - AppSettings appSettings = GetAppSettings(subfolder: "AppProcess", policyFilename: "notfound.xml"); - IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); - await Assert.ThrowsAsync( - async () => await appMetadata.GetApplicationBPMNProcess() - ); - } + [Fact] + public async Task GetApplicationBPMNProcess_throws_ApplicationConfigException_if_file_not_found() + { + AppSettings appSettings = GetAppSettings(subfolder: "AppProcess", policyFilename: "notfound.xml"); + IAppMetadata appMetadata = SetupAppMedata(Microsoft.Extensions.Options.Options.Create(appSettings)); + await Assert.ThrowsAsync(async () => await appMetadata.GetApplicationBPMNProcess()); + } - private AppSettings GetAppSettings( - string subfolder, - string appMetadataFilename = "", - string bpmnFilename = "", - string policyFilename = "" - ) + private AppSettings GetAppSettings( + string subfolder, + string appMetadataFilename = "", + string bpmnFilename = "", + string policyFilename = "" + ) + { + AppSettings appSettings = new AppSettings() { - AppSettings appSettings = new AppSettings() - { - AppBasePath = appBasePath, - ConfigurationFolder = subfolder + Path.DirectorySeparatorChar, - AuthorizationFolder = string.Empty, - ProcessFolder = string.Empty, - ApplicationMetadataFileName = appMetadataFilename, - ProcessFileName = bpmnFilename, - ApplicationXACMLPolicyFileName = policyFilename - }; - return appSettings; - } + AppBasePath = appBasePath, + ConfigurationFolder = subfolder + Path.DirectorySeparatorChar, + AuthorizationFolder = string.Empty, + ProcessFolder = string.Empty, + ApplicationMetadataFileName = appMetadataFilename, + ProcessFileName = bpmnFilename, + ApplicationXACMLPolicyFileName = policyFilename + }; + return appSettings; + } - private static IAppMetadata SetupAppMedata( - IOptions appsettings, - IFrontendFeatures frontendFeatures = null, - TelemetrySink telemetrySink = null - ) + private static IAppMetadata SetupAppMedata( + IOptions appsettings, + IFrontendFeatures frontendFeatures = null, + TelemetrySink telemetrySink = null + ) + { + var featureManagerMock = new Mock(); + telemetrySink ??= new TelemetrySink(); + if (frontendFeatures == null) { - var featureManagerMock = new Mock(); - telemetrySink ??= new TelemetrySink(); - if (frontendFeatures == null) - { - return new AppMetadata( - appsettings, - new FrontendFeatures(featureManagerMock.Object), - telemetrySink.Object - ); - } - - return new AppMetadata(appsettings, frontendFeatures, telemetrySink.Object); + return new AppMetadata(appsettings, new FrontendFeatures(featureManagerMock.Object), telemetrySink.Object); } + + return new AppMetadata(appsettings, frontendFeatures, telemetrySink.Object); } } diff --git a/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs b/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs index 0a6b033d3..b5f6f5f74 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs @@ -4,43 +4,41 @@ using FluentAssertions; using Microsoft.FeatureManagement; using Moq; -using Xunit; -namespace Altinn.App.Core.Tests.Internal.App +namespace Altinn.App.Core.Tests.Internal.App; + +public class FrontendFeaturesTest { - public class FrontendFeaturesTest + [Fact] + public async Task GetFeatures_returns_list_of_enabled_features() { - [Fact] - public async Task GetFeatures_returns_list_of_enabled_features() + Dictionary expected = new Dictionary() { - Dictionary expected = new Dictionary() - { - { "footer", true }, - { "processActions", true }, - { "jsonObjectInDataResponse", false }, - }; - var featureManagerMock = new Mock(); - IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + { "footer", true }, + { "processActions", true }, + { "jsonObjectInDataResponse", false }, + }; + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); - var actual = await frontendFeatures.GetFrontendFeatures(); + var actual = await frontendFeatures.GetFrontendFeatures(); - actual.Should().BeEquivalentTo(expected); - } + actual.Should().BeEquivalentTo(expected); + } - [Fact] - public async Task GetFeatures_returns_list_of_enabled_features_when_feature_flag_is_enabled() + [Fact] + public async Task GetFeatures_returns_list_of_enabled_features_when_feature_flag_is_enabled() + { + Dictionary expected = new Dictionary() { - Dictionary expected = new Dictionary() - { - { "footer", true }, - { "processActions", true }, - { "jsonObjectInDataResponse", true }, - }; - var featureManagerMock = new Mock(); - featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse)).ReturnsAsync(true); - IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); - var actual = await frontendFeatures.GetFrontendFeatures(); - actual.Should().BeEquivalentTo(expected); - } + { "footer", true }, + { "processActions", true }, + { "jsonObjectInDataResponse", true }, + }; + var featureManagerMock = new Mock(); + featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse)).ReturnsAsync(true); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + var actual = await frontendFeatures.GetFrontendFeatures(); + actual.Should().BeEquivalentTo(expected); } } diff --git a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs index e198a65f8..a7b363eaf 100644 --- a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs @@ -11,7 +11,6 @@ using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Auth; diff --git a/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs index d32a44adc..27004e0e2 100644 --- a/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs @@ -4,217 +4,210 @@ using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Moq; -using Xunit; -namespace Altinn.App.Core.Tests.Internal.Data +namespace Altinn.App.Core.Tests.Internal.Data; + +public class DataServiceTests { - public class DataServiceTests - { - private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); + private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); - private readonly Mock _mockDataClient; - private readonly Mock _mockAppMetadata; - private readonly DataService _dataService; + private readonly Mock _mockDataClient; + private readonly Mock _mockAppMetadata; + private readonly DataService _dataService; - public DataServiceTests() - { - _mockDataClient = new Mock(); - _mockAppMetadata = new Mock(); - _dataService = new DataService(_mockDataClient.Object, _mockAppMetadata.Object); - } + public DataServiceTests() + { + _mockDataClient = new Mock(); + _mockAppMetadata = new Mock(); + _dataService = new DataService(_mockDataClient.Object, _mockAppMetadata.Object); + } - [Fact] - public async Task GetByType_ReturnsCorrectDataElementAndModel_WhenDataElementExists() - { - // Arrange - Instance instance = CreateInstance(); - InstanceIdentifier instanceIdentifier = new(instance); - const string dataType = "dataType"; - instance.Data = [new DataElement { DataType = dataType, Id = Guid.NewGuid().ToString() }]; - - ApplicationMetadata applicationMetadata = CreateAppMetadata(instance); - - TestModel expectedModel = new(); - using var referenceStream = new MemoryStream(); - await JsonSerializer.SerializeAsync(referenceStream, expectedModel, _jsonSerializerOptions); - referenceStream.Position = 0; - - _mockDataClient - .Setup(dc => - dc.GetBinaryData( - applicationMetadata.AppIdentifier.Org, - applicationMetadata.AppIdentifier.App, - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - new Guid(instance.Data.First().Id) - ) + [Fact] + public async Task GetByType_ReturnsCorrectDataElementAndModel_WhenDataElementExists() + { + // Arrange + Instance instance = CreateInstance(); + InstanceIdentifier instanceIdentifier = new(instance); + const string dataType = "dataType"; + instance.Data = [new DataElement { DataType = dataType, Id = Guid.NewGuid().ToString() }]; + + ApplicationMetadata applicationMetadata = CreateAppMetadata(instance); + + TestModel expectedModel = new(); + using var referenceStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(referenceStream, expectedModel, _jsonSerializerOptions); + referenceStream.Position = 0; + + _mockDataClient + .Setup(dc => + dc.GetBinaryData( + applicationMetadata.AppIdentifier.Org, + applicationMetadata.AppIdentifier.App, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + new Guid(instance.Data.First().Id) ) - .ReturnsAsync(referenceStream); + ) + .ReturnsAsync(referenceStream); + + _mockAppMetadata.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); - _mockAppMetadata.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + // Act + (Guid dataElementId, TestModel? model) = await _dataService.GetByType(instance, dataType); - // Act - (Guid dataElementId, TestModel? model) = await _dataService.GetByType(instance, dataType); + // Assert + Assert.Equal(instance.Data.First().Id, dataElementId.ToString()); + Assert.Equivalent(expectedModel, model); + } - // Assert - Assert.Equal(instance.Data.First().Id, dataElementId.ToString()); - Assert.Equivalent(expectedModel, model); - } + [Fact] + public async Task GetByType_ReturnsEmptyGuidAndNullModel_WhenDataElementDoesNotExist() + { + // Arrange + Instance instance = CreateInstance(); + const string dataType = "dataType"; + const string otherType = "otherType"; + instance.Data = [new DataElement() { DataType = otherType, Id = Guid.NewGuid().ToString() }]; + + // Act + (Guid dataElementId, TestModel? model) = await _dataService.GetByType(instance, dataType); + + // Assert + Assert.Equal(Guid.Empty, dataElementId); + Assert.Null(model); + } - [Fact] - public async Task GetByType_ReturnsEmptyGuidAndNullModel_WhenDataElementDoesNotExist() - { - // Arrange - Instance instance = CreateInstance(); - const string dataType = "dataType"; - const string otherType = "otherType"; - instance.Data = [new DataElement() { DataType = otherType, Id = Guid.NewGuid().ToString() }]; - - // Act - (Guid dataElementId, TestModel? model) = await _dataService.GetByType(instance, dataType); - - // Assert - Assert.Equal(Guid.Empty, dataElementId); - Assert.Null(model); - } - - [Fact] - public async Task GetById_ReturnsCorrectModel_WhenDataElementExists() - { - // Arrange - var expectedDataId = Guid.NewGuid(); - Instance instance = CreateInstance(); - InstanceIdentifier instanceIdentifier = new(instance); - instance.Data = [new DataElement { Id = expectedDataId.ToString() }]; - - ApplicationMetadata applicationMetadata = CreateAppMetadata(instance); - - var expectedModel = new TestModel(); - using var referenceStream = new MemoryStream(); - await JsonSerializer.SerializeAsync(referenceStream, expectedModel, _jsonSerializerOptions); - referenceStream.Position = 0; - - _mockDataClient - .Setup(dc => - dc.GetBinaryData( - applicationMetadata.AppIdentifier.Org, - applicationMetadata.AppIdentifier.App, - instanceIdentifier.InstanceOwnerPartyId, - instanceIdentifier.InstanceGuid, - expectedDataId - ) + [Fact] + public async Task GetById_ReturnsCorrectModel_WhenDataElementExists() + { + // Arrange + var expectedDataId = Guid.NewGuid(); + Instance instance = CreateInstance(); + InstanceIdentifier instanceIdentifier = new(instance); + instance.Data = [new DataElement { Id = expectedDataId.ToString() }]; + + ApplicationMetadata applicationMetadata = CreateAppMetadata(instance); + + var expectedModel = new TestModel(); + using var referenceStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(referenceStream, expectedModel, _jsonSerializerOptions); + referenceStream.Position = 0; + + _mockDataClient + .Setup(dc => + dc.GetBinaryData( + applicationMetadata.AppIdentifier.Org, + applicationMetadata.AppIdentifier.App, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + expectedDataId ) - .ReturnsAsync(referenceStream); + ) + .ReturnsAsync(referenceStream); - _mockAppMetadata.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + _mockAppMetadata.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); - // Act - var model = await _dataService.GetById(instance, expectedDataId); + // Act + var model = await _dataService.GetById(instance, expectedDataId); - // Assert - Assert.Equivalent(expectedModel, model); - } + // Assert + Assert.Equivalent(expectedModel, model); + } - [Fact] - public async Task GetById_ThrowsArgumentException_WhenDataElementDoesNotExist() - { - // Arrange - Instance instance = CreateInstance(); - instance.Data = [new DataElement { Id = Guid.NewGuid().ToString() }]; - - // Act & Assert - await Assert.ThrowsAsync( - () => _dataService.GetById(instance, Guid.NewGuid()) - ); - } - - [Fact] - public async Task InsertJsonObject_ReturnsExpectedResult() - { - // Arrange - Instance instance = CreateInstance(); - InstanceIdentifier instanceIdentifier = new(instance); - const string dataTypeId = "dataTypeId"; - object data = new(); - DataElement expectedDataElement = new(); - - _mockDataClient - .Setup(x => - x.InsertBinaryData( - instanceIdentifier.ToString(), - dataTypeId, - "application/json", - dataTypeId + ".json", - It.IsAny(), - null - ) + [Fact] + public async Task GetById_ThrowsArgumentException_WhenDataElementDoesNotExist() + { + // Arrange + Instance instance = CreateInstance(); + instance.Data = [new DataElement { Id = Guid.NewGuid().ToString() }]; + + // Act & Assert + await Assert.ThrowsAsync( + () => _dataService.GetById(instance, Guid.NewGuid()) + ); + } + + [Fact] + public async Task InsertJsonObject_ReturnsExpectedResult() + { + // Arrange + Instance instance = CreateInstance(); + InstanceIdentifier instanceIdentifier = new(instance); + const string dataTypeId = "dataTypeId"; + object data = new(); + DataElement expectedDataElement = new(); + + _mockDataClient + .Setup(x => + x.InsertBinaryData( + instanceIdentifier.ToString(), + dataTypeId, + "application/json", + dataTypeId + ".json", + It.IsAny(), + null ) - .ReturnsAsync(expectedDataElement); + ) + .ReturnsAsync(expectedDataElement); - // Act - DataElement result = await _dataService.InsertJsonObject(instanceIdentifier, dataTypeId, data); + // Act + DataElement result = await _dataService.InsertJsonObject(instanceIdentifier, dataTypeId, data); - // Assert - Assert.Equivalent(expectedDataElement, result); - } + // Assert + Assert.Equivalent(expectedDataElement, result); + } - [Fact] - public async Task UpdateJsonObject_ReturnsExpectedResult() - { - // Arrange - Instance instance = CreateInstance(); - InstanceIdentifier instanceIdentifier = new(instance); - const string dataTypeId = "dataTypeId"; - var dataElementId = Guid.NewGuid(); - object data = new(); - DataElement expectedDataElement = new(); - - _mockDataClient - .Setup(x => - x.UpdateBinaryData( - instanceIdentifier, - "application/json", - dataTypeId + ".json", - dataElementId, - It.IsAny() - ) + [Fact] + public async Task UpdateJsonObject_ReturnsExpectedResult() + { + // Arrange + Instance instance = CreateInstance(); + InstanceIdentifier instanceIdentifier = new(instance); + const string dataTypeId = "dataTypeId"; + var dataElementId = Guid.NewGuid(); + object data = new(); + DataElement expectedDataElement = new(); + + _mockDataClient + .Setup(x => + x.UpdateBinaryData( + instanceIdentifier, + "application/json", + dataTypeId + ".json", + dataElementId, + It.IsAny() ) - .ReturnsAsync(expectedDataElement); + ) + .ReturnsAsync(expectedDataElement); - // Act - DataElement result = await _dataService.UpdateJsonObject( - instanceIdentifier, - dataTypeId, - dataElementId, - data - ); + // Act + DataElement result = await _dataService.UpdateJsonObject(instanceIdentifier, dataTypeId, dataElementId, data); - // Assert - Assert.Equivalent(expectedDataElement, result); - } + // Assert + Assert.Equivalent(expectedDataElement, result); + } - private static Instance CreateInstance() - { - return new Instance() - { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", - AppId = "ttd/test", - InstanceOwner = new InstanceOwner { PartyId = "123" }, - Process = new ProcessState - { - CurrentTask = new ProcessElementInfo { AltinnTaskType = "dataTask", ElementId = "Task_1", }, - }, - }; - } - - private static ApplicationMetadata CreateAppMetadata(Instance instance) + private static Instance CreateInstance() + { + return new Instance() { - return new ApplicationMetadata(instance.AppId) + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + InstanceOwner = new InstanceOwner { PartyId = "123" }, + Process = new ProcessState { - CopyInstanceSettings = new CopyInstanceSettings { Enabled = true }, - }; - } + CurrentTask = new ProcessElementInfo { AltinnTaskType = "dataTask", ElementId = "Task_1", }, + }, + }; } - public class TestModel { } + private static ApplicationMetadata CreateAppMetadata(Instance instance) + { + return new ApplicationMetadata(instance.AppId) + { + CopyInstanceSettings = new CopyInstanceSettings { Enabled = true }, + }; + } } + +public class TestModel { } diff --git a/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs b/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs index 45cffdcaa..ce1cdefa5 100644 --- a/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Events/EventHandlerResolverTests.cs @@ -3,47 +3,44 @@ using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Models; using FluentAssertions; -using Moq; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.Internal.Events +namespace Altinn.App.PlatformServices.Tests.Internal.Events; + +public class EventHandlerResolverTests { - public class EventHandlerResolverTests + [Fact] + public async Task ResolveEventHandler_SubscriptionValidationHandler_ShouldReturnSubscriptionValidationHandler() { - [Fact] - public async Task ResolveEventHandler_SubscriptionValidationHandler_ShouldReturnSubscriptionValidationHandler() - { - var factory = new EventHandlerResolver(new List() { new SubscriptionValidationHandler() }); + var factory = new EventHandlerResolver(new List() { new SubscriptionValidationHandler() }); - IEventHandler eventHandler = factory.ResolveEventHandler("platform.events.validatesubscription"); + IEventHandler eventHandler = factory.ResolveEventHandler("platform.events.validatesubscription"); - eventHandler.Should().BeOfType(); - eventHandler.EventType.Should().Be("platform.events.validatesubscription"); - var success = await eventHandler.ProcessEvent(new CloudEvent()); - success.Should().BeTrue(); - } + eventHandler.Should().BeOfType(); + eventHandler.EventType.Should().Be("platform.events.validatesubscription"); + var success = await eventHandler.ProcessEvent(new CloudEvent()); + success.Should().BeTrue(); + } - [Fact] - public void ResolveEventHandler_InvalidEventType_ShouldReturnUnhandledEventHandler() - { - var factory = new EventHandlerResolver(new List()); + [Fact] + public void ResolveEventHandler_InvalidEventType_ShouldReturnUnhandledEventHandler() + { + var factory = new EventHandlerResolver(new List()); - IEventHandler eventHandler = factory.ResolveEventHandler("this.event.should.not.exists"); - Action action = () => eventHandler.ProcessEvent(new CloudEvent()); + IEventHandler eventHandler = factory.ResolveEventHandler("this.event.should.not.exists"); + Action action = () => eventHandler.ProcessEvent(new CloudEvent()); - eventHandler.Should().BeOfType(); - eventHandler.EventType.Should().Be("app.events.unhandled"); - action.Should().Throw(); - } + eventHandler.Should().BeOfType(); + eventHandler.EventType.Should().Be("app.events.unhandled"); + action.Should().Throw(); + } - [Fact] - public void ResolveEventHandler_Null_ShouldReturnUnhandledEventHandler() - { - var factory = new EventHandlerResolver(new List()); + [Fact] + public void ResolveEventHandler_Null_ShouldReturnUnhandledEventHandler() + { + var factory = new EventHandlerResolver(new List()); - IEventHandler eventHandler = factory.ResolveEventHandler(null); + IEventHandler eventHandler = factory.ResolveEventHandler(null); - eventHandler.Should().BeOfType(); - } + eventHandler.Should().BeOfType(); } } diff --git a/test/Altinn.App.Core.Tests/Internal/Events/UnhandledEventHandlerTests.cs b/test/Altinn.App.Core.Tests/Internal/Events/UnhandledEventHandlerTests.cs index c2d011cd7..4e01d33be 100644 --- a/test/Altinn.App.Core.Tests/Internal/Events/UnhandledEventHandlerTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Events/UnhandledEventHandlerTests.cs @@ -1,20 +1,18 @@ #nullable disable using Altinn.App.Core.Internal.Events; using FluentAssertions; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.Internal.Events +namespace Altinn.App.PlatformServices.Tests.Internal.Events; + +public class UnhandledEventHandlerTests { - public class UnhandledEventHandlerTests + [Fact] + public void ProcessEvent_ShouldThrowNotImplementedException() { - [Fact] - public void ProcessEvent_ShouldThrowNotImplementedException() - { - var handler = new UnhandledEventHandler(); + var handler = new UnhandledEventHandler(); - Action action = () => handler.ProcessEvent(null); + Action action = () => handler.ProcessEvent(null); - action.Should().Throw(); - } + action.Should().Throw(); } } diff --git a/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenExtensionsTests.cs b/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenExtensionsTests.cs index 9e8c7d8dd..c57c744f8 100644 --- a/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenExtensionsTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenExtensionsTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Core.Internal.Maskinporten; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Maskinporten; diff --git a/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenJwkTokenProviderTests.cs b/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenJwkTokenProviderTests.cs index 1da1bf1b8..6447fb410 100644 --- a/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenJwkTokenProviderTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Maskinporten/MaskinportenJwkTokenProviderTests.cs @@ -5,9 +5,7 @@ using Altinn.App.Core.Internal.Secrets; using FluentAssertions; using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Maskinporten; diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index 079ad52cd..c2251d74e 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -15,242 +15,239 @@ using Microsoft.Extensions.Options; using Moq; -namespace Altinn.App.PlatformServices.Tests.Internal.Pdf -{ - public class PdfServiceTests - { - private const string HostName = "at22.altinn.cloud"; - - private readonly Mock _appResources = new(); - private readonly Mock _dataClient = new(); - private readonly Mock _httpContextAccessor = new(); - private readonly Mock _pdfGeneratorClient = new(); - private readonly Mock _profile = new(); - private readonly IOptions _pdfGeneratorSettingsOptions = - Microsoft.Extensions.Options.Options.Create(new() { }); +namespace Altinn.App.PlatformServices.Tests.Internal.Pdf; - private readonly IOptions _generalSettingsOptions = - Microsoft.Extensions.Options.Options.Create(new() { HostName = HostName }); - - private readonly IOptions _platformSettingsOptions = - Microsoft.Extensions.Options.Options.Create(new() { }); +public class PdfServiceTests +{ + private const string HostName = "at22.altinn.cloud"; - private readonly Mock _userTokenProvider; + private readonly Mock _appResources = new(); + private readonly Mock _dataClient = new(); + private readonly Mock _httpContextAccessor = new(); + private readonly Mock _pdfGeneratorClient = new(); + private readonly Mock _profile = new(); + private readonly IOptions _pdfGeneratorSettingsOptions = + Microsoft.Extensions.Options.Options.Create(new() { }); - public PdfServiceTests() - { - var resource = new TextResource() - { - Id = "digdir-not-really-an-app-nb", - Language = "nb", - Org = "digdir", - Resources = new List() - }; - _appResources - .Setup(s => s.GetTexts(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(resource); + private readonly IOptions _generalSettingsOptions = + Microsoft.Extensions.Options.Options.Create(new() { HostName = HostName }); - DefaultHttpContext httpContext = new(); - httpContext.Request.Protocol = "https"; - httpContext.Request.Host = new(HostName); - _httpContextAccessor.Setup(s => s.HttpContext!).Returns(httpContext); + private readonly IOptions _platformSettingsOptions = + Microsoft.Extensions.Options.Options.Create(new() { }); - _userTokenProvider = new Mock(); - _userTokenProvider.Setup(s => s.GetUserToken()).Returns("usertoken"); - } + private readonly Mock _userTokenProvider; - [Fact] - public async Task ValidRequest_ShouldReturnPdf() + public PdfServiceTests() + { + var resource = new TextResource() { - DelegatingHandlerStub delegatingHandler = - new( - async (HttpRequestMessage request, CancellationToken token) => - { - await Task.CompletedTask; - return new HttpResponseMessage() - { - Content = new StreamContent( - EmbeddedResource.LoadDataAsStream( - "Altinn.App.Core.Tests.Internal.Pdf.TestData.example.pdf" - ) - ) - }; - } - ); - - var httpClient = new HttpClient(delegatingHandler); - var pdfGeneratorClient = new PdfGeneratorClient( - httpClient, - _pdfGeneratorSettingsOptions, - _platformSettingsOptions, - _userTokenProvider.Object, - _httpContextAccessor.Object - ); - - Stream pdf = await pdfGeneratorClient.GeneratePdf( - new Uri(@"https://org.apps.hostName/appId/#/instance/instanceId"), - CancellationToken.None - ); - - pdf.Length.Should().Be(17814L); - } + Id = "digdir-not-really-an-app-nb", + Language = "nb", + Org = "digdir", + Resources = new List() + }; + _appResources + .Setup(s => s.GetTexts(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(resource); + + DefaultHttpContext httpContext = new(); + httpContext.Request.Protocol = "https"; + httpContext.Request.Host = new(HostName); + _httpContextAccessor.Setup(s => s.HttpContext!).Returns(httpContext); + + _userTokenProvider = new Mock(); + _userTokenProvider.Setup(s => s.GetUserToken()).Returns("usertoken"); + } - [Fact] - public async Task ValidRequest_PdfGenerationFails_ShouldThrowException() - { - DelegatingHandlerStub delegatingHandler = - new( - async (HttpRequestMessage request, CancellationToken token) => + [Fact] + public async Task ValidRequest_ShouldReturnPdf() + { + DelegatingHandlerStub delegatingHandler = + new( + async (HttpRequestMessage request, CancellationToken token) => + { + await Task.CompletedTask; + return new HttpResponseMessage() { - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.RequestTimeout }; - } - ); - - var httpClient = new HttpClient(delegatingHandler); - var pdfGeneratorClient = new PdfGeneratorClient( - httpClient, - _pdfGeneratorSettingsOptions, - _platformSettingsOptions, - _userTokenProvider.Object, - _httpContextAccessor.Object + Content = new StreamContent( + EmbeddedResource.LoadDataAsStream("Altinn.App.Core.Tests.Internal.Pdf.TestData.example.pdf") + ) + }; + } ); - var func = async () => - await pdfGeneratorClient.GeneratePdf( - new Uri(@"https://org.apps.hostName/appId/#/instance/instanceId"), - CancellationToken.None - ); - - await func.Should().ThrowAsync(); - } - - [Fact] - public async Task GenerateAndStorePdf() - { - // Arrange - TelemetrySink telemetrySink = new(); - _pdfGeneratorClient.Setup(s => s.GeneratePdf(It.IsAny(), It.IsAny())); - _generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}"; - - var target = new PdfService( - _appResources.Object, - _dataClient.Object, - _httpContextAccessor.Object, - _profile.Object, - _pdfGeneratorClient.Object, - _pdfGeneratorSettingsOptions, - _generalSettingsOptions, - telemetrySink.Object - ); + var httpClient = new HttpClient(delegatingHandler); + var pdfGeneratorClient = new PdfGeneratorClient( + httpClient, + _pdfGeneratorSettingsOptions, + _platformSettingsOptions, + _userTokenProvider.Object, + _httpContextAccessor.Object + ); + + Stream pdf = await pdfGeneratorClient.GeneratePdf( + new Uri(@"https://org.apps.hostName/appId/#/instance/instanceId"), + CancellationToken.None + ); + + pdf.Length.Should().Be(17814L); + } - Instance instance = - new() + [Fact] + public async Task ValidRequest_PdfGenerationFails_ShouldThrowException() + { + DelegatingHandlerStub delegatingHandler = + new( + async (HttpRequestMessage request, CancellationToken token) => { - Id = $"509378/{Guid.NewGuid()}", - AppId = "digdir/not-really-an-app", - Org = "digdir" - }; - - // Act - await target.GenerateAndStorePdf(instance, "Task_1", CancellationToken.None); - - // Asserts - _pdfGeneratorClient.Verify( - s => - s.GeneratePdf( - It.Is(u => - u.Scheme == "https" - && u.Host == $"{instance.Org}.apps.{HostName}" - && u.AbsoluteUri.Contains(instance.AppId) - && u.AbsoluteUri.Contains(instance.Id) - ), - It.IsAny() - ), - Times.Once + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.RequestTimeout }; + } ); - _dataClient.Verify( - s => - s.InsertBinaryData( - It.Is(s => s == instance.Id), - It.Is(s => s == "ref-data-as-pdf"), - It.Is(s => s == "application/pdf"), - It.Is(s => s == "not-really-an-app.pdf"), - It.IsAny(), - It.Is(s => s == "Task_1") - ), - Times.Once + var httpClient = new HttpClient(delegatingHandler); + var pdfGeneratorClient = new PdfGeneratorClient( + httpClient, + _pdfGeneratorSettingsOptions, + _platformSettingsOptions, + _userTokenProvider.Object, + _httpContextAccessor.Object + ); + + var func = async () => + await pdfGeneratorClient.GeneratePdf( + new Uri(@"https://org.apps.hostName/appId/#/instance/instanceId"), + CancellationToken.None ); - await Verify(telemetrySink.GetSnapshot()); - } + await func.Should().ThrowAsync(); + } - [Fact] - public async Task GenerateAndStorePdf_with_generatedFrom() - { - // Arrange - _pdfGeneratorClient.Setup(s => s.GeneratePdf(It.IsAny(), It.IsAny())); - - _generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}"; - - var target = new PdfService( - _appResources.Object, - _dataClient.Object, - _httpContextAccessor.Object, - _profile.Object, - _pdfGeneratorClient.Object, - _pdfGeneratorSettingsOptions, - _generalSettingsOptions - ); + [Fact] + public async Task GenerateAndStorePdf() + { + // Arrange + TelemetrySink telemetrySink = new(); + _pdfGeneratorClient.Setup(s => s.GeneratePdf(It.IsAny(), It.IsAny())); + _generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}"; + + var target = new PdfService( + _appResources.Object, + _dataClient.Object, + _httpContextAccessor.Object, + _profile.Object, + _pdfGeneratorClient.Object, + _pdfGeneratorSettingsOptions, + _generalSettingsOptions, + telemetrySink.Object + ); + + Instance instance = + new() + { + Id = $"509378/{Guid.NewGuid()}", + AppId = "digdir/not-really-an-app", + Org = "digdir" + }; - var dataModelId = Guid.NewGuid(); - var attachmentId = Guid.NewGuid(); + // Act + await target.GenerateAndStorePdf(instance, "Task_1", CancellationToken.None); + + // Asserts + _pdfGeneratorClient.Verify( + s => + s.GeneratePdf( + It.Is(u => + u.Scheme == "https" + && u.Host == $"{instance.Org}.apps.{HostName}" + && u.AbsoluteUri.Contains(instance.AppId) + && u.AbsoluteUri.Contains(instance.Id) + ), + It.IsAny() + ), + Times.Once + ); + + _dataClient.Verify( + s => + s.InsertBinaryData( + It.Is(s => s == instance.Id), + It.Is(s => s == "ref-data-as-pdf"), + It.Is(s => s == "application/pdf"), + It.Is(s => s == "not-really-an-app.pdf"), + It.IsAny(), + It.Is(s => s == "Task_1") + ), + Times.Once + ); + + await Verify(telemetrySink.GetSnapshot()); + } - Instance instance = - new() + [Fact] + public async Task GenerateAndStorePdf_with_generatedFrom() + { + // Arrange + _pdfGeneratorClient.Setup(s => s.GeneratePdf(It.IsAny(), It.IsAny())); + + _generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}"; + + var target = new PdfService( + _appResources.Object, + _dataClient.Object, + _httpContextAccessor.Object, + _profile.Object, + _pdfGeneratorClient.Object, + _pdfGeneratorSettingsOptions, + _generalSettingsOptions + ); + + var dataModelId = Guid.NewGuid(); + var attachmentId = Guid.NewGuid(); + + Instance instance = + new() + { + Id = $"509378/{Guid.NewGuid()}", + AppId = "digdir/not-really-an-app", + Org = "digdir", + Process = new() { CurrentTask = new() { ElementId = "Task_1" } }, + Data = new() { - Id = $"509378/{Guid.NewGuid()}", - AppId = "digdir/not-really-an-app", - Org = "digdir", - Process = new() { CurrentTask = new() { ElementId = "Task_1" } }, - Data = new() - { - new() { Id = dataModelId.ToString(), DataType = "Model" }, - new() { Id = attachmentId.ToString(), DataType = "attachment" } - } - }; - - // Act - await target.GenerateAndStorePdf(instance, "Task_1", CancellationToken.None); - - // Asserts - _pdfGeneratorClient.Verify( - s => - s.GeneratePdf( - It.Is(u => - u.Scheme == "https" - && u.Host == $"{instance.Org}.apps.{HostName}" - && u.AbsoluteUri.Contains(instance.AppId) - && u.AbsoluteUri.Contains(instance.Id) - ), - It.IsAny() - ), - Times.Once - ); + new() { Id = dataModelId.ToString(), DataType = "Model" }, + new() { Id = attachmentId.ToString(), DataType = "attachment" } + } + }; - _dataClient.Verify( - s => - s.InsertBinaryData( - It.Is(s => s == instance.Id), - It.Is(s => s == "ref-data-as-pdf"), - It.Is(s => s == "application/pdf"), - It.Is(s => s == "not-really-an-app.pdf"), - It.IsAny(), - It.Is(s => s == "Task_1") + // Act + await target.GenerateAndStorePdf(instance, "Task_1", CancellationToken.None); + + // Asserts + _pdfGeneratorClient.Verify( + s => + s.GeneratePdf( + It.Is(u => + u.Scheme == "https" + && u.Host == $"{instance.Org}.apps.{HostName}" + && u.AbsoluteUri.Contains(instance.AppId) + && u.AbsoluteUri.Contains(instance.Id) ), - Times.Once - ); - } + It.IsAny() + ), + Times.Once + ); + + _dataClient.Verify( + s => + s.InsertBinaryData( + It.Is(s => s == instance.Id), + It.Is(s => s == "ref-data-as-pdf"), + It.Is(s => s == "application/pdf"), + It.Is(s => s == "not-really-an-app.pdf"), + It.IsAny(), + It.Is(s => s == "Task_1") + ), + Times.Once + ); } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Authorization/TestData/UserActionAuthorizerStub.cs b/test/Altinn.App.Core.Tests/Internal/Process/Authorization/TestData/UserActionAuthorizerStub.cs index 62bb2f6e2..284b51c9f 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/Authorization/TestData/UserActionAuthorizerStub.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/Authorization/TestData/UserActionAuthorizerStub.cs @@ -2,13 +2,12 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; -namespace Altinn.App.Core.Tests.Internal.Process.Action.TestData +namespace Altinn.App.Core.Tests.Internal.Process.Action.TestData; + +public class UserActionAuthorizerStub : IUserActionAuthorizer { - public class UserActionAuthorizerStub : IUserActionAuthorizer + public Task AuthorizeAction(UserActionAuthorizerContext context) { - public Task AuthorizeAction(UserActionAuthorizerContext context) - { - return Task.FromResult(true); - } + return Task.FromResult(true); } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Authorization/UserActionAuthorizerServiceCollectionExtensionTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/Authorization/UserActionAuthorizerServiceCollectionExtensionTests.cs index 8b7bd88d5..9bcf6ccb3 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/Authorization/UserActionAuthorizerServiceCollectionExtensionTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/Authorization/UserActionAuthorizerServiceCollectionExtensionTests.cs @@ -4,7 +4,6 @@ using Altinn.App.Core.Tests.Internal.Process.Action.TestData; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process.Action; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Elements/AppProcessStateTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/Elements/AppProcessStateTests.cs index 9d9b8f110..ccc02868c 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/Elements/AppProcessStateTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/Elements/AppProcessStateTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Core.Internal.Process.Elements; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process.Elements; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandlerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandlerTests.cs index 27d720b10..82b791882 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandlerTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandlerTests.cs @@ -3,7 +3,6 @@ using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process.EventHandlers.ProcessTask; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandlerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandlerTests.cs index c22e21b64..9880e61ad 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandlerTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandlerTests.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process.EventHandlers.ProcessTask; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandlerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandlerTests.cs index 4a5c4a602..c0ba4110b 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandlerTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandlerTests.cs @@ -3,7 +3,6 @@ using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.Platform.Storage.Interface.Models; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process.EventHandlers.ProcessTask; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs index 699ac0087..caa25b4dc 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs @@ -15,7 +15,6 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Options; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 67cc00fae..043239c4b 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -17,7 +17,6 @@ using FluentAssertions; using Moq; using Newtonsoft.Json; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventHandlingTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventHandlingTests.cs index c518fe77f..fc5c3516c 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventHandlingTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventHandlingTests.cs @@ -14,7 +14,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs index 7b48d1f80..634e8149b 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs @@ -8,7 +8,6 @@ using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs index 2d44a63fe..5d03fbed1 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs @@ -4,170 +4,162 @@ using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Moq; -using Xunit; -namespace Altinn.App.Core.Tests.Internal.Process.ProcessTasks +namespace Altinn.App.Core.Tests.Internal.Process.ProcessTasks; + +public class ProcessTaskDataLockerTests { - public class ProcessTaskDataLockerTests + private readonly Mock _appMetadataMock; + private readonly Mock _dataClientMock; + private readonly ProcessTaskDataLocker _processTaskDataLocker; + + public ProcessTaskDataLockerTests() { - private readonly Mock _appMetadataMock; - private readonly Mock _dataClientMock; - private readonly ProcessTaskDataLocker _processTaskDataLocker; + _appMetadataMock = new Mock(); + _dataClientMock = new Mock(); + _processTaskDataLocker = new ProcessTaskDataLocker(_appMetadataMock.Object, _dataClientMock.Object); + } - public ProcessTaskDataLockerTests() - { - _appMetadataMock = new Mock(); - _dataClientMock = new Mock(); - _processTaskDataLocker = new ProcessTaskDataLocker(_appMetadataMock.Object, _dataClientMock.Object); - } + [Fact] + public async Task Unlock_ShouldUnlockAllDataElementsConnectedToTask() + { + // Arrange + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; - [Fact] - public async Task Unlock_ShouldUnlockAllDataElementsConnectedToTask() - { - // Arrange - Instance instance = CreateInstance(); - string taskId = instance.Process.CurrentTask.ElementId; + instance.Data = + [ + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } + ]; - instance.Data = + var applicationMetadata = new ApplicationMetadata(instance.AppId) + { + DataTypes = [ - new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, - new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } - ]; + new DataType { Id = "dataType1", TaskId = taskId }, + new DataType { Id = "dataType2", TaskId = taskId } + ] + }; - var applicationMetadata = new ApplicationMetadata(instance.AppId) - { - DataTypes = - [ - new DataType { Id = "dataType1", TaskId = taskId }, - new DataType { Id = "dataType2", TaskId = taskId } - ] - }; - - _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); - - // Act - await _processTaskDataLocker.Unlock(taskId, instance); - - // Assert - _dataClientMock.Verify( - x => x.UnlockDataElement(It.IsAny(), It.IsAny()), - Times.Exactly(2) - ); - } - - [Fact] - public async Task Lock_ShouldLockAllDataElementsConnectedToTask() - { - // Arrange - Instance instance = CreateInstance(); - string taskId = instance.Process.CurrentTask.ElementId; + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); - instance.Data = - [ - new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, - new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } - ]; + // Act + await _processTaskDataLocker.Unlock(taskId, instance); - var applicationMetadata = new ApplicationMetadata(instance.AppId) - { - DataTypes = - [ - new DataType { Id = "dataType1", TaskId = taskId }, - new DataType { Id = "dataType2", TaskId = taskId } - ] - }; - - _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); - - // Act - await _processTaskDataLocker.Lock(taskId, instance); - - // Assert - _dataClientMock.Verify( - x => x.LockDataElement(It.IsAny(), It.IsAny()), - Times.Exactly(2) - ); - } - - [Fact] - public async Task Unlock_ShouldNotUnlockDataElementsNotConnectedToTask() - { - // Arrange - Instance instance = CreateInstance(); - string taskId = instance.Process.CurrentTask.ElementId; + // Assert + _dataClientMock.Verify( + x => x.UnlockDataElement(It.IsAny(), It.IsAny()), + Times.Exactly(2) + ); + } + + [Fact] + public async Task Lock_ShouldLockAllDataElementsConnectedToTask() + { + // Arrange + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; - instance.Data = + instance.Data = + [ + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } + ]; + + var applicationMetadata = new ApplicationMetadata(instance.AppId) + { + DataTypes = [ - new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, - new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } - ]; + new DataType { Id = "dataType1", TaskId = taskId }, + new DataType { Id = "dataType2", TaskId = taskId } + ] + }; - var applicationMetadata = new ApplicationMetadata(instance.AppId) - { - DataTypes = - [ - new DataType { Id = "dataType1", TaskId = taskId }, - new DataType { Id = "dataType3", TaskId = "task2" } - ] - }; - - _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); - - // Act - await _processTaskDataLocker.Unlock(taskId, instance); - - // Assert - _dataClientMock.Verify( - x => x.UnlockDataElement(It.IsAny(), It.IsAny()), - Times.Once - ); - } - - [Fact] - public async Task Lock_ShouldNotLockDataElementsNotConnectedToTask() + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + await _processTaskDataLocker.Lock(taskId, instance); + + // Assert + _dataClientMock.Verify( + x => x.LockDataElement(It.IsAny(), It.IsAny()), + Times.Exactly(2) + ); + } + + [Fact] + public async Task Unlock_ShouldNotUnlockDataElementsNotConnectedToTask() + { + // Arrange + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; + + instance.Data = + [ + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } + ]; + + var applicationMetadata = new ApplicationMetadata(instance.AppId) { - // Arrange - Instance instance = CreateInstance(); - string taskId = instance.Process.CurrentTask.ElementId; + DataTypes = + [ + new DataType { Id = "dataType1", TaskId = taskId }, + new DataType { Id = "dataType3", TaskId = "task2" } + ] + }; + + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + await _processTaskDataLocker.Unlock(taskId, instance); + + // Assert + _dataClientMock.Verify(x => x.UnlockDataElement(It.IsAny(), It.IsAny()), Times.Once); + } - instance.Data = + [Fact] + public async Task Lock_ShouldNotLockDataElementsNotConnectedToTask() + { + // Arrange + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; + + instance.Data = + [ + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } + ]; + + var applicationMetadata = new ApplicationMetadata(instance.AppId) + { + DataTypes = [ - new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, - new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } - ]; + new DataType { Id = "dataType1", TaskId = taskId }, + new DataType { Id = "dataType3", TaskId = "task2" } + ] + }; - var applicationMetadata = new ApplicationMetadata(instance.AppId) - { - DataTypes = - [ - new DataType { Id = "dataType1", TaskId = taskId }, - new DataType { Id = "dataType3", TaskId = "task2" } - ] - }; - - _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); - - // Act - await _processTaskDataLocker.Lock(taskId, instance); - - // Assert - _dataClientMock.Verify( - x => x.LockDataElement(It.IsAny(), It.IsAny()), - Times.Once - ); - } - - private static Instance CreateInstance() + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + await _processTaskDataLocker.Lock(taskId, instance); + + // Assert + _dataClientMock.Verify(x => x.LockDataElement(It.IsAny(), It.IsAny()), Times.Once); + } + + private static Instance CreateInstance() + { + return new Instance() { - return new Instance() + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + Process = new ProcessState { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", - AppId = "ttd/test", - Process = new ProcessState - { - CurrentTask = new ProcessElementInfo { AltinnTaskType = "datatask", ElementId = "datatask", }, - }, - }; - } + CurrentTask = new ProcessElementInfo { AltinnTaskType = "datatask", ElementId = "datatask", }, + }, + }; } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs index 2d9e04978..12faf1d37 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs @@ -9,86 +9,84 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Options; using Moq; -using Xunit; -namespace Altinn.App.Core.Tests.Internal.Process.ProcessTasks.Common -{ - public class ProcessTaskFinalizerTests - { - private readonly Mock _appMetadataMock; - private readonly Mock _dataClientMock; - private readonly Mock _appModelMock; - private readonly Mock _appResourcesMock; - private readonly Mock _layoutEvaluatorStateInitializerMock; - private readonly IOptions _appSettingsMock; - private readonly ProcessTaskFinalizer _processTaskFinalizer; +namespace Altinn.App.Core.Tests.Internal.Process.ProcessTasks.Common; - public ProcessTaskFinalizerTests() - { - _appMetadataMock = new Mock(); - _dataClientMock = new Mock(); - _appModelMock = new Mock(); - _appResourcesMock = new Mock(); - var frontendSettingsMock = new Mock>(); - _layoutEvaluatorStateInitializerMock = new Mock( - MockBehavior.Strict, - [_appResourcesMock.Object, frontendSettingsMock.Object] - ); - _appSettingsMock = Options.Create(new AppSettings()); +public class ProcessTaskFinalizerTests +{ + private readonly Mock _appMetadataMock; + private readonly Mock _dataClientMock; + private readonly Mock _appModelMock; + private readonly Mock _appResourcesMock; + private readonly Mock _layoutEvaluatorStateInitializerMock; + private readonly IOptions _appSettingsMock; + private readonly ProcessTaskFinalizer _processTaskFinalizer; - _processTaskFinalizer = new ProcessTaskFinalizer( - _appMetadataMock.Object, - _dataClientMock.Object, - _appModelMock.Object, - _appResourcesMock.Object, - _layoutEvaluatorStateInitializerMock.Object, - _appSettingsMock - ); - } + public ProcessTaskFinalizerTests() + { + _appMetadataMock = new Mock(); + _dataClientMock = new Mock(); + _appModelMock = new Mock(); + _appResourcesMock = new Mock(); + var frontendSettingsMock = new Mock>(); + _layoutEvaluatorStateInitializerMock = new Mock( + MockBehavior.Strict, + [_appResourcesMock.Object, frontendSettingsMock.Object] + ); + _appSettingsMock = Options.Create(new AppSettings()); - [Fact] - public async Task Finalize_WithValidInputs_ShouldCallCorrectMethods() - { - // Arrange - Instance instance = CreateInstance(); + _processTaskFinalizer = new ProcessTaskFinalizer( + _appMetadataMock.Object, + _dataClientMock.Object, + _appModelMock.Object, + _appResourcesMock.Object, + _layoutEvaluatorStateInitializerMock.Object, + _appSettingsMock + ); + } - instance.Data = - [ - new DataElement - { - Id = Guid.NewGuid().ToString(), - References = - [ - new Reference { ValueType = ReferenceType.Task, Value = instance.Process.CurrentTask.ElementId } - ] - } - ]; + [Fact] + public async Task Finalize_WithValidInputs_ShouldCallCorrectMethods() + { + // Arrange + Instance instance = CreateInstance(); - var applicationMetadata = new ApplicationMetadata(instance.AppId) + instance.Data = + [ + new DataElement { - DataTypes = [new DataType { TaskId = instance.Process.CurrentTask.ElementId }] - }; + Id = Guid.NewGuid().ToString(), + References = + [ + new Reference { ValueType = ReferenceType.Task, Value = instance.Process.CurrentTask.ElementId } + ] + } + ]; + + var applicationMetadata = new ApplicationMetadata(instance.AppId) + { + DataTypes = [new DataType { TaskId = instance.Process.CurrentTask.ElementId }] + }; - _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); - // Act - await _processTaskFinalizer.Finalize(instance.Process.CurrentTask.ElementId, instance); + // Act + await _processTaskFinalizer.Finalize(instance.Process.CurrentTask.ElementId, instance); - // Assert - _appMetadataMock.Verify(x => x.GetApplicationMetadata(), Times.Once); - } + // Assert + _appMetadataMock.Verify(x => x.GetApplicationMetadata(), Times.Once); + } - private static Instance CreateInstance() + private static Instance CreateInstance() + { + return new Instance() { - return new Instance() + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + Process = new ProcessState { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", - AppId = "ttd/test", - Process = new ProcessState - { - CurrentTask = new ProcessElementInfo { AltinnTaskType = "signing", ElementId = "EndEvent", }, - }, - }; - } + CurrentTask = new ProcessElementInfo { AltinnTaskType = "signing", ElementId = "EndEvent", }, + }, + }; } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs index 0e0f7b291..e8c73a578 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/PaymentProcessTaskTests.cs @@ -9,7 +9,6 @@ using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process.ProcessTasks; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs index c974c9277..9a2f9b553 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs index 14b4441da..aa08b7808 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs @@ -5,7 +5,6 @@ using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Moq; -using Xunit; namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/TestData/DummyDataType.cs b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/TestData/DummyDataType.cs index 31d18f0b3..62f79e727 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/TestData/DummyDataType.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/TestData/DummyDataType.cs @@ -1,4 +1,3 @@ -namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks.TestData -{ - public class DummyDataType { } -} +namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks.TestData; + +public class DummyDataType { } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/DummyModel.cs b/test/Altinn.App.Core.Tests/Internal/Process/TestData/DummyModel.cs index 1678e1f81..357a79db9 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/DummyModel.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/DummyModel.cs @@ -1,10 +1,9 @@ #nullable disable -namespace Altinn.App.Core.Tests.Internal.Process.TestData +namespace Altinn.App.Core.Tests.Internal.Process.TestData; + +public class DummyModel { - public class DummyModel - { - public string Submitter { get; set; } + public string Submitter { get; set; } - public decimal Amount { get; set; } - } + public decimal Amount { get; set; } } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs index f3e539afb..659ccda47 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs @@ -3,7 +3,6 @@ using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Tests.Helpers; using FluentAssertions; -using Xunit; using Xunit.Abstractions; using Xunit.Sdk; diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestContextList.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestContextList.cs index f2f08af7a..70ba6c7e9 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestContextList.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestContextList.cs @@ -4,7 +4,6 @@ using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Tests.Helpers; using FluentAssertions; -using Xunit; using Xunit.Abstractions; using Xunit.Sdk; diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs index 0f8d5c8f5..c0c41dcfd 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs @@ -3,7 +3,6 @@ using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Tests.Helpers; using FluentAssertions; -using Xunit; using Xunit.Abstractions; using Xunit.Sdk; diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs index 6c5cf82a7..1271618fa 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs @@ -3,7 +3,6 @@ using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Tests.Helpers; using FluentAssertions; -using Xunit; using Xunit.Abstractions; using Xunit.Sdk; diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test1/RunTest1.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test1/RunTest1.cs index 6ccab17a2..6ab3b36e7 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test1/RunTest1.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test1/RunTest1.cs @@ -2,7 +2,6 @@ using System.Text.Json.Serialization; using Altinn.App.Core.Internal.Expressions; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.LayoutExpressions.FullTests.Test1; diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/RunTest2.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/RunTest2.cs index c1dfe43e5..e9c5d0b03 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/RunTest2.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/RunTest2.cs @@ -3,7 +3,6 @@ using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Expressions; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.LayoutExpressions.FullTests.Test2; diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/RunTest3.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/RunTest3.cs index 8485cf549..e4bbb8bc5 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/RunTest3.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/RunTest3.cs @@ -3,7 +3,6 @@ using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Expressions; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.LayoutExpressions.FullTests.Test3; diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs index 97a2ac37f..9fa2b41c7 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs @@ -6,7 +6,6 @@ using Altinn.App.Core.Tests.Helpers; using FluentAssertions; using Newtonsoft.Json; -using Xunit; using JsonSerializer = System.Text.Json.JsonSerializer; namespace Altinn.App.Core.Tests.LayoutExpressions.CSharpTests; diff --git a/test/Altinn.App.Core.Tests/Mocks/DelegatingHandlerStub.cs b/test/Altinn.App.Core.Tests/Mocks/DelegatingHandlerStub.cs index 988f53a13..5ad99f9b3 100644 --- a/test/Altinn.App.Core.Tests/Mocks/DelegatingHandlerStub.cs +++ b/test/Altinn.App.Core.Tests/Mocks/DelegatingHandlerStub.cs @@ -1,28 +1,27 @@ #nullable disable using System.Net; -namespace Altinn.App.PlatformServices.Tests.Mocks +namespace Altinn.App.PlatformServices.Tests.Mocks; + +public class DelegatingHandlerStub : DelegatingHandler { - public class DelegatingHandlerStub : DelegatingHandler - { - private readonly Func> _handlerFunc; + private readonly Func> _handlerFunc; - public DelegatingHandlerStub() - { - _handlerFunc = (request, cancellationToken) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); - } + public DelegatingHandlerStub() + { + _handlerFunc = (request, cancellationToken) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + } - public DelegatingHandlerStub(Func> handlerFunc) - { - _handlerFunc = handlerFunc; - } + public DelegatingHandlerStub(Func> handlerFunc) + { + _handlerFunc = handlerFunc; + } - protected override Task SendAsync( - HttpRequestMessage request, - CancellationToken cancellationToken - ) - { - return _handlerFunc(request, cancellationToken); - } + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken + ) + { + return _handlerFunc(request, cancellationToken); } } diff --git a/test/Altinn.App.Core.Tests/Mocks/RequestInterceptor.cs b/test/Altinn.App.Core.Tests/Mocks/RequestInterceptor.cs index 13b50429b..688cf498c 100644 --- a/test/Altinn.App.Core.Tests/Mocks/RequestInterceptor.cs +++ b/test/Altinn.App.Core.Tests/Mocks/RequestInterceptor.cs @@ -1,51 +1,50 @@ using System.Net; -namespace Altinn.App.PlatformServices.Tests.Mocks +namespace Altinn.App.PlatformServices.Tests.Mocks; + +public class RequestInterceptor : DelegatingHandler { - public class RequestInterceptor : DelegatingHandler + private HttpRequestMessage? _request; + + private readonly HttpResponseMessage _response; + + public RequestInterceptor(HttpStatusCode responseCode, Stream responseContent) { - private HttpRequestMessage? _request; + StreamContent streamContent = new StreamContent(responseContent); + _response = new HttpResponseMessage(responseCode) { Content = streamContent }; + } - private readonly HttpResponseMessage _response; + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken + ) + { + _request = request; + return Task.FromResult(_response); + } - public RequestInterceptor(HttpStatusCode responseCode, Stream responseContent) + /// + /// Ensures that the request body is serialized. + /// + /// A stringified version of the request body. + public async Task GetRequestContentAsStringAsync() + { + if (_request is null) { - StreamContent streamContent = new StreamContent(responseContent); - _response = new HttpResponseMessage(responseCode) { Content = streamContent }; + throw new RequestInterceptorException("No request has been captured."); } - protected override Task SendAsync( - HttpRequestMessage request, - CancellationToken cancellationToken - ) + if (_request.Method != HttpMethod.Post) { - _request = request; - return Task.FromResult(_response); + throw new RequestInterceptorException("Only POST requests are assumed to have request body"); } - /// - /// Ensures that the request body is serialized. - /// - /// A stringified version of the request body. - public async Task GetRequestContentAsStringAsync() - { - if (_request is null) - { - throw new RequestInterceptorException("No request has been captured."); - } - - if (_request.Method != HttpMethod.Post) - { - throw new RequestInterceptorException("Only POST requests are assumed to have request body"); - } - - return await _request.Content!.ReadAsStringAsync(); - } + return await _request.Content!.ReadAsStringAsync(); + } - internal class RequestInterceptorException : Exception - { - internal RequestInterceptorException(string? message) - : base(message) { } - } + internal class RequestInterceptorException : Exception + { + internal RequestInterceptorException(string? message) + : base(message) { } } } diff --git a/test/Altinn.App.Core.Tests/Models/AppIdentifierTests.cs b/test/Altinn.App.Core.Tests/Models/AppIdentifierTests.cs index 30847785b..a83a85d50 100644 --- a/test/Altinn.App.Core.Tests/Models/AppIdentifierTests.cs +++ b/test/Altinn.App.Core.Tests/Models/AppIdentifierTests.cs @@ -1,116 +1,114 @@ #nullable disable using Altinn.App.Core.Models; using FluentAssertions; -using Xunit; #pragma warning disable CA1806 // Do not ignore method results -namespace Altinn.App.PlatformServices.Tests.Models +namespace Altinn.App.PlatformServices.Tests.Models; + +public class AppIdentifierTests { - public class AppIdentifierTests + [Fact] + public void Constructor_AppId_ShouldReturnValidInstance() { - [Fact] - public void Constructor_AppId_ShouldReturnValidInstance() - { - var org = "ttd"; - var app = "test-app"; - var appId = $"{org}/{app}"; - - var appIdentifier = new AppIdentifier(appId); - - appIdentifier.Org.Should().Be(org); - appIdentifier.App.Should().Be(app); - appIdentifier.ToString().Should().Be(appId); - } - - [Theory] - [InlineData("ttd//test-app")] - [InlineData("ttd\\test-app")] - [InlineData("test-app")] - public void Constructor_AppId_ShouldThrowException(string appId) - { - Action action = () => new AppIdentifier(appId); - - action.Should().Throw(); - } - - [Fact] - public void Constructor_OrgApp_ShouldReturnValidInstance() - { - var org = "ttd"; - var app = "test-app"; - var appId = $"{org}/{app}"; - - var appIdentifier = new AppIdentifier(org, app); - - appIdentifier.Org.Should().Be(org); - appIdentifier.App.Should().Be(app); - appIdentifier.ToString().Should().Be(appId); - } - - [Theory] - [InlineData("ttd", null)] - [InlineData(null, "test-app")] - public void Constructor_Null_ShouldThrowException(string org, string app) - { - Action action = () => new AppIdentifier(org, app); - - action.Should().Throw(); - } - - [Fact] - public void Constructor_NullAppId_ShouldThrowException() - { - Action action = () => new AppIdentifier((string)null); - - action.Should().Throw(); - } - - [Fact] - public void Constructor_Equals_ShouldBeEqual() - { - var org = "ttd"; - var app = "test-app"; - - var appIdentifier1 = new AppIdentifier(org, app); - var appIdentifier2 = new AppIdentifier(org, app); - - appIdentifier1.Should().BeEquivalentTo(appIdentifier2); - appIdentifier1.GetHashCode().Should().Be(appIdentifier2.GetHashCode()); - } - - [Theory] - [InlineData( - "https://dihe.apps.tt02.altinn.no/dihe/redusert-foreldrebetaling-bhg/api/v1/eventsreceiver?code=16eda4f0-653a-4fdc-b516-c4702392a4eb", - "dihe", - "redusert-foreldrebetaling-bhg" - )] - public void CreateFromUrl_ValidUrl_ShouldConstruct(string url, string expectedOrg, string expectedApp) - { - AppIdentifier appIdentifier = AppIdentifier.CreateFromUrl(url); - - appIdentifier.Org.Should().Be(expectedOrg); - appIdentifier.App.Should().Be(expectedApp); - } - - [Theory] - [InlineData("https://dihe.apps.tt02.altinn.no/dihe")] - [InlineData("dihe/redusert-foreldrebetaling-bhg")] - public void CreateFromUrl_InvalidUrl_ShouldThrowArgumentException(string url) - { - Action action = () => AppIdentifier.CreateFromUrl(url); - - action.Should().Throw(); - } - - [Fact] - public void CreateFromUrl_Null_ShouldThrowArgumentNullException() - { - Action action = () => AppIdentifier.CreateFromUrl(null); - - action.Should().Throw(); - } + var org = "ttd"; + var app = "test-app"; + var appId = $"{org}/{app}"; + + var appIdentifier = new AppIdentifier(appId); + + appIdentifier.Org.Should().Be(org); + appIdentifier.App.Should().Be(app); + appIdentifier.ToString().Should().Be(appId); } -#pragma warning restore CA1806 // Do not ignore method results + [Theory] + [InlineData("ttd//test-app")] + [InlineData("ttd\\test-app")] + [InlineData("test-app")] + public void Constructor_AppId_ShouldThrowException(string appId) + { + Action action = () => new AppIdentifier(appId); + + action.Should().Throw(); + } + + [Fact] + public void Constructor_OrgApp_ShouldReturnValidInstance() + { + var org = "ttd"; + var app = "test-app"; + var appId = $"{org}/{app}"; + + var appIdentifier = new AppIdentifier(org, app); + + appIdentifier.Org.Should().Be(org); + appIdentifier.App.Should().Be(app); + appIdentifier.ToString().Should().Be(appId); + } + + [Theory] + [InlineData("ttd", null)] + [InlineData(null, "test-app")] + public void Constructor_Null_ShouldThrowException(string org, string app) + { + Action action = () => new AppIdentifier(org, app); + + action.Should().Throw(); + } + + [Fact] + public void Constructor_NullAppId_ShouldThrowException() + { + Action action = () => new AppIdentifier((string)null); + + action.Should().Throw(); + } + + [Fact] + public void Constructor_Equals_ShouldBeEqual() + { + var org = "ttd"; + var app = "test-app"; + + var appIdentifier1 = new AppIdentifier(org, app); + var appIdentifier2 = new AppIdentifier(org, app); + + appIdentifier1.Should().BeEquivalentTo(appIdentifier2); + appIdentifier1.GetHashCode().Should().Be(appIdentifier2.GetHashCode()); + } + + [Theory] + [InlineData( + "https://dihe.apps.tt02.altinn.no/dihe/redusert-foreldrebetaling-bhg/api/v1/eventsreceiver?code=16eda4f0-653a-4fdc-b516-c4702392a4eb", + "dihe", + "redusert-foreldrebetaling-bhg" + )] + public void CreateFromUrl_ValidUrl_ShouldConstruct(string url, string expectedOrg, string expectedApp) + { + AppIdentifier appIdentifier = AppIdentifier.CreateFromUrl(url); + + appIdentifier.Org.Should().Be(expectedOrg); + appIdentifier.App.Should().Be(expectedApp); + } + + [Theory] + [InlineData("https://dihe.apps.tt02.altinn.no/dihe")] + [InlineData("dihe/redusert-foreldrebetaling-bhg")] + public void CreateFromUrl_InvalidUrl_ShouldThrowArgumentException(string url) + { + Action action = () => AppIdentifier.CreateFromUrl(url); + + action.Should().Throw(); + } + + [Fact] + public void CreateFromUrl_Null_ShouldThrowArgumentNullException() + { + Action action = () => AppIdentifier.CreateFromUrl(null); + + action.Should().Throw(); + } } + +#pragma warning restore CA1806 // Do not ignore method results diff --git a/test/Altinn.App.Core.Tests/Models/ApplicationMetdataTests.cs b/test/Altinn.App.Core.Tests/Models/ApplicationMetdataTests.cs index 041833f24..366214380 100644 --- a/test/Altinn.App.Core.Tests/Models/ApplicationMetdataTests.cs +++ b/test/Altinn.App.Core.Tests/Models/ApplicationMetdataTests.cs @@ -1,7 +1,6 @@ #nullable disable using Altinn.App.Core.Models; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Models; diff --git a/test/Altinn.App.Core.Tests/Models/InstanceIdentifierTests.cs b/test/Altinn.App.Core.Tests/Models/InstanceIdentifierTests.cs index cc3f3107a..d66065368 100644 --- a/test/Altinn.App.Core.Tests/Models/InstanceIdentifierTests.cs +++ b/test/Altinn.App.Core.Tests/Models/InstanceIdentifierTests.cs @@ -1,90 +1,88 @@ #nullable disable using Altinn.App.Core.Models; using FluentAssertions; -using Xunit; -namespace Altinn.App.PlatformServices.Tests.Models +namespace Altinn.App.PlatformServices.Tests.Models; + +public class InstanceIdentifierTests { - public class InstanceIdentifierTests + [Theory] + [InlineData( + "/yabbin/hvem-er-hvem/instances/512345/2539cacc-1f49-4852-907b-d184e7285a60/process/next", + 512345, + "2539cacc-1f49-4852-907b-d184e7285a60" + )] + [InlineData( + @"https://www.altinn.no/yabbin/hvem-er-hvem/instances/512345/2539cacc-1f49-4852-907b-d184e7285a60/process/next", + 512345, + "2539cacc-1f49-4852-907b-d184e7285a60" + )] + [InlineData( + "/yabbin/hvem-er-hvem/instance/512345/2539cacc-1f49-4852-907b-d184e7285a60/process/next", + 512345, + "2539cacc-1f49-4852-907b-d184e7285a60" + )] + public void Url_CreateFromUrl_ShouldReturnOwnerIdAndInstaceGuid( + string url, + int expectedInstanceOwnerId, + Guid expectedInstanceGuid + ) { - [Theory] - [InlineData( - "/yabbin/hvem-er-hvem/instances/512345/2539cacc-1f49-4852-907b-d184e7285a60/process/next", - 512345, - "2539cacc-1f49-4852-907b-d184e7285a60" - )] - [InlineData( - @"https://www.altinn.no/yabbin/hvem-er-hvem/instances/512345/2539cacc-1f49-4852-907b-d184e7285a60/process/next", - 512345, - "2539cacc-1f49-4852-907b-d184e7285a60" - )] - [InlineData( - "/yabbin/hvem-er-hvem/instance/512345/2539cacc-1f49-4852-907b-d184e7285a60/process/next", - 512345, - "2539cacc-1f49-4852-907b-d184e7285a60" - )] - public void Url_CreateFromUrl_ShouldReturnOwnerIdAndInstaceGuid( - string url, - int expectedInstanceOwnerId, - Guid expectedInstanceGuid - ) - { - var instanceIdentifier = InstanceIdentifier.CreateFromUrl(url); + var instanceIdentifier = InstanceIdentifier.CreateFromUrl(url); - Assert.Equal(expectedInstanceOwnerId, instanceIdentifier.InstanceOwnerPartyId); - Assert.Equal(expectedInstanceGuid, instanceIdentifier.InstanceGuid); - Assert.Equal($"{expectedInstanceOwnerId}/{expectedInstanceGuid}", instanceIdentifier.GetInstanceId()); - instanceIdentifier.IsNoInstance.Should().BeFalse(); - } + Assert.Equal(expectedInstanceOwnerId, instanceIdentifier.InstanceOwnerPartyId); + Assert.Equal(expectedInstanceGuid, instanceIdentifier.InstanceGuid); + Assert.Equal($"{expectedInstanceOwnerId}/{expectedInstanceGuid}", instanceIdentifier.GetInstanceId()); + instanceIdentifier.IsNoInstance.Should().BeFalse(); + } - [Fact] - public void Url_CreateFromUrl_ShouldThrowExceptionOnInvalidUrl() - { - var url = "/yabbin/hvem-er-hvem/512345/2539cacc-1f49-4852-907b-d184e7285a60/process/next"; + [Fact] + public void Url_CreateFromUrl_ShouldThrowExceptionOnInvalidUrl() + { + var url = "/yabbin/hvem-er-hvem/512345/2539cacc-1f49-4852-907b-d184e7285a60/process/next"; - Action action = () => InstanceIdentifier.CreateFromUrl(url); + Action action = () => InstanceIdentifier.CreateFromUrl(url); - action.Should().Throw(); - } + action.Should().Throw(); + } - [Theory] - [InlineData(512345, "2539cacc-1f49-4852-907b-d184e7285a60")] - public void Constructor_FromParts_ShouldReturnInstance(int expectedInstanceOwnerId, Guid expectedInstanceGuid) - { - var instanceIdentifier = new InstanceIdentifier(expectedInstanceOwnerId, expectedInstanceGuid); + [Theory] + [InlineData(512345, "2539cacc-1f49-4852-907b-d184e7285a60")] + public void Constructor_FromParts_ShouldReturnInstance(int expectedInstanceOwnerId, Guid expectedInstanceGuid) + { + var instanceIdentifier = new InstanceIdentifier(expectedInstanceOwnerId, expectedInstanceGuid); - instanceIdentifier.GetInstanceId().Should().Be($"{expectedInstanceOwnerId}/{expectedInstanceGuid}"); - instanceIdentifier.IsNoInstance.Should().BeFalse(); - } + instanceIdentifier.GetInstanceId().Should().Be($"{expectedInstanceOwnerId}/{expectedInstanceGuid}"); + instanceIdentifier.IsNoInstance.Should().BeFalse(); + } - [Fact] - public void Constructor_FromInstanceId_ShouldReturnInstance() - { - var instanceId = @"512345/2539cacc-1f49-4852-907b-d184e7285a60"; + [Fact] + public void Constructor_FromInstanceId_ShouldReturnInstance() + { + var instanceId = @"512345/2539cacc-1f49-4852-907b-d184e7285a60"; - var instanceIdentifier = new InstanceIdentifier(instanceId); + var instanceIdentifier = new InstanceIdentifier(instanceId); - instanceIdentifier.InstanceOwnerPartyId.Should().Be(512345); - instanceIdentifier.InstanceGuid.Should().Be("2539cacc-1f49-4852-907b-d184e7285a60"); - instanceIdentifier.GetInstanceId().Should().Be("512345/2539cacc-1f49-4852-907b-d184e7285a60"); - instanceIdentifier.ToString().Should().Be("512345/2539cacc-1f49-4852-907b-d184e7285a60"); - instanceIdentifier.IsNoInstance.Should().BeFalse(); - } + instanceIdentifier.InstanceOwnerPartyId.Should().Be(512345); + instanceIdentifier.InstanceGuid.Should().Be("2539cacc-1f49-4852-907b-d184e7285a60"); + instanceIdentifier.GetInstanceId().Should().Be("512345/2539cacc-1f49-4852-907b-d184e7285a60"); + instanceIdentifier.ToString().Should().Be("512345/2539cacc-1f49-4852-907b-d184e7285a60"); + instanceIdentifier.IsNoInstance.Should().BeFalse(); + } - [Fact] - public void InstanceIdentifier_NoInstance_Returns_Object_With_IsNoInstance_True() - { - var instanceIdentifier = InstanceIdentifier.NoInstance; + [Fact] + public void InstanceIdentifier_NoInstance_Returns_Object_With_IsNoInstance_True() + { + var instanceIdentifier = InstanceIdentifier.NoInstance; - instanceIdentifier.IsNoInstance.Should().BeTrue(); - } + instanceIdentifier.IsNoInstance.Should().BeTrue(); + } - [Fact] - public void InstanceIdentifier_NoInstance_GetInstanceId_throws_ArgumentNullException() - { - var instanceIdentifier = InstanceIdentifier.NoInstance; + [Fact] + public void InstanceIdentifier_NoInstance_GetInstanceId_throws_ArgumentNullException() + { + var instanceIdentifier = InstanceIdentifier.NoInstance; - Assert.Throws(() => instanceIdentifier.GetInstanceId()); - } + Assert.Throws(() => instanceIdentifier.GetInstanceId()); } } diff --git a/test/Altinn.App.Core.Tests/Models/MimeTypeTests.cs b/test/Altinn.App.Core.Tests/Models/MimeTypeTests.cs index acc44a015..e4ae8cad9 100644 --- a/test/Altinn.App.Core.Tests/Models/MimeTypeTests.cs +++ b/test/Altinn.App.Core.Tests/Models/MimeTypeTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Core.Models; using FluentAssertions; using Microsoft.Extensions.Primitives; -using Xunit; namespace Altinn.App.Core.Tests.Models; diff --git a/test/Altinn.App.Core.Tests/Models/PageComponentConverterTests.cs b/test/Altinn.App.Core.Tests/Models/PageComponentConverterTests.cs index 08254d2e9..461353e7a 100644 --- a/test/Altinn.App.Core.Tests/Models/PageComponentConverterTests.cs +++ b/test/Altinn.App.Core.Tests/Models/PageComponentConverterTests.cs @@ -2,7 +2,6 @@ using System.Text.Json; using Altinn.App.Core.Models.Layout.Components; using FluentAssertions; -using Xunit; using Xunit.Sdk; namespace Altinn.App.Core.Tests.Models; diff --git a/test/Altinn.App.Core.Tests/Models/Result/ServiceResultTests.cs b/test/Altinn.App.Core.Tests/Models/Result/ServiceResultTests.cs index 772827933..9bdcd2c01 100644 --- a/test/Altinn.App.Core.Tests/Models/Result/ServiceResultTests.cs +++ b/test/Altinn.App.Core.Tests/Models/Result/ServiceResultTests.cs @@ -1,6 +1,5 @@ using Altinn.App.Core.Models.Result; using FluentAssertions; -using Xunit; namespace Altinn.App.Core.Tests.Models.Result; diff --git a/test/Directory.Build.props b/test/Directory.Build.props index c4a3a4e9b..bf019140f 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -12,4 +12,4 @@ --> - + \ No newline at end of file From 8774f95db8fc3d08f87fbd5647f5e484ed864993 Mon Sep 17 00:00:00 2001 From: Johannes Haukland <42615991+HauklandJ@users.noreply.github.com> Date: Thu, 30 May 2024 12:17:21 +0200 Subject: [PATCH 22/22] Possibility to ignore formatting commits with git blame (#665) * chore: add git blame ignore file * chore: make commit 18 optional * chore: add 661 to git blame ignore list * chore: whitespace --- .git-blame-ignore-revs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..0238e10fe --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,20 @@ +# Run this command to always ignore formatting commits in `git blame` +# git config blame.ignoreRevsFile .git-blame-ignore-revs + +# More info: +# https://www.stefanjudis.com/today-i-learned/how-to-exclude-commits-from-git-blame/ +# Also, a horrible bash script to list out the 20 commits with the most files changed: +# git log --pretty='@%h' --shortstat | grep -v \| | tr "\n" " " | tr "@" "\n" | sed 's/,.*//' | sort -k2 -n | tail -n 20 | awk '{print "echo $(git log -1 --format=\"%H # %s\" " $1 ") - " $2 " files changed"}' | bash + +c55ab47f969a67a054b70ee7c82201b2e1f17388 # Altinn app platform services renaming (#3146) - 99 files changed +9a75afa100f7635c18f82f5c821e281fb6495e2d # Remove BOM on C# files now that .editorconfig states that BOM should not be used (#620) - 137 files changed +7f562dd57fe7c0299d29f2312b7a84ef170fca78 # Add pipeline job to verify clean dotnet format (#455) - 186 files changed +5fa3778bd09ed608cdc563e8ee15ff1b9de31201 # Csharpier for more consistent formatting (#533) - 471 files changed +f1ed9343c6345cf3f72bd3b9b38946176c377013 # Misc project cleanup - props, editorconfig, refactorings, docs (#661) - 482 files changed + +# Optional ignores. Stuff that absolutely do make code changes, but make a lot of code changes (which you might want +# to ignore, depending on what you're looking for). Comment these in if you need them to be ignored: +# c40e14cb84b571952981940f5871b6a349f3ad0d # merge main into v8 (#284) - 174 files changed +# f30340be522c3d6595ebc3b5a211efda439af126 # Enable nullable annotations on Api and Core.Tests (#447) - 171 files changed +# d9847039cbb79e1bac307c1cc004ff3d92243c4d # Feature/unittestcleanupapp (#3672) - 111 files changed +# 590e7751010bb7786f5b492b439100d387689b5f # Chore/90 appbase simplified (#18) - 255 files changed