From 0ddcb61ad0bf00b4ad7b147d1feca3396dfa5c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Tue, 17 Jun 2025 14:00:41 +0200 Subject: [PATCH 01/60] =?UTF-8?q?=EF=BB=BFAdd=20support=20for=20service=20?= =?UTF-8?q?tasks,=20and=20add=20new=20service=20tasks=20for=20pdf=20genera?= =?UTF-8?q?tion=20and=20eFormidling.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/ProcessController.cs | 315 +++++++----------- .../Extensions/ServiceCollectionExtensions.cs | 9 +- .../Features/Telemetry/Telemetry.Processes.cs | 8 + .../Features/Telemetry/Telemetry.cs | 1 + .../Internal/Pdf/IPdfService.cs | 15 + .../Internal/Pdf/PdfService.cs | 89 +++-- ...tinnExtensionConfigValidationExtensions.cs | 38 +++ .../AltinnPaymentConfiguration.cs | 18 - .../AltinnPdfConfiguration.cs | 22 ++ .../AltinnTaskExtension.cs | 6 + .../Internal/Process/Elements/Process.cs | 6 + .../Internal/Process/Elements/ServiceTask.cs | 16 + .../{Interfaces => }/IEndEventEventHandler.cs | 0 .../ProcessTask/EndTaskEventHandler.cs | 21 +- .../ProcessTask/StartTaskEventHandler.cs | 1 + .../Process/Interfaces/IProcessEngine.cs | 8 +- .../Internal/Process/ProcessEngine.cs | 300 +++++++++++++++-- .../Process/ProcessEngineAuthorizer.cs | 2 +- .../Process/ProcessEventHandlingDelegator.cs | 9 + .../Internal/Process/ProcessReader.cs | 8 +- .../{Interfaces => }/IProcessTaskCleaner.cs | 2 +- .../IProcessTaskDataLocker.cs | 2 +- .../{Interfaces => }/IProcessTaskFinalizer.cs | 2 +- .../IProcessTaskInitializer.cs | 2 +- .../ProcessTasks/Common/ProcessTaskCleaner.cs | 2 +- .../Common/ProcessTaskDataLocker.cs | 2 +- .../Common/ProcessTaskFinalizer.cs | 2 +- .../Common/ProcessTaskInitializer.cs | 2 +- .../{Interfaces => }/IProcessTask.cs | 15 +- .../ServiceTasks/EFormidlingServiceTask.cs | 77 +++++ .../ProcessTasks/ServiceTasks/IServiceTask.cs | 72 ++++ .../Legacy/EformidlingServiceTaskLegacy.cs} | 27 +- .../Legacy/PdfServiceTaskLegacy.cs} | 25 +- .../ServiceTasks/PdfServiceTask.cs | 74 ++++ .../ServiceTasks/Interfaces/IServiceTask.cs | 16 - .../Models/Process/ProcessChangeResult.cs | 11 + .../Models/Process/ProcessNextRequest.cs | 4 +- .../Altinn.App.Api.Tests.csproj | 12 +- ...dator_ReturnsValidationErrors.verified.txt | 14 + ...sNext_PdfFails_DataIsUnlocked.verified.txt | 3 - .../Instances/ttd/service-tasks/.gitignore | 4 + ...d-db99-45f9-9625-9dfa1223485f.pretest.json | 41 +++ ...4c42a4-a6a0-4aec-b8e9-9f5d34c445ca.pretest | 1 + ...4-a6a0-4aec-b8e9-9f5d34c445ca.pretest.json | 17 + ...d-db99-45f9-9625-9dfa1223485f.pretest.json | 41 +++ .../apps/ttd/service-tasks/appsettings.json | 37 ++ .../config/applicationmetadata.json | 54 +++ .../config/authorization/policy.xml | 293 ++++++++++++++++ .../service-tasks/config/process/process.bpmn | 63 ++++ .../config/texts/resource.nb.json | 17 + .../apps/ttd/service-tasks/models/Model.cs | 20 ++ .../service-tasks/models/Model.schema.json | 28 ++ .../apps/ttd/service-tasks/models/Model.xsd | 17 + .../apps/ttd/service-tasks/ui/footer.json | 11 + .../ttd/service-tasks/ui/form/RuleHandler.js | 63 ++++ .../ttd/service-tasks/ui/form/Settings.json | 6 + .../service-tasks/ui/form/layouts/Side1.json | 26 ++ .../ttd/service-tasks/ui/layout-sets.json | 11 + .../Mocks/Event/EventsClientMock.cs | 12 + .../Mocks/ProcessClientMock.cs | 57 ++++ .../Mocks/SignClientMock.cs | 11 + .../EFormidlingServiceTaskTests.cs | 147 ++++++++ .../ServiceTasks/Pdf/PdfServiceTaskTests.cs | 164 +++++++++ test/Altinn.App.Api.Tests/Program.cs | 3 + .../ProcessTask/EndTaskEventHandlerTests.cs | 69 ++-- .../ProcessTask/StartTaskEventHandlerTests.cs | 1 + .../Internal/Process/ProcessEngineTest.cs | 106 +++++- .../Process/ProcessEventHandlingTests.cs | 7 + .../Common/ProcessTaskDataLockerTests.cs | 1 + .../Common/ProcessTaskFinalizerTests.cs | 2 +- .../EFormidlingServiceTaskTests.cs | 89 +++++ .../EformidlingServiceTaskLegacyTests.cs} | 26 +- .../Legacy/PdfServiceTaskLegacyTests.cs | 181 ++++++++++ .../{ => Legacy}/TestData/DummyDataType.cs | 0 .../ServiceTasks/PdfServiceTaskTests.cs | 187 ++--------- 75 files changed, 2514 insertions(+), 557 deletions(-) create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnExtensionConfigValidationExtensions.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/ServiceTask.cs rename src/Altinn.App.Core/Internal/Process/EventHandlers/{Interfaces => }/IEndEventEventHandler.cs (100%) rename src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/{Interfaces => }/IProcessTaskCleaner.cs (88%) rename src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/{Interfaces => }/IProcessTaskDataLocker.cs (91%) rename src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/{Interfaces => }/IProcessTaskFinalizer.cs (89%) rename src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/{Interfaces => }/IProcessTaskInitializer.cs (91%) rename src/Altinn.App.Core/Internal/Process/ProcessTasks/{Interfaces => }/IProcessTask.cs (74%) create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs rename src/Altinn.App.Core/Internal/Process/{ServiceTasks/EformidlingServiceTask.cs => ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs} (69%) rename src/Altinn.App.Core/Internal/Process/{ServiceTasks/PdfServiceTask.cs => ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs} (58%) create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs delete mode 100644 src/Altinn.App.Core/Internal/Process/ServiceTasks/Interfaces/IServiceTask.cs create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/.gitignore create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f/blob/fd4c42a4-a6a0-4aec-b8e9-9f5d34c445ca.pretest create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f/fd4c42a4-a6a0-4aec-b8e9-9f5d34c445ca.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/b1af1cfd-db99-45f9-9625-9dfa1223485f.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/appsettings.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/applicationmetadata.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/authorization/policy.xml create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/process/process.bpmn create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/texts/resource.nb.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.cs create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.schema.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.xsd create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/footer.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/RuleHandler.js create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/Settings.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/layouts/Side1.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/layout-sets.json create mode 100644 test/Altinn.App.Api.Tests/Mocks/Event/EventsClientMock.cs create mode 100644 test/Altinn.App.Api.Tests/Mocks/ProcessClientMock.cs create mode 100644 test/Altinn.App.Api.Tests/Mocks/SignClientMock.cs create mode 100644 test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs create mode 100644 test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs rename test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/{EformidlingServiceTaskTests.cs => Legacy/EformidlingServiceTaskLegacyTests.cs} (87%) create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/Legacy/PdfServiceTaskLegacyTests.cs rename test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/{ => Legacy}/TestData/DummyDataType.cs (100%) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index c6d2ac647..6254b66e8 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -9,9 +9,9 @@ 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.ProcessTasks.ServiceTasks; using Altinn.App.Core.Internal.Validation; using Altinn.App.Core.Models.Process; -using Altinn.App.Core.Models.UserAction; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; @@ -35,12 +35,12 @@ public class ProcessController : ControllerBase 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; - private readonly InstanceDataUnitOfWorkInitializer _instanceDataUnitOfWorkInitializer; private readonly IProcessEngineAuthorizer _processEngineAuthorizer; + private readonly IValidationService _validationService; + private readonly InstanceDataUnitOfWorkInitializer _instanceDataUnitOfWorkInitializer; /// /// Initializes a new instance of the @@ -60,12 +60,12 @@ IProcessEngineAuthorizer processEngineAuthorizer _logger = logger; _instanceClient = instanceClient; _processClient = processClient; - _validationService = validationService; _authorization = authorization; _processReader = processReader; _processEngine = processEngine; - _instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService(); _processEngineAuthorizer = processEngineAuthorizer; + _validationService = validationService; + _instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService(); } /// @@ -238,38 +238,6 @@ [FromRoute] Guid instanceGuid } } - private async Task GetValidationProblemDetails( - Instance instance, - string currentTaskId, - string? language - ) - { - var dataAccessor = await _instanceDataUnitOfWorkInitializer.Init(instance, currentTaskId, language); - - var validationIssues = await _validationService.ValidateInstanceAtTask( - dataAccessor, - currentTaskId, // run full validation - ignoredValidators: null, - onlyIncrementalValidators: null, - language: language - ); - var success = validationIssues.TrueForAll(v => v.Severity != ValidationIssueSeverity.Error); - - if (!success) - { - var errorCount = validationIssues.Count(v => v.Severity == ValidationIssueSeverity.Error); - return new ProblemDetails() - { - Detail = $"{errorCount} validation errors found for task {currentTaskId}", - Status = StatusCodes.Status409Conflict, - Title = "Validation failed for task", - Extensions = new Dictionary() { { "validationIssues", validationIssues } }, - }; - } - - return null; - } - /// /// Change the instance's process state to next process element in accordance with process definition. /// @@ -286,7 +254,7 @@ [FromRoute] Guid instanceGuid [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> NextElement( + public async Task> ProcessNext( [FromRoute] string org, [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, @@ -299,117 +267,35 @@ public async Task> NextElement( { try { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + Instance instance; + ProcessChangeResult result; - string? currentTaskId = instance.Process.CurrentTask?.ElementId; + bool moveToNextTaskAutomatically; + bool firstIteration = true; - if (currentTaskId is null) + do { - return Conflict( - new ProblemDetails() - { - Status = StatusCodes.Status409Conflict, - Title = "Process is not started. Use start!", - } - ); - } - - if (instance.Process.Ended.HasValue) - { - return Conflict( - new ProblemDetails() { Status = StatusCodes.Status409Conflict, Title = "Process is ended." } - ); - } - - string? altinnTaskType = instance.Process.CurrentTask?.AltinnTaskType; + instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (altinnTaskType == null) - { - return Conflict( - new ProblemDetails() - { - Status = StatusCodes.Status409Conflict, - Title = "Instance does not have current altinn task type information!", - } - ); - } - - bool authorized = await _processEngineAuthorizer.AuthorizeProcessNext(instance, processNext?.Action); - - if (!authorized) - { - return StatusCode( - 403, - new ProblemDetails() - { - Status = StatusCodes.Status403Forbidden, - Detail = - $"User is not authorized to perform process next. Task ID: {currentTaskId}. Task type: {altinnTaskType}. Action: {processNext?.Action ?? "none"}.", - Title = "Unauthorized", - } - ); - } - - _logger.LogDebug( - "User successfully authorized to perform process next. Task ID: {CurrentTaskId}. Task type: {AltinnTaskType}. Action: {ProcessNextAction}.", - currentTaskId, - altinnTaskType, - LogSanitizer.Sanitize(processNext?.Action ?? "none") - ); - - string checkedAction = processNext?.Action ?? ConvertTaskTypeToAction(altinnTaskType); - - var request = new ProcessNextRequest() - { - Instance = instance, - User = User, - Action = checkedAction, - ActionOnBehalfOf = processNext?.ActionOnBehalfOf, - Language = language, - }; - - if (processNext?.Action is not null) - { - UserActionResult userActionResult = await _processEngine.HandleUserAction(request, ct); - if (userActionResult.ResultType is ResultType.Failure) + var processNextRequest = new ProcessNextRequest { - var failedUserActionResult = new ProcessChangeResult() - { - Success = false, - ErrorMessage = $"Action handler for action {request.Action} failed!", - ErrorType = userActionResult.ErrorType, - }; + User = User, + Instance = instance, + Action = firstIteration ? processNext?.Action : null, + ActionOnBehalfOf = firstIteration ? processNext?.ActionOnBehalfOf : null, + Language = language, + }; - return GetResultForError(failedUserActionResult); - } - } + result = await _processEngine.Next(processNextRequest, ct); + moveToNextTaskAutomatically = await ShouldMoveToNextTaskAutomatically(instance, ct); - // If the action is 'reject' the task is being abandoned, and we should skip validation, but only if reject has been allowed for the task in bpmn. - if (checkedAction == "reject" && _processReader.IsActionAllowedForTask(currentTaskId, checkedAction)) - { - _logger.LogInformation( - "Skipping validation during process next because the action is 'reject' and the task is being abandoned." - ); - } - else - { - ProblemDetails? validationProblem = await GetValidationProblemDetails( - instance, - currentTaskId, - language - ); - if (validationProblem is not null) + if (!result.Success) { - return Conflict(validationProblem); + return GetResultForError(result); } - } - - ProcessChangeResult result = await _processEngine.Next(request); - if (!result.Success) - { - return GetResultForError(result); - } + firstIteration = false; + } while (moveToNextTaskAutomatically); AppProcessState appProcessState = await ConvertAndAuthorizeActions( instance, @@ -429,52 +315,6 @@ public async Task> NextElement( } } - private ActionResult GetResultForError(ProcessChangeResult result) - { - switch (result.ErrorType) - { - case ProcessErrorType.Conflict: - return Conflict( - new ProblemDetails() - { - Detail = result.ErrorMessage, - Status = StatusCodes.Status409Conflict, - Title = "Conflict", - } - ); - case ProcessErrorType.Internal: - return StatusCode( - 500, - new ProblemDetails() - { - Detail = result.ErrorMessage, - Status = StatusCodes.Status500InternalServerError, - Title = "Internal server error", - } - ); - case ProcessErrorType.Unauthorized: - return StatusCode( - 403, - new ProblemDetails() - { - Detail = result.ErrorMessage, - Status = StatusCodes.Status403Forbidden, - Title = "Unauthorized", - } - ); - default: - return StatusCode( - 500, - new ProblemDetails() - { - Detail = $"Unknown ProcessErrorType {result.ErrorType}", - Status = StatusCodes.Status500InternalServerError, - Title = "Internal server error", - } - ); - } - } - /// /// Attemts to end the process by running next until an end event is reached. /// Notice that process must have been started. @@ -561,14 +401,14 @@ instance.Process.EndEvent is null try { - ProcessNextRequest request = new ProcessNextRequest() + ProcessNextRequest request = new() { Instance = instance, User = User, Action = ConvertTaskTypeToAction(instance.Process.CurrentTask.AltinnTaskType), Language = language, }; - var result = await _processEngine.Next(request); + ProcessChangeResult result = await _processEngine.Next(request); if (!result.Success) { @@ -676,6 +516,73 @@ private async Task ConvertAndAuthorizeActions(Instance instance return appProcessState; } + private async Task ShouldMoveToNextTaskAutomatically(Instance instance, CancellationToken ct) + { + if (instance.Process.CurrentTask is null) + { + return false; + } + + IServiceTask? serviceTask = _processEngine.CheckIfServiceTask(instance.Process.CurrentTask.AltinnTaskType); + + if (serviceTask is not null) + { + return await serviceTask.MoveToNextTaskAfterExecution(instance.Process.CurrentTask.ElementId, instance, ct); + } + + return false; + } + + private ActionResult GetResultForError(ProcessChangeResult result) + { + switch (result.ErrorType) + { + case ProcessErrorType.Conflict: + return Conflict( + new ProblemDetails() + { + Detail = result.ErrorMessage, + Status = StatusCodes.Status409Conflict, + Title = result.ErrorTitle, + Extensions = new Dictionary + { + { "validationIssues", result.ValidationIssues }, + }, + } + ); + case ProcessErrorType.Internal: + return StatusCode( + 500, + new ProblemDetails() + { + Detail = result.ErrorMessage, + Status = StatusCodes.Status500InternalServerError, + Title = result.ErrorTitle ?? "Internal server error", + } + ); + case ProcessErrorType.Unauthorized: + return StatusCode( + 403, + new ProblemDetails() + { + Detail = result.ErrorMessage, + Status = StatusCodes.Status403Forbidden, + Title = result.ErrorTitle ?? "Unauthorized", + } + ); + default: + return StatusCode( + 500, + new ProblemDetails() + { + Detail = $"Unknown ProcessErrorType {result.ErrorType}", + Status = StatusCodes.Status500InternalServerError, + Title = result.ErrorTitle ?? "Internal server error", + } + ); + } + } + private ObjectResult ExceptionResponse(Exception exception, string message) { _logger.LogError(exception, message); @@ -717,6 +624,38 @@ private ObjectResult ExceptionResponse(Exception exception, string message) ); } + private async Task GetValidationProblemDetails( + Instance instance, + string currentTaskId, + string? language + ) + { + var dataAccessor = await _instanceDataUnitOfWorkInitializer.Init(instance, currentTaskId, language); + + var validationIssues = await _validationService.ValidateInstanceAtTask( + dataAccessor, + currentTaskId, // run full validation + ignoredValidators: null, + onlyIncrementalValidators: null, + language: language + ); + var success = validationIssues.TrueForAll(v => v.Severity != ValidationIssueSeverity.Error); + + if (!success) + { + var errorCount = validationIssues.Count(v => v.Severity == ValidationIssueSeverity.Error); + return new ProblemDetails() + { + Detail = $"{errorCount} validation errors found for task {currentTaskId}", + Status = StatusCodes.Status409Conflict, + Title = "Validation failed for task", + Extensions = new Dictionary() { { "validationIssues", validationIssues } }, + }; + } + + return null; + } + private async Task> AuthorizeActions(List actions, Instance instance) { return await _authorization.AuthorizeActions(instance, HttpContext.User, actions); @@ -728,6 +667,8 @@ private static string ConvertTaskTypeToAction(string actionOrTaskType) { case "data": case "feedback": + case "pdf": + case "eFormidling": return "write"; case "confirmation": return "confirm"; diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index a4bcfbcab..c81f39925 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -47,7 +47,9 @@ using Altinn.App.Core.Internal.Process.EventHandlers; using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; using Altinn.App.Core.Internal.Process.ProcessTasks; -using Altinn.App.Core.Internal.Process.ServiceTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks.Common; +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Internal.Secrets; using Altinn.App.Core.Internal.Sign; @@ -369,8 +371,11 @@ private static void AddProcessServices(IServiceCollection services) services.AddTransient(); // Service tasks + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); - services.AddTransient(); + services.AddTransient(); } private static void AddActionServices(IServiceCollection services) diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.Processes.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.Processes.cs index 6f5303dfa..a30f19645 100644 --- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.Processes.cs +++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.Processes.cs @@ -62,6 +62,14 @@ internal void ProcessEnded(ProcessStateChange processChange) return activity; } + internal Activity? StartProcessExecuteServiceTaskActivity(Instance instance, string serviceTaskType) + { + var activity = ActivitySource.StartActivity($"{Prefix}.ExecuteServiceTask"); + activity?.SetInstanceId(instance); + activity?.SetTag(InternalLabels.ProcessServiceTaskType, serviceTaskType); + return activity; + } + internal Activity? StartProcessEndActivity(Instance instance) { var activity = ActivitySource.StartActivity($"{Prefix}.End"); diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs index 9f6c21816..865855077 100644 --- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs +++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs @@ -235,6 +235,7 @@ internal static class InternalLabels internal const string ProcessErrorType = "process.error.type"; internal const string ProcessAction = "process.action"; + internal const string ProcessServiceTaskType = "process.service.task.type"; internal const string ProblemType = "problem.type"; internal const string ProblemTitle = "problem.title"; diff --git a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs index 1f8abf2ad..57bb78447 100644 --- a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs @@ -16,6 +16,21 @@ public interface IPdfService /// 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. 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 + /// A text resource element id for the file name of the PDF. If no text resource is found, the literal value will be used. If null, a default file name will be used. + /// Cancellation token for when a request should be stopped before it's completed. + Task GenerateAndStorePdf( + Instance instance, + string taskId, + string? fileNameTextResourceElementId, + CancellationToken ct = default + ); + /// /// Generate a PDF of what the user can currently see from the given instance of an app. /// diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index 43a2e9508..4533ad2a2 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -72,26 +72,20 @@ public async Task GenerateAndStorePdf(Instance instance, string taskId, Cancella { using var activity = _telemetry?.StartGenerateAndStorePdfActivity(instance, taskId); - HttpContext? httpContext = _httpContextAccessor.HttpContext; - var queries = httpContext?.Request.Query; - var auth = _authenticationContext.Current; - - var language = GetOverriddenLanguage(queries) ?? await auth.GetLanguage(); - - TextResource? textResource = await GetTextResource(instance, language); + await GenerateAndStorePdfInternal(instance, taskId, null, ct); + } - var pdfContent = await GeneratePdfContent(instance, language, false, textResource, ct); + /// + public async Task GenerateAndStorePdf( + Instance instance, + string taskId, + string? fileNameTextResourceElementId, + CancellationToken ct = default + ) + { + using var activity = _telemetry?.StartGenerateAndStorePdfActivity(instance, taskId); - string fileName = GetFileName(instance, textResource); - await _dataClient.InsertBinaryData( - instance.Id, - PdfElementType, - PdfContentType, - fileName, - pdfContent, - taskId, - cancellationToken: ct - ); + await GenerateAndStorePdfInternal(instance, taskId, fileNameTextResourceElementId, ct); } /// @@ -116,6 +110,35 @@ public async Task GeneratePdf(Instance instance, string taskId, Cancella return await GeneratePdf(instance, taskId, false, ct); } + private async Task GenerateAndStorePdfInternal( + Instance instance, + string taskId, + string? fileNameTextResourceElementId, + CancellationToken ct = default + ) + { + HttpContext? httpContext = _httpContextAccessor.HttpContext; + var queries = httpContext?.Request.Query; + var auth = _authenticationContext.Current; + + var language = GetOverriddenLanguage(queries) ?? await auth.GetLanguage(); + + TextResource? textResource = await GetTextResource(instance, language); + + var pdfContent = await GeneratePdfContent(instance, language, false, textResource, ct); + + string fileName = GetFileName(instance, textResource, fileNameTextResourceElementId); + await _dataClient.InsertBinaryData( + instance.Id, + PdfElementType, + PdfContentType, + fileName, + pdfContent, + taskId, + cancellationToken: ct + ); + } + private async Task GeneratePdfContent( Instance instance, string language, @@ -200,10 +223,26 @@ private static Uri BuildUri(string baseUrl, string pagePath, string language) return textResource; } - private static string GetFileName(Instance instance, TextResource? textResource) + private static string GetFileName( + Instance instance, + TextResource? textResource, + string? fileNameTextResourceElementId = null + ) { + if (!string.IsNullOrEmpty(fileNameTextResourceElementId)) + { + TextResourceElement? textResourceElement = textResource?.Resources.Find(textResourceElement => + textResourceElement.Id.Equals(fileNameTextResourceElementId, StringComparison.Ordinal) + ); + + if (textResourceElement is not null) + return GetValidFileName(textResourceElement.Value); + + return GetValidFileName(fileNameTextResourceElementId); + } + string app = instance.AppId.Split("/")[1]; - string fileName = $"{app}.pdf"; + var fileName = $"{app}.pdf"; if (textResource is null) { @@ -261,6 +300,16 @@ private static string GetPdfPreviewText(TextResource? textResource) private static string GetValidFileName(string fileName) { fileName = Uri.EscapeDataString(fileName.AsFileName(false)); + return AddPdfFileTypeIfMissing(fileName); + } + + private static string AddPdfFileTypeIfMissing(string fileName) + { + if (!fileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) + { + return fileName + ".pdf"; + } + return fileName; } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnExtensionConfigValidationExtensions.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnExtensionConfigValidationExtensions.cs new file mode 100644 index 000000000..4f98c6d66 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnExtensionConfigValidationExtensions.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +internal static class AltinnExtensionConfigValidationExtensions +{ + 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; + } + + internal static bool IsEmpty( + [NotNullWhen(false)] this IEnumerable? value, + [NotNullWhen(true)] ref List? errors, + string error + ) + { + bool isEmpty = value?.Any() == false; + if (isEmpty) + { + errors ??= new List(1); + errors.Add(error); + } + + return isEmpty; + } +} 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 51c2e8cfa..a64a0f95f 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPaymentConfiguration.cs @@ -50,21 +50,3 @@ internal readonly record struct ValidAltinnPaymentConfiguration( string PaymentDataType, string PaymentReceiptPdfDataType ); - -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/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs new file mode 100644 index 000000000..44f7048ea --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs @@ -0,0 +1,22 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +/// +/// Configuration properties for PDF in a process task +/// +public class AltinnPdfConfiguration +{ + /// + /// Set the filename of the PDF. Supports text resource keys for language support. + /// + [XmlElement("filename", Namespace = "http://altinn.no/process")] + public string? Filename { get; set; } + + internal ValidAltinnPdfConfiguration Validate() + { + return new ValidAltinnPdfConfiguration(Filename); + } +} + +internal readonly record struct ValidAltinnPdfConfiguration(string? Filename); 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 0a79c772e..d0f2e04c0 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs @@ -34,6 +34,12 @@ public class AltinnTaskExtension [XmlElement("paymentConfig", Namespace = "http://altinn.no/process")] public AltinnPaymentConfiguration? PaymentConfiguration { get; set; } = new AltinnPaymentConfiguration(); + /// + /// Gets or sets the configuration for PDF + /// + [XmlElement("pdfConfig", Namespace = "http://altinn.no/process")] + public AltinnPdfConfiguration? PdfConfiguration { get; set; } + /// /// Retrieves a configuration item for given environment, in a predictable manner. /// Specific configurations (those specifying an environment) takes precedence over global configurations. diff --git a/src/Altinn.App.Core/Internal/Process/Elements/Process.cs b/src/Altinn.App.Core/Internal/Process/Elements/Process.cs index 70b4e3179..1c2112f8c 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/Process.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/Process.cs @@ -35,6 +35,12 @@ public class Process [XmlElement("task")] public List Tasks { get; set; } + /// + /// Gets or sets the list of service tasks for the process of a workflow + /// + [XmlElement("serviceTask")] + public List ServiceTasks { get; set; } + /// /// Gets or sets the end event of the process of a workflow /// diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ServiceTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/ServiceTask.cs new file mode 100644 index 000000000..b0525f310 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/ServiceTask.cs @@ -0,0 +1,16 @@ +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Class representing the task of a process +/// +public class ServiceTask : ProcessTask +{ + /// + /// String representation of process element type + /// + /// Task + public override string ElementType() + { + return "ServiceTask"; + } +} diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/IEndEventEventHandler.cs similarity index 100% rename from src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs rename to src/Altinn.App.Core/Internal/Process/EventHandlers/IEndEventEventHandler.cs 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 d1a9f6ed8..ef5cb86d8 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs @@ -1,6 +1,7 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.ProcessTasks; -using Altinn.App.Core.Internal.Process.ServiceTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks.Common; +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -15,6 +16,8 @@ public class EndTaskEventHandler : IEndTaskEventHandler private readonly IProcessTaskDataLocker _processTaskDataLocker; private readonly IProcessTaskFinalizer _processTaskFinisher; private readonly AppImplementationFactory _appImplementationFactory; + private readonly IPdfServiceTaskLegacy _pdfServiceTaskLegacy; + private readonly IEFormidlingServiceTaskLegacy _eformidlingServiceTaskLegacy; private readonly ILogger _logger; /// @@ -30,6 +33,8 @@ ILogger logger _processTaskDataLocker = processTaskDataLocker; _processTaskFinisher = processTaskFinisher; _appImplementationFactory = serviceProvider.GetRequiredService(); + _pdfServiceTaskLegacy = serviceProvider.GetRequiredService(); + _eformidlingServiceTaskLegacy = serviceProvider.GetRequiredService(); _logger = logger; } @@ -38,23 +43,15 @@ ILogger logger /// public async Task Execute(IProcessTask processTask, string taskId, Instance instance) { - var serviceTasks = _appImplementationFactory.GetAll(); - var pdfServiceTask = - serviceTasks.FirstOrDefault(x => x is IPdfServiceTask) - ?? throw new InvalidOperationException("PdfServiceTask not found in serviceTasks"); - var eformidlingServiceTask = - serviceTasks.FirstOrDefault(x => x is IEformidlingServiceTask) - ?? throw new InvalidOperationException("EformidlingServiceTask not found in serviceTasks"); - 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. + //These two services are scheduled to be removed in a major version. Pdf and eFormidling have been implemented as service tasks and can be added to the app using the process bpmn file. try { - await pdfServiceTask.Execute(taskId, instance); + await _pdfServiceTaskLegacy.Execute(taskId, instance); } catch (Exception e) { @@ -65,7 +62,7 @@ public async Task Execute(IProcessTask processTask, string taskId, Instance inst try { - await eformidlingServiceTask.Execute(taskId, instance); + await _eformidlingServiceTaskLegacy.Execute(taskId, instance); } catch (Exception e) { 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 6fe6b5200..b089216ea 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs @@ -1,5 +1,6 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks.Common; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs index 5e479b230..f5018b552 100644 --- a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs @@ -1,5 +1,5 @@ +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; using Altinn.App.Core.Models.Process; -using Altinn.App.Core.Models.UserAction; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process; @@ -17,12 +17,12 @@ public interface IProcessEngine /// /// Method to move process to next task/event /// - Task Next(ProcessNextRequest request); + Task Next(ProcessNextRequest request, CancellationToken ct = default); /// - /// Method to handle user action + /// Check if the Altinn task type is a service task /// - Task HandleUserAction(ProcessNextRequest request, CancellationToken ct); + IServiceTask? CheckIfServiceTask(string? altinnTaskType); /// /// Handle process events and update storage diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 60de63b26..becde8b3f 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -8,11 +8,16 @@ using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; +using Altinn.App.Core.Internal.Validation; +using Altinn.App.Core.Models; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.UserAction; +using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Altinn.App.Core.Internal.Process; @@ -30,9 +35,12 @@ public class ProcessEngine : IProcessEngine private readonly IAuthenticationContext _authenticationContext; private readonly InstanceDataUnitOfWorkInitializer _instanceDataUnitOfWorkInitializer; private readonly AppImplementationFactory _appImplementationFactory; + private readonly IProcessEngineAuthorizer _processEngineAuthorizer; + private readonly ILogger _logger; + private readonly IValidationService _validationService; /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class. /// public ProcessEngine( IProcessReader processReader, @@ -42,6 +50,9 @@ public ProcessEngine( UserActionService userActionService, IAuthenticationContext authenticationContext, IServiceProvider serviceProvider, + IProcessEngineAuthorizer processEngineAuthorizer, + IValidationService validationService, + ILogger logger, Telemetry? telemetry = null ) { @@ -52,6 +63,9 @@ public ProcessEngine( _userActionService = userActionService; _telemetry = telemetry; _authenticationContext = authenticationContext; + _processEngineAuthorizer = processEngineAuthorizer; + _validationService = validationService; + _logger = logger; _appImplementationFactory = serviceProvider.GetRequiredService(); _instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService(); } @@ -127,17 +141,225 @@ out ProcessError? startEventError } /// - public async Task HandleUserAction(ProcessNextRequest request, CancellationToken ct) + public async Task Next(ProcessNextRequest request, CancellationToken ct = default) { Instance instance = request.Instance; - var currentAuth = _authenticationContext.Current; + using Activity? activity = _telemetry?.StartProcessNextActivity(instance, request.Action); + + if (instance.Process is null) + { + var result = new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Conflict, + ErrorMessage = "The instance is missing process information.", + }; + activity?.SetProcessChangeResult(result); + return result; + } + + if (instance.Process?.Ended != null) + { + var result = new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Conflict, + ErrorMessage = "Process is ended.", + }; + activity?.SetProcessChangeResult(result); + return result; + } + + string? currentTaskId = instance.Process?.CurrentTask?.ElementId; + if (currentTaskId is null) + { + var result = new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Conflict, + ErrorMessage = "Process is not started. Use start!", + }; + activity?.SetProcessChangeResult(result); + return result; + } + + string? altinnTaskType = instance.Process?.CurrentTask?.AltinnTaskType; + if (altinnTaskType == null) + { + var result = new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Conflict, + ErrorMessage = "Instance does not have current altinn task type information!", + }; + activity?.SetProcessChangeResult(result); + return result; + } + + bool authorized = await _processEngineAuthorizer.AuthorizeProcessNext(instance, request.Action); + + if (!authorized) + { + var result = new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Unauthorized, + ErrorMessage = + $"User is not authorized to perform process next. Task ID: {currentTaskId}. Task type: {altinnTaskType}. Action: {request.Action ?? "none"}.", + }; + activity?.SetProcessChangeResult(result); + return result; + } + + _logger.LogDebug( + "User successfully authorized to perform process next. Task ID: {CurrentTaskId}. Task type: {AltinnTaskType}. Action: {ProcessNextAction}.", + currentTaskId, + altinnTaskType, + LogSanitizer.Sanitize(request.Action ?? "none") + ); + + string checkedAction = request.Action ?? ConvertTaskTypeToAction(altinnTaskType); + + // If the action is 'reject', we should not run any service task and there is no need to check for a user action handler, since 'reject' doesn't have one. + if (request.Action is not "reject") + { + IServiceTask? serviceTask = CheckIfServiceTask(altinnTaskType); + if (serviceTask is not null) + { + ServiceTaskResult serviceActionResult = await HandleServiceTask( + instance, + serviceTask, + request with + { + Action = checkedAction, + }, + ct + ); + + if (serviceActionResult.Result is ServiceTaskResult.ResultType.Failure) + { + var result = new ProcessChangeResult() + { + Success = false, + ErrorMessage = serviceActionResult.ErrorMessage, + ErrorType = serviceActionResult.ErrorType, + }; + activity?.SetProcessChangeResult(result); + return result; + } + } + else + { + if (request.Action is not null) + { + UserActionResult userActionResult = await HandleUserAction(instance, request, ct); + + if (userActionResult.ResultType is ResultType.Failure) + { + var result = new ProcessChangeResult() + { + Success = false, + ErrorMessage = $"Action handler for action {request.Action} failed!", + ErrorType = userActionResult.ErrorType, + }; + activity?.SetProcessChangeResult(result); + return result; + } + } + } + } + + // If the action is 'reject' the task is being abandoned, and we should skip validation, but only if reject has been allowed for the task in bpmn. + if (checkedAction == "reject" && _processReader.IsActionAllowedForTask(currentTaskId, checkedAction)) + { + _logger.LogInformation( + "Skipping validation during process next because the action is 'reject' and the task is being abandoned." + ); + } + else + { + InstanceDataUnitOfWork dataAccessor = await _instanceDataUnitOfWorkInitializer.Init( + instance, + currentTaskId, + request.Language + ); + + List validationIssues = await _validationService.ValidateInstanceAtTask( + dataAccessor, + currentTaskId, // run full validation + ignoredValidators: null, + onlyIncrementalValidators: null, + language: request.Language + ); + + int errorCount = validationIssues.Count(v => v.Severity == ValidationIssueSeverity.Error); + + if (errorCount > 0) + { + var result = new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Conflict, + ErrorTitle = "Validation failed for task", + ErrorMessage = $"{errorCount} validation errors found for task {currentTaskId}", + ValidationIssues = validationIssues, + }; + activity?.SetProcessChangeResult(result); + return result; + } + } + + MoveToNextResult moveToNextResult = await HandleMoveToNext(instance, request.Action); + + if (moveToNextResult.IsEndEvent) + { + _telemetry?.ProcessEnded(moveToNextResult.ProcessStateChange); + await RunAppDefinedProcessEndHandlers(instance, moveToNextResult.ProcessStateChange?.Events); + } + + var changeResult = new ProcessChangeResult() + { + Success = true, + ProcessStateChange = moveToNextResult.ProcessStateChange, + }; + + activity?.SetProcessChangeResult(changeResult); + return changeResult; + } + + /// + public IServiceTask? CheckIfServiceTask(string? altinnTaskType) + { + if (altinnTaskType is null) + return null; + + IEnumerable serviceTasks = _appImplementationFactory.GetAll(); + IServiceTask? serviceTask = serviceTasks.FirstOrDefault(x => + x.Type.Equals(altinnTaskType, StringComparison.OrdinalIgnoreCase) + ); + + return serviceTask; + } + + /// + private async Task HandleUserAction( + Instance instance, + ProcessNextRequest request, + CancellationToken ct + ) + { + Authenticated currentAuth = _authenticationContext.Current; IUserAction? actionHandler = _userActionService.GetActionHandler(request.Action); if (actionHandler is null) return UserActionResult.SuccessResult(); - var cachedDataMutator = await _instanceDataUnitOfWorkInitializer.Init(instance, taskId: null, request.Language); + InstanceDataUnitOfWork cachedDataMutator = await _instanceDataUnitOfWorkInitializer.Init( + instance, + taskId: null, + request.Language + ); int? userId = currentAuth switch { @@ -168,7 +390,7 @@ public async Task HandleUserAction(ProcessNextRequest request, ); } - var changes = cachedDataMutator.GetDataElementChanges(initializeAltinnRowId: false); + DataElementChanges changes = cachedDataMutator.GetDataElementChanges(initializeAltinnRowId: false); await cachedDataMutator.UpdateInstanceData(changes); await cachedDataMutator.SaveChanges(changes); @@ -176,40 +398,45 @@ public async Task HandleUserAction(ProcessNextRequest request, } /// - public async Task Next(ProcessNextRequest request) + private async Task HandleServiceTask( + Instance instance, + IServiceTask serviceTask, + ProcessNextRequest request, + CancellationToken ct = default + ) { - using var activity = _telemetry?.StartProcessNextActivity(request.Instance, request.Action); + using Activity? activity = _telemetry?.StartProcessExecuteServiceTaskActivity(instance, serviceTask.Type); - Instance instance = request.Instance; - string? currentElementId = instance.Process?.CurrentTask?.ElementId; - - if (currentElementId == null) + if (request.Action is not "write" && request.Action != serviceTask.Type) // serviceTask.Type is accepted to support custom service task types { - var result = new ProcessChangeResult() + var result = new ServiceTaskResult { - Success = false, - ErrorMessage = $"Instance does not have current task information!", + Result = ServiceTaskResult.ResultType.Failure, + ErrorMessage = + $"Service tasks do not support running user actions! Received action param {request.Action}.", ErrorType = ProcessErrorType.Conflict, }; - activity?.SetProcessChangeResult(result); + return result; } - MoveToNextResult moveToNextResult = await HandleMoveToNext(instance, request.Action); - - if (moveToNextResult.IsEndEvent) + try { - _telemetry?.ProcessEnded(moveToNextResult.ProcessStateChange); - await RunAppDefinedProcessEndHandlers(instance, moveToNextResult.ProcessStateChange?.Events); - } + await serviceTask.Execute(instance.Process.CurrentTask.ElementId, instance, ct); - var changeResult = new ProcessChangeResult() + return new ServiceTaskResult { Result = ServiceTaskResult.ResultType.Success }; + } + catch (Exception ex) { - Success = true, - ProcessStateChange = moveToNextResult.ProcessStateChange, - }; - activity?.SetProcessChangeResult(changeResult); - return changeResult; + activity?.Errored(ex); + + return new ServiceTaskResult() + { + Result = ServiceTaskResult.ResultType.Failure, + ErrorMessage = $"Service task {serviceTask.Type} failed!", + ErrorType = ProcessErrorType.Internal, + }; + } } /// @@ -451,4 +678,23 @@ private sealed record MoveToNextResult(Instance Instance, ProcessStateChange? Pr [MemberNotNullWhen(true, nameof(ProcessStateChange))] public bool IsEndEvent => ProcessStateChange?.NewProcessState?.Ended is not null; }; + + private static string ConvertTaskTypeToAction(string actionOrTaskType) + { + switch (actionOrTaskType) + { + case "data": + case "feedback": + case "pdf": + case "eFormidling": + return "write"; + case "confirmation": + return "confirm"; + case "signing": + return "sign"; + default: + // Not any known task type, so assume it is an action type + return actionOrTaskType; + } + } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngineAuthorizer.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngineAuthorizer.cs index 152970b95..346c68ed1 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngineAuthorizer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngineAuthorizer.cs @@ -106,7 +106,7 @@ public static string[] GetActionsThatAllowProcessNextForTaskType(string taskType { return taskType switch { - "data" or "feedback" => ["write"], + "data" or "feedback" or "pdf" or "eFormidling" => ["write"], "payment" => ["pay", "write"], "confirmation" => ["confirm"], "signing" => ["sign", "write"], diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs b/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs index f70a25f0a..1d71826a5 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs @@ -2,6 +2,7 @@ using Altinn.App.Core.Internal.Process.EventHandlers; using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; @@ -113,6 +114,14 @@ private IProcessTask GetProcessTaskInstance(string? altinnTaskType) altinnTaskType = "NullType"; } + IEnumerable serviceTasks = _appImplementationFactory.GetAll(); + IServiceTask? serviceTask = serviceTasks.FirstOrDefault(pt => pt.Type == altinnTaskType); + + if (serviceTask is not null) + { + return serviceTask; + } + var tasks = _appImplementationFactory.GetAll(); IProcessTask? processTask = tasks.FirstOrDefault(pt => pt.Type == altinnTaskType); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs index c854cda60..762156bf5 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs @@ -62,7 +62,7 @@ public bool IsStartEvent([NotNullWhen(true)] string? elementId) public List GetProcessTasks() { using var activity = _telemetry?.StartGetProcessTasksActivity(); - return _definitions.Process.Tasks; + return [.. _definitions.Process.Tasks, .. _definitions.Process.ServiceTasks]; } /// @@ -140,6 +140,12 @@ public List GetSequenceFlowIds() return task; } + ServiceTask? serviceTask = _definitions.Process.ServiceTasks.Find(t => t.Id == elementId); + if (serviceTask != null) + { + return serviceTask; + } + EndEvent? endEvent = _definitions.Process.EndEvents.Find(e => e.Id == elementId); if (endEvent != null) { diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskCleaner.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskCleaner.cs similarity index 88% rename from src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskCleaner.cs rename to src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskCleaner.cs index 4a6d92994..88f36a27e 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskCleaner.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskCleaner.cs @@ -1,6 +1,6 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks; +namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; /// /// Contains common logic to clean up process data diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskDataLocker.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskDataLocker.cs similarity index 91% rename from src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskDataLocker.cs rename to src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskDataLocker.cs index c574fbcf2..a2a385c22 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskDataLocker.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskDataLocker.cs @@ -1,6 +1,6 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks; +namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; /// /// Can be used to lock data elements connected to a process task diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskFinalizer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskFinalizer.cs similarity index 89% rename from src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskFinalizer.cs rename to src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskFinalizer.cs index 1f1268c05..f67e0f955 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskFinalizer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskFinalizer.cs @@ -1,6 +1,6 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks; +namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; /// /// Contains common logic for ending a process task. diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskInitializer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskInitializer.cs similarity index 91% rename from src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskInitializer.cs rename to src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskInitializer.cs index 7eb59e3ac..50c6cdfd5 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskInitializer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskInitializer.cs @@ -1,6 +1,6 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks; +namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; /// /// Contains common logic for initializing a process task. diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs index f78cbedbb..f4cc894f7 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs @@ -4,7 +4,7 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; -namespace Altinn.App.Core.Internal.Process.ProcessTasks; +namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; internal sealed class ProcessTaskCleaner : IProcessTaskCleaner { diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskDataLocker.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskDataLocker.cs index 2e0ad99f5..c7a77cd1d 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskDataLocker.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskDataLocker.cs @@ -3,7 +3,7 @@ using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks; +namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; /// public class ProcessTaskDataLocker : IProcessTaskDataLocker 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 753c7376c..d8e2f709d 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Internal.Process.ProcessTasks; +namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; /// public class ProcessTaskFinalizer : IProcessTaskFinalizer 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 4ce9b5de3..c939a93cd 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Altinn.App.Core.Internal.Process.ProcessTasks; +namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; /// public class ProcessTaskInitializer : IProcessTaskInitializer diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Interfaces/IProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/IProcessTask.cs similarity index 74% rename from src/Altinn.App.Core/Internal/Process/ProcessTasks/Interfaces/IProcessTask.cs rename to src/Altinn.App.Core/Internal/Process/ProcessTasks/IProcessTask.cs index c7faaccf3..f56f4a660 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Interfaces/IProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/IProcessTask.cs @@ -17,15 +17,24 @@ public interface IProcessTask /// /// Any logic to be executed when a task is started should be put in this method. /// - Task Start(string taskId, Instance instance); + Task Start(string taskId, Instance instance) + { + return Task.CompletedTask; + } /// /// Any logic to be executed when a task is ended should be put in this method. /// - Task End(string taskId, Instance instance); + Task End(string taskId, Instance instance) + { + return Task.CompletedTask; + } /// /// Any logic to be executed when a task is abandoned should be put in this method. /// - Task Abandon(string taskId, Instance instance); + Task Abandon(string taskId, Instance instance) + { + return Task.CompletedTask; + } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs new file mode 100644 index 000000000..84c950507 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -0,0 +1,77 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.EFormidling.Interface; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; + +internal interface IEFormidlingServiceTask : IServiceTask { } + +/// +/// Service task that sends eFormidling shipment, if EFormidling is enabled in config. +/// +public class EFormidlingServiceTask : IEFormidlingServiceTask +{ + private readonly ILogger _logger; + private readonly IEFormidlingService? _eFormidlingService; + private readonly IOptions? _appSettings; + + /// + /// Initializes a new instance of the class. + /// + public EFormidlingServiceTask( + ILogger logger, + IEFormidlingService? eFormidlingService = null, + IOptions? appSettings = null + ) + { + _logger = logger; + _eFormidlingService = eFormidlingService; + _appSettings = appSettings; + } + + /// + public string Type => "eFormidling"; + + /// + public async Task Execute(string taskId, Instance instance, CancellationToken cancellationToken = default) + { + if (_appSettings?.Value.EnableEFormidling is false) + { + _logger.LogWarning( + "EFormidling has been added as a service task in the BPMN process definition but is not enabled in appsettings.json. No eFormidling shipment will be sent, but the service task will be completed." + ); + return; + } + + if (_eFormidlingService is null) + { + throw new ProcessException( + $"No implementation of {nameof(IEFormidlingService)} has been added to the DI container." + ); + } + + _logger.LogDebug("Calling eFormidlingService for eFormidling Service Task {TaskId}.", taskId); + await _eFormidlingService.SendEFormidlingShipment(instance); + _logger.LogDebug("Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", taskId); + } + + /// + public Task Start(string taskId, Instance instance) + { + return Task.CompletedTask; + } + + /// + public Task End(string taskId, Instance instance) + { + return Task.CompletedTask; + } + + /// + public Task Abandon(string taskId, Instance instance) + { + return Task.CompletedTask; + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs new file mode 100644 index 000000000..1cb06c7d6 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs @@ -0,0 +1,72 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Models.Process; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; + +/// +/// Interface for service tasks that can be executed during a process. +/// +[ImplementableByApps] +public interface IServiceTask : IProcessTask +{ + /// + /// Executes the service task. + /// + /// TODO: Fortsette å ta in taskId og instance, som de andre metodene, eller hoppe over på IInstanceDataAccessor? + public Task Execute(string taskId, Instance instance, CancellationToken cancellationToken = default); + + /// + /// Method that is called to determine if the process should move to the next task after executing the service task, or wait for another process next call. + /// + /// + /// + /// + /// + public Task MoveToNextTaskAfterExecution( + string taskId, + Instance instance, + CancellationToken cancellationToken = default + ) + { + // The default implementation is to move to the next task after execution + return Task.FromResult(true); + } +} + +/// +/// This class represents the result of executing a service task. +/// +public class ServiceTaskResult +{ + /// + /// The result of the service task execution. + /// + public ResultType Result { get; set; } + + /// + /// Error type to return when the service task was not successful + /// + public ProcessErrorType? ErrorType { get; set; } + + /// + /// Error message to return when the service task was not successful + /// + public string? ErrorMessage { get; set; } + + /// + /// An enum representing the status of the service task execution. + /// + public enum ResultType + { + /// + /// The service task was executed successfully. + /// + Success, + + /// + /// The service task failed to execute. + /// + Failure, + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ServiceTasks/EformidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs similarity index 69% rename from src/Altinn.App.Core/Internal/Process/ServiceTasks/EformidlingServiceTask.cs rename to src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs index f47fea883..307d29772 100644 --- a/src/Altinn.App.Core/Internal/Process/ServiceTasks/EformidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs @@ -2,31 +2,40 @@ using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Internal.Process.ServiceTasks; - -internal interface IEformidlingServiceTask : IServiceTask { } +namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; /// /// Service task that sends eFormidling shipment, if EFormidling is enabled in config and EFormidling.SendAfterTaskId matches the current task. /// -public class EformidlingServiceTask : IEformidlingServiceTask +/// Planned to be replaced by , but kept for now for backwards compatability. Called inline in instead of through the service task system. +internal interface IEFormidlingServiceTaskLegacy +{ + /// + /// Executes the service task. + /// + Task Execute(string taskId, Instance instance); +}; + +/// +internal class EformidlingServiceTaskLegacy : IEFormidlingServiceTaskLegacy { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IAppMetadata _appMetadata; private readonly IInstanceClient _instanceClient; private readonly IEFormidlingService? _eFormidlingService; private readonly IOptions? _appSettings; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public EformidlingServiceTask( - ILogger logger, + public EformidlingServiceTaskLegacy( + ILogger logger, IAppMetadata appMetadata, IInstanceClient instanceClient, IEFormidlingService? eFormidlingService = null, @@ -40,7 +49,7 @@ public EformidlingServiceTask( _appSettings = appSettings; } - /// + /// public async Task Execute(string taskId, Instance instance) { ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); diff --git a/src/Altinn.App.Core/Internal/Process/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs similarity index 58% rename from src/Altinn.App.Core/Internal/Process/ServiceTasks/PdfServiceTask.cs rename to src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs index d59cdcd0a..34632cdf6 100644 --- a/src/Altinn.App.Core/Internal/Process/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs @@ -1,30 +1,39 @@ using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ServiceTasks; - -internal interface IPdfServiceTask : IServiceTask { } +namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; /// -/// Service task that generates PDFs for all connected datatypes that have the EnablePdfCreation flag set to true. +/// Service task that generates PDFs for all connected data types that have the EnablePdfCreation flag set to true. /// -public class PdfServiceTask : IPdfServiceTask +/// Planned to be replaced by , but kept for now for backwards compatability. Called inline in , instead of through the service task system. +internal interface IPdfServiceTaskLegacy +{ + /// + /// Executes the service task. + /// + Task Execute(string taskId, Instance instance); +}; + +/// +internal class PdfServiceTaskLegacy : IPdfServiceTaskLegacy { private readonly IAppMetadata _appMetadata; private readonly IPdfService _pdfService; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public PdfServiceTask(IAppMetadata appMetadata, IPdfService pdfService) + public PdfServiceTaskLegacy(IAppMetadata appMetadata, IPdfService pdfService) { _pdfService = pdfService; _appMetadata = appMetadata; } - /// + /// public async Task Execute(string taskId, Instance instance) { ArgumentNullException.ThrowIfNull(taskId); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs new file mode 100644 index 000000000..1bafdb803 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -0,0 +1,74 @@ +using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; + +internal interface IPdfServiceTask : IServiceTask { } + +/// +/// Service task that generates PDFs for tasks specified in the process configuration. +/// +public class PdfServiceTask : IPdfServiceTask +{ + private readonly IPdfService _pdfService; + private readonly IProcessReader _processReader; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + public PdfServiceTask(IPdfService pdfService, IProcessReader processReader, ILogger logger) + { + _logger = logger; + _pdfService = pdfService; + _processReader = processReader; + } + + /// + public string Type => "pdf"; + + /// + public async Task Execute(string taskId, Instance instance, CancellationToken cancellationToken = default) + { + _logger.LogDebug("Calling PdfService for PDF Service Task {TaskId}.", taskId); + + ValidAltinnPdfConfiguration config = GetValidAltinnPdfConfiguration(taskId); + await _pdfService.GenerateAndStorePdf(instance, taskId, config.Filename, cancellationToken); + + _logger.LogDebug("Successfully called PdfService for PDF Service Task {TaskId}.", taskId); + } + + /// + public Task Start(string taskId, Instance instance) + { + return Task.CompletedTask; + } + + /// + public Task End(string taskId, Instance instance) + { + return Task.CompletedTask; + } + + /// + public Task Abandon(string taskId, Instance instance) + { + return Task.CompletedTask; + } + + private ValidAltinnPdfConfiguration GetValidAltinnPdfConfiguration(string taskId) + { + AltinnTaskExtension? altinnTaskExtension = _processReader.GetAltinnTaskExtension(taskId); + AltinnPdfConfiguration? pdfConfiguration = altinnTaskExtension?.PdfConfiguration; + + if (pdfConfiguration == null) + { + // If no PDF configuration is specified, return a default valid configuration. No required config as of now. + return new ValidAltinnPdfConfiguration(); + } + + return pdfConfiguration.Validate(); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ServiceTasks/Interfaces/IServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ServiceTasks/Interfaces/IServiceTask.cs deleted file mode 100644 index ab2818588..000000000 --- a/src/Altinn.App.Core/Internal/Process/ServiceTasks/Interfaces/IServiceTask.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Altinn.App.Core.Features; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Internal.Process.ServiceTasks; - -/// -/// Interface for service tasks that can be executed during a process. -/// -[ImplementableByApps] -public interface IServiceTask -{ - /// - /// Executes the service task. - /// - public Task Execute(string taskId, Instance instance); -} diff --git a/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs b/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs index d72bf714e..7e731080a 100644 --- a/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Altinn.App.Core.Models.Validation; namespace Altinn.App.Core.Models.Process; @@ -14,11 +15,21 @@ public class ProcessChangeResult [MemberNotNullWhen(false, nameof(ErrorMessage), nameof(ErrorType))] public bool Success { get; init; } + /// + /// Gets or sets the error title if the process change was not successful + /// + public string? ErrorTitle { get; set; } + /// /// Gets or sets the error message if the process change was not successful /// public string? ErrorMessage { get; init; } + /// + /// Validation issues that occurred during the process change + /// + public List? ValidationIssues { get; set; } + /// /// Gets or sets the error type if the process change was not successful /// diff --git a/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs b/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs index 956a6c04e..1edaacbfd 100644 --- a/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs @@ -6,10 +6,10 @@ namespace Altinn.App.Core.Models.Process; /// /// Class that defines the request for moving the process to the next task /// -public class ProcessNextRequest +public sealed record ProcessNextRequest { /// - /// The instance to be moved to the next task + /// The instance that is being processed /// public required Instance Instance { get; init; } 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 c57016b76..9b0ae996d 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -32,13 +32,17 @@ - - + + - - + + + + + + diff --git a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_FailingValidator_ReturnsValidationErrors.verified.txt b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_FailingValidator_ReturnsValidationErrors.verified.txt index 429328b5d..4e00ee84f 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_FailingValidator_ReturnsValidationErrors.verified.txt +++ b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_FailingValidator_ReturnsValidationErrors.verified.txt @@ -174,6 +174,20 @@ ], HasParent: false }, + { + Name: Process.Next, + IdFormat: W3C, + Status: Error, + Tags: [ + { + instance.guid: Guid_1 + }, + { + process.error.type: Conflict + } + ], + HasParent: true + }, { Name: ProcessClient.GetProcessDefinition, IdFormat: W3C, diff --git a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_PdfFails_DataIsUnlocked.verified.txt b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_PdfFails_DataIsUnlocked.verified.txt index 928aa0257..205426327 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_PdfFails_DataIsUnlocked.verified.txt +++ b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_PdfFails_DataIsUnlocked.verified.txt @@ -245,9 +245,6 @@ Tags: [ { instance.guid: Guid_1 - }, - { - process.action: write } ], HasParent: true diff --git a/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/.gitignore b/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/.gitignore new file mode 100644 index 000000000..dcf6ac329 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/.gitignore @@ -0,0 +1,4 @@ +# Ignore guid.json files +????????-????-????-????-????????????.json +# ignore copied blobs +*/*/blob/????????-????-????-????-???????????? \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f.pretest.json new file mode 100644 index 000000000..2de31e21a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f.pretest.json @@ -0,0 +1,41 @@ +{ + "id": "501337/a2af1cfd-db99-45f9-9625-9dfa1223485f", + "instanceOwner": { + "partyId": "501337", + "personNumber": "01039012345" + }, + "appId": "ttd/service-tasks", + "org": "ttd", + "visibleAfter": "2024-08-28T05:10:09.8629034Z", + "process": { + "started": "2024-08-28T05:10:09.8564665Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 2, + "started": "2024-08-28T05:23:48.7768741Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "flowType": "CompleteCurrentMoveToNext" + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "fd4c42a4-a6a0-4aec-b8e9-9f5d34c445ca", + "dataType": "Model", + "contentType": "application/xml", + "size": 173, + "locked": false + } + ], + "created": "2024-08-28T05:10:09.8629034Z", + "createdBy": "1337", + "lastChanged": "2024-08-28T05:10:22.1350004Z", + "lastChangedBy": "1337" +} diff --git a/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f/blob/fd4c42a4-a6a0-4aec-b8e9-9f5d34c445ca.pretest b/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f/blob/fd4c42a4-a6a0-4aec-b8e9-9f5d34c445ca.pretest new file mode 100644 index 000000000..e0afb7cf2 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f/blob/fd4c42a4-a6a0-4aec-b8e9-9f5d34c445ca.pretest @@ -0,0 +1 @@ +Testnavn diff --git a/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f/fd4c42a4-a6a0-4aec-b8e9-9f5d34c445ca.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f/fd4c42a4-a6a0-4aec-b8e9-9f5d34c445ca.pretest.json new file mode 100644 index 000000000..54adf222f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/a2af1cfd-db99-45f9-9625-9dfa1223485f/fd4c42a4-a6a0-4aec-b8e9-9f5d34c445ca.pretest.json @@ -0,0 +1,17 @@ +{ + "id": "fd4c42a4-a6a0-4aec-b8e9-9f5d34c445ca", + "instanceGuid": "a2af1cfd-db99-45f9-9625-9dfa1223485f", + "dataType": "Model", + "contentType": "application/xml", + "blobStoragePath": null, + "size": 173, + "locked": false, + "refs": [], + "isRead": true, + "tags": [], + "fileScanResult": "NotApplicable", + "created": "2024-09-02T05:10:09.8772942Z", + "createdBy": "1337", + "lastChanged": "2024-09-02T05:10:22.1147281Z", + "lastChangedBy": "1337" +} diff --git a/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/b1af1cfd-db99-45f9-9625-9dfa1223485f.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/b1af1cfd-db99-45f9-9625-9dfa1223485f.pretest.json new file mode 100644 index 000000000..caf20f234 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/ttd/service-tasks/501337/b1af1cfd-db99-45f9-9625-9dfa1223485f.pretest.json @@ -0,0 +1,41 @@ +{ + "id": "501337/b1af1cfd-db99-45f9-9625-9dfa1223485f", + "instanceOwner": { + "partyId": "501337", + "personNumber": "01039012345" + }, + "appId": "ttd/service-tasks", + "org": "ttd", + "visibleAfter": "2024-08-28T05:10:09.8629034Z", + "process": { + "started": "2024-08-28T05:10:09.8564665Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 3, + "started": "2024-08-28T05:23:48.7768741Z", + "elementId": "Task_2", + "name": "Pdf", + "altinnTaskType": "pdf", + "flowType": "CompleteCurrentMoveToNext" + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "fd4c42a4-a6a0-4aec-b8e9-9f5d34c445ca", + "dataType": "Model", + "contentType": "application/xml", + "size": 173, + "locked": false + } + ], + "created": "2024-08-28T05:10:09.8629034Z", + "createdBy": "1337", + "lastChanged": "2024-08-28T05:10:22.1350004Z", + "lastChangedBy": "1337" +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/appsettings.json b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/appsettings.json new file mode 100644 index 000000000..cc9c9a0c6 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/appsettings.json @@ -0,0 +1,37 @@ +{ + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://*:5005" + } + } + }, + "AppSettings": { + "OpenIdWellKnownEndpoint": "http://localhost:5101/authentication/api/v1/openid/", + "RuntimeCookieName": "AltinnStudioRuntime", + "RegisterEventsWithEventsComponent": false, + "EnableEFormidling": true + }, + "GeneralSettings": { + "HostName": "local.altinn.cloud", + "SoftValidationPrefix": "*WARNING*", + "FixedValidationPrefix": "*FIXED*", + "AltinnPartyCookieName": "AltinnPartyId" + }, + "EFormidlingClientSettings": { + "BaseUrl": "http://localhost:9093/api/" + }, + "PlatformSettings": { + "ApiStorageEndpoint": "http://localhost:5101/storage/api/v1/", + "ApiRegisterEndpoint": "http://localhost:5101/register/api/v1/", + "ApiProfileEndpoint": "http://localhost:5101/profile/api/v1/", + "ApiAuthenticationEndpoint": "http://localhost:5101/authentication/api/v1/", + "ApiAuthorizationEndpoint": "http://localhost:5101/authorization/api/v1/", + "ApiEventsEndpoint": "http://localhost:5101/events/api/v1/", + "ApiPdfEndpoint": "http://localhost:5070/api/v1/", + "SubscriptionKey": "retrieved from environment at runtime" + }, + "ApplicationInsights": { + "InstrumentationKey": "retrieved from environment at runtime" + } +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/applicationmetadata.json b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/applicationmetadata.json new file mode 100644 index 000000000..6af597e2b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/applicationmetadata.json @@ -0,0 +1,54 @@ +{ + "id": "ttd/service-tasks", + "org": "ttd", + "title": { + "nb": "service-tasks" + }, + "dataTypes": [ + { + "id": "ref-data-as-pdf", + "allowedContentTypes": [ + "application/pdf" + ], + "maxCount": 0, + "minCount": 0, + "enablePdfCreation": true, + "enableFileScan": false, + "validationErrorOnPendingFileScan": false, + "enabledFileAnalysers": [], + "enabledFileValidators": [] + }, + { + "id": "Model", + "allowedContentTypes": [ + "application/xml" + ], + "appLogic": { + "autoCreate": true, + "classRef": "Altinn.App.Models.model.Model", + "allowAnonymousOnStateless": false, + "autoDeleteOnProcessEnd": false + }, + "taskId": "Task_1", + "maxCount": 1, + "minCount": 1, + "enablePdfCreation": false, + "enableFileScan": false, + "validationErrorOnPendingFileScan": false, + "enabledFileAnalysers": [], + "enabledFileValidators": [] + } + ], + "partyTypesAllowed": { + "bankruptcyEstate": false, + "organisation": false, + "person": false, + "subUnit": false + }, + "autoDeleteOnProcessEnd": false, + "disallowUserInstantiation": false, + "created": "2024-07-05T08:18:03.6221823Z", + "createdBy": "bjorntore", + "lastChanged": "2024-07-05T08:18:03.6221831Z", + "lastChangedBy": "bjorntore" +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/authorization/policy.xml b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/authorization/policy.xml new file mode 100644 index 000000000..bca322d5a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/authorization/policy.xml @@ -0,0 +1,293 @@ + + + + + A rule giving user with role REGNA or DAGL and the app owner [ORG] the right to instantiate a instance of a given app of [ORG]/[APP] + + + + + REGNA + + + + + + DAGL + + + + + + [ORG] + + + + + + + + [ORG] + + + + [APP] + + + + + + + + instantiate + + + + + + read + + + + + + + + Rule that defines that user with role REGNA or DAGL can read and write for [ORG]/[APP] when it is in Task_1 + + + + + REGNA + + + + + + DAGL + + + + + + + + [ORG] + + + + [APP] + + + + Task_1 + + + + + + [ORG] + + + + [APP] + + + + Task_2 + + + + + + [ORG] + + + + [APP] + + + + EndEvent_1 + + + + + + + + read + + + + + + write + + + + + + + + Rule that defines that user with role REGNA or DAGL can delete instances of [ORG]/[APP] + + + + + REGNA + + + + + + DAGL + + + + + + + + [ORG] + + + + [APP] + + + + delete + + + + + + + + Rule that defines that org can write to instances of [ORG]/[APP] for any states + + + + + [ORG] + + + + + + + + [ORG] + + + + [APP] + + + + + + + + write + + + + + + + + Rule that defines that org can complete an instance of [ORG]/[APP] which state is at the end event. + + + + + [ORG] + + + + + + + + [ORG] + + + + [APP] + + + + EndEvent_1 + + + + + + + + complete + + + + + + + + A rule giving user with role REGNA or DAGL and the app owner [ORG] the right to read the appresource events of a given app of [ORG]/[APP] + + + + + REGNA + + + + + + DAGL + + + + + + [ORG] + + + + + + + + [ORG] + + + + [APP] + + + + events + + + + + + + + read + + + + + + + + + + 2 + + + + diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/process/process.bpmn b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/process/process.bpmn new file mode 100644 index 000000000..d094cde30 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/process/process.bpmn @@ -0,0 +1,63 @@ + + + + + SequenceFlow_1n56yn5 + + + SequenceFlow_1n56yn5 + SequenceFlow_gateway1_task1 + SequenceFlow_1oot28q + + + data + + + + + SequenceFlow_1oot28q + SequenceFlow_task2_gateway1 + + + pdf + + + Task_1 + + + + + + + SequenceFlow_task2_gateway1 + SequenceFlow_gateway1_task1 + Flow_g1_task3 + + + Flow_g1_task3 + SequenceFlow_5assd2s + + + eFormidling + + + + + SequenceFlow_5assd2s + + + + + + ["equals", ["gatewayAction"], "reject"] + + + ["notEquals", ["gatewayAction"], "reject"] + + + + diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/texts/resource.nb.json b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/texts/resource.nb.json new file mode 100644 index 000000000..47cfdd2dd --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/texts/resource.nb.json @@ -0,0 +1,17 @@ +{ + "language": "nb", + "resources": [ + { + "id": "appName", + "value": "service-tasks" + }, + { + "id": "name", + "value": "Navn" + }, + { + "id": "submit", + "value": "Send inn" + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.cs b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.cs new file mode 100644 index 000000000..829279b01 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.cs @@ -0,0 +1,20 @@ +#nullable disable +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.Json.Serialization; +using System.Xml.Serialization; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Newtonsoft.Json; + +namespace Altinn.App.Models.model; + +[XmlRoot(ElementName = "Model")] +public class Model +{ + [XmlElement("Navn", Order = 1)] + [JsonProperty(nameof(Navn))] + [JsonPropertyName(nameof(Navn))] + public string Navn { get; set; } +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.schema.json b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.schema.json new file mode 100644 index 000000000..3f3a20f1c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://altinn-repositories:3000/bjosttveit/v4-data/App/models/model.schema.json", + "info": { + "rootNode": "" + }, + "@xsdNamespaces": { + "xsd": "http://www.w3.org/2001/XMLSchema", + "xsi": "http://www.w3.org/2001/XMLSchema-instance", + "seres": "http://seres.no/xsd/forvaltningsdata" + }, + "@xsdSchemaAttributes": { + "AttributeFormDefault": "Unqualified", + "ElementFormDefault": "Qualified", + "BlockDefault": "None", + "FinalDefault": "None" + }, + "@xsdRootElement": "Model", + "type": "object", + "required": [ + "Navn" + ], + "properties": { + "Navn": { + "type": "string" + } + } +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.xsd b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.xsd new file mode 100644 index 000000000..7dd8b9174 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/models/Model.xsd @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/footer.json b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/footer.json new file mode 100644 index 000000000..72c632c51 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/footer.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/footer.schema.v1.json", + "footer": [ + { + "type": "Link", + "icon": "information", + "title": "general.accessibility", + "target": "general.accessibility_url" + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/RuleHandler.js b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/RuleHandler.js new file mode 100644 index 000000000..b08970798 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/RuleHandler.js @@ -0,0 +1,63 @@ +var ruleHandlerObject = { + sum: function(obj) { + obj.a = obj.a ? +obj.a : 0; + obj.b = obj.b ? +obj.b : 0; + return obj.a + obj.b; + }, + + fullName: function(obj) { + return obj.first + ' ' + obj.last; + } +} +var ruleHandlerHelper = { + fullName: function() { + return { + first: "first name", + last: "last name" + }; + }, + + sum: function() { + return { + a: "a", + b: "b", + } + } +} + +var conditionalRuleHandlerObject = { + biggerThan10: function(obj) { + obj.number = +obj.number; + return obj.number > 10; + }, + + smallerThan10: function(obj) { + obj.number = +obj.number; + return obj.number < 10; + }, + + lengthBiggerThan4: function(obj) { + if (obj.value == null) return false; + return obj.value.length > 4; + } +} +var conditionalRuleHandlerHelper = { + biggerThan10: function() { + return { + number: "number" + }; + }, + + smallerThan10: function() { + return { + number: "number" + } + }, + + lengthBiggerThan4: function() { + return { + value: "value" + } + } + +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/Settings.json b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/Settings.json new file mode 100644 index 000000000..200168630 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/Settings.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layoutSettings.schema.v1.json", + "pages": { + "order": ["Side1"] + } +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/layouts/Side1.json b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/layouts/Side1.json new file mode 100644 index 000000000..253f5a5fa --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/form/layouts/Side1.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layout.schema.v1.json", + "data": { + "hidden": false, + "layout": [ + { + "dataModelBindings": { + "simpleBinding": "Navn" + }, + "id": "Navn-input", + "type": "Input", + "required": true, + "textResourceBindings": { + "title": "name" + } + }, + { + "id": "next-button", + "type": "Button", + "textResourceBindings": { + "title": "submit" + } + } + ] + } +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/layout-sets.json b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/layout-sets.json new file mode 100644 index 000000000..cd59008e0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/ui/layout-sets.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layout-sets.schema.v1.json", + "sets": [ + { + "id": "form", + "dataType": "model", + "tasks": ["Task_1"] + } + ], + "uiSettings": {} +} diff --git a/test/Altinn.App.Api.Tests/Mocks/Event/EventsClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/Event/EventsClientMock.cs new file mode 100644 index 000000000..cbbf10486 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/Event/EventsClientMock.cs @@ -0,0 +1,12 @@ +using Altinn.App.Core.Internal.Events; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Api.Tests.Mocks.Event; + +public class EventsClientMock : IEventsClient +{ + public Task AddEvent(string eventType, Instance instance) + { + throw new NotImplementedException(); + } +} diff --git a/test/Altinn.App.Api.Tests/Mocks/ProcessClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/ProcessClientMock.cs new file mode 100644 index 000000000..0f4f70a71 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/ProcessClientMock.cs @@ -0,0 +1,57 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; +using Altinn.App.Core.Infrastructure.Clients.Storage; +using Altinn.App.Core.Internal.Process; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Api.Tests.Mocks; + +public class ProcessClientMock : IProcessClient +{ + private readonly ILogger _logger; + private readonly Telemetry? _telemetry; + private readonly AppSettings _appSettings; + + public ProcessClientMock( + IOptions appSettings, + ILogger logger, + Telemetry? telemetry = null + ) + { + _appSettings = appSettings.Value; + _logger = logger; + _telemetry = telemetry; + } + + public Stream GetProcessDefinition() + { + using var activity = _telemetry?.StartGetProcessDefinitionActivity(); + string bpmnFilePath = Path.Join( + _appSettings.AppBasePath, + _appSettings.ConfigurationFolder, + _appSettings.ProcessFolder, + _appSettings.ProcessFileName + ); + + 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; + } + } + + public Task GetProcessHistory(string instanceGuid, string instanceOwnerPartyId) + { + throw new NotImplementedException(); + } +} diff --git a/test/Altinn.App.Api.Tests/Mocks/SignClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/SignClientMock.cs new file mode 100644 index 000000000..7c0edaee3 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/SignClientMock.cs @@ -0,0 +1,11 @@ +using Altinn.App.Core.Internal.Sign; + +namespace Altinn.App.Api.Tests.Mocks; + +public class SignClientMock : ISignClient +{ + public Task SignDataElements(SignatureContext signatureContext) + { + throw new NotImplementedException(); + } +} diff --git a/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs b/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs new file mode 100644 index 000000000..8be1d0a04 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs @@ -0,0 +1,147 @@ +using System.Net; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Api.Tests.Mocks; +using Altinn.App.Core.EFormidling.Interface; +using Altinn.App.Core.Internal.Process; +using Altinn.Platform.Storage.Interface.Models; +using Argon; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit.Abstractions; + +namespace Altinn.App.Api.Tests.Process.ServiceTasks.EFormidling; + +public class EFormidlingServiceTaskTests : ApiTestBase, IClassFixture> +{ + private const string Org = "ttd"; + private const string App = "service-tasks"; + private const int InstanceOwnerPartyId = 501337; //Sofie Salt + private const string Language = "nb"; + private static readonly Guid _instanceGuid = new("b1af1cfd-db99-45f9-9625-9dfa1223485f"); + private static readonly string _instanceId = $"{InstanceOwnerPartyId}/{_instanceGuid}"; + + private readonly Mock _eFormidlingServiceMock = new Mock(); + + public EFormidlingServiceTaskTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) + : base(factory, outputHelper) + { + OverrideServicesForAllTests = (services) => + { + services.AddSingleton(_eFormidlingServiceMock.Object); + services.AddTransient(); + }; + + TestData.DeleteInstanceAndData(Org, App, InstanceOwnerPartyId, _instanceGuid); + TestData.PrepareInstance(Org, App, InstanceOwnerPartyId, _instanceGuid); + } + + [Fact] + public async Task Can_Set_EFormidlingServiceTask_As_CurrentTask() + { + SendAsync = message => + { + if (message.RequestUri!.PathAndQuery.Contains("pdf")) + { + return Task.FromResult( + new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("this is the binary pdf content"), + } + ); + } + + throw new Exception($"Not mocked http request: {message.RequestUri!.PathAndQuery}"); + }; + + using HttpClient client = GetRootedUserClient(Org, App); + + // Run process next + using HttpResponseMessage nextResponse = await client.PutAsync( + $"{Org}/{App}/instances/{_instanceId}/process/next?language={Language}", + null + ); + + string nextResponseContent = await nextResponse.Content.ReadAsStringAsync(); + OutputHelper.WriteLine(nextResponseContent); + + nextResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task Can_Execute_EFormidlingServiceTask_And_Move_To_Next_Task() + { + // Make sure a request to eFormidling is made + SendAsync = message => + { + if (message.RequestUri!.PathAndQuery.Contains("pdf")) + { + return Task.FromResult( + new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("this is the binary pdf content"), + } + ); + } + + throw new Exception($"Not mocked http request: {message.RequestUri!.PathAndQuery}"); + }; + + using HttpClient client = GetRootedUserClient(Org, App); + + // Run process next to move from PdfServiceTask to EFormidlingServiceTask + using HttpResponseMessage processNextResponse = await client.PutAsync( + $"{Org}/{App}/instances/{_instanceId}/process/next?language={Language}", + null + ); + + string nextResponseContent = await processNextResponse.Content.ReadAsStringAsync(); + OutputHelper.WriteLine(nextResponseContent); + processNextResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + // Check that the process has been moved to end task + var processState = JsonConvert.DeserializeObject(nextResponseContent); + processState.Ended.Should().NotBeNull(); + } + + [Fact] + public async Task Does_Not_Change_Task_When_EFormidling_Fails() + { + // Make sure a request to eFormidling is made + SendAsync = message => + { + if (message.RequestUri!.PathAndQuery.Contains("pdf")) + { + return Task.FromResult( + new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("this is the binary pdf content"), + } + ); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)); + }; + + // Setup eFormidling service to throw exception + _eFormidlingServiceMock + .Setup(x => x.SendEFormidlingShipment(It.IsAny())) + .ThrowsAsync(new Exception()); + + using HttpClient client = GetRootedUserClient(Org, App); + + // Run process next to move from PdfServiceTask to EFormidlingServiceTask + using HttpResponseMessage firstNextResponse = await client.PutAsync( + $"{Org}/{App}/instances/{_instanceId}/process/next?language={Language}", + null + ); + + firstNextResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); + + // Check that the process is still in Task_3 + Instance instance = await TestData.GetInstance(Org, App, InstanceOwnerPartyId, _instanceGuid); + instance.Process.CurrentTask.ElementId.Should().Be("Task_3"); + instance.Process.CurrentTask.AltinnTaskType.Should().Be("eFormidling"); + } +} diff --git a/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs new file mode 100644 index 000000000..4d8a96206 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs @@ -0,0 +1,164 @@ +using System.Net; +using System.Text; +using Altinn.App.Api.Models; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Core.EFormidling.Interface; +using Altinn.Platform.Storage.Interface.Models; +using Argon; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit.Abstractions; + +namespace Altinn.App.Api.Tests.Process.ServiceTasks.Pdf; + +public class PdfServiceTaskTests : ApiTestBase, IClassFixture> +{ + private const string Org = "ttd"; + private const string App = "service-tasks"; + private const int InstanceOwnerPartyId = 501337; //Sofie Salt + private const string Language = "nb"; + private static readonly Guid _instanceGuid = new("a2af1cfd-db99-45f9-9625-9dfa1223485f"); + private static readonly string _instanceId = $"{InstanceOwnerPartyId}/{_instanceGuid}"; + + public PdfServiceTaskTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) + : base(factory, outputHelper) + { + var eFormidlingServiceMock = new Mock(); + OverrideServicesForAllTests = (services) => + { + services.AddSingleton(eFormidlingServiceMock.Object); + }; + + TestData.DeleteInstanceAndData(Org, App, InstanceOwnerPartyId, _instanceGuid); + TestData.PrepareInstance(Org, App, InstanceOwnerPartyId, _instanceGuid); + } + + [Fact] + public async Task Can_Reject_PdfServiceTask_If_It_Failed_And_Reject_Is_Configured() + { + var sendAsyncCalled = false; + + // Mock HttpClient for the expected pdf service call + SendAsync = message => + { + message.RequestUri!.PathAndQuery.Should().Be($"/pdf"); + sendAsyncCalled = true; + + // Simulate failing PDF service + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)); + }; + + using HttpClient client = GetRootedUserClient(Org, App); + + // Run process next to enter PDF task + using HttpResponseMessage nextResponse = await client.PutAsync( + $"{Org}/{App}/instances/{_instanceId}/process/next?language={Language}", + null + ); + + string nextResponseContent = await nextResponse.Content.ReadAsStringAsync(); + OutputHelper.WriteLine(nextResponseContent); + + nextResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); + sendAsyncCalled.Should().BeTrue(); + + // Run process next with reject to return to data task + var rejectProcessNext = new ProcessNext { Action = "reject" }; + using var rejectContent = new StringContent( + JsonConvert.SerializeObject(rejectProcessNext), + Encoding.UTF8, + "application/json" + ); + + using HttpResponseMessage rejectResponse = await client.PutAsync( + $"{Org}/{App}/instances/{_instanceId}/process/next?language={Language}", + rejectContent + ); + + rejectResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + // Double check that process moved back to the data task + Instance instance = await TestData.GetInstance(Org, App, InstanceOwnerPartyId, _instanceGuid); + instance.Process.CurrentTask.ElementId.Should().Be("Task_1"); + instance.Process.CurrentTask.AltinnTaskType.Should().Be("data"); + } + + [Fact] + public async Task Can_Execute_PdfServiceTask_And_Move_To_Next_Task() + { + var sendAsyncCalled = false; + + // Mock HttpClient for the expected pdf service call + SendAsync = message => + { + message.RequestUri!.PathAndQuery.Should().Be($"/pdf"); + sendAsyncCalled = true; + + return Task.FromResult( + new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("this is the binary pdf content"), + } + ); + }; + + using HttpClient client = GetRootedUserClient(Org, App); + + // Run process next + using HttpResponseMessage processNextResponse = await client.PutAsync( + $"{Org}/{App}/instances/{_instanceId}/process/next?language={Language}", + null + ); + + string responseAsString = await processNextResponse.Content.ReadAsStringAsync(); + OutputHelper.WriteLine(responseAsString); + + processNextResponse.Should().HaveStatusCode(HttpStatusCode.OK); + sendAsyncCalled.Should().BeTrue(); + + // Check that the process has been moved to the next task that is not a service task. + var processState = JsonConvert.DeserializeObject(responseAsString); + processState.Ended.Should().NotBeNull(); + } + + [Fact] + public async Task CurrentTask_Is_ServiceTask_If_Execute_Fails() + { + var sendAsyncCalled = false; + + // Mock HttpClient for the expected pdf service call + SendAsync = message => + { + message.RequestUri!.PathAndQuery.Should().Be($"/pdf"); + sendAsyncCalled = true; + + // Simulate failing PDF service + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)); + }; + + using HttpClient client = GetRootedUserClient(Org, App); + + // Run process next + using HttpResponseMessage processNextResponse = await client.PutAsync( + $"{Org}/{App}/instances/{_instanceId}/process/next?language={Language}", + null + ); + + string responseAsString = await processNextResponse.Content.ReadAsStringAsync(); + OutputHelper.WriteLine(responseAsString); + + processNextResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); + sendAsyncCalled.Should().BeTrue(); + + responseAsString + .Should() + .Be("{\"title\":\"Internal server error\",\"status\":500,\"detail\":\"Service task pdf failed!\"}"); + + // Double check that process did not move to the next task + Instance instance = await TestData.GetInstance(Org, App, InstanceOwnerPartyId, _instanceGuid); + instance.Process.CurrentTask.ElementId.Should().Be("Task_2"); + instance.Process.CurrentTask.AltinnTaskType.Should().Be("pdf"); + } +} diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs index 62ea9566c..8f4d5874b 100644 --- a/test/Altinn.App.Api.Tests/Program.cs +++ b/test/Altinn.App.Api.Tests/Program.cs @@ -16,6 +16,7 @@ using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Internal.Registers; +using Altinn.App.Core.Internal.Sign; using AltinnCore.Authentication.JwtCookie; using App.IntegrationTests.Mocks.Services; using Microsoft.ApplicationInsights.AspNetCore.Extensions; @@ -111,6 +112,8 @@ void ConfigureMockServices(IServiceCollection services, ConfigurationManager con services.AddTransient(); services.AddTransient(); services.AddTransient>(); + services.AddTransient(); + services.AddTransient(); services.PostConfigureAll(options => { 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 2a582669e..5bb0893e7 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 @@ -1,7 +1,8 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; using Altinn.App.Core.Internal.Process.ProcessTasks; -using Altinn.App.Core.Internal.Process.ServiceTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks.Common; +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -37,16 +38,14 @@ public static Fixture Create( if (addPdfServiceTask) { - Mock pdfServiceTask = new(); + Mock pdfServiceTask = new(); services.AddTransient(_ => pdfServiceTask.Object); - services.AddTransient(_ => pdfServiceTask.Object); } if (addEformidlingServiceTask) { - Mock eformidlingServiceTask = new(); + Mock eformidlingServiceTask = new(); services.AddTransient(_ => eformidlingServiceTask.Object); - services.AddTransient(_ => eformidlingServiceTask.Object); } services.AddTransient(); @@ -70,14 +69,14 @@ public async Task Execute_handles_no_IProcessTaskAbandon_injected() await eteh.Execute(mockProcessTask.Object, "Task_1", instance); fixture.Mock().Verify(p => p.Lock("Task_1", instance)); fixture.Mock().Verify(p => p.Finalize("Task_1", instance)); - fixture.Mock().Verify(p => p.Execute("Task_1", instance)); - fixture.Mock().Verify(p => p.Execute("Task_1", instance)); + fixture.Mock().Verify(p => p.Execute("Task_1", instance)); + fixture.Mock().Verify(p => p.Execute("Task_1", instance)); mockProcessTask.Verify(p => p.End("Task_1", instance)); fixture.Mock().VerifyNoOtherCalls(); fixture.Mock().VerifyNoOtherCalls(); - fixture.Mock().VerifyNoOtherCalls(); - fixture.Mock().VerifyNoOtherCalls(); + fixture.Mock().VerifyNoOtherCalls(); + fixture.Mock().VerifyNoOtherCalls(); mockProcessTask.VerifyNoOtherCalls(); } @@ -96,14 +95,14 @@ public async Task Execute_calls_all_added_implementations_of_IProcessTaskEnd() endTwo.Verify(a => a.End("Task_1", instance)); fixture.Mock().Verify(p => p.Lock("Task_1", instance)); fixture.Mock().Verify(p => p.Finalize("Task_1", instance)); - fixture.Mock().Verify(p => p.Execute("Task_1", instance)); - fixture.Mock().Verify(p => p.Execute("Task_1", instance)); + fixture.Mock().Verify(p => p.Execute("Task_1", instance)); + fixture.Mock().Verify(p => p.Execute("Task_1", instance)); mockProcessTask.Verify(p => p.End("Task_1", instance)); fixture.Mock().VerifyNoOtherCalls(); fixture.Mock().VerifyNoOtherCalls(); - fixture.Mock().VerifyNoOtherCalls(); - fixture.Mock().VerifyNoOtherCalls(); + fixture.Mock().VerifyNoOtherCalls(); + fixture.Mock().VerifyNoOtherCalls(); mockProcessTask.VerifyNoOtherCalls(); endOne.VerifyNoOtherCalls(); endTwo.VerifyNoOtherCalls(); @@ -123,7 +122,7 @@ public async Task Calls_unlock_if_pdf_fails() // Make PDF service task throw exception to simulate a failure situation. fixture - .Mock() + .Mock() .Setup(x => x.Execute(It.IsAny(), instance)) .ThrowsAsync(new Exception()); @@ -134,13 +133,13 @@ public async Task Calls_unlock_if_pdf_fails() fixture.Mock().Verify(p => p.Lock(taskId, instance)); fixture.Mock().Verify(p => p.Finalize(taskId, instance)); mockProcessTask.Verify(p => p.End(taskId, instance)); - fixture.Mock().Verify(p => p.Execute(taskId, instance)); + fixture.Mock().Verify(p => p.Execute(taskId, instance)); // Make sure unlock data is called fixture.Mock().Verify(p => p.Unlock(taskId, instance)); // Make sure eFormidling service task is not called if PDF failed. - fixture.Mock().Verify(p => p.Execute(taskId, instance), Times.Never); + fixture.Mock().Verify(p => p.Execute(taskId, instance), Times.Never); } [Fact] @@ -157,7 +156,7 @@ public async Task Calls_unlock_if_eFormidling_fails() // Make eFormidling service task throw exception to simulate a failure situation. fixture - .Mock() + .Mock() .Setup(x => x.Execute(It.IsAny(), instance)) .ThrowsAsync(new Exception()); @@ -168,45 +167,31 @@ public async Task Calls_unlock_if_eFormidling_fails() fixture.Mock().Verify(p => p.Lock(taskId, instance)); fixture.Mock().Verify(p => p.Finalize(taskId, instance)); mockProcessTask.Verify(p => p.End(taskId, instance)); - fixture.Mock().Verify(p => p.Execute(taskId, instance)); + fixture.Mock().Verify(p => p.Execute(taskId, instance)); // Make sure unlock data is called fixture.Mock().Verify(p => p.Unlock(taskId, instance)); } [Fact] - public async Task Throws_If_Missing_Pdf_ServiceTask() + public void Throws_If_Missing_Pdf_ServiceTask() { using var fixture = Fixture.Create([], addPdfServiceTask: false); - var eteh = fixture.Handler; - - var instance = new Instance() { Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", AppId = "ttd/test" }; - - var taskId = "Task_1"; - Mock mockProcessTask = new(); - - var ex = await Assert.ThrowsAsync(async () => - await eteh.Execute(mockProcessTask.Object, taskId, instance) - ); - Assert.Equal("PdfServiceTask not found in serviceTasks", ex.Message); + Assert.Throws(() => + { + EndTaskEventHandler eteh = fixture.Handler; + }); } [Fact] - public async Task Throws_If_Missing_Eformidling_ServiceTask() + public void Throws_If_Missing_Eformidling_ServiceTask() { using var fixture = Fixture.Create([], addEformidlingServiceTask: false); - var eteh = fixture.Handler; - - var instance = new Instance() { Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", AppId = "ttd/test" }; - - var taskId = "Task_1"; - Mock mockProcessTask = new(); - - var ex = await Assert.ThrowsAsync(async () => - await eteh.Execute(mockProcessTask.Object, taskId, instance) - ); - Assert.Equal("EformidlingServiceTask not found in serviceTasks", ex.Message); + Assert.Throws(() => + { + EndTaskEventHandler eteh = fixture.Handler; + }); } } 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 b1e19e704..24bd1cc85 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 @@ -1,6 +1,7 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks.Common; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 5d9433299..4c750f20e 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Security.Claims; using Altinn.App.Core.Extensions; @@ -11,9 +12,11 @@ using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Validation; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.UserAction; +using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Constants; @@ -382,13 +385,13 @@ public async Task Next_returns_unsuccessful_when_process_null() { using var fixture = Fixture.Create(); ProcessEngine processEngine = fixture.ProcessEngine; - Instance instance = new Instance() + var instance = new Instance() { Id = _instanceId, AppId = "org/app", Process = null, }; - ProcessNextRequest processNextRequest = new ProcessNextRequest() + var processNextRequest = new ProcessNextRequest() { Instance = instance, Action = null, @@ -397,7 +400,7 @@ public async Task Next_returns_unsuccessful_when_process_null() }; ProcessChangeResult result = await processEngine.Next(processNextRequest); result.Success.Should().BeFalse(); - result.ErrorMessage.Should().Be("Instance does not have current task information!"); + result.ErrorMessage.Should().Be("The instance is missing process information."); result.ErrorType.Should().Be(ProcessErrorType.Conflict); } @@ -421,7 +424,34 @@ public async Task Next_returns_unsuccessful_when_process_currenttask_null() }; ProcessChangeResult result = await processEngine.Next(processNextRequest); result.Success.Should().BeFalse(); - result.ErrorMessage.Should().Be("Instance does not have current task information!"); + result.ErrorMessage.Should().Be("Process is not started. Use start!"); + result.ErrorType.Should().Be(ProcessErrorType.Conflict); + } + + [Fact] + public async Task Next_returns_unsuccessful_when_process_altinnTaskType_null() + { + using var fixture = Fixture.Create(); + ProcessEngine processEngine = fixture.ProcessEngine; + Instance instance = new Instance() + { + Id = _instanceId, + AppId = "org/app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo { ElementId = "elementId", AltinnTaskType = null }, + }, + }; + ProcessNextRequest processNextRequest = new ProcessNextRequest() + { + Instance = instance, + User = null!, + Action = null, + Language = null, + }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); + result.Success.Should().BeFalse(); + result.ErrorMessage.Should().Be("Instance does not have current altinn task type information!"); result.ErrorType.Should().Be(ProcessErrorType.Conflict); } @@ -441,18 +471,22 @@ public async Task HandleUserAction_returns_successful_when_handler_succeeds() EndEvent = "EndEvent_1", }, }; + Mock userActionMock = new Mock(MockBehavior.Strict); userActionMock.Setup(u => u.Id).Returns("sign"); userActionMock .Setup(u => u.HandleAction(It.IsAny())) .ReturnsAsync(UserActionResult.SuccessResult()); + using var fixture = Fixture.Create(updatedInstance: expectedInstance, userActions: [userActionMock.Object]); fixture .Mock() .Setup(x => x.GetApplicationMetadata()) .ReturnsAsync(new ApplicationMetadata("org/app")); + ProcessEngine processEngine = fixture.ProcessEngine; - Instance instance = new Instance() + + var instance = new Instance() { Id = _instanceId, AppId = "org/app", @@ -470,9 +504,11 @@ public async Task HandleUserAction_returns_successful_when_handler_succeeds() }, }, }; + ClaimsPrincipal user = new( new ClaimsIdentity(new List() { new(AltinnCoreClaimTypes.AuthenticationLevel, "2") }) ); + ProcessNextRequest processNextRequest = new ProcessNextRequest() { Instance = instance, @@ -480,7 +516,8 @@ public async Task HandleUserAction_returns_successful_when_handler_succeeds() Action = "sign", Language = null, }; - UserActionResult result = await processEngine.HandleUserAction(processNextRequest, CancellationToken.None); + + ProcessChangeResult result = await processEngine.Next(processNextRequest, CancellationToken.None); result.Success.Should().BeTrue(); result.ErrorType.Should().Be(null); } @@ -501,7 +538,8 @@ public async Task HandleUserAction_returns_unsuccessful_unauthorized_when_action EndEvent = "EndEvent_1", }, }; - Mock userActionMock = new Mock(MockBehavior.Strict); + + var userActionMock = new Mock(MockBehavior.Strict); userActionMock.Setup(u => u.Id).Returns("sign"); userActionMock .Setup(u => u.HandleAction(It.IsAny())) @@ -511,13 +549,16 @@ public async Task HandleUserAction_returns_unsuccessful_unauthorized_when_action errorType: ProcessErrorType.Unauthorized ) ); + using var fixture = Fixture.Create(updatedInstance: expectedInstance, userActions: [userActionMock.Object]); fixture .Mock() .Setup(x => x.GetApplicationMetadata()) .ReturnsAsync(new ApplicationMetadata("org/app")); + ProcessEngine processEngine = fixture.ProcessEngine; - Instance instance = new Instance() + + var instance = new Instance() { Id = _instanceId, AppId = "org/app", @@ -535,17 +576,20 @@ public async Task HandleUserAction_returns_unsuccessful_unauthorized_when_action }, }, }; + ClaimsPrincipal user = new( new ClaimsIdentity(new List() { new(AltinnCoreClaimTypes.AuthenticationLevel, "2") }) ); - ProcessNextRequest processNextRequest = new ProcessNextRequest() + + var processNextRequest = new ProcessNextRequest() { Instance = instance, User = user, Action = "sign", Language = null, }; - UserActionResult result = await processEngine.HandleUserAction(processNextRequest, CancellationToken.None); + + ProcessChangeResult result = await processEngine.Next(processNextRequest, CancellationToken.None); result.Success.Should().BeFalse(); result.ErrorType.Should().Be(ProcessErrorType.Unauthorized); } @@ -572,13 +616,16 @@ public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() StartEvent = "StartEvent_1", }, }; + using var fixture = Fixture.Create(updatedInstance: expectedInstance); fixture .Mock() .Setup(x => x.GetApplicationMetadata()) .ReturnsAsync(new ApplicationMetadata("org/app")); + ProcessEngine processEngine = fixture.ProcessEngine; - Instance instance = new Instance() + + var instance = new Instance() { Id = _instanceId, AppId = "org/app", @@ -596,7 +643,9 @@ public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() }, }, }; + ProcessState originalProcessState = instance.Process.Copy(); + ClaimsPrincipal user = new( new ClaimsIdentity( new List() @@ -607,13 +656,15 @@ public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() } ) ); - ProcessNextRequest processNextRequest = new ProcessNextRequest() + + var processNextRequest = new ProcessNextRequest() { Instance = instance, User = user, Action = null, Language = null, }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); fixture.Mock().Verify(r => r.IsProcessTask("Task_1"), Times.Once); fixture.Mock().Verify(r => r.IsEndEvent("Task_2"), Times.Once); @@ -908,11 +959,12 @@ public async Task Next_moves_instance_to_end_event_and_ends_process(bool registe } ProcessEngine processEngine = fixture.ProcessEngine; + InstanceOwner instanceOwner = new() { PartyId = _instanceOwnerPartyId.ToString() }; Instance instance = new Instance() { Id = _instanceId, AppId = "org/app", - InstanceOwner = new() { PartyId = "1337" }, + InstanceOwner = instanceOwner, Data = [], Process = new ProcessState() { @@ -936,13 +988,15 @@ public async Task Next_moves_instance_to_end_event_and_ends_process(bool registe } ) ); - ProcessNextRequest processNextRequest = new ProcessNextRequest() + + var processNextRequest = new ProcessNextRequest() { - Instance = instance, User = user, Action = null, Language = null, + Instance = instance, }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); fixture.Mock().Verify(r => r.IsProcessTask("Task_2"), Times.Once); fixture.Mock().Verify(r => r.IsEndEvent("EndEvent_1"), Times.Once); @@ -1254,6 +1308,26 @@ public static Fixture Create( Incoming = new List { "Flow_3" }, } ); + + var processEngineAuthorizerMock = new Mock(MockBehavior.Strict); + processEngineAuthorizerMock + .Setup(x => x.AuthorizeProcessNext(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + ; + + var validationServiceMock = new Mock(MockBehavior.Strict); + validationServiceMock + .Setup(v => + v.ValidateInstanceAtTask( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + null, + null + ) + ) + .ReturnsAsync(new List()); + if (updatedInstance is not null) { processEventDispatcherMock @@ -1263,6 +1337,7 @@ public static Fixture Create( services.TryAddTransient(_ => authenticationContextMock.Object); services.TryAddTransient(_ => processNavigatorMock.Object); + services.TryAddTransient(_ => processEngineAuthorizerMock.Object); services.TryAddTransient(_ => processEventHandlingDelegatorMock.Object); services.TryAddTransient(_ => processEventDispatcherMock.Object); services.TryAddTransient(_ => dataClientMock.Object); @@ -1271,6 +1346,7 @@ public static Fixture Create( services.TryAddTransient(_ => appMetadataMock.Object); services.TryAddTransient(_ => appResourcesMock.Object); services.TryAddTransient(); + services.TryAddTransient(_ => validationServiceMock.Object); if (registerProcessEnd) services.AddSingleton(_ => new Mock().Object); diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventHandlingTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventHandlingTests.cs index 6b6743637..203787308 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventHandlingTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventHandlingTests.cs @@ -7,6 +7,7 @@ using Altinn.App.Core.Internal.Process.EventHandlers; using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; @@ -79,6 +80,12 @@ void AddMock() } } + private readonly List _serviceTasks = + [ + new Mock().Object, + new Mock().Object, + ]; + [Fact] public async Task UpdateProcessAndDispatchEvents_StartEvent_instance_updated_and_events_sent_to_storage() { 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 9ef9304ef..f9e87278f 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 @@ -2,6 +2,7 @@ using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks.Common; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Moq; 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 5202b59b7..288deed9a 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 @@ -5,7 +5,7 @@ using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Internal.Instances; -using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks.Common; using Altinn.App.Core.Internal.Texts; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Enums; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs new file mode 100644 index 000000000..f7a10f60a --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs @@ -0,0 +1,89 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.EFormidling.Interface; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; + +namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks; + +public class EFormidlingServiceTaskTests +{ + private readonly Mock> _loggerMock = new(); + private readonly Mock _eFormidlingServiceMock = new(); + private readonly Mock> _appSettingsMock = new(); + private readonly EFormidlingServiceTask _serviceTask; + + public EFormidlingServiceTaskTests() + { + _serviceTask = new EFormidlingServiceTask( + _loggerMock.Object, + _eFormidlingServiceMock.Object, + _appSettingsMock.Object + ); + } + + [Fact] + public async Task Execute_Should_LogWarning_When_EFormidlingDisabled() + { + // Arrange + var instance = new Instance(); + var appSettings = new AppSettings { EnableEFormidling = false }; + _appSettingsMock.Setup(x => x.Value).Returns(appSettings); + + // Act + await _serviceTask.Execute("taskId", instance); + + // Assert + _loggerMock.Verify( + x => + x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is( + (v, t) => + v.ToString()! + .Contains( + "EFormidling has been added as a service task in the BPMN process definition but is not enabled in appsettings.json. No eFormidling shipment will be sent, but the service task will be completed." + ) + ), + It.IsAny(), + It.Is>((v, t) => true) + ), + Times.Once + ); + } + + [Fact] + public async Task Execute_Should_ThrowException_When_EFormidlingServiceIsNull() + { + // Arrange + var instance = new Instance(); + + var appSettings = new AppSettings { EnableEFormidling = true }; + _appSettingsMock.Setup(x => x.Value).Returns(appSettings); + + var serviceTask = new EFormidlingServiceTask(_loggerMock.Object, null, _appSettingsMock.Object); + + // Act & Assert + await Assert.ThrowsAsync(() => serviceTask.Execute("taskId", instance)); + } + + [Fact] + public async Task Execute_Should_Call_SendEFormidlingShipment_When_EFormidlingEnabled() + { + // Arrange + var instance = new Instance(); + var appSettings = new AppSettings { EnableEFormidling = true }; + _appSettingsMock.Setup(x => x.Value).Returns(appSettings); + + // Act + await _serviceTask.Execute("taskId", instance); + + // Assert + _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/Legacy/EformidlingServiceTaskLegacyTests.cs similarity index 87% rename from test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs rename to test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/Legacy/EformidlingServiceTaskLegacyTests.cs index fec05427e..dac89900a 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/Legacy/EformidlingServiceTaskLegacyTests.cs @@ -2,7 +2,7 @@ using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Instances; -using Altinn.App.Core.Internal.Process.ServiceTasks; +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; @@ -10,22 +10,14 @@ using Microsoft.Extensions.Options; using Moq; -namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks; +namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks.Legacy; -public class EformidlingServiceTaskTests +public class EformidlingServiceTaskLegacyTests { - private readonly ILogger _logger; - private readonly Mock _appMetadata; - private readonly Mock _instanceClient; - private readonly Mock _eFormidlingService; - - public EformidlingServiceTaskTests() - { - _logger = NullLogger.Instance; - _appMetadata = new Mock(); - _instanceClient = new Mock(); - _eFormidlingService = new Mock(); - } + private readonly ILogger _logger = NullLogger.Instance; + private readonly Mock _appMetadata = new(); + private readonly Mock _instanceClient = new(); + private readonly Mock _eFormidlingService = new(); [Fact] public async Task Execute_EFormidlingIsEnabledAndSendAfterTaskIdMatchesCurrentTask_EFormidlingShipment_is_sent() @@ -123,12 +115,12 @@ public async Task Execute_EFormidlingIsEnabledAndSendAfterTaskIdNotMatchingCurre _eFormidlingService.VerifyNoOtherCalls(); } - public EformidlingServiceTask GetEformidlingServiceTask( + private EformidlingServiceTaskLegacy GetEformidlingServiceTask( AppSettings? appSettings, IEFormidlingService? eFormidlingService = null ) { - return new EformidlingServiceTask( + return new EformidlingServiceTaskLegacy( _logger, _appMetadata.Object, _instanceClient.Object, diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/Legacy/PdfServiceTaskLegacyTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/Legacy/PdfServiceTaskLegacyTests.cs new file mode 100644 index 000000000..15575bc3c --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/Legacy/PdfServiceTaskLegacyTests.cs @@ -0,0 +1,181 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Moq; + +namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks.Legacy; + +public class PdfServiceTaskLegacyTests +{ + private readonly Mock _appMetadata = new(); + private readonly Mock _pdfService = new(); + private readonly Mock _appModel = new(); + + [Fact] + public async Task Execute_calls_pdf_service() + { + Instance i = new() { Data = [new DataElement() { DataType = "DataType_1" }] }; + SetupAppMetadataWithDataTypes( + [ + new DataType + { + Id = "DataType_1", + TaskId = "Task_1", + AppLogic = new ApplicationLogic() { ClassRef = "DataType_1" }, + EnablePdfCreation = true, + }, + ] + ); + + PdfServiceTaskLegacy pst = new(_appMetadata.Object, _pdfService.Object); + await pst.Execute("Task_1", i); + + _appMetadata.Verify(am => am.GetApplicationMetadata(), Times.Once); + _pdfService.Verify(ps => ps.GenerateAndStorePdf(i, "Task_1", CancellationToken.None), Times.Once); + _appMetadata.VerifyNoOtherCalls(); + _pdfService.VerifyNoOtherCalls(); + _appModel.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Execute_pdf_service_is_called_only_once() + { + Instance i = new() + { + Data = + [ + new DataElement() { DataType = "DataType_1" }, + new DataElement() { DataType = "DataType_1" }, + new DataElement() { DataType = "DataType_2" }, + new DataElement() { DataType = "DataType_2" }, + ], + }; + SetupAppMetadataWithDataTypes( + [ + new DataType + { + Id = "DataType_1", + TaskId = "Task_1", + AppLogic = new ApplicationLogic() { ClassRef = "DataType_1" }, + EnablePdfCreation = true, + }, + new DataType + { + Id = "DataType_2", + TaskId = "Task_1", + AppLogic = new ApplicationLogic() { ClassRef = "DataType_2" }, + EnablePdfCreation = true, + }, + ] + ); + + PdfServiceTaskLegacy pst = new(_appMetadata.Object, _pdfService.Object); + await pst.Execute("Task_1", i); + + _appMetadata.Verify(am => am.GetApplicationMetadata()); + _pdfService.Verify(ps => ps.GenerateAndStorePdf(i, "Task_1", CancellationToken.None), Times.Once); + _appMetadata.VerifyNoOtherCalls(); + _pdfService.VerifyNoOtherCalls(); + _appModel.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Execute_pdf_generation_is_never_called_if_no_dataelements_for_datatype() + { + Instance i = new() { Data = [] }; + SetupAppMetadataWithDataTypes( + [ + new DataType + { + Id = "DataType_1", + TaskId = "Task_1", + AppLogic = new ApplicationLogic() { ClassRef = "DataType_1" }, + EnablePdfCreation = true, + }, + new DataType + { + Id = "DataType_2", + TaskId = "Task_1", + AppLogic = new ApplicationLogic() { ClassRef = "DataType_2" }, + EnablePdfCreation = true, + }, + ] + ); + + PdfServiceTaskLegacy pst = new(_appMetadata.Object, _pdfService.Object); + await pst.Execute("Task_1", i); + + _appMetadata.Verify(am => am.GetApplicationMetadata()); + _appMetadata.VerifyNoOtherCalls(); + _pdfService.VerifyNoOtherCalls(); + _appModel.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Execute_does_not_call_pdfservice_if_generate_pdf_are_false_for_all_datatypes() + { + DataElement d = new() { Id = "DataElement_1", DataType = "DataType_1" }; + Instance i = new() { Data = [d] }; + SetupAppMetadataWithDataTypes( + [ + new DataType + { + Id = "DataType_1", + TaskId = "Task_1", + AppLogic = new ApplicationLogic() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.ServiceTasks.TestData.DummyDataType", + }, + EnablePdfCreation = false, + }, + ] + ); + + PdfServiceTaskLegacy pst = new(_appMetadata.Object, _pdfService.Object); + await pst.Execute("Task_1", i); + + _appMetadata.Verify(am => am.GetApplicationMetadata(), Times.Once); + _appMetadata.VerifyNoOtherCalls(); + _pdfService.VerifyNoOtherCalls(); + _appModel.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Execute_does_not_call_pdfservice_if_generate_pdf_are_false_for_all_datatypes_nde_pdf_flag_true() + { + DataElement d = new() { Id = "DataElement_1", DataType = "DataType_1" }; + Instance i = new() { Data = [d] }; + SetupAppMetadataWithDataTypes( + [ + new DataType + { + Id = "DataType_1", + TaskId = "Task_1", + AppLogic = new ApplicationLogic() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.ServiceTasks.TestData.DummyDataType", + }, + EnablePdfCreation = false, + }, + ] + ); + + PdfServiceTaskLegacy pst = new(_appMetadata.Object, _pdfService.Object); + await pst.Execute("Task_1", i); + + _appMetadata.Verify(am => am.GetApplicationMetadata(), Times.Once); + _appMetadata.VerifyNoOtherCalls(); + _pdfService.VerifyNoOtherCalls(); + _appModel.VerifyNoOtherCalls(); + } + + private void SetupAppMetadataWithDataTypes(List? dataTypes = null) + { + _appMetadata + .Setup(am => am.GetApplicationMetadata()) + .ReturnsAsync(new ApplicationMetadata("ttd/test") { DataTypes = dataTypes ?? new List { } }); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/TestData/DummyDataType.cs b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/Legacy/TestData/DummyDataType.cs similarity index 100% rename from test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/TestData/DummyDataType.cs rename to test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/Legacy/TestData/DummyDataType.cs 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 993f98d15..975742a0d 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs @@ -1,178 +1,51 @@ -using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Internal.AppModel; -using Altinn.App.Core.Internal.Pdf; -using Altinn.App.Core.Internal.Process.ServiceTasks; -using Altinn.App.Core.Models; +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.ServiceTasks; using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; using Moq; namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks; public class PdfServiceTaskTests { - private readonly Mock _appMetadata; - private readonly Mock _pdfService; - private readonly Mock _appModel; + private readonly Mock _pdfServiceMock = new(); + private readonly Mock> _loggerMock = new(); + private readonly Mock _processReaderMock = new(); + private readonly PdfServiceTask _serviceTask; - public PdfServiceTaskTests() - { - _appMetadata = new Mock(); - _pdfService = new Mock(); - _appModel = new Mock(); - } + private const string FileName = "My file name"; - [Fact] - public async Task Execute_calls_pdf_service() + public PdfServiceTaskTests() { - Instance i = new Instance() { Data = [new DataElement() { DataType = "DataType_1" }] }; - SetupAppMetadataWithDataTypes( - [ - new DataType + _processReaderMock + .Setup(x => x.GetAltinnTaskExtension(It.IsAny())) + .Returns( + new AltinnTaskExtension { - Id = "DataType_1", - TaskId = "Task_1", - AppLogic = new ApplicationLogic() { ClassRef = "DataType_1" }, - EnablePdfCreation = true, - }, - ] - ); - PdfServiceTask pst = new PdfServiceTask(_appMetadata.Object, _pdfService.Object); - await pst.Execute("Task_1", i); - _appMetadata.Verify(am => am.GetApplicationMetadata(), Times.Once); - _pdfService.Verify(ps => ps.GenerateAndStorePdf(i, "Task_1", CancellationToken.None), Times.Once); - _appMetadata.VerifyNoOtherCalls(); - _pdfService.VerifyNoOtherCalls(); - _appModel.VerifyNoOtherCalls(); - } + TaskType = "pdf", + PdfConfiguration = new AltinnPdfConfiguration { Filename = FileName }, + } + ); - [Fact] - public async Task Execute_pdf_service_is_called_only_once() - { - Instance i = new Instance() - { - Data = - [ - new DataElement() { DataType = "DataType_1" }, - new DataElement() { DataType = "DataType_1" }, - new DataElement() { DataType = "DataType_2" }, - new DataElement() { DataType = "DataType_2" }, - ], - }; - SetupAppMetadataWithDataTypes( - [ - new DataType - { - Id = "DataType_1", - TaskId = "Task_1", - AppLogic = new ApplicationLogic() { ClassRef = "DataType_1" }, - EnablePdfCreation = true, - }, - new DataType - { - Id = "DataType_2", - TaskId = "Task_1", - AppLogic = new ApplicationLogic() { ClassRef = "DataType_2" }, - EnablePdfCreation = true, - }, - ] - ); - PdfServiceTask pst = new PdfServiceTask(_appMetadata.Object, _pdfService.Object); - await pst.Execute("Task_1", i); - _appMetadata.Verify(am => am.GetApplicationMetadata()); - _pdfService.Verify(ps => ps.GenerateAndStorePdf(i, "Task_1", CancellationToken.None), Times.Once); - _appMetadata.VerifyNoOtherCalls(); - _pdfService.VerifyNoOtherCalls(); - _appModel.VerifyNoOtherCalls(); + _serviceTask = new PdfServiceTask(_pdfServiceMock.Object, _processReaderMock.Object, _loggerMock.Object); } [Fact] - public async Task Execute_pdf_generation_is_never_called_if_no_dataelements_for_datatype() + public async Task Execute_Should_Call_GenerateAndStorePdf() { - Instance i = new Instance() { Data = [] }; - SetupAppMetadataWithDataTypes( - [ - new DataType - { - Id = "DataType_1", - TaskId = "Task_1", - AppLogic = new ApplicationLogic() { ClassRef = "DataType_1" }, - EnablePdfCreation = true, - }, - new DataType - { - Id = "DataType_2", - TaskId = "Task_1", - AppLogic = new ApplicationLogic() { ClassRef = "DataType_2" }, - EnablePdfCreation = true, - }, - ] - ); - PdfServiceTask pst = new PdfServiceTask(_appMetadata.Object, _pdfService.Object); - await pst.Execute("Task_1", i); - _appMetadata.Verify(am => am.GetApplicationMetadata()); - _appMetadata.VerifyNoOtherCalls(); - _pdfService.VerifyNoOtherCalls(); - _appModel.VerifyNoOtherCalls(); - } + // Arrange + var instance = new Instance(); + var taskId = "taskId"; - [Fact] - public async Task Execute_does_not_call_pdfservice_if_generate_pdf_are_false_for_all_datatypes() - { - DataElement d = new DataElement() { Id = "DataElement_1", DataType = "DataType_1" }; - Instance i = new Instance() { Data = [d] }; - SetupAppMetadataWithDataTypes( - [ - new DataType - { - Id = "DataType_1", - TaskId = "Task_1", - AppLogic = new ApplicationLogic() - { - ClassRef = "Altinn.App.Core.Tests.Internal.Process.ServiceTasks.TestData.DummyDataType", - }, - EnablePdfCreation = false, - }, - ] - ); - PdfServiceTask pst = new PdfServiceTask(_appMetadata.Object, _pdfService.Object); - await pst.Execute("Task_1", i); - _appMetadata.Verify(am => am.GetApplicationMetadata(), Times.Once); - _appMetadata.VerifyNoOtherCalls(); - _pdfService.VerifyNoOtherCalls(); - _appModel.VerifyNoOtherCalls(); - } + // Act + await _serviceTask.Execute(taskId, instance); - [Fact] - public async Task Execute_does_not_call_pdfservice_if_generate_pdf_are_false_for_all_datatypes_nde_pdf_flag_true() - { - DataElement d = new DataElement() { Id = "DataElement_1", DataType = "DataType_1" }; - Instance i = new Instance() { Data = [d] }; - SetupAppMetadataWithDataTypes( - [ - new DataType - { - Id = "DataType_1", - TaskId = "Task_1", - AppLogic = new ApplicationLogic() - { - ClassRef = "Altinn.App.Core.Tests.Internal.Process.ServiceTasks.TestData.DummyDataType", - }, - EnablePdfCreation = false, - }, - ] + // Assert + _pdfServiceMock.Verify( + x => x.GenerateAndStorePdf(instance, taskId, FileName, It.IsAny()), + Times.Once ); - PdfServiceTask pst = new PdfServiceTask(_appMetadata.Object, _pdfService.Object); - await pst.Execute("Task_1", i); - _appMetadata.Verify(am => am.GetApplicationMetadata(), Times.Once); - _appMetadata.VerifyNoOtherCalls(); - _pdfService.VerifyNoOtherCalls(); - _appModel.VerifyNoOtherCalls(); - } - - private void SetupAppMetadataWithDataTypes(List? dataTypes = null) - { - _appMetadata - .Setup(am => am.GetApplicationMetadata()) - .ReturnsAsync(new ApplicationMetadata("ttd/test") { DataTypes = dataTypes ?? new List { } }); } } From b1bcc4df4f3e27de70b1d9ab5bbfbe931a618fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Mon, 30 Jun 2025 10:58:07 +0200 Subject: [PATCH 02/60] Remove default implementation of hook methods in standard service tasks. --- .../ServiceTasks/EFormidlingServiceTask.cs | 18 ------------------ .../ServiceTasks/PdfServiceTask.cs | 18 ------------------ 2 files changed, 36 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index 84c950507..2c2656f9f 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -56,22 +56,4 @@ public async Task Execute(string taskId, Instance instance, CancellationToken ca await _eFormidlingService.SendEFormidlingShipment(instance); _logger.LogDebug("Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", taskId); } - - /// - public Task Start(string taskId, Instance instance) - { - return Task.CompletedTask; - } - - /// - public Task End(string taskId, Instance instance) - { - return Task.CompletedTask; - } - - /// - public Task Abandon(string taskId, Instance instance) - { - return Task.CompletedTask; - } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs index 1bafdb803..3e1c45a7d 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -40,24 +40,6 @@ public async Task Execute(string taskId, Instance instance, CancellationToken ca _logger.LogDebug("Successfully called PdfService for PDF Service Task {TaskId}.", taskId); } - /// - public Task Start(string taskId, Instance instance) - { - return Task.CompletedTask; - } - - /// - public Task End(string taskId, Instance instance) - { - return Task.CompletedTask; - } - - /// - public Task Abandon(string taskId, Instance instance) - { - return Task.CompletedTask; - } - private ValidAltinnPdfConfiguration GetValidAltinnPdfConfiguration(string taskId) { AltinnTaskExtension? altinnTaskExtension = _processReader.GetAltinnTaskExtension(taskId); From 3ad41747a7dd5ead98855370b491bfa3199041a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Mon, 30 Jun 2025 12:33:48 +0200 Subject: [PATCH 03/60] Skip validation if service task. --- src/Altinn.App.Core/Internal/Process/ProcessEngine.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index becde8b3f..fddb4a7b0 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -220,13 +220,14 @@ public async Task Next(ProcessNextRequest request, Cancella ); string checkedAction = request.Action ?? ConvertTaskTypeToAction(altinnTaskType); - + var isServiceTask = false; // If the action is 'reject', we should not run any service task and there is no need to check for a user action handler, since 'reject' doesn't have one. if (request.Action is not "reject") { IServiceTask? serviceTask = CheckIfServiceTask(altinnTaskType); if (serviceTask is not null) { + isServiceTask = true; ServiceTaskResult serviceActionResult = await HandleServiceTask( instance, serviceTask, @@ -277,6 +278,10 @@ request with "Skipping validation during process next because the action is 'reject' and the task is being abandoned." ); } + else if (isServiceTask) + { + _logger.LogInformation("Skipping validation during process next because the task is a service task."); + } else { InstanceDataUnitOfWork dataAccessor = await _instanceDataUnitOfWorkInitializer.Init( From 2e795652ee2eca84b9a7ffc1bb2fecd8e551a797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 2 Jul 2025 13:30:34 +0200 Subject: [PATCH 04/60] Always move to next for service tasks for now. Remoe service task result enum and use result classes instead, for expandability. --- .../Controllers/ProcessController.cs | 12 +-- .../Internal/Process/ProcessEngine.cs | 68 +++++++++-------- .../ServiceTasks/EFormidlingServiceTask.cs | 9 ++- .../ProcessTasks/ServiceTasks/IServiceTask.cs | 75 ++++++++++--------- .../ServiceTasks/PdfServiceTask.cs | 9 ++- .../ServiceTasks/Pdf/PdfServiceTaskTests.cs | 4 +- .../EFormidlingServiceTaskTests.cs | 37 +++++++-- .../ServiceTasks/PdfServiceTaskTests.cs | 24 ++++-- 8 files changed, 145 insertions(+), 93 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 6254b66e8..d4ad3f212 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -287,7 +287,7 @@ public async Task> ProcessNext( }; result = await _processEngine.Next(processNextRequest, ct); - moveToNextTaskAutomatically = await ShouldMoveToNextTaskAutomatically(instance, ct); + moveToNextTaskAutomatically = IsServiceTask(instance); if (!result.Success) { @@ -516,7 +516,7 @@ private async Task ConvertAndAuthorizeActions(Instance instance return appProcessState; } - private async Task ShouldMoveToNextTaskAutomatically(Instance instance, CancellationToken ct) + private bool IsServiceTask(Instance instance) { if (instance.Process.CurrentTask is null) { @@ -524,13 +524,7 @@ private async Task ShouldMoveToNextTaskAutomatically(Instance instance, Ca } IServiceTask? serviceTask = _processEngine.CheckIfServiceTask(instance.Process.CurrentTask.AltinnTaskType); - - if (serviceTask is not null) - { - return await serviceTask.MoveToNextTaskAfterExecution(instance.Process.CurrentTask.ElementId, instance, ct); - } - - return false; + return serviceTask is not null; } private ActionResult GetResultForError(ProcessChangeResult result) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index fddb4a7b0..268199b02 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -238,14 +238,9 @@ request with ct ); - if (serviceActionResult.Result is ServiceTaskResult.ResultType.Failure) + if (serviceActionResult is ServiceTaskFailedResult failedServiceTask) { - var result = new ProcessChangeResult() - { - Success = false, - ErrorMessage = serviceActionResult.ErrorMessage, - ErrorType = serviceActionResult.ErrorType, - }; + ProcessChangeResult result = failedServiceTask.ToProcessChangeResult(); activity?.SetProcessChangeResult(result); return result; } @@ -333,6 +328,24 @@ request with return changeResult; } + /// + public async Task HandleEventsAndUpdateStorage( + Instance instance, + Dictionary? prefill, + List? events + ) + { + using (var activity = _telemetry?.StartProcessHandleEventsActivity(instance)) + { + await _processEventHandlerDelegator.HandleEvents(instance, prefill, events); + } + + using (var activity = _telemetry?.StartProcessStoreEventsActivity(instance)) + { + return await _processEventDispatcher.DispatchToStorage(instance, events); + } + } + /// public IServiceTask? CheckIfServiceTask(string? altinnTaskType) { @@ -347,7 +360,6 @@ request with return serviceTask; } - /// private async Task HandleUserAction( Instance instance, ProcessNextRequest request, @@ -402,7 +414,6 @@ CancellationToken ct return actionResult; } - /// private async Task HandleServiceTask( Instance instance, IServiceTask serviceTask, @@ -414,9 +425,9 @@ private async Task HandleServiceTask( if (request.Action is not "write" && request.Action != serviceTask.Type) // serviceTask.Type is accepted to support custom service task types { - var result = new ServiceTaskResult + var result = new ServiceTaskFailedResult() { - Result = ServiceTaskResult.ResultType.Failure, + ErrorTitle = "User action not supported!", ErrorMessage = $"Service tasks do not support running user actions! Received action param {request.Action}.", ErrorType = ProcessErrorType.Conflict, @@ -427,40 +438,31 @@ private async Task HandleServiceTask( try { - await serviceTask.Execute(instance.Process.CurrentTask.ElementId, instance, ct); + ServiceTaskParameters parameters = new() + { + InstanceDataMutator = await _instanceDataUnitOfWorkInitializer.Init( + instance, + instance.Process?.CurrentTask?.ElementId, + request.Language + ), + CancellationToken = ct, + }; - return new ServiceTaskResult { Result = ServiceTaskResult.ResultType.Success }; + return await serviceTask.Execute(parameters); } catch (Exception ex) { activity?.Errored(ex); - return new ServiceTaskResult() + return new ServiceTaskFailedResult() { - Result = ServiceTaskResult.ResultType.Failure, - ErrorMessage = $"Service task {serviceTask.Type} failed!", + ErrorTitle = "Service task failed!", + ErrorMessage = $"Service task {serviceTask.Type} failed with an exception!", ErrorType = ProcessErrorType.Internal, }; } } - /// - public async Task HandleEventsAndUpdateStorage( - Instance instance, - Dictionary? prefill, - List? events - ) - { - using (var activity = _telemetry?.StartProcessHandleEventsActivity(instance)) - { - await _processEventHandlerDelegator.HandleEvents(instance, prefill, events); - } - using (var activity = _telemetry?.StartProcessStoreEventsActivity(instance)) - { - return await _processEventDispatcher.DispatchToStorage(instance, events); - } - } - /// /// Does not save process. Instance object is updated. /// diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index 2c2656f9f..8ab00aaa2 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -35,14 +35,17 @@ public EFormidlingServiceTask( public string Type => "eFormidling"; /// - public async Task Execute(string taskId, Instance instance, CancellationToken cancellationToken = default) + public async Task Execute(ServiceTaskParameters parameters) { + string taskId = parameters.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; + Instance instance = parameters.InstanceDataMutator.Instance; + if (_appSettings?.Value.EnableEFormidling is false) { _logger.LogWarning( "EFormidling has been added as a service task in the BPMN process definition but is not enabled in appsettings.json. No eFormidling shipment will be sent, but the service task will be completed." ); - return; + return new ServiceTaskSuccessResult(); } if (_eFormidlingService is null) @@ -55,5 +58,7 @@ public async Task Execute(string taskId, Instance instance, CancellationToken ca _logger.LogDebug("Calling eFormidlingService for eFormidling Service Task {TaskId}.", taskId); await _eFormidlingService.SendEFormidlingShipment(instance); _logger.LogDebug("Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", taskId); + + return new ServiceTaskSuccessResult(); } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs index 1cb06c7d6..958eddde1 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs @@ -1,6 +1,5 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Models.Process; -using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; @@ -13,60 +12,66 @@ public interface IServiceTask : IProcessTask /// /// Executes the service task. /// - /// TODO: Fortsette å ta in taskId og instance, som de andre metodene, eller hoppe over på IInstanceDataAccessor? - public Task Execute(string taskId, Instance instance, CancellationToken cancellationToken = default); + public Task Execute(ServiceTaskParameters parameters); +} +/// +/// This class represents the parameters for executing a service task. +/// +public sealed record ServiceTaskParameters +{ /// - /// Method that is called to determine if the process should move to the next task after executing the service task, or wait for another process next call. + /// An instance data mutator that can be used to read and modify the instance data during the service task execution. /// - /// - /// - /// - /// - public Task MoveToNextTaskAfterExecution( - string taskId, - Instance instance, - CancellationToken cancellationToken = default - ) - { - // The default implementation is to move to the next task after execution - return Task.FromResult(true); - } + public required IInstanceDataMutator InstanceDataMutator { get; init; } + + /// + /// Cancellation token for the operation. + /// + public CancellationToken CancellationToken { get; init; } = CancellationToken.None; } /// /// This class represents the result of executing a service task. /// -public class ServiceTaskResult +public abstract class ServiceTaskResult { } + +/// +/// This class represents a successful result of executing a service task. +/// +public sealed class ServiceTaskSuccessResult : ServiceTaskResult { } + +/// +/// This class represents a failed result of executing a service task. +/// +public sealed class ServiceTaskFailedResult : ServiceTaskResult { /// - /// The result of the service task execution. + /// Gets or sets the error title if the service task execution failed. /// - public ResultType Result { get; set; } + public required string ErrorTitle { get; init; } /// - /// Error type to return when the service task was not successful + /// Gets or sets the error message if the service task execution failed. /// - public ProcessErrorType? ErrorType { get; set; } + public required string ErrorMessage { get; init; } /// - /// Error message to return when the service task was not successful + /// Gets or sets the error type if the service task execution failed. /// - public string? ErrorMessage { get; set; } + public required ProcessErrorType ErrorType { get; init; } /// - /// An enum representing the status of the service task execution. + /// Converts the service task failed result to an unsuccessful process change result. /// - public enum ResultType + public ProcessChangeResult ToProcessChangeResult() { - /// - /// The service task was executed successfully. - /// - Success, - - /// - /// The service task failed to execute. - /// - Failure, + return new ProcessChangeResult + { + Success = false, + ErrorTitle = ErrorTitle, + ErrorMessage = ErrorMessage, + ErrorType = ErrorType, + }; } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs index 3e1c45a7d..fd8f602a6 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -30,14 +30,19 @@ public PdfServiceTask(IPdfService pdfService, IProcessReader processReader, ILog public string Type => "pdf"; /// - public async Task Execute(string taskId, Instance instance, CancellationToken cancellationToken = default) + public async Task Execute(ServiceTaskParameters parameters) { + string taskId = parameters.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; + Instance instance = parameters.InstanceDataMutator.Instance; + _logger.LogDebug("Calling PdfService for PDF Service Task {TaskId}.", taskId); ValidAltinnPdfConfiguration config = GetValidAltinnPdfConfiguration(taskId); - await _pdfService.GenerateAndStorePdf(instance, taskId, config.Filename, cancellationToken); + await _pdfService.GenerateAndStorePdf(instance, taskId, config.Filename, parameters.CancellationToken); _logger.LogDebug("Successfully called PdfService for PDF Service Task {TaskId}.", taskId); + + return new ServiceTaskSuccessResult(); } private ValidAltinnPdfConfiguration GetValidAltinnPdfConfiguration(string taskId) diff --git a/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs index 4d8a96206..a06dd5120 100644 --- a/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs @@ -154,7 +154,9 @@ public async Task CurrentTask_Is_ServiceTask_If_Execute_Fails() responseAsString .Should() - .Be("{\"title\":\"Internal server error\",\"status\":500,\"detail\":\"Service task pdf failed!\"}"); + .Be( + "{\"title\":\"Service task failed!\",\"status\":500,\"detail\":\"Service task pdf failed with an exception!\"}" + ); // Double check that process did not move to the next task Instance instance = await TestData.GetInstance(Org, App, InstanceOwnerPartyId, _instanceGuid); 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 f7a10f60a..08f9e0b1d 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs @@ -1,5 +1,6 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.EFormidling.Interface; +using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; @@ -30,12 +31,17 @@ public EFormidlingServiceTaskTests() public async Task Execute_Should_LogWarning_When_EFormidlingDisabled() { // Arrange - var instance = new Instance(); + Instance instance = GetInstance(); var appSettings = new AppSettings { EnableEFormidling = false }; _appSettingsMock.Setup(x => x.Value).Returns(appSettings); + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); + + var parameters = new ServiceTaskParameters { InstanceDataMutator = instanceMutatorMock.Object }; + // Act - await _serviceTask.Execute("taskId", instance); + await _serviceTask.Execute(parameters); // Assert _loggerMock.Verify( @@ -61,29 +67,48 @@ public async Task Execute_Should_LogWarning_When_EFormidlingDisabled() public async Task Execute_Should_ThrowException_When_EFormidlingServiceIsNull() { // Arrange - var instance = new Instance(); + Instance instance = GetInstance(); var appSettings = new AppSettings { EnableEFormidling = true }; _appSettingsMock.Setup(x => x.Value).Returns(appSettings); var serviceTask = new EFormidlingServiceTask(_loggerMock.Object, null, _appSettingsMock.Object); + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); + + var parameters = new ServiceTaskParameters { InstanceDataMutator = instanceMutatorMock.Object }; + // Act & Assert - await Assert.ThrowsAsync(() => serviceTask.Execute("taskId", instance)); + await Assert.ThrowsAsync(() => serviceTask.Execute(parameters)); } [Fact] public async Task Execute_Should_Call_SendEFormidlingShipment_When_EFormidlingEnabled() { // Arrange - var instance = new Instance(); + Instance instance = GetInstance(); + var appSettings = new AppSettings { EnableEFormidling = true }; _appSettingsMock.Setup(x => x.Value).Returns(appSettings); + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); + + var parameters = new ServiceTaskParameters { InstanceDataMutator = instanceMutatorMock.Object }; + // Act - await _serviceTask.Execute("taskId", instance); + await _serviceTask.Execute(parameters); // Assert _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); } + + private static Instance GetInstance() + { + return new Instance + { + Process = new ProcessState { CurrentTask = new ProcessElementInfo { ElementId = "taskId" } }, + }; + } } 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 975742a0d..15ce48704 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs @@ -1,4 +1,5 @@ -using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Features; +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.ServiceTasks; @@ -36,15 +37,28 @@ public PdfServiceTaskTests() public async Task Execute_Should_Call_GenerateAndStorePdf() { // Arrange - var instance = new Instance(); - var taskId = "taskId"; + var instance = new Instance + { + Process = new ProcessState { CurrentTask = new ProcessElementInfo { ElementId = "taskId" } }, + }; + + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); + + var parameters = new ServiceTaskParameters { InstanceDataMutator = instanceMutatorMock.Object }; // Act - await _serviceTask.Execute(taskId, instance); + await _serviceTask.Execute(parameters); // Assert _pdfServiceMock.Verify( - x => x.GenerateAndStorePdf(instance, taskId, FileName, It.IsAny()), + x => + x.GenerateAndStorePdf( + instance, + instance.Process.CurrentTask.ElementId, + FileName, + It.IsAny() + ), Times.Once ); } From 21f71e87183037bf5180603fc30618572af192fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Fri, 4 Jul 2025 13:03:39 +0200 Subject: [PATCH 05/60] Return element type on current task and process tasks to the client. --- src/Altinn.App.Api/Controllers/ProcessController.cs | 4 +++- .../Internal/Process/Elements/AppProcessElementInfo.cs | 6 ++++++ .../Internal/Process/Elements/AppProcessTaskTypeInfo.cs | 6 ++++++ ...enApiSpecChangeDetection.SaveJsonSwagger.verified.json | 8 ++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index d4ad3f212..81d0897d1 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -496,17 +496,19 @@ private async Task ConvertAndAuthorizeActions(Instance instance appProcessState.CurrentTask.HasReadAccess = authDecisions.Single(a => a.Id == "read").Authorized; appProcessState.CurrentTask.HasWriteAccess = authDecisions.Single(a => a.Id == "write").Authorized; appProcessState.CurrentTask.UserActions = authDecisions; + appProcessState.CurrentTask.ElementType = processTask.ElementType(); } } var processTasks = new List(); - foreach (var processElement in _processReader.GetAllFlowElements().OfType()) + foreach (ProcessTask processElement in _processReader.GetAllFlowElements().OfType()) { processTasks.Add( new AppProcessTaskTypeInfo { ElementId = processElement.Id, AltinnTaskType = processElement.ExtensionElements?.TaskExtension?.TaskType, + ElementType = processElement.ElementType(), } ); } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs index 2d904e364..2aa96e0c1 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs @@ -60,4 +60,10 @@ public AppProcessElementInfo(ProcessElementInfo processElementInfo) /// [JsonPropertyName(name: "write")] public bool HasWriteAccess { get; set; } + + /// + /// The type of the element, e.g. "task", "serviceTask". + /// + [JsonPropertyName(name: "elementType")] + public string? ElementType { get; set; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs index 8ba56aeb2..57740fbfb 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs @@ -20,4 +20,10 @@ public class AppProcessTaskTypeInfo /// [JsonPropertyName(name: "elementId")] public string? ElementId { get; set; } + + /// + /// The type of the element, e.g. "task", "serviceTask". + /// + [JsonPropertyName(name: "elementType")] + public string? ElementType { get; set; } } diff --git a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json index e5c3e528c..57c701321 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json +++ b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json @@ -6931,6 +6931,10 @@ }, "write": { "type": "boolean" + }, + "elementType": { + "type": "string", + "nullable": true } }, "additionalProperties": false @@ -6979,6 +6983,10 @@ "elementId": { "type": "string", "nullable": true + }, + "elementType": { + "type": "string", + "nullable": true } }, "additionalProperties": false From fcc13bf98db0eaf57315500c63de2f76d027921a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Fri, 4 Jul 2025 13:15:28 +0200 Subject: [PATCH 06/60] Some process engine method renaming. --- .../Controllers/ProcessController.cs | 4 ++-- .../Process/Interfaces/IProcessEngine.cs | 2 +- .../Internal/Process/ProcessEngine.cs | 20 ++++++++++++------- .../Internal/Process/ProcessEngineTest.cs | 16 +++++++-------- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 81d0897d1..8238ce193 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -286,7 +286,7 @@ public async Task> ProcessNext( Language = language, }; - result = await _processEngine.Next(processNextRequest, ct); + result = await _processEngine.ProcessNext(processNextRequest, ct); moveToNextTaskAutomatically = IsServiceTask(instance); if (!result.Success) @@ -408,7 +408,7 @@ instance.Process.EndEvent is null Action = ConvertTaskTypeToAction(instance.Process.CurrentTask.AltinnTaskType), Language = language, }; - ProcessChangeResult result = await _processEngine.Next(request); + ProcessChangeResult result = await _processEngine.ProcessNext(request); if (!result.Success) { diff --git a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs index f5018b552..bf0886a0a 100644 --- a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs @@ -17,7 +17,7 @@ public interface IProcessEngine /// /// Method to move process to next task/event /// - Task Next(ProcessNextRequest request, CancellationToken ct = default); + Task ProcessNext(ProcessNextRequest request, CancellationToken ct = default); /// /// Check if the Altinn task type is a service task diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 268199b02..198897075 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -113,7 +113,7 @@ out ProcessError? startEventError // start process ProcessStateChange? startChange = await ProcessStart(processStartRequest.Instance, validStartElement); InstanceEvent? startEvent = startChange?.Events?[0].CopyValues(); - ProcessStateChange? nextChange = await ProcessNext(processStartRequest.Instance); + ProcessStateChange? nextChange = await MoveProcessStateToNextAndGenerateEvents(processStartRequest.Instance); InstanceEvent? goToNextEvent = nextChange?.Events?[0].CopyValues(); List events = []; if (startEvent is not null) @@ -141,7 +141,7 @@ out ProcessError? startEventError } /// - public async Task Next(ProcessNextRequest request, CancellationToken ct = default) + public async Task ProcessNext(ProcessNextRequest request, CancellationToken ct = default) { Instance instance = request.Instance; @@ -256,7 +256,7 @@ request with var result = new ProcessChangeResult() { Success = false, - ErrorMessage = $"Action handler for action {request.Action} failed!", + ErrorMessage = $"Action handler for action {LogSanitizer.Sanitize(request.Action)} failed!", ErrorType = userActionResult.ErrorType, }; activity?.SetProcessChangeResult(result); @@ -500,7 +500,10 @@ await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString() /// /// Moves instance's process to nextElement id. Returns the instance together with process events. /// - private async Task ProcessNext(Instance instance, string? action = null) + private async Task MoveProcessStateToNextAndGenerateEvents( + Instance instance, + string? action = null + ) { if (instance.Process == null) { @@ -515,13 +518,16 @@ await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString() CurrentTask = instance.Process.CurrentTask, StartEvent = instance.Process.StartEvent, }, - Events = await MoveProcessToNext(instance, action), + Events = await GenerateEventsAndUpdateProcessState(instance, action), NewProcessState = instance.Process, }; return result; } - private async Task> MoveProcessToNext(Instance instance, string? action = null) + private async Task> GenerateEventsAndUpdateProcessState( + Instance instance, + string? action = null + ) { List events = []; @@ -649,7 +655,7 @@ private async Task GenerateProcessChangeEvent(string eventType, I private async Task HandleMoveToNext(Instance instance, string? action) { - ProcessStateChange? processStateChange = await ProcessNext(instance, action); + ProcessStateChange? processStateChange = await MoveProcessStateToNextAndGenerateEvents(instance, action); if (processStateChange is null) { diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 4c750f20e..a51247b88 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -398,7 +398,7 @@ public async Task Next_returns_unsuccessful_when_process_null() User = null!, Language = null, }; - ProcessChangeResult result = await processEngine.Next(processNextRequest); + ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); result.Success.Should().BeFalse(); result.ErrorMessage.Should().Be("The instance is missing process information."); result.ErrorType.Should().Be(ProcessErrorType.Conflict); @@ -422,7 +422,7 @@ public async Task Next_returns_unsuccessful_when_process_currenttask_null() Action = null, Language = null, }; - ProcessChangeResult result = await processEngine.Next(processNextRequest); + ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); result.Success.Should().BeFalse(); result.ErrorMessage.Should().Be("Process is not started. Use start!"); result.ErrorType.Should().Be(ProcessErrorType.Conflict); @@ -449,7 +449,7 @@ public async Task Next_returns_unsuccessful_when_process_altinnTaskType_null() Action = null, Language = null, }; - ProcessChangeResult result = await processEngine.Next(processNextRequest); + ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); result.Success.Should().BeFalse(); result.ErrorMessage.Should().Be("Instance does not have current altinn task type information!"); result.ErrorType.Should().Be(ProcessErrorType.Conflict); @@ -517,7 +517,7 @@ public async Task HandleUserAction_returns_successful_when_handler_succeeds() Language = null, }; - ProcessChangeResult result = await processEngine.Next(processNextRequest, CancellationToken.None); + ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest, CancellationToken.None); result.Success.Should().BeTrue(); result.ErrorType.Should().Be(null); } @@ -589,7 +589,7 @@ public async Task HandleUserAction_returns_unsuccessful_unauthorized_when_action Language = null, }; - ProcessChangeResult result = await processEngine.Next(processNextRequest, CancellationToken.None); + ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest, CancellationToken.None); result.Success.Should().BeFalse(); result.ErrorType.Should().Be(ProcessErrorType.Unauthorized); } @@ -665,7 +665,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() Language = null, }; - ProcessChangeResult result = await processEngine.Next(processNextRequest); + ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); fixture.Mock().Verify(r => r.IsProcessTask("Task_1"), Times.Once); fixture.Mock().Verify(r => r.IsEndEvent("Task_2"), Times.Once); fixture.Mock().Verify(r => r.IsProcessTask("Task_2"), Times.Once); @@ -824,7 +824,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_abandon_instance Action = "reject", Language = null, }; - ProcessChangeResult result = await processEngine.Next(processNextRequest); + ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); fixture.Mock().Verify(r => r.IsProcessTask("Task_1"), Times.Once); fixture.Mock().Verify(r => r.IsEndEvent("Task_2"), Times.Once); fixture.Mock().Verify(r => r.IsProcessTask("Task_2"), Times.Once); @@ -997,7 +997,7 @@ public async Task Next_moves_instance_to_end_event_and_ends_process(bool registe Instance = instance, }; - ProcessChangeResult result = await processEngine.Next(processNextRequest); + ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); fixture.Mock().Verify(r => r.IsProcessTask("Task_2"), Times.Once); fixture.Mock().Verify(r => r.IsEndEvent("EndEvent_1"), Times.Once); fixture.Mock().Verify(n => n.GetNextTask(It.IsAny(), "Task_2", null), Times.Once); From 35097ee838d5169c221f938e142a4441bf1fb500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Fri, 4 Jul 2025 13:51:29 +0200 Subject: [PATCH 07/60] Move current process state validation in process next into it's own method. --- .../Internal/Process/ProcessEngine.cs | 117 +++++++++++------- 1 file changed, 71 insertions(+), 46 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 198897075..f964ec192 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -147,55 +147,19 @@ public async Task ProcessNext(ProcessNextRequest request, C using Activity? activity = _telemetry?.StartProcessNextActivity(instance, request.Action); - if (instance.Process is null) - { - var result = new ProcessChangeResult - { - Success = false, - ErrorType = ProcessErrorType.Conflict, - ErrorMessage = "The instance is missing process information.", - }; - activity?.SetProcessChangeResult(result); - return result; - } - - if (instance.Process?.Ended != null) - { - var result = new ProcessChangeResult - { - Success = false, - ErrorType = ProcessErrorType.Conflict, - ErrorMessage = "Process is ended.", - }; - activity?.SetProcessChangeResult(result); - return result; - } - - string? currentTaskId = instance.Process?.CurrentTask?.ElementId; - if (currentTaskId is null) + if ( + !TryGetCurrentTaskIdAndAltinnTaskType( + instance, + out CurrentTaskIdAndAltinnTaskType? currentTaskIdAndAltinnTaskType, + out ProcessChangeResult? invalidProcessStateError + ) + ) { - var result = new ProcessChangeResult - { - Success = false, - ErrorType = ProcessErrorType.Conflict, - ErrorMessage = "Process is not started. Use start!", - }; - activity?.SetProcessChangeResult(result); - return result; + activity?.SetProcessChangeResult(invalidProcessStateError); + return invalidProcessStateError; } - string? altinnTaskType = instance.Process?.CurrentTask?.AltinnTaskType; - if (altinnTaskType == null) - { - var result = new ProcessChangeResult - { - Success = false, - ErrorType = ProcessErrorType.Conflict, - ErrorMessage = "Instance does not have current altinn task type information!", - }; - activity?.SetProcessChangeResult(result); - return result; - } + (string currentTaskId, string altinnTaskType) = currentTaskIdAndAltinnTaskType; bool authorized = await _processEngineAuthorizer.AuthorizeProcessNext(instance, request.Action); @@ -710,4 +674,65 @@ private static string ConvertTaskTypeToAction(string actionOrTaskType) return actionOrTaskType; } } + + private static bool TryGetCurrentTaskIdAndAltinnTaskType( + Instance instance, + [NotNullWhen(true)] out CurrentTaskIdAndAltinnTaskType? state, + [NotNullWhen(false)] out ProcessChangeResult? error + ) + { + state = null; // allowed because the method may return false + error = null; + + ProcessState? process = instance.Process; + + if (process is null) + { + error = new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Conflict, + ErrorMessage = "The instance is missing process information.", + }; + return false; + } + + if (process.Ended is not null) + { + error = new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Conflict, + ErrorMessage = "Process is ended.", + }; + return false; + } + + if (process.CurrentTask?.ElementId is not string taskId) + { + error = new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Conflict, + ErrorMessage = "Process is not started. Use start!", + }; + return false; + } + + if (process.CurrentTask.AltinnTaskType is not string taskType) + { + error = new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Conflict, + ErrorMessage = "Instance does not have current altinn task type information!", + }; + return false; + } + + state = new CurrentTaskIdAndAltinnTaskType(taskId, taskType); + return true; + } + + private sealed record CurrentTaskIdAndAltinnTaskType(string CurrentTaskId, string AltinnTaskType); } From 4602c0a5a651120ff7bd7dbdc6953e09f3559b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Fri, 4 Jul 2025 14:52:30 +0200 Subject: [PATCH 08/60] Move "move next" check below result check. --- src/Altinn.App.Api/Controllers/ProcessController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 8238ce193..e3812c23b 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -287,13 +287,13 @@ public async Task> ProcessNext( }; result = await _processEngine.ProcessNext(processNextRequest, ct); - moveToNextTaskAutomatically = IsServiceTask(instance); if (!result.Success) { return GetResultForError(result); } + moveToNextTaskAutomatically = IsServiceTask(instance); firstIteration = false; } while (moveToNextTaskAutomatically); From 2cd3e82665bd17b5dd03c7db239a2fa7197df904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Tue, 5 Aug 2025 11:56:53 +0200 Subject: [PATCH 09/60] Update PublicApi_ShouldNotChange_Unintentionally.verified.txt --- ...ouldNotChange_Unintentionally.verified.txt | 2 +- ...ouldNotChange_Unintentionally.verified.txt | 142 ++++++++++++------ 2 files changed, 97 insertions(+), 47 deletions(-) diff --git a/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index b95a715d8..f62e4057d 100644 --- a/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -474,7 +474,7 @@ namespace Altinn.App.Api.Controllers [Microsoft.AspNetCore.Mvc.ProducesResponseType(200)] [Microsoft.AspNetCore.Mvc.ProducesResponseType(404)] [Microsoft.AspNetCore.Mvc.ProducesResponseType(409)] - public System.Threading.Tasks.Task> NextElement([Microsoft.AspNetCore.Mvc.FromRoute] string org, [Microsoft.AspNetCore.Mvc.FromRoute] string app, [Microsoft.AspNetCore.Mvc.FromRoute] int instanceOwnerPartyId, [Microsoft.AspNetCore.Mvc.FromRoute] System.Guid instanceGuid, System.Threading.CancellationToken ct, [Microsoft.AspNetCore.Mvc.FromQuery] string? elementId = null, [Microsoft.AspNetCore.Mvc.FromQuery] string? language = null, [Microsoft.AspNetCore.Mvc.FromBody] Altinn.App.Api.Models.ProcessNext? processNext = null) { } + public System.Threading.Tasks.Task> ProcessNext([Microsoft.AspNetCore.Mvc.FromRoute] string org, [Microsoft.AspNetCore.Mvc.FromRoute] string app, [Microsoft.AspNetCore.Mvc.FromRoute] int instanceOwnerPartyId, [Microsoft.AspNetCore.Mvc.FromRoute] System.Guid instanceGuid, System.Threading.CancellationToken ct, [Microsoft.AspNetCore.Mvc.FromQuery] string? elementId = null, [Microsoft.AspNetCore.Mvc.FromQuery] string? language = null, [Microsoft.AspNetCore.Mvc.FromBody] Altinn.App.Api.Models.ProcessNext? processNext = null) { } [Microsoft.AspNetCore.Authorization.Authorize(Policy="InstanceInstantiate")] [Microsoft.AspNetCore.Mvc.HttpPost("start")] [Microsoft.AspNetCore.Mvc.ProducesResponseType(200)] diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 7f6af687f..4c777cf38 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3098,6 +3098,7 @@ namespace Altinn.App.Core.Internal.Pdf public interface IPdfService { System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct); + System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? fileNameTextResourceElementId, System.Threading.CancellationToken ct = default); System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct); System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, bool isPreview, System.Threading.CancellationToken ct); } @@ -3119,6 +3120,7 @@ namespace Altinn.App.Core.Internal.Pdf { public PdfService(Altinn.App.Core.Internal.App.IAppResources appResources, Altinn.App.Core.Internal.Data.IDataClient dataClient, Microsoft.AspNetCore.Http.IHttpContextAccessor httpContextAccessor, Altinn.App.Core.Internal.Pdf.IPdfGeneratorClient pdfGeneratorClient, Microsoft.Extensions.Options.IOptions pdfGeneratorSettings, Microsoft.Extensions.Options.IOptions generalSettings, Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Features.Auth.IAuthenticationContext authenticationContext, Altinn.App.Core.Features.Telemetry? telemetry = null) { } public System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct) { } + public System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? fileNameTextResourceElementId, System.Threading.CancellationToken ct = default) { } public System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct) { } public System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, bool isPreview, System.Threading.CancellationToken ct) { } } @@ -3199,6 +3201,12 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties [System.Xml.Serialization.XmlElement("paymentReceiptPdfDataType", Namespace="http://altinn.no/process")] public string? PaymentReceiptPdfDataType { get; set; } } + public class AltinnPdfConfiguration + { + public AltinnPdfConfiguration() { } + [System.Xml.Serialization.XmlElement("filename", Namespace="http://altinn.no/process")] + public string? Filename { get; set; } + } public class AltinnSignatureConfiguration { public AltinnSignatureConfiguration() { } @@ -3229,6 +3237,8 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties public System.Collections.Generic.List? AltinnActions { get; set; } [System.Xml.Serialization.XmlElement("paymentConfig", Namespace="http://altinn.no/process")] public Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties.AltinnPaymentConfiguration? PaymentConfiguration { get; set; } + [System.Xml.Serialization.XmlElement("pdfConfig", Namespace="http://altinn.no/process")] + public Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties.AltinnPdfConfiguration? PdfConfiguration { get; set; } [System.Xml.Serialization.XmlElement("signatureConfig", Namespace="http://altinn.no/process")] public Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties.AltinnSignatureConfiguration? SignatureConfiguration { get; set; } [System.Xml.Serialization.XmlElement("taskType", Namespace="http://altinn.no/process")] @@ -3243,6 +3253,8 @@ namespace Altinn.App.Core.Internal.Process.Elements public AppProcessElementInfo(Altinn.Platform.Storage.Interface.Models.ProcessElementInfo processElementInfo) { } [System.Text.Json.Serialization.JsonPropertyName("actions")] public System.Collections.Generic.Dictionary? Actions { get; set; } + [System.Text.Json.Serialization.JsonPropertyName("elementType")] + public string? ElementType { get; set; } [System.Text.Json.Serialization.JsonPropertyName("read")] public bool HasReadAccess { get; set; } [System.Text.Json.Serialization.JsonPropertyName("write")] @@ -3264,6 +3276,8 @@ namespace Altinn.App.Core.Internal.Process.Elements public string? AltinnTaskType { get; set; } [System.Text.Json.Serialization.JsonPropertyName("elementId")] public string? ElementId { get; set; } + [System.Text.Json.Serialization.JsonPropertyName("elementType")] + public string? ElementType { get; set; } } [System.Xml.Serialization.XmlRoot("definitions", Namespace="http://www.omg.org/spec/BPMN/20100524/MODEL")] [System.Xml.Serialization.XmlType(Namespace="http://www.omg.org/spec/BPMN/20100524/MODEL")] @@ -3312,6 +3326,8 @@ namespace Altinn.App.Core.Internal.Process.Elements public bool IsExecutable { get; set; } [System.Xml.Serialization.XmlElement("sequenceFlow")] public System.Collections.Generic.List SequenceFlow { get; set; } + [System.Xml.Serialization.XmlElement("serviceTask")] + public System.Collections.Generic.List ServiceTasks { get; set; } [System.Xml.Serialization.XmlElement("startEvent")] public System.Collections.Generic.List StartEvents { get; set; } [System.Xml.Serialization.XmlElement("task")] @@ -3338,6 +3354,11 @@ namespace Altinn.App.Core.Internal.Process.Elements [System.Xml.Serialization.XmlAttribute("targetRef")] public string TargetRef { get; set; } } + public class ServiceTask : Altinn.App.Core.Internal.Process.Elements.ProcessTask + { + public ServiceTask() { } + public override string ElementType() { } + } public class StartEvent : Altinn.App.Core.Internal.Process.Elements.Base.ProcessElement { public StartEvent() { } @@ -3392,7 +3413,7 @@ namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask } public class EndTaskEventHandler : Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask.IEndTaskEventHandler { - public EndTaskEventHandler(Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskDataLocker processTaskDataLocker, Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskFinalizer processTaskFinisher, System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILogger logger) { } + public EndTaskEventHandler(Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskDataLocker processTaskDataLocker, Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskFinalizer processTaskFinisher, System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILogger logger) { } public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask processTask, string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } } public interface IAbandonTaskEventHandler @@ -3409,7 +3430,7 @@ namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask } public class StartTaskEventHandler : Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask.IStartTaskEventHandler { - public StartTaskEventHandler(Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskDataLocker processTaskDataLocker, Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskInitializer processTaskInitializer, System.IServiceProvider serviceProvider) { } + public StartTaskEventHandler(Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskDataLocker processTaskDataLocker, Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskInitializer processTaskInitializer, System.IServiceProvider serviceProvider) { } public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask processTask, string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill) { } } } @@ -3434,10 +3455,10 @@ namespace Altinn.App.Core.Internal.Process } public interface IProcessEngine { + Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.IServiceTask? CheckIfServiceTask(string? altinnTaskType); System.Threading.Tasks.Task GenerateProcessStartEvents(Altinn.App.Core.Models.Process.ProcessStartRequest processStartRequest); System.Threading.Tasks.Task HandleEventsAndUpdateStorage(Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill, System.Collections.Generic.List? events); - System.Threading.Tasks.Task HandleUserAction(Altinn.App.Core.Models.Process.ProcessNextRequest request, System.Threading.CancellationToken ct); - System.Threading.Tasks.Task Next(Altinn.App.Core.Models.Process.ProcessNextRequest request); + System.Threading.Tasks.Task ProcessNext(Altinn.App.Core.Models.Process.ProcessNextRequest request, System.Threading.CancellationToken ct = default); } public interface IProcessEngineAuthorizer { @@ -3479,11 +3500,11 @@ namespace Altinn.App.Core.Internal.Process } public class ProcessEngine : Altinn.App.Core.Internal.Process.IProcessEngine { - public ProcessEngine(Altinn.App.Core.Internal.Process.IProcessReader processReader, Altinn.App.Core.Internal.Process.IProcessNavigator processNavigator, Altinn.App.Core.Internal.Process.IProcessEventHandlerDelegator processEventsDelegator, Altinn.App.Core.Internal.Process.IProcessEventDispatcher processEventDispatcher, Altinn.App.Core.Features.Action.UserActionService userActionService, Altinn.App.Core.Features.Auth.IAuthenticationContext authenticationContext, System.IServiceProvider serviceProvider, Altinn.App.Core.Features.Telemetry? telemetry = null) { } + public ProcessEngine(Altinn.App.Core.Internal.Process.IProcessReader processReader, Altinn.App.Core.Internal.Process.IProcessNavigator processNavigator, Altinn.App.Core.Internal.Process.IProcessEventHandlerDelegator processEventsDelegator, Altinn.App.Core.Internal.Process.IProcessEventDispatcher processEventDispatcher, Altinn.App.Core.Features.Action.UserActionService userActionService, Altinn.App.Core.Features.Auth.IAuthenticationContext authenticationContext, System.IServiceProvider serviceProvider, Altinn.App.Core.Internal.Process.IProcessEngineAuthorizer processEngineAuthorizer, Altinn.App.Core.Internal.Validation.IValidationService validationService, Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Features.Telemetry? telemetry = null) { } + public Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.IServiceTask? CheckIfServiceTask(string? altinnTaskType) { } public System.Threading.Tasks.Task GenerateProcessStartEvents(Altinn.App.Core.Models.Process.ProcessStartRequest processStartRequest) { } public System.Threading.Tasks.Task HandleEventsAndUpdateStorage(Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill, System.Collections.Generic.List? events) { } - public System.Threading.Tasks.Task HandleUserAction(Altinn.App.Core.Models.Process.ProcessNextRequest request, System.Threading.CancellationToken ct) { } - public System.Threading.Tasks.Task Next(Altinn.App.Core.Models.Process.ProcessNextRequest request) { } + public System.Threading.Tasks.Task ProcessNext(Altinn.App.Core.Models.Process.ProcessNextRequest request, System.Threading.CancellationToken ct = default) { } } public class ProcessEventDispatcher : Altinn.App.Core.Internal.Process.IProcessEventDispatcher { @@ -3535,6 +3556,42 @@ namespace Altinn.App.Core.Internal.Process AbandonCurrentMoveToNext = 2, } } +namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common +{ + public interface IProcessTaskCleaner + { + System.Threading.Tasks.Task RemoveAllDataElementsGeneratedFromTask(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId); + } + public interface IProcessTaskDataLocker + { + System.Threading.Tasks.Task Lock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); + System.Threading.Tasks.Task Unlock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); + } + public interface IProcessTaskFinalizer + { + System.Threading.Tasks.Task Finalize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); + } + public interface IProcessTaskInitializer + { + System.Threading.Tasks.Task Initialize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill); + } + public class ProcessTaskDataLocker : Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskDataLocker + { + public ProcessTaskDataLocker(Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient) { } + public System.Threading.Tasks.Task Lock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } + public System.Threading.Tasks.Task Unlock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } + } + public class ProcessTaskFinalizer : Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskFinalizer + { + public ProcessTaskFinalizer(Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.AppModel.IAppModel appModel, Altinn.App.Core.Internal.Expressions.ILayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, System.IServiceProvider serviceProvider, Microsoft.Extensions.Options.IOptions appSettings) { } + public System.Threading.Tasks.Task Finalize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } + } + public class ProcessTaskInitializer : Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskInitializer + { + public ProcessTaskInitializer(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient, Altinn.App.Core.Internal.Prefill.IPrefill prefillService, Altinn.App.Core.Internal.AppModel.IAppModel appModel, System.IServiceProvider serviceProvider, Altinn.App.Core.Internal.Instances.IInstanceClient instanceClient, Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskCleaner processTaskCleaner) { } + public System.Threading.Tasks.Task Initialize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill) { } + } +} namespace Altinn.App.Core.Internal.Process.ProcessTasks { public class ConfirmationProcessTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask @@ -3568,23 +3625,6 @@ namespace Altinn.App.Core.Internal.Process.ProcessTasks System.Threading.Tasks.Task End(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); System.Threading.Tasks.Task Start(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); } - public interface IProcessTaskCleaner - { - System.Threading.Tasks.Task RemoveAllDataElementsGeneratedFromTask(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId); - } - public interface IProcessTaskDataLocker - { - System.Threading.Tasks.Task Lock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); - System.Threading.Tasks.Task Unlock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); - } - public interface IProcessTaskFinalizer - { - System.Threading.Tasks.Task Finalize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); - } - public interface IProcessTaskInitializer - { - System.Threading.Tasks.Task Initialize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill); - } public class NullTypeProcessTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask { public NullTypeProcessTask() { } @@ -3593,38 +3633,46 @@ namespace Altinn.App.Core.Internal.Process.ProcessTasks public System.Threading.Tasks.Task End(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } public System.Threading.Tasks.Task Start(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } } - public class ProcessTaskDataLocker : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskDataLocker +} +namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks +{ + public class EFormidlingServiceTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask, Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.IServiceTask { - public ProcessTaskDataLocker(Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient) { } - public System.Threading.Tasks.Task Lock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } - public System.Threading.Tasks.Task Unlock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } + public EFormidlingServiceTask(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.EFormidling.Interface.IEFormidlingService? eFormidlingService = null, Microsoft.Extensions.Options.IOptions? appSettings = null) { } + public string Type { get; } + public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskParameters parameters) { } } - public class ProcessTaskFinalizer : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskFinalizer + public interface IServiceTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask { - public ProcessTaskFinalizer(Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.AppModel.IAppModel appModel, Altinn.App.Core.Internal.Expressions.ILayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, System.IServiceProvider serviceProvider, Microsoft.Extensions.Options.IOptions appSettings) { } - public System.Threading.Tasks.Task Finalize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } + System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskParameters parameters); } - public class ProcessTaskInitializer : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskInitializer + public class PdfServiceTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask, Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.IServiceTask { - public ProcessTaskInitializer(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient, Altinn.App.Core.Internal.Prefill.IPrefill prefillService, Altinn.App.Core.Internal.AppModel.IAppModel appModel, System.IServiceProvider serviceProvider, Altinn.App.Core.Internal.Instances.IInstanceClient instanceClient, Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskCleaner processTaskCleaner) { } - public System.Threading.Tasks.Task Initialize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill) { } + public PdfServiceTask(Altinn.App.Core.Internal.Pdf.IPdfService pdfService, Altinn.App.Core.Internal.Process.IProcessReader processReader, Microsoft.Extensions.Logging.ILogger logger) { } + public string Type { get; } + public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskParameters parameters) { } } -} -namespace Altinn.App.Core.Internal.Process.ServiceTasks -{ - public class EformidlingServiceTask : Altinn.App.Core.Internal.Process.ServiceTasks.IServiceTask + public sealed class ServiceTaskFailedResult : Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskResult + { + public ServiceTaskFailedResult() { } + public required string ErrorMessage { get; init; } + public required string ErrorTitle { get; init; } + public required Altinn.App.Core.Models.Process.ProcessErrorType ErrorType { get; init; } + public Altinn.App.Core.Models.Process.ProcessChangeResult ToProcessChangeResult() { } + } + public sealed class ServiceTaskParameters : System.IEquatable { - public EformidlingServiceTask(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Instances.IInstanceClient instanceClient, Altinn.App.Core.EFormidling.Interface.IEFormidlingService? eFormidlingService = null, Microsoft.Extensions.Options.IOptions? appSettings = null) { } - public System.Threading.Tasks.Task Execute(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } + public ServiceTaskParameters() { } + public System.Threading.CancellationToken CancellationToken { get; init; } + public required Altinn.App.Core.Features.IInstanceDataMutator InstanceDataMutator { get; init; } } - public interface IServiceTask + public abstract class ServiceTaskResult { - System.Threading.Tasks.Task Execute(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); + protected ServiceTaskResult() { } } - public class PdfServiceTask : Altinn.App.Core.Internal.Process.ServiceTasks.IServiceTask + public sealed class ServiceTaskSuccessResult : Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskResult { - public PdfServiceTask(Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Pdf.IPdfService pdfService) { } - public System.Threading.Tasks.Task Execute(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } + public ServiceTaskSuccessResult() { } } } namespace Altinn.App.Core.Internal.Profile @@ -4506,6 +4554,7 @@ namespace Altinn.App.Core.Models.Process { public ProcessChangeResult() { } public string? ErrorMessage { get; init; } + public string? ErrorTitle { get; set; } public Altinn.App.Core.Models.Process.ProcessErrorType? ErrorType { get; init; } public Altinn.App.Core.Models.Process.ProcessStateChange? ProcessStateChange { get; init; } [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, new string?[]?[] { @@ -4521,6 +4570,7 @@ namespace Altinn.App.Core.Models.Process "ErrorType"})] [set: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ProcessStateChange")] public bool Success { get; init; } + public System.Collections.Generic.List? ValidationIssues { get; set; } } public enum ProcessErrorType { @@ -4535,7 +4585,7 @@ namespace Altinn.App.Core.Models.Process public string? Action { get; set; } public string? DataTypeId { get; set; } } - public class ProcessNextRequest + public sealed class ProcessNextRequest : System.IEquatable { public ProcessNextRequest() { } public required string? Action { get; init; } From 9fef45abcc46e0591fc8c34006e72f0d285924a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 6 Aug 2025 10:13:46 +0200 Subject: [PATCH 10/60] IServiceTask: Rename from params to context. Remove detailed error fields from result class and instead return generic failed ProcessChangeResult. --- .../Internal/Process/ProcessEngine.cs | 56 +++++++++++++------ .../ServiceTasks/EFormidlingServiceTask.cs | 6 +- .../ProcessTasks/ServiceTasks/IServiceTask.cs | 39 ++----------- .../ServiceTasks/PdfServiceTask.cs | 8 +-- .../EFormidlingServiceTaskTests.cs | 6 +- .../ServiceTasks/PdfServiceTaskTests.cs | 2 +- ...ouldNotChange_Unintentionally.verified.txt | 22 +++----- 7 files changed, 63 insertions(+), 76 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index f964ec192..74ceb8997 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -192,7 +192,7 @@ out ProcessChangeResult? invalidProcessStateError if (serviceTask is not null) { isServiceTask = true; - ServiceTaskResult serviceActionResult = await HandleServiceTask( + ProcessChangeResult serviceTaskProcessChangeResult = await HandleServiceTask( instance, serviceTask, request with @@ -202,11 +202,10 @@ request with ct ); - if (serviceActionResult is ServiceTaskFailedResult failedServiceTask) + if (!serviceTaskProcessChangeResult.Success) { - ProcessChangeResult result = failedServiceTask.ToProcessChangeResult(); - activity?.SetProcessChangeResult(result); - return result; + activity?.SetProcessChangeResult(serviceTaskProcessChangeResult); + return serviceTaskProcessChangeResult; } } else @@ -378,7 +377,7 @@ CancellationToken ct return actionResult; } - private async Task HandleServiceTask( + private async Task HandleServiceTask( Instance instance, IServiceTask serviceTask, ProcessNextRequest request, @@ -389,7 +388,7 @@ private async Task HandleServiceTask( if (request.Action is not "write" && request.Action != serviceTask.Type) // serviceTask.Type is accepted to support custom service task types { - var result = new ServiceTaskFailedResult() + var result = new ProcessChangeResult() { ErrorTitle = "User action not supported!", ErrorMessage = @@ -402,24 +401,47 @@ private async Task HandleServiceTask( try { - ServiceTaskParameters parameters = new() + InstanceDataUnitOfWork cachedDataMutator = await _instanceDataUnitOfWorkInitializer.Init( + instance, + instance.Process?.CurrentTask?.ElementId, + request.Language + ); + + ServiceTaskContext context = new() { InstanceDataMutator = cachedDataMutator, CancellationToken = ct }; + + ServiceTaskResult result = await serviceTask.Execute(context); + + if (result is ServiceTaskFailedResult) { - InstanceDataMutator = await _instanceDataUnitOfWorkInitializer.Init( - instance, - instance.Process?.CurrentTask?.ElementId, - request.Language - ), - CancellationToken = ct, - }; + return new ProcessChangeResult() + { + Success = false, + ErrorTitle = "Service task failed!", + ErrorMessage = $"Service task {serviceTask.Type} returned a failed result!", + ErrorType = ProcessErrorType.Internal, + }; + } - return await serviceTask.Execute(parameters); + if (cachedDataMutator.HasAbandonIssues) + { + throw new Exception( + "Abandon issues found in data elements. Abandon issues should be handled by the service task." + ); + } + + DataElementChanges changes = cachedDataMutator.GetDataElementChanges(initializeAltinnRowId: false); + await cachedDataMutator.UpdateInstanceData(changes); + await cachedDataMutator.SaveChanges(changes); + + return new ProcessChangeResult { Success = true }; } catch (Exception ex) { activity?.Errored(ex); - return new ServiceTaskFailedResult() + return new ProcessChangeResult() { + Success = false, ErrorTitle = "Service task failed!", ErrorMessage = $"Service task {serviceTask.Type} failed with an exception!", ErrorType = ProcessErrorType.Internal, diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index 8ab00aaa2..a72ec07ad 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -35,10 +35,10 @@ public EFormidlingServiceTask( public string Type => "eFormidling"; /// - public async Task Execute(ServiceTaskParameters parameters) + public async Task Execute(ServiceTaskContext context) { - string taskId = parameters.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; - Instance instance = parameters.InstanceDataMutator.Instance; + string taskId = context.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; + Instance instance = context.InstanceDataMutator.Instance; if (_appSettings?.Value.EnableEFormidling is false) { diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs index 958eddde1..3a3ef88bb 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs @@ -1,5 +1,4 @@ using Altinn.App.Core.Features; -using Altinn.App.Core.Models.Process; namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; @@ -12,13 +11,13 @@ public interface IServiceTask : IProcessTask /// /// Executes the service task. /// - public Task Execute(ServiceTaskParameters parameters); + public Task Execute(ServiceTaskContext context); } /// /// This class represents the parameters for executing a service task. /// -public sealed record ServiceTaskParameters +public sealed record ServiceTaskContext { /// /// An instance data mutator that can be used to read and modify the instance data during the service task execution. @@ -39,39 +38,9 @@ public abstract class ServiceTaskResult { } /// /// This class represents a successful result of executing a service task. /// -public sealed class ServiceTaskSuccessResult : ServiceTaskResult { } +public sealed class ServiceTaskSuccessResult : ServiceTaskResult; /// /// This class represents a failed result of executing a service task. /// -public sealed class ServiceTaskFailedResult : ServiceTaskResult -{ - /// - /// Gets or sets the error title if the service task execution failed. - /// - public required string ErrorTitle { get; init; } - - /// - /// Gets or sets the error message if the service task execution failed. - /// - public required string ErrorMessage { get; init; } - - /// - /// Gets or sets the error type if the service task execution failed. - /// - public required ProcessErrorType ErrorType { get; init; } - - /// - /// Converts the service task failed result to an unsuccessful process change result. - /// - public ProcessChangeResult ToProcessChangeResult() - { - return new ProcessChangeResult - { - Success = false, - ErrorTitle = ErrorTitle, - ErrorMessage = ErrorMessage, - ErrorType = ErrorType, - }; - } -} +public sealed class ServiceTaskFailedResult : ServiceTaskResult; diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs index fd8f602a6..4e4c89ed4 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -30,15 +30,15 @@ public PdfServiceTask(IPdfService pdfService, IProcessReader processReader, ILog public string Type => "pdf"; /// - public async Task Execute(ServiceTaskParameters parameters) + public async Task Execute(ServiceTaskContext context) { - string taskId = parameters.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; - Instance instance = parameters.InstanceDataMutator.Instance; + string taskId = context.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; + Instance instance = context.InstanceDataMutator.Instance; _logger.LogDebug("Calling PdfService for PDF Service Task {TaskId}.", taskId); ValidAltinnPdfConfiguration config = GetValidAltinnPdfConfiguration(taskId); - await _pdfService.GenerateAndStorePdf(instance, taskId, config.Filename, parameters.CancellationToken); + await _pdfService.GenerateAndStorePdf(instance, taskId, config.Filename, context.CancellationToken); _logger.LogDebug("Successfully called PdfService for PDF Service Task {TaskId}.", taskId); 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 08f9e0b1d..e28c68ee4 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs @@ -38,7 +38,7 @@ public async Task Execute_Should_LogWarning_When_EFormidlingDisabled() var instanceMutatorMock = new Mock(); instanceMutatorMock.Setup(x => x.Instance).Returns(instance); - var parameters = new ServiceTaskParameters { InstanceDataMutator = instanceMutatorMock.Object }; + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; // Act await _serviceTask.Execute(parameters); @@ -77,7 +77,7 @@ public async Task Execute_Should_ThrowException_When_EFormidlingServiceIsNull() var instanceMutatorMock = new Mock(); instanceMutatorMock.Setup(x => x.Instance).Returns(instance); - var parameters = new ServiceTaskParameters { InstanceDataMutator = instanceMutatorMock.Object }; + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; // Act & Assert await Assert.ThrowsAsync(() => serviceTask.Execute(parameters)); @@ -95,7 +95,7 @@ public async Task Execute_Should_Call_SendEFormidlingShipment_When_EFormidlingEn var instanceMutatorMock = new Mock(); instanceMutatorMock.Setup(x => x.Instance).Returns(instance); - var parameters = new ServiceTaskParameters { InstanceDataMutator = instanceMutatorMock.Object }; + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; // Act await _serviceTask.Execute(parameters); 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 15ce48704..3b30831ea 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs @@ -45,7 +45,7 @@ public async Task Execute_Should_Call_GenerateAndStorePdf() var instanceMutatorMock = new Mock(); instanceMutatorMock.Setup(x => x.Instance).Returns(instance); - var parameters = new ServiceTaskParameters { InstanceDataMutator = instanceMutatorMock.Object }; + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; // Act await _serviceTask.Execute(parameters); diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 4c777cf38..d20919970 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3640,32 +3640,28 @@ namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks { public EFormidlingServiceTask(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.EFormidling.Interface.IEFormidlingService? eFormidlingService = null, Microsoft.Extensions.Options.IOptions? appSettings = null) { } public string Type { get; } - public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskParameters parameters) { } + public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskContext context) { } } public interface IServiceTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask { - System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskParameters parameters); + System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskContext context); } public class PdfServiceTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask, Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.IServiceTask { public PdfServiceTask(Altinn.App.Core.Internal.Pdf.IPdfService pdfService, Altinn.App.Core.Internal.Process.IProcessReader processReader, Microsoft.Extensions.Logging.ILogger logger) { } public string Type { get; } - public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskParameters parameters) { } + public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskContext context) { } } - public sealed class ServiceTaskFailedResult : Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskResult - { - public ServiceTaskFailedResult() { } - public required string ErrorMessage { get; init; } - public required string ErrorTitle { get; init; } - public required Altinn.App.Core.Models.Process.ProcessErrorType ErrorType { get; init; } - public Altinn.App.Core.Models.Process.ProcessChangeResult ToProcessChangeResult() { } - } - public sealed class ServiceTaskParameters : System.IEquatable + public sealed class ServiceTaskContext : System.IEquatable { - public ServiceTaskParameters() { } + public ServiceTaskContext() { } public System.Threading.CancellationToken CancellationToken { get; init; } public required Altinn.App.Core.Features.IInstanceDataMutator InstanceDataMutator { get; init; } } + public sealed class ServiceTaskFailedResult : Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskResult + { + public ServiceTaskFailedResult() { } + } public abstract class ServiceTaskResult { protected ServiceTaskResult() { } From 1632b82f2867af61a96f95246edd8f59196c6e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 6 Aug 2025 10:19:24 +0200 Subject: [PATCH 11/60] Remove ElementType again, as frontend didn't end up using it. --- src/Altinn.App.Api/Controllers/ProcessController.cs | 2 -- .../Internal/Process/Elements/AppProcessElementInfo.cs | 6 ------ .../Internal/Process/Elements/AppProcessTaskTypeInfo.cs | 6 ------ ...enApiSpecChangeDetection.SaveJsonSwagger.verified.json | 8 -------- ...PublicApi_ShouldNotChange_Unintentionally.verified.txt | 4 ---- 5 files changed, 26 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index e3812c23b..0ead63e36 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -496,7 +496,6 @@ private async Task ConvertAndAuthorizeActions(Instance instance appProcessState.CurrentTask.HasReadAccess = authDecisions.Single(a => a.Id == "read").Authorized; appProcessState.CurrentTask.HasWriteAccess = authDecisions.Single(a => a.Id == "write").Authorized; appProcessState.CurrentTask.UserActions = authDecisions; - appProcessState.CurrentTask.ElementType = processTask.ElementType(); } } @@ -508,7 +507,6 @@ private async Task ConvertAndAuthorizeActions(Instance instance { ElementId = processElement.Id, AltinnTaskType = processElement.ExtensionElements?.TaskExtension?.TaskType, - ElementType = processElement.ElementType(), } ); } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs index 2aa96e0c1..2d904e364 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs @@ -60,10 +60,4 @@ public AppProcessElementInfo(ProcessElementInfo processElementInfo) /// [JsonPropertyName(name: "write")] public bool HasWriteAccess { get; set; } - - /// - /// The type of the element, e.g. "task", "serviceTask". - /// - [JsonPropertyName(name: "elementType")] - public string? ElementType { get; set; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs index 57740fbfb..8ba56aeb2 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs @@ -20,10 +20,4 @@ public class AppProcessTaskTypeInfo /// [JsonPropertyName(name: "elementId")] public string? ElementId { get; set; } - - /// - /// The type of the element, e.g. "task", "serviceTask". - /// - [JsonPropertyName(name: "elementType")] - public string? ElementType { get; set; } } diff --git a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json index 57c701321..e5c3e528c 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json +++ b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json @@ -6931,10 +6931,6 @@ }, "write": { "type": "boolean" - }, - "elementType": { - "type": "string", - "nullable": true } }, "additionalProperties": false @@ -6983,10 +6979,6 @@ "elementId": { "type": "string", "nullable": true - }, - "elementType": { - "type": "string", - "nullable": true } }, "additionalProperties": false diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index d20919970..73e78e9a2 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3253,8 +3253,6 @@ namespace Altinn.App.Core.Internal.Process.Elements public AppProcessElementInfo(Altinn.Platform.Storage.Interface.Models.ProcessElementInfo processElementInfo) { } [System.Text.Json.Serialization.JsonPropertyName("actions")] public System.Collections.Generic.Dictionary? Actions { get; set; } - [System.Text.Json.Serialization.JsonPropertyName("elementType")] - public string? ElementType { get; set; } [System.Text.Json.Serialization.JsonPropertyName("read")] public bool HasReadAccess { get; set; } [System.Text.Json.Serialization.JsonPropertyName("write")] @@ -3276,8 +3274,6 @@ namespace Altinn.App.Core.Internal.Process.Elements public string? AltinnTaskType { get; set; } [System.Text.Json.Serialization.JsonPropertyName("elementId")] public string? ElementId { get; set; } - [System.Text.Json.Serialization.JsonPropertyName("elementType")] - public string? ElementType { get; set; } } [System.Xml.Serialization.XmlRoot("definitions", Namespace="http://www.omg.org/spec/BPMN/20100524/MODEL")] [System.Xml.Serialization.XmlType(Namespace="http://www.omg.org/spec/BPMN/20100524/MODEL")] From 6dba9549137bfa8cbf33d4fdc9903ce5b442dccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 6 Aug 2025 10:23:26 +0200 Subject: [PATCH 12/60] Revert adding Common namespace within ProcessTasks namespace. --- .../Extensions/ServiceCollectionExtensions.cs | 1 - .../ProcessTask/EndTaskEventHandler.cs | 1 - .../ProcessTask/StartTaskEventHandler.cs | 1 - .../Common/IProcessTaskCleaner.cs | 2 +- .../Common/IProcessTaskDataLocker.cs | 2 +- .../Common/IProcessTaskFinalizer.cs | 2 +- .../Common/IProcessTaskInitializer.cs | 2 +- .../ProcessTasks/Common/ProcessTaskCleaner.cs | 2 +- .../Common/ProcessTaskDataLocker.cs | 2 +- .../Common/ProcessTaskFinalizer.cs | 2 +- .../Common/ProcessTaskInitializer.cs | 2 +- .../ProcessTask/EndTaskEventHandlerTests.cs | 1 - .../ProcessTask/StartTaskEventHandlerTests.cs | 1 - .../Common/ProcessTaskDataLockerTests.cs | 1 - .../Common/ProcessTaskFinalizerTests.cs | 2 +- ...ouldNotChange_Unintentionally.verified.txt | 73 +++++++++---------- 16 files changed, 44 insertions(+), 53 deletions(-) diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index c81f39925..5c83158e9 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -47,7 +47,6 @@ using Altinn.App.Core.Internal.Process.EventHandlers; using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; using Altinn.App.Core.Internal.Process.ProcessTasks; -using Altinn.App.Core.Internal.Process.ProcessTasks.Common; using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; using Altinn.App.Core.Internal.Registers; 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 ef5cb86d8..cca3c39e2 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs @@ -1,6 +1,5 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.ProcessTasks; -using Altinn.App.Core.Internal.Process.ProcessTasks.Common; using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; 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 b089216ea..6fe6b5200 100644 --- a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs @@ -1,6 +1,5 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.ProcessTasks; -using Altinn.App.Core.Internal.Process.ProcessTasks.Common; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskCleaner.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskCleaner.cs index 88f36a27e..4a6d92994 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskCleaner.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskCleaner.cs @@ -1,6 +1,6 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; +namespace Altinn.App.Core.Internal.Process.ProcessTasks; /// /// Contains common logic to clean up process data diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskDataLocker.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskDataLocker.cs index a2a385c22..c574fbcf2 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskDataLocker.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskDataLocker.cs @@ -1,6 +1,6 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; +namespace Altinn.App.Core.Internal.Process.ProcessTasks; /// /// Can be used to lock data elements connected to a process task diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskFinalizer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskFinalizer.cs index f67e0f955..1f1268c05 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskFinalizer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskFinalizer.cs @@ -1,6 +1,6 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; +namespace Altinn.App.Core.Internal.Process.ProcessTasks; /// /// Contains common logic for ending a process task. diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskInitializer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskInitializer.cs index 50c6cdfd5..7eb59e3ac 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskInitializer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/IProcessTaskInitializer.cs @@ -1,6 +1,6 @@ using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; +namespace Altinn.App.Core.Internal.Process.ProcessTasks; /// /// Contains common logic for initializing a process task. diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs index f4cc894f7..f78cbedbb 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs @@ -4,7 +4,7 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; -namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; +namespace Altinn.App.Core.Internal.Process.ProcessTasks; internal sealed class ProcessTaskCleaner : IProcessTaskCleaner { diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskDataLocker.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskDataLocker.cs index c7a77cd1d..2e0ad99f5 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskDataLocker.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskDataLocker.cs @@ -3,7 +3,7 @@ using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; +namespace Altinn.App.Core.Internal.Process.ProcessTasks; /// public class ProcessTaskDataLocker : IProcessTaskDataLocker 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 d8e2f709d..753c7376c 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; +namespace Altinn.App.Core.Internal.Process.ProcessTasks; /// public class ProcessTaskFinalizer : IProcessTaskFinalizer 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 c939a93cd..4ce9b5de3 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common; +namespace Altinn.App.Core.Internal.Process.ProcessTasks; /// public class ProcessTaskInitializer : IProcessTaskInitializer 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 5bb0893e7..8df4477ed 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 @@ -1,7 +1,6 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; using Altinn.App.Core.Internal.Process.ProcessTasks; -using Altinn.App.Core.Internal.Process.ProcessTasks.Common; using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; 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 24bd1cc85..b1e19e704 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 @@ -1,7 +1,6 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; using Altinn.App.Core.Internal.Process.ProcessTasks; -using Altinn.App.Core.Internal.Process.ProcessTasks.Common; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; 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 f9e87278f..9ef9304ef 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 @@ -2,7 +2,6 @@ using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Process.ProcessTasks; -using Altinn.App.Core.Internal.Process.ProcessTasks.Common; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Moq; 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 288deed9a..5202b59b7 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 @@ -5,7 +5,7 @@ using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Internal.Instances; -using Altinn.App.Core.Internal.Process.ProcessTasks.Common; +using Altinn.App.Core.Internal.Process.ProcessTasks; using Altinn.App.Core.Internal.Texts; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Enums; diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 73e78e9a2..2c041c65a 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3409,7 +3409,7 @@ namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask } public class EndTaskEventHandler : Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask.IEndTaskEventHandler { - public EndTaskEventHandler(Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskDataLocker processTaskDataLocker, Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskFinalizer processTaskFinisher, System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILogger logger) { } + public EndTaskEventHandler(Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskDataLocker processTaskDataLocker, Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskFinalizer processTaskFinisher, System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILogger logger) { } public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask processTask, string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } } public interface IAbandonTaskEventHandler @@ -3426,7 +3426,7 @@ namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask } public class StartTaskEventHandler : Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask.IStartTaskEventHandler { - public StartTaskEventHandler(Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskDataLocker processTaskDataLocker, Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskInitializer processTaskInitializer, System.IServiceProvider serviceProvider) { } + public StartTaskEventHandler(Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskDataLocker processTaskDataLocker, Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskInitializer processTaskInitializer, System.IServiceProvider serviceProvider) { } public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask processTask, string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill) { } } } @@ -3552,42 +3552,6 @@ namespace Altinn.App.Core.Internal.Process AbandonCurrentMoveToNext = 2, } } -namespace Altinn.App.Core.Internal.Process.ProcessTasks.Common -{ - public interface IProcessTaskCleaner - { - System.Threading.Tasks.Task RemoveAllDataElementsGeneratedFromTask(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId); - } - public interface IProcessTaskDataLocker - { - System.Threading.Tasks.Task Lock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); - System.Threading.Tasks.Task Unlock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); - } - public interface IProcessTaskFinalizer - { - System.Threading.Tasks.Task Finalize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); - } - public interface IProcessTaskInitializer - { - System.Threading.Tasks.Task Initialize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill); - } - public class ProcessTaskDataLocker : Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskDataLocker - { - public ProcessTaskDataLocker(Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient) { } - public System.Threading.Tasks.Task Lock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } - public System.Threading.Tasks.Task Unlock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } - } - public class ProcessTaskFinalizer : Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskFinalizer - { - public ProcessTaskFinalizer(Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.AppModel.IAppModel appModel, Altinn.App.Core.Internal.Expressions.ILayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, System.IServiceProvider serviceProvider, Microsoft.Extensions.Options.IOptions appSettings) { } - public System.Threading.Tasks.Task Finalize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } - } - public class ProcessTaskInitializer : Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskInitializer - { - public ProcessTaskInitializer(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient, Altinn.App.Core.Internal.Prefill.IPrefill prefillService, Altinn.App.Core.Internal.AppModel.IAppModel appModel, System.IServiceProvider serviceProvider, Altinn.App.Core.Internal.Instances.IInstanceClient instanceClient, Altinn.App.Core.Internal.Process.ProcessTasks.Common.IProcessTaskCleaner processTaskCleaner) { } - public System.Threading.Tasks.Task Initialize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill) { } - } -} namespace Altinn.App.Core.Internal.Process.ProcessTasks { public class ConfirmationProcessTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask @@ -3621,6 +3585,23 @@ namespace Altinn.App.Core.Internal.Process.ProcessTasks System.Threading.Tasks.Task End(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); System.Threading.Tasks.Task Start(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); } + public interface IProcessTaskCleaner + { + System.Threading.Tasks.Task RemoveAllDataElementsGeneratedFromTask(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId); + } + public interface IProcessTaskDataLocker + { + System.Threading.Tasks.Task Lock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); + System.Threading.Tasks.Task Unlock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); + } + public interface IProcessTaskFinalizer + { + System.Threading.Tasks.Task Finalize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); + } + public interface IProcessTaskInitializer + { + System.Threading.Tasks.Task Initialize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill); + } public class NullTypeProcessTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask { public NullTypeProcessTask() { } @@ -3629,6 +3610,22 @@ namespace Altinn.App.Core.Internal.Process.ProcessTasks public System.Threading.Tasks.Task End(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } public System.Threading.Tasks.Task Start(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } } + public class ProcessTaskDataLocker : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskDataLocker + { + public ProcessTaskDataLocker(Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient) { } + public System.Threading.Tasks.Task Lock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } + public System.Threading.Tasks.Task Unlock(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } + } + public class ProcessTaskFinalizer : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskFinalizer + { + public ProcessTaskFinalizer(Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.AppModel.IAppModel appModel, Altinn.App.Core.Internal.Expressions.ILayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, System.IServiceProvider serviceProvider, Microsoft.Extensions.Options.IOptions appSettings) { } + public System.Threading.Tasks.Task Finalize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance) { } + } + public class ProcessTaskInitializer : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskInitializer + { + public ProcessTaskInitializer(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient, Altinn.App.Core.Internal.Prefill.IPrefill prefillService, Altinn.App.Core.Internal.AppModel.IAppModel appModel, System.IServiceProvider serviceProvider, Altinn.App.Core.Internal.Instances.IInstanceClient instanceClient, Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTaskCleaner processTaskCleaner) { } + public System.Threading.Tasks.Task Initialize(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill) { } + } } namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks { From b8fa40e49f04b1a35759adf5b5f25e632e36fc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 6 Aug 2025 10:25:34 +0200 Subject: [PATCH 13/60] Revert NextElement to ProcessNext rename in ProcessController. --- src/Altinn.App.Api/Controllers/ProcessController.cs | 2 +- ...Tests.PublicApi_ShouldNotChange_Unintentionally.verified.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 0ead63e36..171b88c6c 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -254,7 +254,7 @@ [FromRoute] Guid instanceGuid [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> ProcessNext( + public async Task> NextElement( [FromRoute] string org, [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, diff --git a/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index f62e4057d..b95a715d8 100644 --- a/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -474,7 +474,7 @@ namespace Altinn.App.Api.Controllers [Microsoft.AspNetCore.Mvc.ProducesResponseType(200)] [Microsoft.AspNetCore.Mvc.ProducesResponseType(404)] [Microsoft.AspNetCore.Mvc.ProducesResponseType(409)] - public System.Threading.Tasks.Task> ProcessNext([Microsoft.AspNetCore.Mvc.FromRoute] string org, [Microsoft.AspNetCore.Mvc.FromRoute] string app, [Microsoft.AspNetCore.Mvc.FromRoute] int instanceOwnerPartyId, [Microsoft.AspNetCore.Mvc.FromRoute] System.Guid instanceGuid, System.Threading.CancellationToken ct, [Microsoft.AspNetCore.Mvc.FromQuery] string? elementId = null, [Microsoft.AspNetCore.Mvc.FromQuery] string? language = null, [Microsoft.AspNetCore.Mvc.FromBody] Altinn.App.Api.Models.ProcessNext? processNext = null) { } + public System.Threading.Tasks.Task> NextElement([Microsoft.AspNetCore.Mvc.FromRoute] string org, [Microsoft.AspNetCore.Mvc.FromRoute] string app, [Microsoft.AspNetCore.Mvc.FromRoute] int instanceOwnerPartyId, [Microsoft.AspNetCore.Mvc.FromRoute] System.Guid instanceGuid, System.Threading.CancellationToken ct, [Microsoft.AspNetCore.Mvc.FromQuery] string? elementId = null, [Microsoft.AspNetCore.Mvc.FromQuery] string? language = null, [Microsoft.AspNetCore.Mvc.FromBody] Altinn.App.Api.Models.ProcessNext? processNext = null) { } [Microsoft.AspNetCore.Authorization.Authorize(Policy="InstanceInstantiate")] [Microsoft.AspNetCore.Mvc.HttpPost("start")] [Microsoft.AspNetCore.Mvc.ProducesResponseType(200)] From 3b33b4a0074d1c79069e5c46243824b5f4fd2e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 6 Aug 2025 10:31:31 +0200 Subject: [PATCH 14/60] IProcessEngine: Revert the rename from Next to ProcessNext back to reserved key word Next. --- .../Controllers/ProcessController.cs | 4 ++-- .../Process/Interfaces/IProcessEngine.cs | 2 +- .../Internal/Process/ProcessEngine.cs | 2 +- .../Internal/Process/ProcessEngineTest.cs | 16 ++++++++-------- ..._ShouldNotChange_Unintentionally.verified.txt | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 171b88c6c..237f92807 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -286,7 +286,7 @@ public async Task> NextElement( Language = language, }; - result = await _processEngine.ProcessNext(processNextRequest, ct); + result = await _processEngine.Next(processNextRequest, ct); if (!result.Success) { @@ -408,7 +408,7 @@ instance.Process.EndEvent is null Action = ConvertTaskTypeToAction(instance.Process.CurrentTask.AltinnTaskType), Language = language, }; - ProcessChangeResult result = await _processEngine.ProcessNext(request); + ProcessChangeResult result = await _processEngine.Next(request); if (!result.Success) { diff --git a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs index bf0886a0a..f5018b552 100644 --- a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs @@ -17,7 +17,7 @@ public interface IProcessEngine /// /// Method to move process to next task/event /// - Task ProcessNext(ProcessNextRequest request, CancellationToken ct = default); + Task Next(ProcessNextRequest request, CancellationToken ct = default); /// /// Check if the Altinn task type is a service task diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 74ceb8997..d4b90d1bf 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -141,7 +141,7 @@ out ProcessError? startEventError } /// - public async Task ProcessNext(ProcessNextRequest request, CancellationToken ct = default) + public async Task Next(ProcessNextRequest request, CancellationToken ct = default) { Instance instance = request.Instance; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index a51247b88..4c750f20e 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -398,7 +398,7 @@ public async Task Next_returns_unsuccessful_when_process_null() User = null!, Language = null, }; - ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); + ProcessChangeResult result = await processEngine.Next(processNextRequest); result.Success.Should().BeFalse(); result.ErrorMessage.Should().Be("The instance is missing process information."); result.ErrorType.Should().Be(ProcessErrorType.Conflict); @@ -422,7 +422,7 @@ public async Task Next_returns_unsuccessful_when_process_currenttask_null() Action = null, Language = null, }; - ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); + ProcessChangeResult result = await processEngine.Next(processNextRequest); result.Success.Should().BeFalse(); result.ErrorMessage.Should().Be("Process is not started. Use start!"); result.ErrorType.Should().Be(ProcessErrorType.Conflict); @@ -449,7 +449,7 @@ public async Task Next_returns_unsuccessful_when_process_altinnTaskType_null() Action = null, Language = null, }; - ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); + ProcessChangeResult result = await processEngine.Next(processNextRequest); result.Success.Should().BeFalse(); result.ErrorMessage.Should().Be("Instance does not have current altinn task type information!"); result.ErrorType.Should().Be(ProcessErrorType.Conflict); @@ -517,7 +517,7 @@ public async Task HandleUserAction_returns_successful_when_handler_succeeds() Language = null, }; - ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest, CancellationToken.None); + ProcessChangeResult result = await processEngine.Next(processNextRequest, CancellationToken.None); result.Success.Should().BeTrue(); result.ErrorType.Should().Be(null); } @@ -589,7 +589,7 @@ public async Task HandleUserAction_returns_unsuccessful_unauthorized_when_action Language = null, }; - ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest, CancellationToken.None); + ProcessChangeResult result = await processEngine.Next(processNextRequest, CancellationToken.None); result.Success.Should().BeFalse(); result.ErrorType.Should().Be(ProcessErrorType.Unauthorized); } @@ -665,7 +665,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() Language = null, }; - ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); + ProcessChangeResult result = await processEngine.Next(processNextRequest); fixture.Mock().Verify(r => r.IsProcessTask("Task_1"), Times.Once); fixture.Mock().Verify(r => r.IsEndEvent("Task_2"), Times.Once); fixture.Mock().Verify(r => r.IsProcessTask("Task_2"), Times.Once); @@ -824,7 +824,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_abandon_instance Action = "reject", Language = null, }; - ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); + ProcessChangeResult result = await processEngine.Next(processNextRequest); fixture.Mock().Verify(r => r.IsProcessTask("Task_1"), Times.Once); fixture.Mock().Verify(r => r.IsEndEvent("Task_2"), Times.Once); fixture.Mock().Verify(r => r.IsProcessTask("Task_2"), Times.Once); @@ -997,7 +997,7 @@ public async Task Next_moves_instance_to_end_event_and_ends_process(bool registe Instance = instance, }; - ProcessChangeResult result = await processEngine.ProcessNext(processNextRequest); + ProcessChangeResult result = await processEngine.Next(processNextRequest); fixture.Mock().Verify(r => r.IsProcessTask("Task_2"), Times.Once); fixture.Mock().Verify(r => r.IsEndEvent("EndEvent_1"), Times.Once); fixture.Mock().Verify(n => n.GetNextTask(It.IsAny(), "Task_2", null), Times.Once); diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 2c041c65a..7fbf821e5 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3454,7 +3454,7 @@ namespace Altinn.App.Core.Internal.Process Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.IServiceTask? CheckIfServiceTask(string? altinnTaskType); System.Threading.Tasks.Task GenerateProcessStartEvents(Altinn.App.Core.Models.Process.ProcessStartRequest processStartRequest); System.Threading.Tasks.Task HandleEventsAndUpdateStorage(Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill, System.Collections.Generic.List? events); - System.Threading.Tasks.Task ProcessNext(Altinn.App.Core.Models.Process.ProcessNextRequest request, System.Threading.CancellationToken ct = default); + System.Threading.Tasks.Task Next(Altinn.App.Core.Models.Process.ProcessNextRequest request, System.Threading.CancellationToken ct = default); } public interface IProcessEngineAuthorizer { @@ -3500,7 +3500,7 @@ namespace Altinn.App.Core.Internal.Process public Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.IServiceTask? CheckIfServiceTask(string? altinnTaskType) { } public System.Threading.Tasks.Task GenerateProcessStartEvents(Altinn.App.Core.Models.Process.ProcessStartRequest processStartRequest) { } public System.Threading.Tasks.Task HandleEventsAndUpdateStorage(Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill, System.Collections.Generic.List? events) { } - public System.Threading.Tasks.Task ProcessNext(Altinn.App.Core.Models.Process.ProcessNextRequest request, System.Threading.CancellationToken ct = default) { } + public System.Threading.Tasks.Task Next(Altinn.App.Core.Models.Process.ProcessNextRequest request, System.Threading.CancellationToken ct = default) { } } public class ProcessEventDispatcher : Altinn.App.Core.Internal.Process.IProcessEventDispatcher { From c13241501b59b90013351e441fa4e0289e29ed11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 6 Aug 2025 14:09:51 +0200 Subject: [PATCH 15/60] Add remark about saving of data when execute returns successful result. --- .../Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs index 3a3ef88bb..b09d57888 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs @@ -22,6 +22,7 @@ public sealed record ServiceTaskContext /// /// An instance data mutator that can be used to read and modify the instance data during the service task execution. /// + /// Changes are saved after Execute returns a successful result. Keep in mind that data elements from previous tasks are locked. public required IInstanceDataMutator InstanceDataMutator { get; init; } /// From 75eab852e544c948db1501b3888ff606a218e037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 6 Aug 2025 14:10:08 +0200 Subject: [PATCH 16/60] Log service task failures as error. --- src/Altinn.App.Core/Internal/Process/ProcessEngine.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index d4b90d1bf..eec8849bf 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -413,6 +413,8 @@ private async Task HandleServiceTask( if (result is ServiceTaskFailedResult) { + _logger.LogError("Service task {ServiceTaskType} returned a failed result.", serviceTask.Type); + return new ProcessChangeResult() { Success = false, @@ -438,6 +440,7 @@ private async Task HandleServiceTask( catch (Exception ex) { activity?.Errored(ex); + _logger.LogError(ex, "Service task {ServiceTaskType} returned a failed result.", serviceTask.Type); return new ProcessChangeResult() { From d375ce428faa7d8cab86589f17505f4337bb8010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 7 Aug 2025 13:29:54 +0200 Subject: [PATCH 17/60] Add test for error message when process is ended. --- .../Internal/Process/ProcessEngineTest.cs | 85 +++++++------------ 1 file changed, 31 insertions(+), 54 deletions(-) diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 4c750f20e..2e6c3e29e 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -380,78 +380,55 @@ public async Task StartProcess_starts_process_and_moves_to_first_task_with_prefi result.Success.Should().BeTrue(); } - [Fact] - public async Task Next_returns_unsuccessful_when_process_null() - { - using var fixture = Fixture.Create(); - ProcessEngine processEngine = fixture.ProcessEngine; - var instance = new Instance() + public static TheoryData InvalidProcessStatesData => + new() { - Id = _instanceId, - AppId = "org/app", - Process = null, - }; - var processNextRequest = new ProcessNextRequest() - { - Instance = instance, - Action = null, - User = null!, - Language = null, + { null, "The instance is missing process information." }, + { + new ProcessState { Ended = new DateTime() }, + "Process is ended." + }, + { + new ProcessState { CurrentTask = null }, + "Process is not started. Use start!" + }, + { + new ProcessState + { + CurrentTask = new ProcessElementInfo { ElementId = "elementId", AltinnTaskType = null }, + }, + "Instance does not have current altinn task type information!" + }, }; - ProcessChangeResult result = await processEngine.Next(processNextRequest); - result.Success.Should().BeFalse(); - result.ErrorMessage.Should().Be("The instance is missing process information."); - result.ErrorType.Should().Be(ProcessErrorType.Conflict); - } - [Fact] - public async Task Next_returns_unsuccessful_when_process_currenttask_null() + [Theory] + [MemberData(nameof(InvalidProcessStatesData))] + public async Task Next_returns_unsuccessful_for_invalid_process_states( + ProcessState? processState, + string expectedErrorMessage + ) { using var fixture = Fixture.Create(); ProcessEngine processEngine = fixture.ProcessEngine; - Instance instance = new Instance() - { - Id = _instanceId, - AppId = "org/app", - Process = new ProcessState() { CurrentTask = null }, - }; - ProcessNextRequest processNextRequest = new ProcessNextRequest() - { - Instance = instance, - User = null!, - Action = null, - Language = null, - }; - ProcessChangeResult result = await processEngine.Next(processNextRequest); - result.Success.Should().BeFalse(); - result.ErrorMessage.Should().Be("Process is not started. Use start!"); - result.ErrorType.Should().Be(ProcessErrorType.Conflict); - } - [Fact] - public async Task Next_returns_unsuccessful_when_process_altinnTaskType_null() - { - using var fixture = Fixture.Create(); - ProcessEngine processEngine = fixture.ProcessEngine; - Instance instance = new Instance() + var instance = new Instance() { Id = _instanceId, AppId = "org/app", - Process = new ProcessState() - { - CurrentTask = new ProcessElementInfo { ElementId = "elementId", AltinnTaskType = null }, - }, + Process = processState, }; - ProcessNextRequest processNextRequest = new ProcessNextRequest() + + var processNextRequest = new ProcessNextRequest() { Instance = instance, - User = null!, Action = null, + User = null!, Language = null, }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); result.Success.Should().BeFalse(); - result.ErrorMessage.Should().Be("Instance does not have current altinn task type information!"); + result.ErrorMessage.Should().Be(expectedErrorMessage); result.ErrorType.Should().Be(ProcessErrorType.Conflict); } From 8e058b5dfcb99462f1b0580a316ae2a71c3134f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 27 Aug 2025 15:35:32 +0200 Subject: [PATCH 18/60] Return bpmn element type to frontend. --- src/Altinn.App.Api/Controllers/ProcessController.cs | 2 ++ .../Internal/Process/Elements/AppProcessElementInfo.cs | 6 ++++++ .../Internal/Process/Elements/AppProcessTaskTypeInfo.cs | 6 ++++++ ...enApiSpecChangeDetection.SaveJsonSwagger.verified.json | 8 ++++++++ ...PublicApi_ShouldNotChange_Unintentionally.verified.txt | 4 ++++ 5 files changed, 26 insertions(+) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 237f92807..722c8a144 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -496,6 +496,7 @@ private async Task ConvertAndAuthorizeActions(Instance instance appProcessState.CurrentTask.HasReadAccess = authDecisions.Single(a => a.Id == "read").Authorized; appProcessState.CurrentTask.HasWriteAccess = authDecisions.Single(a => a.Id == "write").Authorized; appProcessState.CurrentTask.UserActions = authDecisions; + appProcessState.CurrentTask.ElementType = flowElement.ElementType(); } } @@ -506,6 +507,7 @@ private async Task ConvertAndAuthorizeActions(Instance instance new AppProcessTaskTypeInfo { ElementId = processElement.Id, + ElementType = processElement.ElementType(), AltinnTaskType = processElement.ExtensionElements?.TaskExtension?.TaskType, } ); diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs index 2d904e364..6a7367ec9 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs @@ -60,4 +60,10 @@ public AppProcessElementInfo(ProcessElementInfo processElementInfo) /// [JsonPropertyName(name: "write")] public bool HasWriteAccess { get; set; } + + /// + /// Specifies the type of BPMN element. + /// + [JsonPropertyName(name: "elementType")] + public string? ElementType { get; set; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs index 8ba56aeb2..d4eececa7 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs @@ -20,4 +20,10 @@ public class AppProcessTaskTypeInfo /// [JsonPropertyName(name: "elementId")] public string? ElementId { get; set; } + + /// + /// The BPMN element type. + /// + [JsonPropertyName(name: "elementType")] + public string? ElementType { get; set; } } diff --git a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json index e5c3e528c..57c701321 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json +++ b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json @@ -6931,6 +6931,10 @@ }, "write": { "type": "boolean" + }, + "elementType": { + "type": "string", + "nullable": true } }, "additionalProperties": false @@ -6979,6 +6983,10 @@ "elementId": { "type": "string", "nullable": true + }, + "elementType": { + "type": "string", + "nullable": true } }, "additionalProperties": false diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 7fbf821e5..b800fbbed 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3253,6 +3253,8 @@ namespace Altinn.App.Core.Internal.Process.Elements public AppProcessElementInfo(Altinn.Platform.Storage.Interface.Models.ProcessElementInfo processElementInfo) { } [System.Text.Json.Serialization.JsonPropertyName("actions")] public System.Collections.Generic.Dictionary? Actions { get; set; } + [System.Text.Json.Serialization.JsonPropertyName("elementType")] + public string? ElementType { get; set; } [System.Text.Json.Serialization.JsonPropertyName("read")] public bool HasReadAccess { get; set; } [System.Text.Json.Serialization.JsonPropertyName("write")] @@ -3274,6 +3276,8 @@ namespace Altinn.App.Core.Internal.Process.Elements public string? AltinnTaskType { get; set; } [System.Text.Json.Serialization.JsonPropertyName("elementId")] public string? ElementId { get; set; } + [System.Text.Json.Serialization.JsonPropertyName("elementType")] + public string? ElementType { get; set; } } [System.Xml.Serialization.XmlRoot("definitions", Namespace="http://www.omg.org/spec/BPMN/20100524/MODEL")] [System.Xml.Serialization.XmlType(Namespace="http://www.omg.org/spec/BPMN/20100524/MODEL")] From 010385dda27021765bf92ba3c0faf867e6303166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Fri, 5 Sep 2025 15:35:41 +0200 Subject: [PATCH 19/60] Remove useless variables. --- src/Altinn.App.Core/Internal/Process/ProcessEngine.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index eec8849bf..5f53248bb 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -298,12 +298,12 @@ public async Task HandleEventsAndUpdateStorage( List? events ) { - using (var activity = _telemetry?.StartProcessHandleEventsActivity(instance)) + using (_telemetry?.StartProcessHandleEventsActivity(instance)) { await _processEventHandlerDelegator.HandleEvents(instance, prefill, events); } - using (var activity = _telemetry?.StartProcessStoreEventsActivity(instance)) + using (_telemetry?.StartProcessStoreEventsActivity(instance)) { return await _processEventDispatcher.DispatchToStorage(instance, events); } From 81aa1c3471bfd901ace1712e6e6e3e1587300949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 10:27:20 +0200 Subject: [PATCH 20/60] Add default body for new method on IPdfService to avoid breaking change. --- src/Altinn.App.Core/Internal/Pdf/IPdfService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs index 57bb78447..e4ea6ab08 100644 --- a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs @@ -12,7 +12,7 @@ public interface IPdfService /// 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 + /// The task id for which 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); @@ -21,7 +21,7 @@ public interface IPdfService /// 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 + /// The task id for which the pdf is generated /// A text resource element id for the file name of the PDF. If no text resource is found, the literal value will be used. If null, a default file name will be used. /// Cancellation token for when a request should be stopped before it's completed. Task GenerateAndStorePdf( @@ -29,13 +29,13 @@ Task GenerateAndStorePdf( string taskId, string? fileNameTextResourceElementId, CancellationToken ct = default - ); + ) => GenerateAndStorePdf(instance, taskId, 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 + /// The task id for which 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); From 064c17c30eb7501fc033fe6839f00ad8814c7f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 10:29:28 +0200 Subject: [PATCH 21/60] Sanitize a log. --- src/Altinn.App.Core/Internal/Process/ProcessEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 5f53248bb..388ccc46d 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -392,7 +392,7 @@ private async Task HandleServiceTask( { ErrorTitle = "User action not supported!", ErrorMessage = - $"Service tasks do not support running user actions! Received action param {request.Action}.", + $"Service tasks do not support running user actions! Received action param {LogSanitizer.Sanitize(request.Action)}.", ErrorType = ProcessErrorType.Conflict, }; From ae04d4c645981fcc7a52ebed7572dc859b95fbfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 11:10:15 +0200 Subject: [PATCH 22/60] Nitpick: Use PascalCase for named placeholders. --- src/Altinn.App.Api/Controllers/ProcessController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 722c8a144..99f2139d4 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -306,7 +306,7 @@ public async Task> NextElement( } catch (PlatformHttpException e) { - _logger.LogError("Platform exception when processing next. {message}", e.Message); + _logger.LogError("Platform exception when processing next. {Message}", e.Message); return HandlePlatformHttpException(e, "Process next failed."); } catch (Exception exception) From 8fa3a92fee32cdf8eb9542a987224a628cccf20d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 11:18:03 +0200 Subject: [PATCH 23/60] Prevent NRE in ProcessReader when no service tasks exists. --- src/Altinn.App.Core/Internal/Process/Elements/Process.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Internal/Process/Elements/Process.cs b/src/Altinn.App.Core/Internal/Process/Elements/Process.cs index 1c2112f8c..e52f74a90 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/Process.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/Process.cs @@ -39,7 +39,7 @@ public class Process /// Gets or sets the list of service tasks for the process of a workflow /// [XmlElement("serviceTask")] - public List ServiceTasks { get; set; } + public List ServiceTasks { get; set; } = []; /// /// Gets or sets the end event of the process of a workflow From dd37c8aae370a128837a983b085c34fb6c486a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 11:18:25 +0200 Subject: [PATCH 24/60] Add usings in PdfService to dispose streams when done. --- src/Altinn.App.Core/Internal/Pdf/PdfService.cs | 2 +- .../Internal/Process/ProcessTasks/PaymentProcessTask.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index 4533ad2a2..36e64d303 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -125,7 +125,7 @@ private async Task GenerateAndStorePdfInternal( TextResource? textResource = await GetTextResource(instance, language); - var pdfContent = await GeneratePdfContent(instance, language, false, textResource, ct); + await using Stream pdfContent = await GeneratePdfContent(instance, language, false, textResource, ct); string fileName = GetFileName(instance, textResource, fileNameTextResourceElementId); await _dataClient.InsertBinaryData( diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs index 31c0d8442..d6ab774a0 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/PaymentProcessTask.cs @@ -77,7 +77,7 @@ public async Task End(string taskId, Instance instance) if (paymentStatus != PaymentStatus.Paid) throw new PaymentException("The payment is not completed."); - Stream pdfStream = await _pdfService.GeneratePdf(instance, taskId, false, CancellationToken.None); + await using Stream pdfStream = await _pdfService.GeneratePdf(instance, taskId, false, CancellationToken.None); ValidAltinnPaymentConfiguration validatedPaymentConfiguration = paymentConfiguration.Validate(); From 1da33f0b1bbf87ade55220b115661530252078b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 12:07:10 +0200 Subject: [PATCH 25/60] Hmm --- .../ProcessTasks/ServiceTasks/IServiceTask.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs index b09d57888..e8883a3bb 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs @@ -32,16 +32,23 @@ public sealed record ServiceTaskContext } /// -/// This class represents the result of executing a service task. +/// Base type for the result of executing a service task. /// -public abstract class ServiceTaskResult { } +public abstract record ServiceTaskResult +{ + /// Creates a successful result. + public static ServiceTaskSuccessResult Success() => new(); + + /// Creates a failed result. + public static ServiceTaskFailedResult Failed() => new(); +} /// -/// This class represents a successful result of executing a service task. +/// Represents a successful result of executing a service task. /// -public sealed class ServiceTaskSuccessResult : ServiceTaskResult; +public sealed record ServiceTaskSuccessResult : ServiceTaskResult; /// -/// This class represents a failed result of executing a service task. +/// Represents a failed result of executing a service task. /// -public sealed class ServiceTaskFailedResult : ServiceTaskResult; +public sealed record ServiceTaskFailedResult : ServiceTaskResult; From 74bcaa70917a9d87801649903bf5fb6d4ee95380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 12:18:16 +0200 Subject: [PATCH 26/60] Hmm 2 --- ...PublicApi_ShouldNotChange_Unintentionally.verified.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index f2ab45cee..be27d0f1d 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3677,15 +3677,17 @@ namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks public System.Threading.CancellationToken CancellationToken { get; init; } public required Altinn.App.Core.Features.IInstanceDataMutator InstanceDataMutator { get; init; } } - public sealed class ServiceTaskFailedResult : Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskResult + public sealed class ServiceTaskFailedResult : Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskResult, System.IEquatable { public ServiceTaskFailedResult() { } } - public abstract class ServiceTaskResult + public abstract class ServiceTaskResult : System.IEquatable { protected ServiceTaskResult() { } + public static Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskFailedResult Failed() { } + public static Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskSuccessResult Success() { } } - public sealed class ServiceTaskSuccessResult : Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskResult + public sealed class ServiceTaskSuccessResult : Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskResult, System.IEquatable { public ServiceTaskSuccessResult() { } } From 8e21b269a4928469621b20eab265dc7783a94e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 12:31:29 +0200 Subject: [PATCH 27/60] Add support for custom pdf data type. --- src/Altinn.App.Core/Internal/Pdf/IPdfService.cs | 6 ++++-- src/Altinn.App.Core/Internal/Pdf/PdfService.cs | 8 +++++--- .../AltinnPdfConfiguration.cs | 10 ++++++++-- .../Process/ServiceTasks/PdfServiceTaskTests.cs | 1 + ...licApi_ShouldNotChange_Unintentionally.verified.txt | 6 ++++-- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs index e4ea6ab08..f3b7a351e 100644 --- a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs @@ -12,7 +12,7 @@ public interface IPdfService /// to storage as a new binary file associated with the predefined PDF data type in most apps. /// /// The instance details. - /// The task id for which the pdf is generated + /// The task id for which 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); @@ -21,12 +21,14 @@ public interface IPdfService /// to storage as a new binary file associated with the predefined PDF data type in most apps. /// /// The instance details. - /// The task id for which the pdf is generated + /// The task id for which the PDF is generated. + /// The data type to use when storing the PDF. /// A text resource element id for the file name of the PDF. If no text resource is found, the literal value will be used. If null, a default file name will be used. /// Cancellation token for when a request should be stopped before it's completed. Task GenerateAndStorePdf( Instance instance, string taskId, + string? dataTypeId, string? fileNameTextResourceElementId, CancellationToken ct = default ) => GenerateAndStorePdf(instance, taskId, ct); diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index 36e64d303..a606ca2f5 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -72,20 +72,21 @@ public async Task GenerateAndStorePdf(Instance instance, string taskId, Cancella { using var activity = _telemetry?.StartGenerateAndStorePdfActivity(instance, taskId); - await GenerateAndStorePdfInternal(instance, taskId, null, ct); + await GenerateAndStorePdfInternal(instance, taskId, null, null, ct); } /// public async Task GenerateAndStorePdf( Instance instance, string taskId, + string? dataTypeId, string? fileNameTextResourceElementId, CancellationToken ct = default ) { using var activity = _telemetry?.StartGenerateAndStorePdfActivity(instance, taskId); - await GenerateAndStorePdfInternal(instance, taskId, fileNameTextResourceElementId, ct); + await GenerateAndStorePdfInternal(instance, taskId, dataTypeId, fileNameTextResourceElementId, ct); } /// @@ -113,6 +114,7 @@ public async Task GeneratePdf(Instance instance, string taskId, Cancella private async Task GenerateAndStorePdfInternal( Instance instance, string taskId, + string? dataTypeId, string? fileNameTextResourceElementId, CancellationToken ct = default ) @@ -130,7 +132,7 @@ private async Task GenerateAndStorePdfInternal( string fileName = GetFileName(instance, textResource, fileNameTextResourceElementId); await _dataClient.InsertBinaryData( instance.Id, - PdfElementType, + dataTypeId ?? PdfElementType, PdfContentType, fileName, pdfContent, diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs index 44f7048ea..a92b5fe89 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs @@ -7,6 +7,12 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; /// public class AltinnPdfConfiguration { + /// + /// Set the data type to use when storing the PDF. If not set, ref-data-as-pdf will be used. + /// + [XmlElement("dataTypeId", Namespace = "http://altinn.no/process")] + public string? DataTypeId { get; set; } + /// /// Set the filename of the PDF. Supports text resource keys for language support. /// @@ -15,8 +21,8 @@ public class AltinnPdfConfiguration internal ValidAltinnPdfConfiguration Validate() { - return new ValidAltinnPdfConfiguration(Filename); + return new ValidAltinnPdfConfiguration(DataTypeId, Filename); } } -internal readonly record struct ValidAltinnPdfConfiguration(string? Filename); +internal readonly record struct ValidAltinnPdfConfiguration(string? DataTypeId, string? Filename); 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 3b30831ea..2e8c8af7d 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs @@ -56,6 +56,7 @@ public async Task Execute_Should_Call_GenerateAndStorePdf() x.GenerateAndStorePdf( instance, instance.Process.CurrentTask.ElementId, + null, FileName, It.IsAny() ), diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index be27d0f1d..37e6d12e9 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3120,7 +3120,7 @@ namespace Altinn.App.Core.Internal.Pdf public interface IPdfService { System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct); - System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? fileNameTextResourceElementId, System.Threading.CancellationToken ct = default); + System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? dataTypeId, string? fileNameTextResourceElementId, System.Threading.CancellationToken ct = default); System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct); System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, bool isPreview, System.Threading.CancellationToken ct); } @@ -3142,7 +3142,7 @@ namespace Altinn.App.Core.Internal.Pdf { public PdfService(Altinn.App.Core.Internal.App.IAppResources appResources, Altinn.App.Core.Internal.Data.IDataClient dataClient, Microsoft.AspNetCore.Http.IHttpContextAccessor httpContextAccessor, Altinn.App.Core.Internal.Pdf.IPdfGeneratorClient pdfGeneratorClient, Microsoft.Extensions.Options.IOptions pdfGeneratorSettings, Microsoft.Extensions.Options.IOptions generalSettings, Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Features.Auth.IAuthenticationContext authenticationContext, Altinn.App.Core.Features.Telemetry? telemetry = null) { } public System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct) { } - public System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? fileNameTextResourceElementId, System.Threading.CancellationToken ct = default) { } + public System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? dataTypeId, string? fileNameTextResourceElementId, System.Threading.CancellationToken ct = default) { } public System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct) { } public System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, bool isPreview, System.Threading.CancellationToken ct) { } } @@ -3226,6 +3226,8 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties public class AltinnPdfConfiguration { public AltinnPdfConfiguration() { } + [System.Xml.Serialization.XmlElement("dataTypeId", Namespace="http://altinn.no/process")] + public string? DataTypeId { get; set; } [System.Xml.Serialization.XmlElement("filename", Namespace="http://altinn.no/process")] public string? Filename { get; set; } } From f7f6191bee4eeecc8d27e515af43c16ce8b74853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 12:34:08 +0200 Subject: [PATCH 28/60] Try catch in standard service tasks. --- .../Internal/Process/ProcessEngine.cs | 2 +- .../ServiceTasks/EFormidlingServiceTask.cs | 22 +++++++++++--- .../ServiceTasks/PdfServiceTask.cs | 29 +++++++++++++++---- .../ServiceTasks/Pdf/PdfServiceTaskTests.cs | 2 +- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 388ccc46d..d2ac9b35a 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -440,7 +440,7 @@ private async Task HandleServiceTask( catch (Exception ex) { activity?.Errored(ex); - _logger.LogError(ex, "Service task {ServiceTaskType} returned a failed result.", serviceTask.Type); + _logger.LogError(ex, "Service task {ServiceTaskType} returned an exception.", serviceTask.Type); return new ProcessChangeResult() { diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index a72ec07ad..2f6c4cf05 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -55,10 +55,24 @@ public async Task Execute(ServiceTaskContext context) ); } - _logger.LogDebug("Calling eFormidlingService for eFormidling Service Task {TaskId}.", taskId); - await _eFormidlingService.SendEFormidlingShipment(instance); - _logger.LogDebug("Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", taskId); + try + { + _logger.LogDebug("Calling eFormidlingService for eFormidling Service Task {TaskId}.", taskId); + await _eFormidlingService.SendEFormidlingShipment(instance); + _logger.LogDebug("Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", taskId); + + return ServiceTaskResult.Success(); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "An error occured while executing {Type} Service Task on taskId {TaskId}.", + Type, + taskId + ); - return new ServiceTaskSuccessResult(); + return ServiceTaskResult.Failed(); + } } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs index 4e4c89ed4..ab34b1124 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -35,14 +35,33 @@ public async Task Execute(ServiceTaskContext context) string taskId = context.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; Instance instance = context.InstanceDataMutator.Instance; - _logger.LogDebug("Calling PdfService for PDF Service Task {TaskId}.", taskId); + try + { + _logger.LogDebug("Calling PdfService for PDF Service Task {TaskId}.", taskId); - ValidAltinnPdfConfiguration config = GetValidAltinnPdfConfiguration(taskId); - await _pdfService.GenerateAndStorePdf(instance, taskId, config.Filename, context.CancellationToken); + ValidAltinnPdfConfiguration config = GetValidAltinnPdfConfiguration(taskId); + await _pdfService.GenerateAndStorePdf( + instance, + taskId, + config.DataTypeId, + config.Filename, + context.CancellationToken + ); - _logger.LogDebug("Successfully called PdfService for PDF Service Task {TaskId}.", taskId); + _logger.LogDebug("Successfully called PdfService for PDF Service Task {TaskId}.", taskId); - return new ServiceTaskSuccessResult(); + return ServiceTaskResult.Success(); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "An error occured while executing {Type} Service Task on taskId {TaskId}.", + Type, + taskId + ); + return ServiceTaskResult.Failed(); + } } private ValidAltinnPdfConfiguration GetValidAltinnPdfConfiguration(string taskId) diff --git a/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs index a06dd5120..e1bc3f480 100644 --- a/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs @@ -155,7 +155,7 @@ public async Task CurrentTask_Is_ServiceTask_If_Execute_Fails() responseAsString .Should() .Be( - "{\"title\":\"Service task failed!\",\"status\":500,\"detail\":\"Service task pdf failed with an exception!\"}" + "{\"title\":\"Service task failed!\",\"status\":500,\"detail\":\"Service task pdf returned a failed result!\"}" ); // Double check that process did not move to the next task From e0139d3d2f78b6d2f0fe94dcb4f7ad5cdc2284e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 12:45:54 +0200 Subject: [PATCH 29/60] Ensure no empty strings in AltinnPdfConfiguration. --- .../AltinnExtensionProperties/AltinnPdfConfiguration.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs index a92b5fe89..3e737784a 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs @@ -21,7 +21,10 @@ public class AltinnPdfConfiguration internal ValidAltinnPdfConfiguration Validate() { - return new ValidAltinnPdfConfiguration(DataTypeId, Filename); + string? normalizedDataTypeId = string.IsNullOrWhiteSpace(DataTypeId) ? null : DataTypeId.Trim(); + string? normalizedFilename = string.IsNullOrWhiteSpace(Filename) ? null : Filename.Trim(); + + return new ValidAltinnPdfConfiguration(normalizedDataTypeId, normalizedFilename); } } From 6559707e1251513ced95565a78762f222b8faa6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 13:23:47 +0200 Subject: [PATCH 30/60] Make our service tasks internal sealed. --- .../ServiceTasks/EFormidlingServiceTask.cs | 2 +- .../ProcessTasks/ServiceTasks/PdfServiceTask.cs | 2 +- ...cApi_ShouldNotChange_Unintentionally.verified.txt | 12 ------------ 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index 2f6c4cf05..8fc3729ad 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -11,7 +11,7 @@ internal interface IEFormidlingServiceTask : IServiceTask { } /// /// Service task that sends eFormidling shipment, if EFormidling is enabled in config. /// -public class EFormidlingServiceTask : IEFormidlingServiceTask +internal sealed class EFormidlingServiceTask : IEFormidlingServiceTask { private readonly ILogger _logger; private readonly IEFormidlingService? _eFormidlingService; diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs index ab34b1124..6f79169ce 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -10,7 +10,7 @@ internal interface IPdfServiceTask : IServiceTask { } /// /// Service task that generates PDFs for tasks specified in the process configuration. /// -public class PdfServiceTask : IPdfServiceTask +internal sealed class PdfServiceTask : IPdfServiceTask { private readonly IPdfService _pdfService; private readonly IProcessReader _processReader; diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 37e6d12e9..da1dbeb2e 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3657,22 +3657,10 @@ namespace Altinn.App.Core.Internal.Process.ProcessTasks } namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks { - public class EFormidlingServiceTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask, Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.IServiceTask - { - public EFormidlingServiceTask(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.EFormidling.Interface.IEFormidlingService? eFormidlingService = null, Microsoft.Extensions.Options.IOptions? appSettings = null) { } - public string Type { get; } - public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskContext context) { } - } public interface IServiceTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask { System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskContext context); } - public class PdfServiceTask : Altinn.App.Core.Internal.Process.ProcessTasks.IProcessTask, Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.IServiceTask - { - public PdfServiceTask(Altinn.App.Core.Internal.Pdf.IPdfService pdfService, Altinn.App.Core.Internal.Process.IProcessReader processReader, Microsoft.Extensions.Logging.ILogger logger) { } - public string Type { get; } - public System.Threading.Tasks.Task Execute(Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskContext context) { } - } public sealed class ServiceTaskContext : System.IEquatable { public ServiceTaskContext() { } From 45432b1fa1469b1504e201f4d06ecd3f285a88fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 13:30:26 +0200 Subject: [PATCH 31/60] Revert "Try catch in standard service tasks." This reverts commit f7f6191bee4eeecc8d27e515af43c16ce8b74853. --- .../Internal/Process/ProcessEngine.cs | 2 +- .../ServiceTasks/EFormidlingServiceTask.cs | 22 +++--------- .../ServiceTasks/PdfServiceTask.cs | 35 ++++++------------- .../ServiceTasks/Pdf/PdfServiceTaskTests.cs | 2 +- 4 files changed, 17 insertions(+), 44 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index d2ac9b35a..388ccc46d 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -440,7 +440,7 @@ private async Task HandleServiceTask( catch (Exception ex) { activity?.Errored(ex); - _logger.LogError(ex, "Service task {ServiceTaskType} returned an exception.", serviceTask.Type); + _logger.LogError(ex, "Service task {ServiceTaskType} returned a failed result.", serviceTask.Type); return new ProcessChangeResult() { diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index 8fc3729ad..34a369654 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -55,24 +55,10 @@ public async Task Execute(ServiceTaskContext context) ); } - try - { - _logger.LogDebug("Calling eFormidlingService for eFormidling Service Task {TaskId}.", taskId); - await _eFormidlingService.SendEFormidlingShipment(instance); - _logger.LogDebug("Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", taskId); - - return ServiceTaskResult.Success(); - } - catch (Exception ex) - { - _logger.LogError( - ex, - "An error occured while executing {Type} Service Task on taskId {TaskId}.", - Type, - taskId - ); + _logger.LogDebug("Calling eFormidlingService for eFormidling Service Task {TaskId}.", taskId); + await _eFormidlingService.SendEFormidlingShipment(instance); + _logger.LogDebug("Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", taskId); - return ServiceTaskResult.Failed(); - } + return new ServiceTaskSuccessResult(); } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs index 6f79169ce..b3c2f7504 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -35,33 +35,20 @@ public async Task Execute(ServiceTaskContext context) string taskId = context.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; Instance instance = context.InstanceDataMutator.Instance; - try - { - _logger.LogDebug("Calling PdfService for PDF Service Task {TaskId}.", taskId); + _logger.LogDebug("Calling PdfService for PDF Service Task {TaskId}.", taskId); - ValidAltinnPdfConfiguration config = GetValidAltinnPdfConfiguration(taskId); - await _pdfService.GenerateAndStorePdf( - instance, - taskId, - config.DataTypeId, - config.Filename, - context.CancellationToken - ); + ValidAltinnPdfConfiguration config = GetValidAltinnPdfConfiguration(taskId); + await _pdfService.GenerateAndStorePdf( + instance, + taskId, + config.DataTypeId, + config.Filename, + context.CancellationToken + ); - _logger.LogDebug("Successfully called PdfService for PDF Service Task {TaskId}.", taskId); + _logger.LogDebug("Successfully called PdfService for PDF Service Task {TaskId}.", taskId); - return ServiceTaskResult.Success(); - } - catch (Exception ex) - { - _logger.LogError( - ex, - "An error occured while executing {Type} Service Task on taskId {TaskId}.", - Type, - taskId - ); - return ServiceTaskResult.Failed(); - } + return new ServiceTaskSuccessResult(); } private ValidAltinnPdfConfiguration GetValidAltinnPdfConfiguration(string taskId) diff --git a/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs index e1bc3f480..a06dd5120 100644 --- a/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs @@ -155,7 +155,7 @@ public async Task CurrentTask_Is_ServiceTask_If_Execute_Fails() responseAsString .Should() .Be( - "{\"title\":\"Service task failed!\",\"status\":500,\"detail\":\"Service task pdf returned a failed result!\"}" + "{\"title\":\"Service task failed!\",\"status\":500,\"detail\":\"Service task pdf failed with an exception!\"}" ); // Double check that process did not move to the next task From 1962825db2ce8dc849750efdc435130fe18636d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 15:44:48 +0200 Subject: [PATCH 32/60] Use serviceTaskResult helper methods. --- .../ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs | 4 ++-- .../Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index 34a369654..074f45e8b 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -45,7 +45,7 @@ public async Task Execute(ServiceTaskContext context) _logger.LogWarning( "EFormidling has been added as a service task in the BPMN process definition but is not enabled in appsettings.json. No eFormidling shipment will be sent, but the service task will be completed." ); - return new ServiceTaskSuccessResult(); + return ServiceTaskResult.Success(); } if (_eFormidlingService is null) @@ -59,6 +59,6 @@ public async Task Execute(ServiceTaskContext context) await _eFormidlingService.SendEFormidlingShipment(instance); _logger.LogDebug("Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", taskId); - return new ServiceTaskSuccessResult(); + return ServiceTaskResult.Success(); } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs index b3c2f7504..d4c647258 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -48,7 +48,7 @@ await _pdfService.GenerateAndStorePdf( _logger.LogDebug("Successfully called PdfService for PDF Service Task {TaskId}.", taskId); - return new ServiceTaskSuccessResult(); + return ServiceTaskResult.Success(); } private ValidAltinnPdfConfiguration GetValidAltinnPdfConfiguration(string taskId) From defee86864a70ad34f3f54bfd25f8bac2fe4fc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 15:57:37 +0200 Subject: [PATCH 33/60] Make pdf config sealed. --- .../AltinnExtensionProperties/AltinnPdfConfiguration.cs | 2 +- ...Tests.PublicApi_ShouldNotChange_Unintentionally.verified.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs index 3e737784a..0a103d6d0 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs @@ -5,7 +5,7 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; /// /// Configuration properties for PDF in a process task /// -public class AltinnPdfConfiguration +public sealed class AltinnPdfConfiguration { /// /// Set the data type to use when storing the PDF. If not set, ref-data-as-pdf will be used. diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index da1dbeb2e..8ccaecef6 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3223,7 +3223,7 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties [System.Xml.Serialization.XmlElement("paymentReceiptPdfDataType", Namespace="http://altinn.no/process")] public string? PaymentReceiptPdfDataType { get; set; } } - public class AltinnPdfConfiguration + public sealed class AltinnPdfConfiguration { public AltinnPdfConfiguration() { } [System.Xml.Serialization.XmlElement("dataTypeId", Namespace="http://altinn.no/process")] From debeef06f8065e4cae3348c8600c95da57fc1cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 25 Sep 2025 16:01:30 +0200 Subject: [PATCH 34/60] Add ability to stop EFormidlingServiceTask from sending eFormidling shipment in certain environments. Default is enabled. Ment to be used to disable it locally and in test, if one wants to. --- .../AltinnEFormidlingConfiguration.cs | 18 ++ .../AltinnTaskExtension.cs | 6 + .../ServiceTasks/EFormidlingServiceTask.cs | 51 +++++- .../EFormidlingServiceTaskTests.cs | 155 +++++++++++++++++- 4 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs new file mode 100644 index 000000000..f29a25e8b --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs @@ -0,0 +1,18 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +/// +/// Configuration properties for eFormidling in a process task +/// +public class AltinnEFormidlingConfiguration +{ + /// + /// Controls whether eFormidling should be enabled for this task. + /// Supports environment-specific configuration through the 'env' attribute. + /// If no environment is specified, the value applies to all environments. + /// Environment-specific values take precedence over global values. + /// + [XmlElement(ElementName = "enabled", Namespace = "http://altinn.no/process")] + public List Enabled { 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 d0f2e04c0..649783d4f 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs @@ -40,6 +40,12 @@ public class AltinnTaskExtension [XmlElement("pdfConfig", Namespace = "http://altinn.no/process")] public AltinnPdfConfiguration? PdfConfiguration { get; set; } + /// + /// Gets or sets the configuration for eFormidling + /// + [XmlElement("eFormidlingConfig", Namespace = "http://altinn.no/process")] + public AltinnEFormidlingConfiguration? EFormidlingConfiguration { get; set; } + /// /// Retrieves a configuration item for given environment, in a predictable manner. /// Specific configurations (those specifying an environment) takes precedence over global configurations. diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index 074f45e8b..ddcdee5f9 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -1,6 +1,9 @@ using Altinn.App.Core.Configuration; +using Altinn.App.Core.Constants; using Altinn.App.Core.EFormidling.Interface; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,17 +19,23 @@ internal sealed class EFormidlingServiceTask : IEFormidlingServiceTask private readonly ILogger _logger; private readonly IEFormidlingService? _eFormidlingService; private readonly IOptions? _appSettings; + private readonly IProcessReader _processReader; + private readonly IHostEnvironment _hostEnvironment; /// /// Initializes a new instance of the class. /// public EFormidlingServiceTask( ILogger logger, + IProcessReader processReader, + IHostEnvironment hostEnvironment, IEFormidlingService? eFormidlingService = null, IOptions? appSettings = null ) { _logger = logger; + _processReader = processReader; + _hostEnvironment = hostEnvironment; _eFormidlingService = eFormidlingService; _appSettings = appSettings; } @@ -40,10 +49,12 @@ public async Task Execute(ServiceTaskContext context) string taskId = context.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; Instance instance = context.InstanceDataMutator.Instance; - if (_appSettings?.Value.EnableEFormidling is false) + // Check BPMN configuration first, then fall back to appsettings for backward compatibility + if (!IsEFormidlingEnabled(taskId)) { - _logger.LogWarning( - "EFormidling has been added as a service task in the BPMN process definition but is not enabled in appsettings.json. No eFormidling shipment will be sent, but the service task will be completed." + _logger.LogInformation( + "EFormidling is disabled for task {TaskId}. No eFormidling shipment will be sent, but the service task will be completed.", + taskId ); return ServiceTaskResult.Success(); } @@ -61,4 +72,38 @@ public async Task Execute(ServiceTaskContext context) return ServiceTaskResult.Success(); } + + private bool IsEFormidlingEnabled(string taskId) + { + AltinnTaskExtension? altinnTaskExtension = _processReader.GetAltinnTaskExtension(taskId); + AltinnEFormidlingConfiguration? eFormidlingConfiguration = altinnTaskExtension?.EFormidlingConfiguration; + + // If no BPMN configuration is specified, default to enabled + if (eFormidlingConfiguration?.Enabled.Count is 0 or null) + { + _logger.LogDebug( + "No eFormidling configuration found in BPMN for task {TaskId}. Defaulting to enabled. Add false to disable for specific environments.", + taskId + ); + return true; + } + + // Use environment-aware BPMN configuration + HostingEnvironment env = AltinnEnvironments.GetHostingEnvironment(_hostEnvironment); + AltinnEnvironmentConfig? enabledConfig = AltinnTaskExtension.GetConfigForEnvironment( + env, + eFormidlingConfiguration.Enabled + ); + + if (enabledConfig?.Value is null) + { + _logger.LogWarning( + "EFormidling configuration is present in BPMN but no matching environment configuration found for environment '{Environment}'. EFormidling will be disabled.", + env + ); + return false; + } + + return bool.TryParse(enabledConfig.Value, out bool enabled) && enabled; + } } 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 e28c68ee4..e9e0ffbe3 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs @@ -3,8 +3,10 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; @@ -16,24 +18,27 @@ public class EFormidlingServiceTaskTests private readonly Mock> _loggerMock = new(); private readonly Mock _eFormidlingServiceMock = new(); private readonly Mock> _appSettingsMock = new(); + private readonly Mock _processReaderMock = new(); + private readonly Mock _hostEnvironmentMock = new(); private readonly EFormidlingServiceTask _serviceTask; public EFormidlingServiceTaskTests() { + _hostEnvironmentMock.Setup(x => x.EnvironmentName).Returns("Production"); _serviceTask = new EFormidlingServiceTask( _loggerMock.Object, + _processReaderMock.Object, + _hostEnvironmentMock.Object, _eFormidlingServiceMock.Object, _appSettingsMock.Object ); } [Fact] - public async Task Execute_Should_LogWarning_When_EFormidlingDisabled() + public async Task Execute_Should_BeEnabled_When_NoBpmnConfig() { // Arrange Instance instance = GetInstance(); - var appSettings = new AppSettings { EnableEFormidling = false }; - _appSettingsMock.Setup(x => x.Value).Returns(appSettings); var instanceMutatorMock = new Mock(); instanceMutatorMock.Setup(x => x.Instance).Returns(instance); @@ -44,16 +49,17 @@ public async Task Execute_Should_LogWarning_When_EFormidlingDisabled() await _serviceTask.Execute(parameters); // Assert + _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); _loggerMock.Verify( x => x.Log( - LogLevel.Warning, + LogLevel.Debug, It.IsAny(), It.Is( (v, t) => v.ToString()! .Contains( - "EFormidling has been added as a service task in the BPMN process definition but is not enabled in appsettings.json. No eFormidling shipment will be sent, but the service task will be completed." + "No eFormidling configuration found in BPMN for task taskId. Defaulting to enabled" ) ), It.IsAny(), @@ -72,7 +78,13 @@ public async Task Execute_Should_ThrowException_When_EFormidlingServiceIsNull() var appSettings = new AppSettings { EnableEFormidling = true }; _appSettingsMock.Setup(x => x.Value).Returns(appSettings); - var serviceTask = new EFormidlingServiceTask(_loggerMock.Object, null, _appSettingsMock.Object); + var serviceTask = new EFormidlingServiceTask( + _loggerMock.Object, + _processReaderMock.Object, + _hostEnvironmentMock.Object, + null, + _appSettingsMock.Object + ); var instanceMutatorMock = new Mock(); instanceMutatorMock.Setup(x => x.Instance).Returns(instance); @@ -104,6 +116,137 @@ public async Task Execute_Should_Call_SendEFormidlingShipment_When_EFormidlingEn _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); } + [Fact] + public async Task Execute_Should_UseEnvironmentSpecificBpmnConfig_When_Configured() + { + // Arrange + Instance instance = GetInstance(); + + var eFormidlingConfig = new AltinnEFormidlingConfiguration + { + Enabled = + [ + new AltinnEnvironmentConfig { Environment = "prod", Value = "true" }, + new AltinnEnvironmentConfig { Environment = "staging", Value = "false" }, + ], + }; + + var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = eFormidlingConfig }; + _processReaderMock.Setup(x => x.GetAltinnTaskExtension("taskId")).Returns(taskExtension); + + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); + + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; + + // Act + await _serviceTask.Execute(parameters); + + // Assert + _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); + } + + [Fact] + public async Task Execute_Should_SkipExecution_When_BpmnConfigDisabled() + { + // Arrange + Instance instance = GetInstance(); + + var eFormidlingConfig = new AltinnEFormidlingConfiguration + { + Enabled = [new AltinnEnvironmentConfig { Environment = "prod", Value = "false" }], + }; + + var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = eFormidlingConfig }; + _processReaderMock.Setup(x => x.GetAltinnTaskExtension("taskId")).Returns(taskExtension); + + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); + + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; + + // Act + await _serviceTask.Execute(parameters); + + // Assert + _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(It.IsAny()), Times.Never); + _loggerMock.Verify( + x => + x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("EFormidling is disabled for task taskId")), + It.IsAny(), + It.Is>((v, t) => true) + ), + Times.Once + ); + } + + [Fact] + public async Task Execute_Should_BeEnabled_When_NoBpmnConfigExplicit() + { + // Arrange + Instance instance = GetInstance(); + + // No BPMN configuration (explicit null) + _processReaderMock.Setup(x => x.GetAltinnTaskExtension("taskId")).Returns((AltinnTaskExtension?)null); + + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); + + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; + + // Act + await _serviceTask.Execute(parameters); + + // Assert + _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); + _loggerMock.Verify( + x => + x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is( + (v, t) => + v.ToString()!.Contains("No eFormidling configuration found in BPMN for task taskId. Defaulting to enabled") + ), + It.IsAny(), + It.Is>((v, t) => true) + ), + Times.Once + ); + } + + [Fact] + public async Task Execute_Should_UseGlobalBpmnConfig_When_NoEnvironmentSpecific() + { + // Arrange + Instance instance = GetInstance(); + + var eFormidlingConfig = new AltinnEFormidlingConfiguration + { + Enabled = + [ + new AltinnEnvironmentConfig { Value = "true" }, // Global config (no env specified) + ], + }; + + var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = eFormidlingConfig }; + _processReaderMock.Setup(x => x.GetAltinnTaskExtension("taskId")).Returns(taskExtension); + + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); + + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; + + // Act + await _serviceTask.Execute(parameters); + + // Assert + _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); + } + private static Instance GetInstance() { return new Instance From a80248f23194608d290d40fdb3293588704e394e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Fri, 26 Sep 2025 17:17:06 +0200 Subject: [PATCH 35/60] Refactor configuration of eFormidling to enable setting up everything directly on service task. --- .../Extensions/ServiceCollectionExtensions.cs | 2 + .../DefaultEFormidlingReceivers.cs | 29 +-- .../DefaultEFormidlingService.cs | 54 ++-- .../EFormidlingConfigurationProvider.cs | 80 ++++++ .../Interface/IEFormidlingReceivers.cs | 8 +- .../Interface/IEFormidlingService.cs | 17 +- .../AltinnEFormidlingConfiguration.cs | 200 ++++++++++++++- .../ServiceTasks/EFormidlingServiceTask.cs | 42 ++-- .../ServiceTasks/PdfServiceTask.cs | 2 +- .../EFormidlingServiceTaskTests.cs | 9 +- .../ServiceTasks/Pdf/PdfServiceTaskTests.cs | 3 + .../DefaultEFormidlingServiceTests.cs | 33 ++- .../EFormidlingConfigurationProviderTests.cs | 238 ++++++++++++++++++ .../EFormidlingServiceTaskTests.cs | 37 ++- ...ouldNotChange_Unintentionally.verified.txt | 59 ++++- 15 files changed, 736 insertions(+), 77 deletions(-) create mode 100644 src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs create mode 100644 test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs diff --git a/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs index c83830c1e..d10a8325e 100644 --- a/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs @@ -40,6 +40,7 @@ public static void AddEFormidlingServices(this IServiceCollection servic { services.AddTransient(typeof(IEFormidlingReceivers), typeof(TR)); services.AddHttpClient(); + services.AddTransient(); services.AddTransient(); services.Configure( configuration.GetSection("EFormidlingClientSettings") @@ -63,6 +64,7 @@ public static void AddEFormidlingServices2(this IServiceCollection servi { services.AddTransient(typeof(IEFormidlingReceivers), typeof(TR)); services.AddHttpClient(); + services.AddTransient(); services.AddTransient(); services.Configure( configuration.GetSection("EFormidlingClientSettings") diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingReceivers.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingReceivers.cs index c14a3909f..bc9d1966d 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingReceivers.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingReceivers.cs @@ -1,5 +1,4 @@ using Altinn.App.Core.EFormidling.Interface; -using Altinn.App.Core.Internal.App; using Altinn.Common.EFormidlingClient.Models.SBD; using Altinn.Platform.Storage.Interface.Models; @@ -10,31 +9,25 @@ namespace Altinn.App.Core.EFormidling.Implementation; /// public class DefaultEFormidlingReceivers : IEFormidlingReceivers { - private readonly IAppMetadata _appMetadata; - - /// - /// Initializes a new instance of the class. - /// - /// Service for fetching application metadata - public DefaultEFormidlingReceivers(IAppMetadata appMetadata) - { - _appMetadata = appMetadata; - } - /// - public async Task> GetEFormidlingReceivers(Instance instance) + public Task> GetEFormidlingReceivers(Instance instance, string? receiverFromConfig = null) { - await Task.CompletedTask; + ArgumentNullException.ThrowIfNull(instance); + + if (string.IsNullOrWhiteSpace(receiverFromConfig)) + { + return Task.FromResult(new List()); + } - Identifier identifier = new Identifier + var identifier = new Identifier { // 0192 prefix for all Norwegian organisations. - Value = $"0192:{(await _appMetadata.GetApplicationMetadata()).EFormidling.Receiver.Trim()}", + Value = $"0192:{receiverFromConfig.Trim()}", Authority = "iso6523-actorid-upis", }; - Receiver receiver = new Receiver { Identifier = identifier }; + var receiver = new Receiver { Identifier = identifier }; - return new List { receiver }; + return Task.FromResult>([receiver]); } } diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs index 2ca8a5fe5..98665b012 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs @@ -8,6 +8,7 @@ using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Common.EFormidlingClient; @@ -34,6 +35,7 @@ public class DefaultEFormidlingService : IEFormidlingService private readonly IDataClient _dataClient; private readonly IEventsClient _eventClient; private readonly AppImplementationFactory _appImplementationFactory; + private readonly IEFormidlingConfigurationProvider _configurationProvider; /// /// Initializes a new instance of the class. @@ -45,6 +47,7 @@ public DefaultEFormidlingService( IDataClient dataClient, IEventsClient eventClient, IServiceProvider sp, + IEFormidlingConfigurationProvider configurationProvider, IOptions? appSettings = null, IOptions? platformSettings = null, IEFormidlingClient? eFormidlingClient = null, @@ -61,10 +64,22 @@ public DefaultEFormidlingService( _dataClient = dataClient; _eventClient = eventClient; _appImplementationFactory = sp.GetRequiredService(); + _configurationProvider = configurationProvider; } /// public async Task SendEFormidlingShipment(Instance instance) + { + await SendEFormidlingShipmentInternal(instance, await _configurationProvider.GetLegacyConfiguration()); + } + + /// + public async Task SendEFormidlingShipment(Instance instance, ValidAltinnEFormidlingConfiguration configuration) + { + await SendEFormidlingShipmentInternal(instance, configuration); + } + + private async Task SendEFormidlingShipmentInternal(Instance instance, ValidAltinnEFormidlingConfiguration config) { var metadata = _appImplementationFactory.Get(); if ( @@ -83,32 +98,32 @@ public async Task SendEFormidlingShipment(Instance instance) ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - string accessToken = _tokenGenerator.GenerateAccessToken( + string authToken = _userTokenProvider.GetUserToken(); + string eFormidlingAccessToken = _tokenGenerator.GenerateAccessToken( applicationMetadata.Org, applicationMetadata.AppIdentifier.App ); - string authzToken = _userTokenProvider.GetUserToken(); var requestHeaders = new Dictionary { - { "Authorization", $"{AuthorizationSchemes.Bearer} {authzToken}" }, - { General.EFormidlingAccessTokenHeaderName, accessToken }, + { "Authorization", $"{AuthorizationSchemes.Bearer} {authToken}" }, + { General.EFormidlingAccessTokenHeaderName, eFormidlingAccessToken }, { General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey }, }; string instanceGuid = instance.Id.Split("/")[1]; - StandardBusinessDocument sbd = await ConstructStandardBusinessDocument(instanceGuid, instance); + StandardBusinessDocument sbd = await ConstructStandardBusinessDocument(instanceGuid, instance, config); await _eFormidlingClient.CreateMessage(sbd, requestHeaders); (string metadataFilename, Stream stream) = await metadata.GenerateEFormidlingMetadata(instance); - using (stream) + await using (stream) { await _eFormidlingClient.UploadAttachment(stream, instanceGuid, metadataFilename, requestHeaders); } - await SendInstanceData(instance, requestHeaders, metadataFilename); + await SendInstanceData(instance, requestHeaders, metadataFilename, config); try { @@ -124,7 +139,8 @@ public async Task SendEFormidlingShipment(Instance instance) private async Task ConstructStandardBusinessDocument( string instanceGuid, - Instance instance + Instance instance, + ValidAltinnEFormidlingConfiguration config ) { if (_appSettings is null) @@ -145,12 +161,11 @@ Instance instance }; var eFormidlingReceivers = _appImplementationFactory.GetRequired(); - List receivers = await eFormidlingReceivers.GetEFormidlingReceivers(instance); - ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); + List receivers = await eFormidlingReceivers.GetEFormidlingReceivers(instance, config.Receiver); Scope scope = new Scope { - Identifier = appMetadata.EFormidling.Process, + Identifier = config.Process, InstanceIdentifier = Guid.NewGuid().ToString(), Type = "ConversationId", ScopeInformation = new List @@ -164,10 +179,10 @@ Instance instance DocumentIdentification documentIdentification = new DocumentIdentification { InstanceIdentifier = instanceGuid, - Standard = appMetadata.EFormidling.Standard, - TypeVersion = appMetadata.EFormidling.TypeVersion, + Standard = config.Standard, + TypeVersion = config.TypeVersion, CreationDateAndTime = completedTime, - Type = appMetadata.EFormidling.Type, + Type = config.Type, }; StandardBusinessDocumentHeader sbdHeader = new StandardBusinessDocumentHeader @@ -182,12 +197,12 @@ Instance instance StandardBusinessDocument sbd = new StandardBusinessDocument { StandardBusinessDocumentHeader = sbdHeader, - Arkivmelding = new Arkivmelding { Sikkerhetsnivaa = appMetadata.EFormidling.SecurityLevel }, + Arkivmelding = new Arkivmelding { Sikkerhetsnivaa = config.SecurityLevel }, }; - if (!string.IsNullOrEmpty(appMetadata.EFormidling.DPFShipmentType)) + if (!string.IsNullOrEmpty(config.DpfShipmentType)) { - sbd.Arkivmelding.DPF = new() { ForsendelsesType = appMetadata.EFormidling.DPFShipmentType }; + sbd.Arkivmelding.DPF = new() { ForsendelsesType = config.DpfShipmentType }; } return sbd; @@ -196,7 +211,8 @@ Instance instance private async Task SendInstanceData( Instance instance, Dictionary requestHeaders, - string eformidlingMetadataFilename + string eformidlingMetadataFilename, + ValidAltinnEFormidlingConfiguration config ) { ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); @@ -211,7 +227,7 @@ string eformidlingMetadataFilename foreach (DataElement dataElement in instance.Data.OrderBy(x => x.Created)) { - if (!applicationMetadata.EFormidling.DataTypes.Contains(dataElement.DataType)) + if (!config.DataTypes.Contains(dataElement.DataType)) { continue; } diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs b/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs new file mode 100644 index 000000000..c9b646fc1 --- /dev/null +++ b/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs @@ -0,0 +1,80 @@ +using Altinn.App.Core.Constants; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Hosting; + +namespace Altinn.App.Core.EFormidling.Implementation; + +/// +/// Provides validated eFormidling configuration from various sources. +/// +public interface IEFormidlingConfigurationProvider +{ + /// + /// Gets validated eFormidling configuration from ApplicationMetadata (legacy). + /// + /// Validated eFormidling configuration. + Task GetLegacyConfiguration(); + + /// + /// Gets validated eFormidling configuration from BPMN task extension. + /// + /// The task ID to get configuration for. + /// Validated eFormidling configuration. + Task GetBpmnConfiguration(string taskId); +} + +/// +/// Provides eFormidling configuration from various sources (ApplicationMetadata or BPMN). +/// +internal sealed class EFormidlingConfigurationProvider : IEFormidlingConfigurationProvider +{ + private readonly IAppMetadata _appMetadata; + private readonly IProcessReader _processReader; + private readonly IHostEnvironment _hostEnvironment; + + public EFormidlingConfigurationProvider( + IAppMetadata appMetadata, + IProcessReader processReader, + IHostEnvironment hostEnvironment + ) + { + _appMetadata = appMetadata; + _processReader = processReader; + _hostEnvironment = hostEnvironment; + } + + public async Task GetLegacyConfiguration() + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + EFormidlingContract? eFormidling = applicationMetadata.EFormidling; + + return new ValidAltinnEFormidlingConfiguration( + eFormidling.Receiver, + eFormidling.Process, + eFormidling.Standard, + eFormidling.TypeVersion, + eFormidling.Type, + eFormidling.SecurityLevel, + eFormidling.DPFShipmentType, + eFormidling.DataTypes?.ToList() ?? [] + ); + } + + public Task GetBpmnConfiguration(string taskId) + { + AltinnTaskExtension? taskExtension = _processReader.GetAltinnTaskExtension(taskId); + AltinnEFormidlingConfiguration? eFormidlingConfig = taskExtension?.EFormidlingConfiguration; + + if (eFormidlingConfig == null) + throw new InvalidOperationException($"No eFormidling configuration found in BPMN for task {taskId}"); + + HostingEnvironment env = AltinnEnvironments.GetHostingEnvironment(_hostEnvironment); + ValidAltinnEFormidlingConfiguration validConfig = eFormidlingConfig.Validate(env); + + return Task.FromResult(validConfig); + } +} diff --git a/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs index 3c07c7695..249acdf19 100644 --- a/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs +++ b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs @@ -11,12 +11,16 @@ namespace Altinn.App.Core.EFormidling.Interface; public interface IEFormidlingReceivers { /// - /// Gets a list of eFormidling shipment receivers + /// Gets a list of eFormidling shipment receivers. /// /// + /// /// Note that the identifier value property on the receiver objects should be prefixed with `0192:` for Norwegian organisations. + /// /// /// Instance data + /// Optional receiver organization number from static configuration. /// List of eFormidling receivers - public Task> GetEFormidlingReceivers(Instance instance); + /// Thrown when is null + public Task> GetEFormidlingReceivers(Instance instance, string? receiverFromConfig = null); } diff --git a/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingService.cs index 52b8312b6..1556a05f7 100644 --- a/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingService.cs +++ b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingService.cs @@ -1,3 +1,4 @@ +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.EFormidling.Interface; @@ -8,9 +9,23 @@ namespace Altinn.App.Core.EFormidling.Interface; public interface IEFormidlingService { /// - /// Send the eFormidling shipment + /// Send the eFormidling shipment using ApplicationMetadata configuration (legacy) /// /// Instance data /// public Task SendEFormidlingShipment(Instance instance); + + /// + /// Send the eFormidling shipment with explicit configuration context. + /// Default implementation calls the legacy method for backward compatibility. + /// Override this method to support BPMN-based configuration. + /// + /// Instance data + /// A valid config for eFormidling. + /// + public Task SendEFormidlingShipment(Instance instance, ValidAltinnEFormidlingConfiguration configuration) + { + // Default implementation for backward compatibility - calls legacy method. Only meant to avoid forcing implementers to implement the new method. + return SendEFormidlingShipment(instance); + } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs index f29a25e8b..70e41309f 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs @@ -1,18 +1,210 @@ +using System.Diagnostics.CodeAnalysis; using System.Xml.Serialization; +using Altinn.App.Core.Constants; +using Altinn.App.Core.Internal.App; namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; /// -/// Configuration properties for eFormidling in a process task +/// Configuration properties for eFormidling in a process task. All properties support environment-specific values using 'env' attributes. /// public class AltinnEFormidlingConfiguration { /// /// Controls whether eFormidling should be enabled for this task. - /// Supports environment-specific configuration through the 'env' attribute. - /// If no environment is specified, the value applies to all environments. - /// Environment-specific values take precedence over global values. /// [XmlElement(ElementName = "enabled", Namespace = "http://altinn.no/process")] public List Enabled { get; set; } = []; + + /// + /// The organization number of the receiver of the eFormidling message. Can be omitted. + /// + [XmlElement(ElementName = "receiver", Namespace = "http://altinn.no/process")] + public List Receiver { get; set; } = []; + + /// + /// The process identifier for the eFormidling message. + /// + [XmlElement(ElementName = "process", Namespace = "http://altinn.no/process")] + public List Process { get; set; } = []; + + /// + /// The standard identifier for the document. + /// + [XmlElement(ElementName = "standard", Namespace = "http://altinn.no/process")] + public List Standard { get; set; } = []; + + /// + /// The type version of the document. + /// + [XmlElement(ElementName = "typeVersion", Namespace = "http://altinn.no/process")] + public List TypeVersion { get; set; } = []; + + /// + /// The type of the document. + /// + [XmlElement(ElementName = "type", Namespace = "http://altinn.no/process")] + public List Type { get; set; } = []; + + /// + /// The security level for the eFormidling message. + /// + [XmlElement(ElementName = "securityLevel", Namespace = "http://altinn.no/process")] + public List SecurityLevel { get; set; } = []; + + /// + /// Optional DPF shipment type for the eFormidling message. + /// + [XmlElement(ElementName = "dpfShipmentType", Namespace = "http://altinn.no/process")] + public List DpfShipmentType { get; set; } = []; + + /// + /// List of data type IDs to include in the eFormidling shipment. + /// + [XmlElement(ElementName = "dataTypes", Namespace = "http://altinn.no/process")] + public List DataTypes { get; set; } = []; + + internal ValidAltinnEFormidlingConfiguration Validate(HostingEnvironment env) + { + List? errorMessages = null; + + string? receiver = GetConfigValue(Process, env); + + string? process = GetConfigValue(Process, env); + if (process.IsNullOrWhitespace(ref errorMessages, $"No Process configuration found for environment {env}")) + ThrowApplicationConfigException(errorMessages); + + string? standard = GetConfigValue(Standard, env); + if (standard.IsNullOrWhitespace(ref errorMessages, $"No Standard configuration found for environment {env}")) + ThrowApplicationConfigException(errorMessages); + + string? typeVersion = GetConfigValue(TypeVersion, env); + if ( + typeVersion.IsNullOrWhitespace( + ref errorMessages, + $"No TypeVersion configuration found for environment {env}" + ) + ) + ThrowApplicationConfigException(errorMessages); + + string? type = GetConfigValue(Type, env); + if (type.IsNullOrWhitespace(ref errorMessages, $"No Type configuration found for environment {env}")) + ThrowApplicationConfigException(errorMessages); + + string? securityLevelValue = GetConfigValue(SecurityLevel, env); + if ( + securityLevelValue.IsNullOrWhitespace( + ref errorMessages, + $"No SecurityLevel configuration found for environment {env}" + ) + ) + ThrowApplicationConfigException(errorMessages); + + if (!int.TryParse(securityLevelValue, out int securityLevel)) + { + errorMessages ??= new List(1); + errorMessages.Add($"SecurityLevel must be a valid integer for environment {env}"); + ThrowApplicationConfigException(errorMessages); + } + + string? dpfShipmentType = GetConfigValue(DpfShipmentType, env); + + List dataTypes = GetDataTypesForEnvironment(env); + + return new ValidAltinnEFormidlingConfiguration( + receiver, + process, + standard, + typeVersion, + type, + securityLevel, + dpfShipmentType, + dataTypes + ); + } + + [DoesNotReturn] + private static void ThrowApplicationConfigException(List errorMessages) + { + throw new ApplicationConfigException( + "eFormidling process task configuration is not valid: " + string.Join(",\n", errorMessages) + ); + } + + private static string? GetConfigValue(List configs, HostingEnvironment env) + { + AltinnEnvironmentConfig? config = AltinnTaskExtension.GetConfigForEnvironment(env, configs); + return config?.Value; + } + + /// + /// Gets the data type IDs for the specified environment. + /// Returns environment-specific configuration if available, otherwise returns global configuration. + /// + private List GetDataTypesForEnvironment(HostingEnvironment env) + { + if (DataTypes.Count == 0) + return []; + + const string globalKey = "__global__"; + Dictionary> lookup = new(); + + foreach (var dataTypesConfig in DataTypes) + { + var key = string.IsNullOrWhiteSpace(dataTypesConfig.Environment) + ? globalKey + : AltinnEnvironments.GetHostingEnvironment(dataTypesConfig.Environment).ToString(); + + if (lookup.TryGetValue(key, out var existingList)) + { + existingList.AddRange(dataTypesConfig.DataTypeIds); + } + else + { + lookup[key] = new List(dataTypesConfig.DataTypeIds); + } + } + + return lookup.GetValueOrDefault(env.ToString()) ?? lookup.GetValueOrDefault(globalKey) ?? []; + } +} + +/// +/// Configuration for data types in eFormidling with environment support +/// +public class AltinnEFormidlingDataTypesConfig +{ + /// + /// The environment this configuration applies to. If omitted, applies to all environments. + /// + [XmlAttribute("env")] + public string? Environment { get; set; } + + /// + /// List of data type IDs to include in eFormidling for this environment. + /// + [XmlElement(ElementName = "dataType", Namespace = "http://altinn.no/process")] + public List DataTypeIds { get; set; } = []; } + +/// +/// Validated eFormidling configuration with all required fields guaranteed to be non-null +/// +/// The organisation number of the receiver. Only Norwegian organisations supported. (Can be omitted) +/// The process identifier for the eFormidling message +/// The standard identifier for the document +/// The type version of the document +/// The type of the document +/// The security level for the eFormidling message +/// Optional DPF shipment type for the eFormidling message +/// List of data type IDs to include in the eFormidling shipment +public readonly record struct ValidAltinnEFormidlingConfiguration( + string? Receiver, + string Process, + string Standard, + string TypeVersion, + string Type, + int SecurityLevel, + string? DpfShipmentType, + List DataTypes +); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index ddcdee5f9..fd66b8698 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -1,11 +1,10 @@ -using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; +using Altinn.App.Core.EFormidling.Implementation; using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; @@ -17,10 +16,10 @@ internal interface IEFormidlingServiceTask : IServiceTask { } internal sealed class EFormidlingServiceTask : IEFormidlingServiceTask { private readonly ILogger _logger; - private readonly IEFormidlingService? _eFormidlingService; - private readonly IOptions? _appSettings; private readonly IProcessReader _processReader; private readonly IHostEnvironment _hostEnvironment; + private readonly IEFormidlingService? _eFormidlingService; + private readonly IEFormidlingConfigurationProvider? _eFormidlingConfigurationProvider; /// /// Initializes a new instance of the class. @@ -30,14 +29,14 @@ public EFormidlingServiceTask( IProcessReader processReader, IHostEnvironment hostEnvironment, IEFormidlingService? eFormidlingService = null, - IOptions? appSettings = null + IEFormidlingConfigurationProvider? eFormidlingConfigurationProvider = null ) { _logger = logger; _processReader = processReader; _hostEnvironment = hostEnvironment; _eFormidlingService = eFormidlingService; - _appSettings = appSettings; + _eFormidlingConfigurationProvider = eFormidlingConfigurationProvider; } /// @@ -46,10 +45,23 @@ public EFormidlingServiceTask( /// public async Task Execute(ServiceTaskContext context) { + if (_eFormidlingService is null) + { + throw new ProcessException( + $"No implementation of {nameof(IEFormidlingService)} has been added to the DI container. Remember to add eFormidling services. Use AddEFormidlingServices2 to register eFormidling services." + ); + } + + if (_eFormidlingConfigurationProvider is null) + { + throw new ProcessException( + $"No implementation of {nameof(IEFormidlingConfigurationProvider)} has been added to the DI container. Use AddEFormidlingServices2 to register eFormidling services." + ); + } + string taskId = context.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; Instance instance = context.InstanceDataMutator.Instance; - // Check BPMN configuration first, then fall back to appsettings for backward compatibility if (!IsEFormidlingEnabled(taskId)) { _logger.LogInformation( @@ -59,20 +71,21 @@ public async Task Execute(ServiceTaskContext context) return ServiceTaskResult.Success(); } - if (_eFormidlingService is null) - { - throw new ProcessException( - $"No implementation of {nameof(IEFormidlingService)} has been added to the DI container." - ); - } + ValidAltinnEFormidlingConfiguration configuration = + await _eFormidlingConfigurationProvider.GetBpmnConfiguration(taskId); _logger.LogDebug("Calling eFormidlingService for eFormidling Service Task {TaskId}.", taskId); - await _eFormidlingService.SendEFormidlingShipment(instance); + await _eFormidlingService.SendEFormidlingShipment(instance, configuration); _logger.LogDebug("Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", taskId); return ServiceTaskResult.Success(); } + /// + /// Checks if eFormidling service task is enabled. Even if eFormidling has been added as a service task to the process, it can be disabled for certain environments. + /// + /// + /// private bool IsEFormidlingEnabled(string taskId) { AltinnTaskExtension? altinnTaskExtension = _processReader.GetAltinnTaskExtension(taskId); @@ -88,7 +101,6 @@ private bool IsEFormidlingEnabled(string taskId) return true; } - // Use environment-aware BPMN configuration HostingEnvironment env = AltinnEnvironments.GetHostingEnvironment(_hostEnvironment); AltinnEnvironmentConfig? enabledConfig = AltinnTaskExtension.GetConfigForEnvironment( env, diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs index d4c647258..4615726c0 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -21,9 +21,9 @@ internal sealed class PdfServiceTask : IPdfServiceTask /// public PdfServiceTask(IPdfService pdfService, IProcessReader processReader, ILogger logger) { - _logger = logger; _pdfService = pdfService; _processReader = processReader; + _logger = logger; } /// diff --git a/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs b/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs index 8be1d0a04..9f94368d1 100644 --- a/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs +++ b/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs @@ -1,8 +1,10 @@ using System.Net; using Altinn.App.Api.Tests.Data; using Altinn.App.Api.Tests.Mocks; +using Altinn.App.Core.EFormidling.Implementation; using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.Platform.Storage.Interface.Models; using Argon; using FluentAssertions; @@ -23,6 +25,8 @@ public class EFormidlingServiceTaskTests : ApiTestBase, IClassFixture _eFormidlingServiceMock = new Mock(); + private readonly Mock _eFormidlingConfigurationProviderMock = + new Mock(); public EFormidlingServiceTaskTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) : base(factory, outputHelper) @@ -30,6 +34,7 @@ public EFormidlingServiceTaskTests(WebApplicationFactory factory, ITest OverrideServicesForAllTests = (services) => { services.AddSingleton(_eFormidlingServiceMock.Object); + services.AddSingleton(_eFormidlingConfigurationProviderMock.Object); services.AddTransient(); }; @@ -126,7 +131,9 @@ public async Task Does_Not_Change_Task_When_EFormidling_Fails() // Setup eFormidling service to throw exception _eFormidlingServiceMock - .Setup(x => x.SendEFormidlingShipment(It.IsAny())) + .Setup(x => + x.SendEFormidlingShipment(It.IsAny(), It.IsAny()) + ) .ThrowsAsync(new Exception()); using HttpClient client = GetRootedUserClient(Org, App); diff --git a/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs index a06dd5120..0c7cc9e25 100644 --- a/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs @@ -2,6 +2,7 @@ using System.Text; using Altinn.App.Api.Models; using Altinn.App.Api.Tests.Data; +using Altinn.App.Core.EFormidling.Implementation; using Altinn.App.Core.EFormidling.Interface; using Altinn.Platform.Storage.Interface.Models; using Argon; @@ -26,9 +27,11 @@ public PdfServiceTaskTests(WebApplicationFactory factory, ITestOutputHe : base(factory, outputHelper) { var eFormidlingServiceMock = new Mock(); + var eFormidlingConfigurationProviderMock = new Mock(); OverrideServicesForAllTests = (services) => { services.AddSingleton(eFormidlingServiceMock.Object); + services.AddSingleton(eFormidlingConfigurationProviderMock.Object); }; TestData.DeleteInstanceAndData(Org, App, InstanceOwnerPartyId, _instanceGuid); diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs index ac19829f5..4310b87d8 100644 --- a/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs @@ -8,6 +8,8 @@ using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Common.EFormidlingClient; @@ -16,6 +18,7 @@ using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -71,6 +74,9 @@ private Fixture CreateFixture( var platformSettings = Options.Create(new PlatformSettings { SubscriptionKey = "subscription-key" }); var eFormidlingClient = new Mock(); var tokenGenerator = new Mock(); + var processReader = new Mock(); + var hostEnvironment = new Mock(); + var configurationProvider = new Mock(); var instanceGuid = Guid.Parse("41C1099C-7EDD-47F5-AD1F-6267B497796F"); var instance = new Instance @@ -151,7 +157,9 @@ private Fixture CreateFixture( ); tokenGenerator.Setup(t => t.GenerateAccessToken("ttd", "test-app")).Returns("access-token"); userTokenProvider.Setup(u => u.GetUserToken()).Returns("authz-token"); - eFormidlingReceivers.Setup(er => er.GetEFormidlingReceivers(instance)).ReturnsAsync(new List()); + eFormidlingReceivers + .Setup(er => er.GetEFormidlingReceivers(instance, It.IsAny())) + .ReturnsAsync(new List()); eFormidlingMetadata .Setup(em => em.GenerateEFormidlingMetadata(instance)) .ReturnsAsync(() => @@ -172,6 +180,22 @@ private Fixture CreateFixture( ) .ReturnsAsync(Stream.Null); + // Setup configuration provider to return legacy configuration + configurationProvider + .Setup(cp => cp.GetLegacyConfiguration()) + .ReturnsAsync( + new ValidAltinnEFormidlingConfiguration( + null, // Receiver (matches the test data which has no Receiver) + "urn:no:difi:profile:arkivmelding:plan:3.0", // Process + "urn:no:difi:arkivmelding:xsd::arkivmelding", // Standard + "v8", // TypeVersion + "arkivmelding", // Type + 3, // SecurityLevel + null, // DpfShipmentType (matches test data which has no DPFShipmentType) + new List { ModelDataType, FileAttachmentsDataType } // DataTypes + ) + ); + setupEFormidlingClient?.Invoke(eFormidlingClient); services.TryAddTransient(_ => userTokenProvider.Object); @@ -184,6 +208,9 @@ private Fixture CreateFixture( services.TryAddTransient(_ => eFormidlingClient.Object); services.TryAddTransient(_ => tokenGenerator.Object); services.TryAddTransient(_ => eFormidlingMetadata.Object); + services.TryAddTransient(_ => processReader.Object); + services.TryAddTransient(_ => hostEnvironment.Object); + services.TryAddTransient(_ => configurationProvider.Object); services.TryAddTransient(); @@ -213,7 +240,7 @@ public async Task SendEFormidlingShipment() fixture.Mock().Verify(a => a.GetApplicationMetadata()); fixture.Mock().Verify(t => t.GenerateAccessToken("ttd", "test-app")); fixture.Mock().Verify(u => u.GetUserToken()); - fixture.Mock().Verify(er => er.GetEFormidlingReceivers(instance)); + fixture.Mock().Verify(er => er.GetEFormidlingReceivers(instance, It.IsAny())); fixture.Mock().Verify(em => em.GenerateEFormidlingMetadata(instance)); var eFormidlingClient = fixture.Mock(); eFormidlingClient.Verify(ec => ec.CreateMessage(It.IsAny(), expectedReqHeaders)); @@ -348,7 +375,7 @@ public async Task SendEFormidlingShipment_throws_exception_if_send_fails() fixture.Mock().Verify(a => a.GetApplicationMetadata()); fixture.Mock().Verify(t => t.GenerateAccessToken("ttd", "test-app")); fixture.Mock().Verify(u => u.GetUserToken()); - fixture.Mock().Verify(er => er.GetEFormidlingReceivers(instance)); + fixture.Mock().Verify(er => er.GetEFormidlingReceivers(instance, It.IsAny())); fixture.Mock().Verify(em => em.GenerateEFormidlingMetadata(instance)); var eFormidlingClient = fixture.Mock(); eFormidlingClient.Verify(ec => ec.CreateMessage(It.IsAny(), expectedReqHeaders)); diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs new file mode 100644 index 000000000..7fc4b41f8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs @@ -0,0 +1,238 @@ +using Altinn.App.Core.EFormidling.Implementation; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Hosting; +using Moq; + +namespace Altinn.App.Core.Tests.Eformidling.Implementation; + +public class EFormidlingConfigurationProviderTests +{ + private readonly Mock _appMetadataMock = new(); + private readonly Mock _processReaderMock = new(); + private readonly Mock _hostEnvironmentMock = new(); + private readonly EFormidlingConfigurationProvider _provider; + + public EFormidlingConfigurationProviderTests() + { + _provider = new EFormidlingConfigurationProvider( + _appMetadataMock.Object, + _processReaderMock.Object, + _hostEnvironmentMock.Object + ); + } + + [Fact] + public async Task GetLegacyConfiguration_ReturnsConfigFromApplicationMetadata() + { + // Arrange + var applicationMetadata = new ApplicationMetadata("tdd/test") + { + EFormidling = new EFormidlingContract + { + Process = "urn:no:difi:profile:arkivmelding:administrasjon:ver1.0", + Standard = "urn:no:difi:arkivmelding:xsd::arkivmelding", + TypeVersion = "2.0", + Type = "arkivmelding", + SecurityLevel = 3, + DPFShipmentType = "altinn3.skjema", + DataTypes = new List { "datatype1", "datatype2" }, + }, + }; + + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + var result = await _provider.GetLegacyConfiguration(); + + // Assert + result.Should().NotBeNull(); + result.Process.Should().Be("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0"); + result.Standard.Should().Be("urn:no:difi:arkivmelding:xsd::arkivmelding"); + result.TypeVersion.Should().Be("2.0"); + result.Type.Should().Be("arkivmelding"); + result.SecurityLevel.Should().Be(3); + result.DpfShipmentType.Should().Be("altinn3.skjema"); + result.DataTypes.Should().BeEquivalentTo(new[] { "datatype1", "datatype2" }); + } + + [Fact] + public async Task GetLegacyConfiguration_WithNullDataTypes_ReturnsConfigWithEmptyDataTypes() + { + // Arrange + var applicationMetadata = new ApplicationMetadata("tdd/test") + { + EFormidling = new EFormidlingContract + { + Process = "urn:no:difi:profile:arkivmelding:administrasjon:ver1.0", + Standard = "urn:no:difi:arkivmelding:xsd::arkivmelding", + TypeVersion = "2.0", + Type = "arkivmelding", + SecurityLevel = 3, + DataTypes = null, + }, + }; + + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + var result = await _provider.GetLegacyConfiguration(); + + // Assert + result.DataTypes.Should().BeEmpty(); + } + + [Fact] + public async Task GetBpmnConfiguration_ReturnsConfigFromBpmnTask() + { + // Arrange + var taskId = "Task_1"; + + var taskExtension = new AltinnTaskExtension + { + EFormidlingConfiguration = new AltinnEFormidlingConfiguration + { + Process = CreateEnvironmentConfig("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0"), + Standard = CreateEnvironmentConfig("urn:no:difi:arkivmelding:xsd::arkivmelding"), + TypeVersion = CreateEnvironmentConfig("2.0"), + Type = CreateEnvironmentConfig("arkivmelding"), + SecurityLevel = CreateEnvironmentConfig("3"), + DpfShipmentType = CreateEnvironmentConfig("altinn3.skjema"), + }, + }; + + _processReaderMock.Setup(x => x.GetAltinnTaskExtension(taskId)).Returns(taskExtension); + _hostEnvironmentMock.Setup(x => x.EnvironmentName).Returns("Production"); + + // Act + var result = await _provider.GetBpmnConfiguration(taskId); + + // Assert + result.Should().NotBeNull(); + result.Process.Should().Be("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0"); + result.Standard.Should().Be("urn:no:difi:arkivmelding:xsd::arkivmelding"); + result.TypeVersion.Should().Be("2.0"); + result.Type.Should().Be("arkivmelding"); + result.SecurityLevel.Should().Be(3); + result.DpfShipmentType.Should().Be("altinn3.skjema"); + } + + [Fact] + public async Task GetBpmnConfiguration_NoTaskExtension_ThrowsInvalidOperationException() + { + // Arrange + var taskId = "Task_1"; + + _processReaderMock.Setup(x => x.GetAltinnTaskExtension(taskId)).Returns((AltinnTaskExtension?)null); + + // Act & Assert + var act = async () => await _provider.GetBpmnConfiguration(taskId); + await act.Should() + .ThrowAsync() + .WithMessage("No eFormidling configuration found in BPMN for task Task_1"); + } + + [Fact] + public async Task GetBpmnConfiguration_NoEFormidlingConfiguration_ThrowsInvalidOperationException() + { + // Arrange + var taskId = "Task_1"; + + var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = null }; + + _processReaderMock.Setup(x => x.GetAltinnTaskExtension(taskId)).Returns(taskExtension); + + // Act & Assert + var act = async () => await _provider.GetBpmnConfiguration(taskId); + await act.Should() + .ThrowAsync() + .WithMessage("No eFormidling configuration found in BPMN for task Task_1"); + } + + [Fact] + public async Task GetBpmnConfiguration_MissingRequiredConfig_ThrowsApplicationConfigException() + { + // Arrange + var taskId = "Task_1"; + + var taskExtension = new AltinnTaskExtension + { + EFormidlingConfiguration = new AltinnEFormidlingConfiguration + { + Process = CreateEnvironmentConfig("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0"), + // Missing Standard, TypeVersion, Type, SecurityLevel + }, + }; + + _processReaderMock.Setup(x => x.GetAltinnTaskExtension(taskId)).Returns(taskExtension); + _hostEnvironmentMock.Setup(x => x.EnvironmentName).Returns("Production"); + + // Act & Assert + var act = async () => await _provider.GetBpmnConfiguration(taskId); + await act.Should() + .ThrowAsync() + .WithMessage("*No Standard configuration found for environment Production*"); + } + + [Fact] + public async Task GetBpmnConfiguration_WithDataTypes_ReturnsDataTypesForEnvironment() + { + // Arrange + var taskId = "Task_1"; + + var eFormidlingConfig = new AltinnEFormidlingConfiguration + { + Process = CreateEnvironmentConfig("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0"), + Standard = CreateEnvironmentConfig("urn:no:difi:arkivmelding:xsd::arkivmelding"), + TypeVersion = CreateEnvironmentConfig("2.0"), + Type = CreateEnvironmentConfig("arkivmelding"), + SecurityLevel = CreateEnvironmentConfig("3"), + DataTypes = new List + { + new() + { + Environment = "Production", + DataTypeIds = new List { "datatype1" }, + }, + new() + { + Environment = "Development", + DataTypeIds = new List { "datatype2" }, + }, + }, + }; + + var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = eFormidlingConfig }; + + _processReaderMock.Setup(x => x.GetAltinnTaskExtension(taskId)).Returns(taskExtension); + _hostEnvironmentMock.Setup(x => x.EnvironmentName).Returns("Production"); + + // Act + var result = await _provider.GetBpmnConfiguration(taskId); + + // Assert + result.DataTypes.Should().BeEquivalentTo(new[] { "datatype1" }); + } + + [Fact] + public void GetBpmnConfiguration_NullTaskId_ThrowsArgumentNullException() + { + // Act & Assert + var act = async () => await _provider.GetBpmnConfiguration(null!); + act.Should().ThrowAsync(); + } + + // Note: Test for unknown source is no longer needed with direct method approach + + private static List CreateEnvironmentConfig(string value) + { + return new List + { + new() { Environment = "Production", Value = value }, + }; + } +} 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 e9e0ffbe3..d028518f6 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs @@ -1,7 +1,7 @@ using Altinn.App.Core.Configuration; +using Altinn.App.Core.EFormidling.Implementation; using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Features; -using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; @@ -20,6 +20,7 @@ public class EFormidlingServiceTaskTests private readonly Mock> _appSettingsMock = new(); private readonly Mock _processReaderMock = new(); private readonly Mock _hostEnvironmentMock = new(); + private readonly Mock _eFormidlingConfigurationProvider = new(); private readonly EFormidlingServiceTask _serviceTask; public EFormidlingServiceTaskTests() @@ -30,7 +31,7 @@ public EFormidlingServiceTaskTests() _processReaderMock.Object, _hostEnvironmentMock.Object, _eFormidlingServiceMock.Object, - _appSettingsMock.Object + _eFormidlingConfigurationProvider.Object ); } @@ -49,7 +50,10 @@ public async Task Execute_Should_BeEnabled_When_NoBpmnConfig() await _serviceTask.Execute(parameters); // Assert - _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); + _eFormidlingServiceMock.Verify( + x => x.SendEFormidlingShipment(instance, It.IsAny()), + Times.Once + ); _loggerMock.Verify( x => x.Log( @@ -83,7 +87,7 @@ public async Task Execute_Should_ThrowException_When_EFormidlingServiceIsNull() _processReaderMock.Object, _hostEnvironmentMock.Object, null, - _appSettingsMock.Object + _eFormidlingConfigurationProvider.Object ); var instanceMutatorMock = new Mock(); @@ -113,7 +117,10 @@ public async Task Execute_Should_Call_SendEFormidlingShipment_When_EFormidlingEn await _serviceTask.Execute(parameters); // Assert - _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); + _eFormidlingServiceMock.Verify( + x => x.SendEFormidlingShipment(instance, It.IsAny()), + Times.Once + ); } [Fact] @@ -143,7 +150,10 @@ public async Task Execute_Should_UseEnvironmentSpecificBpmnConfig_When_Configure await _serviceTask.Execute(parameters); // Assert - _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); + _eFormidlingServiceMock.Verify( + x => x.SendEFormidlingShipment(instance, It.IsAny()), + Times.Once + ); } [Fact] @@ -201,7 +211,10 @@ public async Task Execute_Should_BeEnabled_When_NoBpmnConfigExplicit() await _serviceTask.Execute(parameters); // Assert - _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); + _eFormidlingServiceMock.Verify( + x => x.SendEFormidlingShipment(instance, It.IsAny()), + Times.Once + ); _loggerMock.Verify( x => x.Log( @@ -209,7 +222,10 @@ public async Task Execute_Should_BeEnabled_When_NoBpmnConfigExplicit() It.IsAny(), It.Is( (v, t) => - v.ToString()!.Contains("No eFormidling configuration found in BPMN for task taskId. Defaulting to enabled") + v.ToString()! + .Contains( + "No eFormidling configuration found in BPMN for task taskId. Defaulting to enabled" + ) ), It.IsAny(), It.Is>((v, t) => true) @@ -244,7 +260,10 @@ public async Task Execute_Should_UseGlobalBpmnConfig_When_NoEnvironmentSpecific( await _serviceTask.Execute(parameters); // Assert - _eFormidlingServiceMock.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); + _eFormidlingServiceMock.Verify( + x => x.SendEFormidlingShipment(instance, It.IsAny()), + Times.Once + ); } private static Instance GetInstance() diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 8ccaecef6..b5c6573e9 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -144,13 +144,14 @@ namespace Altinn.App.Core.EFormidling.Implementation { public class DefaultEFormidlingReceivers : Altinn.App.Core.EFormidling.Interface.IEFormidlingReceivers { - public DefaultEFormidlingReceivers(Altinn.App.Core.Internal.App.IAppMetadata appMetadata) { } - public System.Threading.Tasks.Task> GetEFormidlingReceivers(Altinn.Platform.Storage.Interface.Models.Instance instance) { } + public DefaultEFormidlingReceivers() { } + public System.Threading.Tasks.Task> GetEFormidlingReceivers(Altinn.Platform.Storage.Interface.Models.Instance instance, string? receiverFromConfig = null) { } } public class DefaultEFormidlingService : Altinn.App.Core.EFormidling.Interface.IEFormidlingService { - public DefaultEFormidlingService(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Internal.Auth.IUserTokenProvider userTokenProvider, Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient, Altinn.App.Core.Internal.Events.IEventsClient eventClient, System.IServiceProvider sp, Microsoft.Extensions.Options.IOptions? appSettings = null, Microsoft.Extensions.Options.IOptions? platformSettings = null, Altinn.Common.EFormidlingClient.IEFormidlingClient? eFormidlingClient = null, Altinn.Common.AccessTokenClient.Services.IAccessTokenGenerator? tokenGenerator = null) { } + public DefaultEFormidlingService(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Internal.Auth.IUserTokenProvider userTokenProvider, Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient, Altinn.App.Core.Internal.Events.IEventsClient eventClient, System.IServiceProvider sp, Altinn.App.Core.EFormidling.Implementation.IEFormidlingConfigurationProvider configurationProvider, Microsoft.Extensions.Options.IOptions? appSettings = null, Microsoft.Extensions.Options.IOptions? platformSettings = null, Altinn.Common.EFormidlingClient.IEFormidlingClient? eFormidlingClient = null, Altinn.Common.AccessTokenClient.Services.IAccessTokenGenerator? tokenGenerator = null) { } public System.Threading.Tasks.Task SendEFormidlingShipment(Altinn.Platform.Storage.Interface.Models.Instance instance) { } + public System.Threading.Tasks.Task SendEFormidlingShipment(Altinn.Platform.Storage.Interface.Models.Instance instance, Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties.ValidAltinnEFormidlingConfiguration configuration) { } } public class EformidlingDeliveryException : Altinn.App.Core.Exceptions.AltinnException { @@ -171,6 +172,11 @@ namespace Altinn.App.Core.EFormidling.Implementation public string EventType { get; } public System.Threading.Tasks.Task ProcessEvent(Altinn.App.Core.Models.CloudEvent cloudEvent) { } } + public interface IEFormidlingConfigurationProvider + { + System.Threading.Tasks.Task GetBpmnConfiguration(string taskId); + System.Threading.Tasks.Task GetLegacyConfiguration(); + } } namespace Altinn.App.Core.EFormidling.Interface { @@ -183,11 +189,12 @@ namespace Altinn.App.Core.EFormidling.Interface } public interface IEFormidlingReceivers { - System.Threading.Tasks.Task> GetEFormidlingReceivers(Altinn.Platform.Storage.Interface.Models.Instance instance); + System.Threading.Tasks.Task> GetEFormidlingReceivers(Altinn.Platform.Storage.Interface.Models.Instance instance, string? receiverFromConfig = null); } public interface IEFormidlingService { System.Threading.Tasks.Task SendEFormidlingShipment(Altinn.Platform.Storage.Interface.Models.Instance instance); + System.Threading.Tasks.Task SendEFormidlingShipment(Altinn.Platform.Storage.Interface.Models.Instance instance, Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties.ValidAltinnEFormidlingConfiguration configuration); } } namespace Altinn.App.Core.Exceptions @@ -3201,6 +3208,36 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties [System.Xml.Serialization.XmlText] public string Value { get; set; } } + public class AltinnEFormidlingConfiguration + { + public AltinnEFormidlingConfiguration() { } + [System.Xml.Serialization.XmlElement(ElementName="dataTypes", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List DataTypes { get; set; } + [System.Xml.Serialization.XmlElement(ElementName="dpfShipmentType", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List DpfShipmentType { get; set; } + [System.Xml.Serialization.XmlElement(ElementName="enabled", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List Enabled { get; set; } + [System.Xml.Serialization.XmlElement(ElementName="process", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List Process { get; set; } + [System.Xml.Serialization.XmlElement(ElementName="receiver", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List Receiver { get; set; } + [System.Xml.Serialization.XmlElement(ElementName="securityLevel", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List SecurityLevel { get; set; } + [System.Xml.Serialization.XmlElement(ElementName="standard", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List Standard { get; set; } + [System.Xml.Serialization.XmlElement(ElementName="type", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List Type { get; set; } + [System.Xml.Serialization.XmlElement(ElementName="typeVersion", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List TypeVersion { get; set; } + } + public class AltinnEFormidlingDataTypesConfig + { + public AltinnEFormidlingDataTypesConfig() { } + [System.Xml.Serialization.XmlElement(ElementName="dataType", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List DataTypeIds { get; set; } + [System.Xml.Serialization.XmlAttribute("env")] + public string? Environment { get; set; } + } public sealed class AltinnEnvironmentConfig { public AltinnEnvironmentConfig() { } @@ -3259,6 +3296,8 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties [System.Xml.Serialization.XmlArray(ElementName="actions", IsNullable=true, Namespace="http://altinn.no/process")] [System.Xml.Serialization.XmlArrayItem(ElementName="action", Namespace="http://altinn.no/process")] public System.Collections.Generic.List? AltinnActions { get; set; } + [System.Xml.Serialization.XmlElement("eFormidlingConfig", Namespace="http://altinn.no/process")] + public Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties.AltinnEFormidlingConfiguration? EFormidlingConfiguration { get; set; } [System.Xml.Serialization.XmlElement("paymentConfig", Namespace="http://altinn.no/process")] public Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties.AltinnPaymentConfiguration? PaymentConfiguration { get; set; } [System.Xml.Serialization.XmlElement("pdfConfig", Namespace="http://altinn.no/process")] @@ -3268,6 +3307,18 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties [System.Xml.Serialization.XmlElement("taskType", Namespace="http://altinn.no/process")] public string? TaskType { get; set; } } + public readonly struct ValidAltinnEFormidlingConfiguration : System.IEquatable + { + public ValidAltinnEFormidlingConfiguration(string? Receiver, string Process, string Standard, string TypeVersion, string Type, int SecurityLevel, string? DpfShipmentType, System.Collections.Generic.List DataTypes) { } + public System.Collections.Generic.List DataTypes { get; init; } + public string? DpfShipmentType { get; init; } + public string Process { get; init; } + public string? Receiver { get; init; } + public int SecurityLevel { get; init; } + public string Standard { get; init; } + public string Type { get; init; } + public string TypeVersion { get; init; } + } } namespace Altinn.App.Core.Internal.Process.Elements { From 80c6b82101f3ed6f2ed6be96a01d47ca20272fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Fri, 26 Sep 2025 21:20:21 +0200 Subject: [PATCH 36/60] Summary doc improvement. --- .../EFormidling/Interface/IEFormidlingReceivers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs index 249acdf19..cc99762a5 100644 --- a/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs +++ b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs @@ -19,7 +19,7 @@ public interface IEFormidlingReceivers /// /// /// Instance data - /// Optional receiver organization number from static configuration. + /// Optional receiver organization number from static configuration (bpmn service task or applicationMetadata.json). /// List of eFormidling receivers /// Thrown when is null public Task> GetEFormidlingReceivers(Instance instance, string? receiverFromConfig = null); From be2d6ab21d8b70779243f8b5c5fd00279deb5a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Mon, 29 Sep 2025 09:59:24 +0200 Subject: [PATCH 37/60] Fix receiver config. --- .../AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs | 2 +- .../Implementation/EFormidlingConfigurationProviderTests.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs index 70e41309f..09ca4c3e5 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs @@ -68,7 +68,7 @@ internal ValidAltinnEFormidlingConfiguration Validate(HostingEnvironment env) { List? errorMessages = null; - string? receiver = GetConfigValue(Process, env); + string? receiver = GetConfigValue(Receiver, env); string? process = GetConfigValue(Process, env); if (process.IsNullOrWhitespace(ref errorMessages, $"No Process configuration found for environment {env}")) diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs index 7fc4b41f8..df72e233b 100644 --- a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs @@ -102,6 +102,7 @@ public async Task GetBpmnConfiguration_ReturnsConfigFromBpmnTask() Type = CreateEnvironmentConfig("arkivmelding"), SecurityLevel = CreateEnvironmentConfig("3"), DpfShipmentType = CreateEnvironmentConfig("altinn3.skjema"), + Receiver = CreateEnvironmentConfig("123456789"), }, }; @@ -119,6 +120,7 @@ public async Task GetBpmnConfiguration_ReturnsConfigFromBpmnTask() result.Type.Should().Be("arkivmelding"); result.SecurityLevel.Should().Be(3); result.DpfShipmentType.Should().Be("altinn3.skjema"); + result.Receiver.Should().Be("123456789"); } [Fact] From 168e18494a0d4995f9a66a6763ed2786f0403f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Mon, 29 Sep 2025 10:14:46 +0200 Subject: [PATCH 38/60] Avoid breaking change in IEFormidlingReceivers. --- .../DefaultEFormidlingReceivers.cs | 45 ++++++++++++++++--- .../Interface/IEFormidlingReceivers.cs | 18 +++++++- ...ouldNotChange_Unintentionally.verified.txt | 8 ++-- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingReceivers.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingReceivers.cs index bc9d1966d..6869c6c36 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingReceivers.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingReceivers.cs @@ -1,4 +1,6 @@ using Altinn.App.Core.EFormidling.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models; using Altinn.Common.EFormidlingClient.Models.SBD; using Altinn.Platform.Storage.Interface.Models; @@ -9,8 +11,36 @@ namespace Altinn.App.Core.EFormidling.Implementation; /// public class DefaultEFormidlingReceivers : IEFormidlingReceivers { + private readonly IAppMetadata _appMetadata; + + /// + /// Initializes a new instance of the class. + /// + /// Service for fetching application metadata + public DefaultEFormidlingReceivers(IAppMetadata appMetadata) + { + _appMetadata = appMetadata; + } + + /// + public async Task> GetEFormidlingReceivers(Instance instance) + { + ArgumentNullException.ThrowIfNull(instance); + + ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); + + if (string.IsNullOrWhiteSpace(appMetadata.EFormidling?.Receiver)) + { + return new List(); + } + + string receiver = appMetadata.EFormidling.Receiver.Trim(); + + return CreateReceiverList(receiver); + } + /// - public Task> GetEFormidlingReceivers(Instance instance, string? receiverFromConfig = null) + public Task> GetEFormidlingReceivers(Instance instance, string? receiverFromConfig) { ArgumentNullException.ThrowIfNull(instance); @@ -19,15 +49,20 @@ public Task> GetEFormidlingReceivers(Instance instance, string? r return Task.FromResult(new List()); } + string receiver = receiverFromConfig.Trim(); + + return Task.FromResult(CreateReceiverList(receiver)); + } + + private static List CreateReceiverList(string receiver) + { var identifier = new Identifier { // 0192 prefix for all Norwegian organisations. - Value = $"0192:{receiverFromConfig.Trim()}", + Value = $"0192:{receiver}", Authority = "iso6523-actorid-upis", }; - var receiver = new Receiver { Identifier = identifier }; - - return Task.FromResult>([receiver]); + return [new Receiver { Identifier = identifier }]; } } diff --git a/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs index cc99762a5..7b5854db1 100644 --- a/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs +++ b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs @@ -19,8 +19,22 @@ public interface IEFormidlingReceivers /// /// /// Instance data - /// Optional receiver organization number from static configuration (bpmn service task or applicationMetadata.json). /// List of eFormidling receivers /// Thrown when is null - public Task> GetEFormidlingReceivers(Instance instance, string? receiverFromConfig = null); + public Task> GetEFormidlingReceivers(Instance instance); + + /// + /// Gets a list of eFormidling shipment receivers. + /// + /// + /// + /// Note that the identifier value property on the receiver objects should be prefixed with `0192:` for Norwegian organisations. + /// + /// + /// Instance data + /// Receiver organization number from static configuration (BPMN or ApplicationMetadata, depending on if service task is used or not). + /// List of eFormidling receivers + /// Thrown when is null + public Task> GetEFormidlingReceivers(Instance instance, string? receiverFromConfig) => + GetEFormidlingReceivers(instance); } diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index b5c6573e9..ea36d43c5 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -144,8 +144,9 @@ namespace Altinn.App.Core.EFormidling.Implementation { public class DefaultEFormidlingReceivers : Altinn.App.Core.EFormidling.Interface.IEFormidlingReceivers { - public DefaultEFormidlingReceivers() { } - public System.Threading.Tasks.Task> GetEFormidlingReceivers(Altinn.Platform.Storage.Interface.Models.Instance instance, string? receiverFromConfig = null) { } + public DefaultEFormidlingReceivers(Altinn.App.Core.Internal.App.IAppMetadata appMetadata) { } + public System.Threading.Tasks.Task> GetEFormidlingReceivers(Altinn.Platform.Storage.Interface.Models.Instance instance) { } + public System.Threading.Tasks.Task> GetEFormidlingReceivers(Altinn.Platform.Storage.Interface.Models.Instance instance, string? receiverFromConfig) { } } public class DefaultEFormidlingService : Altinn.App.Core.EFormidling.Interface.IEFormidlingService { @@ -189,7 +190,8 @@ namespace Altinn.App.Core.EFormidling.Interface } public interface IEFormidlingReceivers { - System.Threading.Tasks.Task> GetEFormidlingReceivers(Altinn.Platform.Storage.Interface.Models.Instance instance, string? receiverFromConfig = null); + System.Threading.Tasks.Task> GetEFormidlingReceivers(Altinn.Platform.Storage.Interface.Models.Instance instance); + System.Threading.Tasks.Task> GetEFormidlingReceivers(Altinn.Platform.Storage.Interface.Models.Instance instance, string? receiverFromConfig); } public interface IEFormidlingService { From c37baaba99d7d89bd7ecaeb41e0f825468dccf72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Mon, 29 Sep 2025 11:29:44 +0200 Subject: [PATCH 39/60] nullcheck --- .../EFormidlingConfigurationProvider.cs | 11 +++++++++-- .../EFormidlingConfigurationProviderTests.cs | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs b/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs index c9b646fc1..789d5dcfa 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs @@ -52,6 +52,11 @@ public async Task GetLegacyConfiguration() ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); EFormidlingContract? eFormidling = applicationMetadata.EFormidling; + if (eFormidling is null) + { + throw new ApplicationConfigException($"No legacy eFormidling configuration found in application metadata."); + } + return new ValidAltinnEFormidlingConfiguration( eFormidling.Receiver, eFormidling.Process, @@ -66,11 +71,13 @@ public async Task GetLegacyConfiguration() public Task GetBpmnConfiguration(string taskId) { + ArgumentNullException.ThrowIfNull(taskId); + AltinnTaskExtension? taskExtension = _processReader.GetAltinnTaskExtension(taskId); AltinnEFormidlingConfiguration? eFormidlingConfig = taskExtension?.EFormidlingConfiguration; - if (eFormidlingConfig == null) - throw new InvalidOperationException($"No eFormidling configuration found in BPMN for task {taskId}"); + if (eFormidlingConfig is null) + throw new ApplicationConfigException($"No eFormidling configuration found in BPMN for task {taskId}"); HostingEnvironment env = AltinnEnvironments.GetHostingEnvironment(_hostEnvironment); ValidAltinnEFormidlingConfiguration validConfig = eFormidlingConfig.Validate(env); diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs index df72e233b..1850e2f2f 100644 --- a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs @@ -134,7 +134,7 @@ public async Task GetBpmnConfiguration_NoTaskExtension_ThrowsInvalidOperationExc // Act & Assert var act = async () => await _provider.GetBpmnConfiguration(taskId); await act.Should() - .ThrowAsync() + .ThrowAsync() .WithMessage("No eFormidling configuration found in BPMN for task Task_1"); } @@ -151,7 +151,7 @@ public async Task GetBpmnConfiguration_NoEFormidlingConfiguration_ThrowsInvalidO // Act & Assert var act = async () => await _provider.GetBpmnConfiguration(taskId); await act.Should() - .ThrowAsync() + .ThrowAsync() .WithMessage("No eFormidling configuration found in BPMN for task Task_1"); } From 9376556ba306357998b559c8539462e89cf367cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Tue, 30 Sep 2025 21:08:02 +0200 Subject: [PATCH 40/60] Don't have common eformidling config provider anyways. Just have a helper that converts old config into the new config. --- .../Extensions/ServiceCollectionExtensions.cs | 10 +- .../DefaultEFormidlingService.cs | 4 +- .../EFormidlingConfigurationProvider.cs | 87 --------- .../EFormidlingLegacyConfigurationProvider.cs | 54 ++++++ .../AltinnEFormidlingConfiguration.cs | 11 +- .../ServiceTasks/EFormidlingServiceTask.cs | 60 ++---- .../service-tasks/config/process/process.bpmn | 14 ++ .../EFormidlingServiceTaskTests.cs | 5 +- .../ServiceTasks/Pdf/PdfServiceTaskTests.cs | 2 +- .../DefaultEFormidlingServiceTests.cs | 38 ++-- .../EFormidlingConfigurationProviderTests.cs | 172 +----------------- .../EFormidlingServiceTaskTests.cs | 135 ++++---------- ...ouldNotChange_Unintentionally.verified.txt | 8 +- 13 files changed, 165 insertions(+), 435 deletions(-) delete mode 100644 src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs create mode 100644 src/Altinn.App.Core/EFormidling/Implementation/EFormidlingLegacyConfigurationProvider.cs diff --git a/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs index d10a8325e..c3487ac3f 100644 --- a/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs @@ -40,7 +40,10 @@ public static void AddEFormidlingServices(this IServiceCollection servic { services.AddTransient(typeof(IEFormidlingReceivers), typeof(TR)); services.AddHttpClient(); - services.AddTransient(); + services.AddTransient< + IEFormidlingLegacyConfigurationProvider, + EFormidlingIeFormidlingLegacyConfigurationProvider + >(); services.AddTransient(); services.Configure( configuration.GetSection("EFormidlingClientSettings") @@ -64,7 +67,10 @@ public static void AddEFormidlingServices2(this IServiceCollection servi { services.AddTransient(typeof(IEFormidlingReceivers), typeof(TR)); services.AddHttpClient(); - services.AddTransient(); + services.AddTransient< + IEFormidlingLegacyConfigurationProvider, + EFormidlingIeFormidlingLegacyConfigurationProvider + >(); services.AddTransient(); services.Configure( configuration.GetSection("EFormidlingClientSettings") diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs index 98665b012..82cafb8c3 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs @@ -35,7 +35,7 @@ public class DefaultEFormidlingService : IEFormidlingService private readonly IDataClient _dataClient; private readonly IEventsClient _eventClient; private readonly AppImplementationFactory _appImplementationFactory; - private readonly IEFormidlingConfigurationProvider _configurationProvider; + private readonly IEFormidlingLegacyConfigurationProvider _configurationProvider; /// /// Initializes a new instance of the class. @@ -47,7 +47,7 @@ public DefaultEFormidlingService( IDataClient dataClient, IEventsClient eventClient, IServiceProvider sp, - IEFormidlingConfigurationProvider configurationProvider, + IEFormidlingLegacyConfigurationProvider configurationProvider, IOptions? appSettings = null, IOptions? platformSettings = null, IEFormidlingClient? eFormidlingClient = null, diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs b/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs deleted file mode 100644 index 789d5dcfa..000000000 --- a/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingConfigurationProvider.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Altinn.App.Core.Constants; -using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Internal.Process; -using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; -using Altinn.App.Core.Models; -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.Extensions.Hosting; - -namespace Altinn.App.Core.EFormidling.Implementation; - -/// -/// Provides validated eFormidling configuration from various sources. -/// -public interface IEFormidlingConfigurationProvider -{ - /// - /// Gets validated eFormidling configuration from ApplicationMetadata (legacy). - /// - /// Validated eFormidling configuration. - Task GetLegacyConfiguration(); - - /// - /// Gets validated eFormidling configuration from BPMN task extension. - /// - /// The task ID to get configuration for. - /// Validated eFormidling configuration. - Task GetBpmnConfiguration(string taskId); -} - -/// -/// Provides eFormidling configuration from various sources (ApplicationMetadata or BPMN). -/// -internal sealed class EFormidlingConfigurationProvider : IEFormidlingConfigurationProvider -{ - private readonly IAppMetadata _appMetadata; - private readonly IProcessReader _processReader; - private readonly IHostEnvironment _hostEnvironment; - - public EFormidlingConfigurationProvider( - IAppMetadata appMetadata, - IProcessReader processReader, - IHostEnvironment hostEnvironment - ) - { - _appMetadata = appMetadata; - _processReader = processReader; - _hostEnvironment = hostEnvironment; - } - - public async Task GetLegacyConfiguration() - { - ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - EFormidlingContract? eFormidling = applicationMetadata.EFormidling; - - if (eFormidling is null) - { - throw new ApplicationConfigException($"No legacy eFormidling configuration found in application metadata."); - } - - return new ValidAltinnEFormidlingConfiguration( - eFormidling.Receiver, - eFormidling.Process, - eFormidling.Standard, - eFormidling.TypeVersion, - eFormidling.Type, - eFormidling.SecurityLevel, - eFormidling.DPFShipmentType, - eFormidling.DataTypes?.ToList() ?? [] - ); - } - - public Task GetBpmnConfiguration(string taskId) - { - ArgumentNullException.ThrowIfNull(taskId); - - AltinnTaskExtension? taskExtension = _processReader.GetAltinnTaskExtension(taskId); - AltinnEFormidlingConfiguration? eFormidlingConfig = taskExtension?.EFormidlingConfiguration; - - if (eFormidlingConfig is null) - throw new ApplicationConfigException($"No eFormidling configuration found in BPMN for task {taskId}"); - - HostingEnvironment env = AltinnEnvironments.GetHostingEnvironment(_hostEnvironment); - ValidAltinnEFormidlingConfiguration validConfig = eFormidlingConfig.Validate(env); - - return Task.FromResult(validConfig); - } -} diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingLegacyConfigurationProvider.cs b/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingLegacyConfigurationProvider.cs new file mode 100644 index 000000000..3cdba60e7 --- /dev/null +++ b/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingLegacyConfigurationProvider.cs @@ -0,0 +1,54 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.EFormidling.Implementation; + +/// +/// A small wrapper around loading legacy eFormidling configuration from ApplicationMetadata, to be able to use the same configuration as the new eFormidling service task. +/// +/// Should be deleted when is removed. +public interface IEFormidlingLegacyConfigurationProvider +{ + /// + /// Gets validated eFormidling configuration from ApplicationMetadata (legacy). + /// + /// Validated eFormidling configuration. + Task GetLegacyConfiguration(); +} + +/// +internal sealed class EFormidlingIeFormidlingLegacyConfigurationProvider : IEFormidlingLegacyConfigurationProvider +{ + private readonly IAppMetadata _appMetadata; + + public EFormidlingIeFormidlingLegacyConfigurationProvider(IAppMetadata appMetadata) + { + _appMetadata = appMetadata; + } + + public async Task GetLegacyConfiguration() + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + EFormidlingContract? eFormidling = applicationMetadata.EFormidling; + + if (eFormidling is null) + { + throw new ApplicationConfigException($"No legacy eFormidling configuration found in application metadata."); + } + + return new ValidAltinnEFormidlingConfiguration( + true, // Enabled prop is not used in legacy mode, as whether eFormidling is enabled or not is determined in the legacy service task. + eFormidling.Receiver, + eFormidling.Process, + eFormidling.Standard, + eFormidling.TypeVersion, + eFormidling.Type, + eFormidling.SecurityLevel, + eFormidling.DPFShipmentType, + eFormidling.DataTypes?.ToList() ?? [] + ); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs index 09ca4c3e5..de99883cf 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs @@ -11,7 +11,7 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; public class AltinnEFormidlingConfiguration { /// - /// Controls whether eFormidling should be enabled for this task. + /// Can be used to disable eFormidling in specific environments. If omitted, defaults to true. /// [XmlElement(ElementName = "enabled", Namespace = "http://altinn.no/process")] public List Enabled { get; set; } = []; @@ -68,6 +68,10 @@ internal ValidAltinnEFormidlingConfiguration Validate(HostingEnvironment env) { List? errorMessages = null; + // Default 'enabled' to true if not specified. + string? enabledValue = GetConfigValue(Enabled, env); + bool enabled = string.IsNullOrWhiteSpace(enabledValue) || bool.Parse(enabledValue); + string? receiver = GetConfigValue(Receiver, env); string? process = GetConfigValue(Process, env); @@ -112,6 +116,7 @@ internal ValidAltinnEFormidlingConfiguration Validate(HostingEnvironment env) List dataTypes = GetDataTypesForEnvironment(env); return new ValidAltinnEFormidlingConfiguration( + enabled, receiver, process, standard, @@ -190,7 +195,8 @@ public class AltinnEFormidlingDataTypesConfig /// /// Validated eFormidling configuration with all required fields guaranteed to be non-null /// -/// The organisation number of the receiver. Only Norwegian organisations supported. (Can be omitted) +/// Whether eFormidling should be sent for the current environment. Only used in service task context, ignored by legacy code. +/// The organization number of the receiver. Only Norwegian organizations supported. (Can be omitted) /// The process identifier for the eFormidling message /// The standard identifier for the document /// The type version of the document @@ -199,6 +205,7 @@ public class AltinnEFormidlingDataTypesConfig /// Optional DPF shipment type for the eFormidling message /// List of data type IDs to include in the eFormidling shipment public readonly record struct ValidAltinnEFormidlingConfiguration( + bool Enabled, string? Receiver, string Process, string Standard, diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index fd66b8698..784876ab1 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -1,6 +1,6 @@ using Altinn.App.Core.Constants; -using Altinn.App.Core.EFormidling.Implementation; using Altinn.App.Core.EFormidling.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Hosting; @@ -19,7 +19,6 @@ internal sealed class EFormidlingServiceTask : IEFormidlingServiceTask private readonly IProcessReader _processReader; private readonly IHostEnvironment _hostEnvironment; private readonly IEFormidlingService? _eFormidlingService; - private readonly IEFormidlingConfigurationProvider? _eFormidlingConfigurationProvider; /// /// Initializes a new instance of the class. @@ -28,15 +27,13 @@ public EFormidlingServiceTask( ILogger logger, IProcessReader processReader, IHostEnvironment hostEnvironment, - IEFormidlingService? eFormidlingService = null, - IEFormidlingConfigurationProvider? eFormidlingConfigurationProvider = null + IEFormidlingService? eFormidlingService = null ) { _logger = logger; _processReader = processReader; _hostEnvironment = hostEnvironment; _eFormidlingService = eFormidlingService; - _eFormidlingConfigurationProvider = eFormidlingConfigurationProvider; } /// @@ -52,17 +49,11 @@ public async Task Execute(ServiceTaskContext context) ); } - if (_eFormidlingConfigurationProvider is null) - { - throw new ProcessException( - $"No implementation of {nameof(IEFormidlingConfigurationProvider)} has been added to the DI container. Use AddEFormidlingServices2 to register eFormidling services." - ); - } - string taskId = context.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; Instance instance = context.InstanceDataMutator.Instance; + ValidAltinnEFormidlingConfiguration configuration = await GetValidAltinnEFormidlingConfiguration(taskId); - if (!IsEFormidlingEnabled(taskId)) + if (!configuration.Enabled) { _logger.LogInformation( "EFormidling is disabled for task {TaskId}. No eFormidling shipment will be sent, but the service task will be completed.", @@ -71,9 +62,6 @@ public async Task Execute(ServiceTaskContext context) return ServiceTaskResult.Success(); } - ValidAltinnEFormidlingConfiguration configuration = - await _eFormidlingConfigurationProvider.GetBpmnConfiguration(taskId); - _logger.LogDebug("Calling eFormidlingService for eFormidling Service Task {TaskId}.", taskId); await _eFormidlingService.SendEFormidlingShipment(instance, configuration); _logger.LogDebug("Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", taskId); @@ -81,41 +69,19 @@ public async Task Execute(ServiceTaskContext context) return ServiceTaskResult.Success(); } - /// - /// Checks if eFormidling service task is enabled. Even if eFormidling has been added as a service task to the process, it can be disabled for certain environments. - /// - /// - /// - private bool IsEFormidlingEnabled(string taskId) + private Task GetValidAltinnEFormidlingConfiguration(string taskId) { - AltinnTaskExtension? altinnTaskExtension = _processReader.GetAltinnTaskExtension(taskId); - AltinnEFormidlingConfiguration? eFormidlingConfiguration = altinnTaskExtension?.EFormidlingConfiguration; + ArgumentNullException.ThrowIfNull(taskId); - // If no BPMN configuration is specified, default to enabled - if (eFormidlingConfiguration?.Enabled.Count is 0 or null) - { - _logger.LogDebug( - "No eFormidling configuration found in BPMN for task {TaskId}. Defaulting to enabled. Add false to disable for specific environments.", - taskId - ); - return true; - } + AltinnTaskExtension? taskExtension = _processReader.GetAltinnTaskExtension(taskId); + AltinnEFormidlingConfiguration? eFormidlingConfig = taskExtension?.EFormidlingConfiguration; - HostingEnvironment env = AltinnEnvironments.GetHostingEnvironment(_hostEnvironment); - AltinnEnvironmentConfig? enabledConfig = AltinnTaskExtension.GetConfigForEnvironment( - env, - eFormidlingConfiguration.Enabled - ); + if (eFormidlingConfig is null) + throw new ApplicationConfigException($"No eFormidling configuration found in BPMN for task {taskId}"); - if (enabledConfig?.Value is null) - { - _logger.LogWarning( - "EFormidling configuration is present in BPMN but no matching environment configuration found for environment '{Environment}'. EFormidling will be disabled.", - env - ); - return false; - } + HostingEnvironment env = AltinnEnvironments.GetHostingEnvironment(_hostEnvironment); + ValidAltinnEFormidlingConfiguration validConfig = eFormidlingConfig.Validate(env); - return bool.TryParse(enabledConfig.Value, out bool enabled) && enabled; + return Task.FromResult(validConfig); } } diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/process/process.bpmn b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/process/process.bpmn index d094cde30..d9737c0ae 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/process/process.bpmn +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/process/process.bpmn @@ -43,6 +43,20 @@ eFormidling + + true + 123456789 + urn:no:difi:profile:arkivmelding:administrasjon:ver1.0 + urn:no:difi:arkivmelding:xsd::arkivmelding + 2.0 + arkivmelding + 3 + digital + + model + ref-data-as-pdf + + diff --git a/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs b/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs index 9f94368d1..476b11e10 100644 --- a/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs +++ b/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs @@ -24,9 +24,7 @@ public class EFormidlingServiceTaskTests : ApiTestBase, IClassFixture _eFormidlingServiceMock = new Mock(); - private readonly Mock _eFormidlingConfigurationProviderMock = - new Mock(); + private readonly Mock _eFormidlingServiceMock = new(); public EFormidlingServiceTaskTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) : base(factory, outputHelper) @@ -34,7 +32,6 @@ public EFormidlingServiceTaskTests(WebApplicationFactory factory, ITest OverrideServicesForAllTests = (services) => { services.AddSingleton(_eFormidlingServiceMock.Object); - services.AddSingleton(_eFormidlingConfigurationProviderMock.Object); services.AddTransient(); }; diff --git a/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs index 0c7cc9e25..e544180da 100644 --- a/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs @@ -27,7 +27,7 @@ public PdfServiceTaskTests(WebApplicationFactory factory, ITestOutputHe : base(factory, outputHelper) { var eFormidlingServiceMock = new Mock(); - var eFormidlingConfigurationProviderMock = new Mock(); + var eFormidlingConfigurationProviderMock = new Mock(); OverrideServicesForAllTests = (services) => { services.AddSingleton(eFormidlingServiceMock.Object); diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs index 4310b87d8..72a33e394 100644 --- a/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs @@ -76,7 +76,7 @@ private Fixture CreateFixture( var tokenGenerator = new Mock(); var processReader = new Mock(); var hostEnvironment = new Mock(); - var configurationProvider = new Mock(); + var eFormidlingLegacyConfigProvider = new Mock(); var instanceGuid = Guid.Parse("41C1099C-7EDD-47F5-AD1F-6267B497796F"); var instance = new Instance @@ -166,6 +166,21 @@ private Fixture CreateFixture( { return (EFormidlingMetadataFilename, Stream.Null); }); + eFormidlingLegacyConfigProvider + .Setup(cp => cp.GetLegacyConfiguration()) + .ReturnsAsync( + new ValidAltinnEFormidlingConfiguration( + true, + null, + "urn:no:difi:profile:arkivmelding:plan:3.0", + "urn:no:difi:arkivmelding:xsd::arkivmelding", + "v8", + "arkivmelding", + 3, + null, + [ModelDataType, FileAttachmentsDataType] + ) + ); dataClient .Setup(x => x.GetBinaryData( @@ -180,38 +195,21 @@ private Fixture CreateFixture( ) .ReturnsAsync(Stream.Null); - // Setup configuration provider to return legacy configuration - configurationProvider - .Setup(cp => cp.GetLegacyConfiguration()) - .ReturnsAsync( - new ValidAltinnEFormidlingConfiguration( - null, // Receiver (matches the test data which has no Receiver) - "urn:no:difi:profile:arkivmelding:plan:3.0", // Process - "urn:no:difi:arkivmelding:xsd::arkivmelding", // Standard - "v8", // TypeVersion - "arkivmelding", // Type - 3, // SecurityLevel - null, // DpfShipmentType (matches test data which has no DPFShipmentType) - new List { ModelDataType, FileAttachmentsDataType } // DataTypes - ) - ); - setupEFormidlingClient?.Invoke(eFormidlingClient); services.TryAddTransient(_ => userTokenProvider.Object); services.TryAddTransient(_ => appMetadata.Object); services.TryAddTransient(_ => dataClient.Object); services.TryAddTransient(_ => eFormidlingReceivers.Object); + services.TryAddTransient(_ => eFormidlingMetadata.Object); + services.TryAddTransient(_ => eFormidlingLegacyConfigProvider.Object); services.TryAddTransient(_ => eventClient.Object); services.TryAddTransient(_ => appSettings); services.TryAddTransient(_ => platformSettings); services.TryAddTransient(_ => eFormidlingClient.Object); services.TryAddTransient(_ => tokenGenerator.Object); - services.TryAddTransient(_ => eFormidlingMetadata.Object); services.TryAddTransient(_ => processReader.Object); services.TryAddTransient(_ => hostEnvironment.Object); - services.TryAddTransient(_ => configurationProvider.Object); - services.TryAddTransient(); var serviceProvider = services.BuildStrictServiceProvider(); diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs index 1850e2f2f..23971d927 100644 --- a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs @@ -10,20 +10,14 @@ namespace Altinn.App.Core.Tests.Eformidling.Implementation; -public class EFormidlingConfigurationProviderTests +public class EFormidlingIeFormidlingLegacyConfigurationProviderTests { private readonly Mock _appMetadataMock = new(); - private readonly Mock _processReaderMock = new(); - private readonly Mock _hostEnvironmentMock = new(); - private readonly EFormidlingConfigurationProvider _provider; + private readonly EFormidlingIeFormidlingLegacyConfigurationProvider _provider; - public EFormidlingConfigurationProviderTests() + public EFormidlingIeFormidlingLegacyConfigurationProviderTests() { - _provider = new EFormidlingConfigurationProvider( - _appMetadataMock.Object, - _processReaderMock.Object, - _hostEnvironmentMock.Object - ); + _provider = new EFormidlingIeFormidlingLegacyConfigurationProvider(_appMetadataMock.Object); } [Fact] @@ -47,7 +41,7 @@ public async Task GetLegacyConfiguration_ReturnsConfigFromApplicationMetadata() _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); // Act - var result = await _provider.GetLegacyConfiguration(); + ValidAltinnEFormidlingConfiguration result = await _provider.GetLegacyConfiguration(); // Assert result.Should().NotBeNull(); @@ -57,7 +51,7 @@ public async Task GetLegacyConfiguration_ReturnsConfigFromApplicationMetadata() result.Type.Should().Be("arkivmelding"); result.SecurityLevel.Should().Be(3); result.DpfShipmentType.Should().Be("altinn3.skjema"); - result.DataTypes.Should().BeEquivalentTo(new[] { "datatype1", "datatype2" }); + result.DataTypes.Should().BeEquivalentTo("datatype1", "datatype2"); } [Fact] @@ -80,161 +74,9 @@ public async Task GetLegacyConfiguration_WithNullDataTypes_ReturnsConfigWithEmpt _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); // Act - var result = await _provider.GetLegacyConfiguration(); + ValidAltinnEFormidlingConfiguration result = await _provider.GetLegacyConfiguration(); // Assert result.DataTypes.Should().BeEmpty(); } - - [Fact] - public async Task GetBpmnConfiguration_ReturnsConfigFromBpmnTask() - { - // Arrange - var taskId = "Task_1"; - - var taskExtension = new AltinnTaskExtension - { - EFormidlingConfiguration = new AltinnEFormidlingConfiguration - { - Process = CreateEnvironmentConfig("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0"), - Standard = CreateEnvironmentConfig("urn:no:difi:arkivmelding:xsd::arkivmelding"), - TypeVersion = CreateEnvironmentConfig("2.0"), - Type = CreateEnvironmentConfig("arkivmelding"), - SecurityLevel = CreateEnvironmentConfig("3"), - DpfShipmentType = CreateEnvironmentConfig("altinn3.skjema"), - Receiver = CreateEnvironmentConfig("123456789"), - }, - }; - - _processReaderMock.Setup(x => x.GetAltinnTaskExtension(taskId)).Returns(taskExtension); - _hostEnvironmentMock.Setup(x => x.EnvironmentName).Returns("Production"); - - // Act - var result = await _provider.GetBpmnConfiguration(taskId); - - // Assert - result.Should().NotBeNull(); - result.Process.Should().Be("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0"); - result.Standard.Should().Be("urn:no:difi:arkivmelding:xsd::arkivmelding"); - result.TypeVersion.Should().Be("2.0"); - result.Type.Should().Be("arkivmelding"); - result.SecurityLevel.Should().Be(3); - result.DpfShipmentType.Should().Be("altinn3.skjema"); - result.Receiver.Should().Be("123456789"); - } - - [Fact] - public async Task GetBpmnConfiguration_NoTaskExtension_ThrowsInvalidOperationException() - { - // Arrange - var taskId = "Task_1"; - - _processReaderMock.Setup(x => x.GetAltinnTaskExtension(taskId)).Returns((AltinnTaskExtension?)null); - - // Act & Assert - var act = async () => await _provider.GetBpmnConfiguration(taskId); - await act.Should() - .ThrowAsync() - .WithMessage("No eFormidling configuration found in BPMN for task Task_1"); - } - - [Fact] - public async Task GetBpmnConfiguration_NoEFormidlingConfiguration_ThrowsInvalidOperationException() - { - // Arrange - var taskId = "Task_1"; - - var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = null }; - - _processReaderMock.Setup(x => x.GetAltinnTaskExtension(taskId)).Returns(taskExtension); - - // Act & Assert - var act = async () => await _provider.GetBpmnConfiguration(taskId); - await act.Should() - .ThrowAsync() - .WithMessage("No eFormidling configuration found in BPMN for task Task_1"); - } - - [Fact] - public async Task GetBpmnConfiguration_MissingRequiredConfig_ThrowsApplicationConfigException() - { - // Arrange - var taskId = "Task_1"; - - var taskExtension = new AltinnTaskExtension - { - EFormidlingConfiguration = new AltinnEFormidlingConfiguration - { - Process = CreateEnvironmentConfig("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0"), - // Missing Standard, TypeVersion, Type, SecurityLevel - }, - }; - - _processReaderMock.Setup(x => x.GetAltinnTaskExtension(taskId)).Returns(taskExtension); - _hostEnvironmentMock.Setup(x => x.EnvironmentName).Returns("Production"); - - // Act & Assert - var act = async () => await _provider.GetBpmnConfiguration(taskId); - await act.Should() - .ThrowAsync() - .WithMessage("*No Standard configuration found for environment Production*"); - } - - [Fact] - public async Task GetBpmnConfiguration_WithDataTypes_ReturnsDataTypesForEnvironment() - { - // Arrange - var taskId = "Task_1"; - - var eFormidlingConfig = new AltinnEFormidlingConfiguration - { - Process = CreateEnvironmentConfig("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0"), - Standard = CreateEnvironmentConfig("urn:no:difi:arkivmelding:xsd::arkivmelding"), - TypeVersion = CreateEnvironmentConfig("2.0"), - Type = CreateEnvironmentConfig("arkivmelding"), - SecurityLevel = CreateEnvironmentConfig("3"), - DataTypes = new List - { - new() - { - Environment = "Production", - DataTypeIds = new List { "datatype1" }, - }, - new() - { - Environment = "Development", - DataTypeIds = new List { "datatype2" }, - }, - }, - }; - - var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = eFormidlingConfig }; - - _processReaderMock.Setup(x => x.GetAltinnTaskExtension(taskId)).Returns(taskExtension); - _hostEnvironmentMock.Setup(x => x.EnvironmentName).Returns("Production"); - - // Act - var result = await _provider.GetBpmnConfiguration(taskId); - - // Assert - result.DataTypes.Should().BeEquivalentTo(new[] { "datatype1" }); - } - - [Fact] - public void GetBpmnConfiguration_NullTaskId_ThrowsArgumentNullException() - { - // Act & Assert - var act = async () => await _provider.GetBpmnConfiguration(null!); - act.Should().ThrowAsync(); - } - - // Note: Test for unknown source is no longer needed with direct method approach - - private static List CreateEnvironmentConfig(string value) - { - return new List - { - new() { Environment = "Production", Value = value }, - }; - } } 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 d028518f6..0bbcb96af 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs @@ -2,13 +2,13 @@ using Altinn.App.Core.EFormidling.Implementation; using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Moq; namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks; @@ -17,10 +17,8 @@ public class EFormidlingServiceTaskTests { private readonly Mock> _loggerMock = new(); private readonly Mock _eFormidlingServiceMock = new(); - private readonly Mock> _appSettingsMock = new(); private readonly Mock _processReaderMock = new(); private readonly Mock _hostEnvironmentMock = new(); - private readonly Mock _eFormidlingConfigurationProvider = new(); private readonly EFormidlingServiceTask _serviceTask; public EFormidlingServiceTaskTests() @@ -30,15 +28,13 @@ public EFormidlingServiceTaskTests() _loggerMock.Object, _processReaderMock.Object, _hostEnvironmentMock.Object, - _eFormidlingServiceMock.Object, - _eFormidlingConfigurationProvider.Object + _eFormidlingServiceMock.Object ); } [Fact] public async Task Execute_Should_BeEnabled_When_NoBpmnConfig() { - // Arrange Instance instance = GetInstance(); var instanceMutatorMock = new Mock(); @@ -46,31 +42,8 @@ public async Task Execute_Should_BeEnabled_When_NoBpmnConfig() var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; - // Act - await _serviceTask.Execute(parameters); - - // Assert - _eFormidlingServiceMock.Verify( - x => x.SendEFormidlingShipment(instance, It.IsAny()), - Times.Once - ); - _loggerMock.Verify( - x => - x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is( - (v, t) => - v.ToString()! - .Contains( - "No eFormidling configuration found in BPMN for task taskId. Defaulting to enabled" - ) - ), - It.IsAny(), - It.Is>((v, t) => true) - ), - Times.Once - ); + var exception = await Assert.ThrowsAsync(() => _serviceTask.Execute(parameters)); + Assert.Contains("No eFormidling configuration found in BPMN for task", exception.Message); } [Fact] @@ -79,15 +52,11 @@ public async Task Execute_Should_ThrowException_When_EFormidlingServiceIsNull() // Arrange Instance instance = GetInstance(); - var appSettings = new AppSettings { EnableEFormidling = true }; - _appSettingsMock.Setup(x => x.Value).Returns(appSettings); - var serviceTask = new EFormidlingServiceTask( _loggerMock.Object, _processReaderMock.Object, _hostEnvironmentMock.Object, - null, - _eFormidlingConfigurationProvider.Object + null ); var instanceMutatorMock = new Mock(); @@ -105,14 +74,14 @@ public async Task Execute_Should_Call_SendEFormidlingShipment_When_EFormidlingEn // Arrange Instance instance = GetInstance(); - var appSettings = new AppSettings { EnableEFormidling = true }; - _appSettingsMock.Setup(x => x.Value).Returns(appSettings); - var instanceMutatorMock = new Mock(); instanceMutatorMock.Setup(x => x.Instance).Returns(instance); var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; + var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = GetConfig() }; + _processReaderMock.Setup(x => x.GetAltinnTaskExtension("taskId")).Returns(taskExtension); + // Act await _serviceTask.Execute(parameters); @@ -123,20 +92,32 @@ public async Task Execute_Should_Call_SendEFormidlingShipment_When_EFormidlingEn ); } + private static AltinnEFormidlingConfiguration GetConfig(bool enabled = true) + { + return new AltinnEFormidlingConfiguration + { + Enabled = [new AltinnEnvironmentConfig { Value = enabled.ToString() }], + Process = [new AltinnEnvironmentConfig { Value = "process" }], + Standard = [new AltinnEnvironmentConfig { Value = "standard" }], + TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], + Type = [new AltinnEnvironmentConfig { Value = "type" }], + SecurityLevel = [new AltinnEnvironmentConfig { Value = "3" }], + DpfShipmentType = [new AltinnEnvironmentConfig { Value = "dpfShipmentType" }], + }; + } + [Fact] public async Task Execute_Should_UseEnvironmentSpecificBpmnConfig_When_Configured() { // Arrange Instance instance = GetInstance(); - var eFormidlingConfig = new AltinnEFormidlingConfiguration - { - Enabled = - [ - new AltinnEnvironmentConfig { Environment = "prod", Value = "true" }, - new AltinnEnvironmentConfig { Environment = "staging", Value = "false" }, - ], - }; + AltinnEFormidlingConfiguration eFormidlingConfig = GetConfig(); + eFormidlingConfig.Enabled = + [ + new AltinnEnvironmentConfig { Environment = "prod", Value = "true" }, + new AltinnEnvironmentConfig { Environment = "staging", Value = "false" }, + ]; var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = eFormidlingConfig }; _processReaderMock.Setup(x => x.GetAltinnTaskExtension("taskId")).Returns(taskExtension); @@ -162,12 +143,7 @@ public async Task Execute_Should_SkipExecution_When_BpmnConfigDisabled() // Arrange Instance instance = GetInstance(); - var eFormidlingConfig = new AltinnEFormidlingConfiguration - { - Enabled = [new AltinnEnvironmentConfig { Environment = "prod", Value = "false" }], - }; - - var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = eFormidlingConfig }; + var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = GetConfig(enabled: false) }; _processReaderMock.Setup(x => x.GetAltinnTaskExtension("taskId")).Returns(taskExtension); var instanceMutatorMock = new Mock(); @@ -193,60 +169,17 @@ public async Task Execute_Should_SkipExecution_When_BpmnConfigDisabled() ); } - [Fact] - public async Task Execute_Should_BeEnabled_When_NoBpmnConfigExplicit() - { - // Arrange - Instance instance = GetInstance(); - - // No BPMN configuration (explicit null) - _processReaderMock.Setup(x => x.GetAltinnTaskExtension("taskId")).Returns((AltinnTaskExtension?)null); - - var instanceMutatorMock = new Mock(); - instanceMutatorMock.Setup(x => x.Instance).Returns(instance); - - var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; - - // Act - await _serviceTask.Execute(parameters); - - // Assert - _eFormidlingServiceMock.Verify( - x => x.SendEFormidlingShipment(instance, It.IsAny()), - Times.Once - ); - _loggerMock.Verify( - x => - x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is( - (v, t) => - v.ToString()! - .Contains( - "No eFormidling configuration found in BPMN for task taskId. Defaulting to enabled" - ) - ), - It.IsAny(), - It.Is>((v, t) => true) - ), - Times.Once - ); - } - [Fact] public async Task Execute_Should_UseGlobalBpmnConfig_When_NoEnvironmentSpecific() { // Arrange Instance instance = GetInstance(); - var eFormidlingConfig = new AltinnEFormidlingConfiguration - { - Enabled = - [ - new AltinnEnvironmentConfig { Value = "true" }, // Global config (no env specified) - ], - }; + AltinnEFormidlingConfiguration eFormidlingConfig = GetConfig(); + eFormidlingConfig.Enabled = + [ + new AltinnEnvironmentConfig { Value = "true" }, // Global config (no env specified) + ]; var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = eFormidlingConfig }; _processReaderMock.Setup(x => x.GetAltinnTaskExtension("taskId")).Returns(taskExtension); diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index ea36d43c5..136cd6ac4 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -150,7 +150,7 @@ namespace Altinn.App.Core.EFormidling.Implementation } public class DefaultEFormidlingService : Altinn.App.Core.EFormidling.Interface.IEFormidlingService { - public DefaultEFormidlingService(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Internal.Auth.IUserTokenProvider userTokenProvider, Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient, Altinn.App.Core.Internal.Events.IEventsClient eventClient, System.IServiceProvider sp, Altinn.App.Core.EFormidling.Implementation.IEFormidlingConfigurationProvider configurationProvider, Microsoft.Extensions.Options.IOptions? appSettings = null, Microsoft.Extensions.Options.IOptions? platformSettings = null, Altinn.Common.EFormidlingClient.IEFormidlingClient? eFormidlingClient = null, Altinn.Common.AccessTokenClient.Services.IAccessTokenGenerator? tokenGenerator = null) { } + public DefaultEFormidlingService(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Internal.Auth.IUserTokenProvider userTokenProvider, Altinn.App.Core.Internal.App.IAppMetadata appMetadata, Altinn.App.Core.Internal.Data.IDataClient dataClient, Altinn.App.Core.Internal.Events.IEventsClient eventClient, System.IServiceProvider sp, Altinn.App.Core.EFormidling.Implementation.IEFormidlingLegacyConfigurationProvider configurationProvider, Microsoft.Extensions.Options.IOptions? appSettings = null, Microsoft.Extensions.Options.IOptions? platformSettings = null, Altinn.Common.EFormidlingClient.IEFormidlingClient? eFormidlingClient = null, Altinn.Common.AccessTokenClient.Services.IAccessTokenGenerator? tokenGenerator = null) { } public System.Threading.Tasks.Task SendEFormidlingShipment(Altinn.Platform.Storage.Interface.Models.Instance instance) { } public System.Threading.Tasks.Task SendEFormidlingShipment(Altinn.Platform.Storage.Interface.Models.Instance instance, Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties.ValidAltinnEFormidlingConfiguration configuration) { } } @@ -173,9 +173,8 @@ namespace Altinn.App.Core.EFormidling.Implementation public string EventType { get; } public System.Threading.Tasks.Task ProcessEvent(Altinn.App.Core.Models.CloudEvent cloudEvent) { } } - public interface IEFormidlingConfigurationProvider + public interface IEFormidlingLegacyConfigurationProvider { - System.Threading.Tasks.Task GetBpmnConfiguration(string taskId); System.Threading.Tasks.Task GetLegacyConfiguration(); } } @@ -3311,9 +3310,10 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties } public readonly struct ValidAltinnEFormidlingConfiguration : System.IEquatable { - public ValidAltinnEFormidlingConfiguration(string? Receiver, string Process, string Standard, string TypeVersion, string Type, int SecurityLevel, string? DpfShipmentType, System.Collections.Generic.List DataTypes) { } + public ValidAltinnEFormidlingConfiguration(bool Enabled, string? Receiver, string Process, string Standard, string TypeVersion, string Type, int SecurityLevel, string? DpfShipmentType, System.Collections.Generic.List DataTypes) { } public System.Collections.Generic.List DataTypes { get; init; } public string? DpfShipmentType { get; init; } + public bool Enabled { get; init; } public string Process { get; init; } public string? Receiver { get; init; } public int SecurityLevel { get; init; } From dab8d2dc55985a4fd10e50a0205dc9f67101c288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 1 Oct 2025 09:18:13 +0200 Subject: [PATCH 41/60] Remove usage of FluentAssertions. --- .../EFormidlingConfigurationProviderTests.cs | 20 ++++++++----------- .../EFormidlingServiceTaskTests.cs | 4 +--- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs index 23971d927..bec70e0cb 100644 --- a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs @@ -1,11 +1,8 @@ using Altinn.App.Core.EFormidling.Implementation; using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; -using FluentAssertions; -using Microsoft.Extensions.Hosting; using Moq; namespace Altinn.App.Core.Tests.Eformidling.Implementation; @@ -44,14 +41,13 @@ public async Task GetLegacyConfiguration_ReturnsConfigFromApplicationMetadata() ValidAltinnEFormidlingConfiguration result = await _provider.GetLegacyConfiguration(); // Assert - result.Should().NotBeNull(); - result.Process.Should().Be("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0"); - result.Standard.Should().Be("urn:no:difi:arkivmelding:xsd::arkivmelding"); - result.TypeVersion.Should().Be("2.0"); - result.Type.Should().Be("arkivmelding"); - result.SecurityLevel.Should().Be(3); - result.DpfShipmentType.Should().Be("altinn3.skjema"); - result.DataTypes.Should().BeEquivalentTo("datatype1", "datatype2"); + Assert.Equal("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0", result.Process); + Assert.Equal("urn:no:difi:arkivmelding:xsd::arkivmelding", result.Standard); + Assert.Equal("2.0", result.TypeVersion); + Assert.Equal("arkivmelding", result.Type); + Assert.Equal(3, result.SecurityLevel); + Assert.Equal("altinn3.skjema", result.DpfShipmentType); + Assert.Equal(new[] { "datatype1", "datatype2" }, result.DataTypes); } [Fact] @@ -77,6 +73,6 @@ public async Task GetLegacyConfiguration_WithNullDataTypes_ReturnsConfigWithEmpt ValidAltinnEFormidlingConfiguration result = await _provider.GetLegacyConfiguration(); // Assert - result.DataTypes.Should().BeEmpty(); + Assert.Empty(result.DataTypes); } } 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 0bbcb96af..7b3afc1bc 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs @@ -1,6 +1,4 @@ -using Altinn.App.Core.Configuration; -using Altinn.App.Core.EFormidling.Implementation; -using Altinn.App.Core.EFormidling.Interface; +using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Features; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Process; From 2cd984834d3db17f388877dd18f7ff420b82d1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 1 Oct 2025 09:33:12 +0200 Subject: [PATCH 42/60] assert receiver --- .../Implementation/EFormidlingConfigurationProviderTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs index bec70e0cb..9b690da85 100644 --- a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs @@ -25,6 +25,7 @@ public async Task GetLegacyConfiguration_ReturnsConfigFromApplicationMetadata() { EFormidling = new EFormidlingContract { + Receiver = "123456789", Process = "urn:no:difi:profile:arkivmelding:administrasjon:ver1.0", Standard = "urn:no:difi:arkivmelding:xsd::arkivmelding", TypeVersion = "2.0", @@ -41,6 +42,7 @@ public async Task GetLegacyConfiguration_ReturnsConfigFromApplicationMetadata() ValidAltinnEFormidlingConfiguration result = await _provider.GetLegacyConfiguration(); // Assert + Assert.Equal("123456789", result.Receiver); Assert.Equal("urn:no:difi:profile:arkivmelding:administrasjon:ver1.0", result.Process); Assert.Equal("urn:no:difi:arkivmelding:xsd::arkivmelding", result.Standard); Assert.Equal("2.0", result.TypeVersion); From 8d7ece0764ae0920e28177a11c69d9ae572424e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 1 Oct 2025 09:53:18 +0200 Subject: [PATCH 43/60] Add test for exception when legacy eformidling config is null. --- .../EFormidlingConfigurationProviderTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs index 9b690da85..c9259763f 100644 --- a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs @@ -77,4 +77,16 @@ public async Task GetLegacyConfiguration_WithNullDataTypes_ReturnsConfigWithEmpt // Assert Assert.Empty(result.DataTypes); } + + [Fact] + public async Task GetLegacyConfiguration_WhenEFormidlingIsNull_ThrowsApplicationConfigException() + { + // Arrange + var applicationMetadata = new ApplicationMetadata("tdd/test") { EFormidling = null }; + + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act & Assert + await Assert.ThrowsAsync(() => _provider.GetLegacyConfiguration()); + } } From 76b634e2cf0815651246cffafcced708d9398474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 1 Oct 2025 10:37:30 +0200 Subject: [PATCH 44/60] Fix legacy config provider name and try to make AltinnEFormidlingconfiguration a bit more readable. --- .../Extensions/ServiceCollectionExtensions.cs | 10 +- .../EFormidlingLegacyConfigurationProvider.cs | 4 +- .../AltinnEFormidlingConfiguration.cs | 130 +++++++++++------- .../EFormidlingConfigurationProviderTests.cs | 8 +- ...ouldNotChange_Unintentionally.verified.txt | 2 +- 5 files changed, 86 insertions(+), 68 deletions(-) diff --git a/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs index c3487ac3f..4f747e809 100644 --- a/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs @@ -40,10 +40,7 @@ public static void AddEFormidlingServices(this IServiceCollection servic { services.AddTransient(typeof(IEFormidlingReceivers), typeof(TR)); services.AddHttpClient(); - services.AddTransient< - IEFormidlingLegacyConfigurationProvider, - EFormidlingIeFormidlingLegacyConfigurationProvider - >(); + services.AddTransient(); services.AddTransient(); services.Configure( configuration.GetSection("EFormidlingClientSettings") @@ -67,10 +64,7 @@ public static void AddEFormidlingServices2(this IServiceCollection servi { services.AddTransient(typeof(IEFormidlingReceivers), typeof(TR)); services.AddHttpClient(); - services.AddTransient< - IEFormidlingLegacyConfigurationProvider, - EFormidlingIeFormidlingLegacyConfigurationProvider - >(); + services.AddTransient(); services.AddTransient(); services.Configure( configuration.GetSection("EFormidlingClientSettings") diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingLegacyConfigurationProvider.cs b/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingLegacyConfigurationProvider.cs index 3cdba60e7..ff8568c55 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingLegacyConfigurationProvider.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingLegacyConfigurationProvider.cs @@ -20,11 +20,11 @@ public interface IEFormidlingLegacyConfigurationProvider } /// -internal sealed class EFormidlingIeFormidlingLegacyConfigurationProvider : IEFormidlingLegacyConfigurationProvider +internal sealed class EFormidlingLegacyConfigurationProvider : IEFormidlingLegacyConfigurationProvider { private readonly IAppMetadata _appMetadata; - public EFormidlingIeFormidlingLegacyConfigurationProvider(IAppMetadata appMetadata) + public EFormidlingLegacyConfigurationProvider(IAppMetadata appMetadata) { _appMetadata = appMetadata; } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs index de99883cf..3ad3aa611 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Xml.Serialization; using Altinn.App.Core.Constants; using Altinn.App.Core.Internal.App; @@ -8,7 +7,7 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; /// /// Configuration properties for eFormidling in a process task. All properties support environment-specific values using 'env' attributes. /// -public class AltinnEFormidlingConfiguration +public sealed class AltinnEFormidlingConfiguration { /// /// Can be used to disable eFormidling in specific environments. If omitted, defaults to true. @@ -66,55 +65,23 @@ public class AltinnEFormidlingConfiguration internal ValidAltinnEFormidlingConfiguration Validate(HostingEnvironment env) { - List? errorMessages = null; + var validator = new ConfigValidator(env); // Default 'enabled' to true if not specified. - string? enabledValue = GetConfigValue(Enabled, env); + string? enabledValue = GetOptionalConfig(Enabled, env); bool enabled = string.IsNullOrWhiteSpace(enabledValue) || bool.Parse(enabledValue); - string? receiver = GetConfigValue(Receiver, env); - - string? process = GetConfigValue(Process, env); - if (process.IsNullOrWhitespace(ref errorMessages, $"No Process configuration found for environment {env}")) - ThrowApplicationConfigException(errorMessages); - - string? standard = GetConfigValue(Standard, env); - if (standard.IsNullOrWhitespace(ref errorMessages, $"No Standard configuration found for environment {env}")) - ThrowApplicationConfigException(errorMessages); - - string? typeVersion = GetConfigValue(TypeVersion, env); - if ( - typeVersion.IsNullOrWhitespace( - ref errorMessages, - $"No TypeVersion configuration found for environment {env}" - ) - ) - ThrowApplicationConfigException(errorMessages); - - string? type = GetConfigValue(Type, env); - if (type.IsNullOrWhitespace(ref errorMessages, $"No Type configuration found for environment {env}")) - ThrowApplicationConfigException(errorMessages); - - string? securityLevelValue = GetConfigValue(SecurityLevel, env); - if ( - securityLevelValue.IsNullOrWhitespace( - ref errorMessages, - $"No SecurityLevel configuration found for environment {env}" - ) - ) - ThrowApplicationConfigException(errorMessages); - - if (!int.TryParse(securityLevelValue, out int securityLevel)) - { - errorMessages ??= new List(1); - errorMessages.Add($"SecurityLevel must be a valid integer for environment {env}"); - ThrowApplicationConfigException(errorMessages); - } - - string? dpfShipmentType = GetConfigValue(DpfShipmentType, env); - + string? receiver = GetOptionalConfig(Receiver, env); + string process = GetRequiredConfig(Process, validator, nameof(Process)); + string standard = GetRequiredConfig(Standard, validator, nameof(Standard)); + string typeVersion = GetRequiredConfig(TypeVersion, validator, nameof(TypeVersion)); + string type = GetRequiredConfig(Type, validator, nameof(Type)); + int securityLevel = GetRequiredIntConfig(SecurityLevel, validator, nameof(SecurityLevel)); + string? dpfShipmentType = GetOptionalConfig(DpfShipmentType, env); List dataTypes = GetDataTypesForEnvironment(env); + validator.ThrowIfErrors(); + return new ValidAltinnEFormidlingConfiguration( enabled, receiver, @@ -128,18 +95,75 @@ internal ValidAltinnEFormidlingConfiguration Validate(HostingEnvironment env) ); } - [DoesNotReturn] - private static void ThrowApplicationConfigException(List errorMessages) + private static string? GetOptionalConfig(List configs, HostingEnvironment env) { - throw new ApplicationConfigException( - "eFormidling process task configuration is not valid: " + string.Join(",\n", errorMessages) - ); + return AltinnTaskExtension.GetConfigForEnvironment(env, configs)?.Value; } - private static string? GetConfigValue(List configs, HostingEnvironment env) + private static string GetRequiredConfig( + List configs, + ConfigValidator validator, + string fieldName + ) { - AltinnEnvironmentConfig? config = AltinnTaskExtension.GetConfigForEnvironment(env, configs); - return config?.Value; + string? value = GetOptionalConfig(configs, validator.Environment); + if (string.IsNullOrWhiteSpace(value)) + { + validator.AddError($"No {fieldName} configuration found for environment {validator.Environment}"); + return string.Empty; + } + + return value; + } + + private static int GetRequiredIntConfig( + List configs, + ConfigValidator validator, + string fieldName + ) + { + string? value = GetOptionalConfig(configs, validator.Environment); + if (string.IsNullOrWhiteSpace(value)) + { + validator.AddError($"No {fieldName} configuration found for environment {validator.Environment}"); + return 0; + } + + if (!int.TryParse(value, out int result)) + { + validator.AddError($"{fieldName} must be a valid integer for environment {validator.Environment}"); + return 0; + } + + return result; + } + + private sealed class ConfigValidator + { + private List? _errors; + + public HostingEnvironment Environment { get; } + + public ConfigValidator(HostingEnvironment environment) + { + Environment = environment; + } + + public void AddError(string message) + { + _errors ??= []; + _errors.Add(message); + } + + public void ThrowIfErrors() + { + if (_errors is not null) + { + throw new ApplicationConfigException( + "eFormidling process task configuration is not valid: " + string.Join(",\n", _errors) + ); + } + } } /// diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs index c9259763f..7dad52815 100644 --- a/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs @@ -7,14 +7,14 @@ namespace Altinn.App.Core.Tests.Eformidling.Implementation; -public class EFormidlingIeFormidlingLegacyConfigurationProviderTests +public class EFormidlingLegacyConfigurationProviderTests { private readonly Mock _appMetadataMock = new(); - private readonly EFormidlingIeFormidlingLegacyConfigurationProvider _provider; + private readonly EFormidlingLegacyConfigurationProvider _provider; - public EFormidlingIeFormidlingLegacyConfigurationProviderTests() + public EFormidlingLegacyConfigurationProviderTests() { - _provider = new EFormidlingIeFormidlingLegacyConfigurationProvider(_appMetadataMock.Object); + _provider = new EFormidlingLegacyConfigurationProvider(_appMetadataMock.Object); } [Fact] diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 136cd6ac4..ec6a23ccf 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3209,7 +3209,7 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties [System.Xml.Serialization.XmlText] public string Value { get; set; } } - public class AltinnEFormidlingConfiguration + public sealed class AltinnEFormidlingConfiguration { public AltinnEFormidlingConfiguration() { } [System.Xml.Serialization.XmlElement(ElementName="dataTypes", Namespace="http://altinn.no/process")] From 5cad79d00ef524f283c82518064273d85a7a0c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 1 Oct 2025 11:58:03 +0200 Subject: [PATCH 45/60] add unit tests for AltinnEFormidlingConfigurationTests validate --- .../AltinnEFormidlingConfigurationTests.cs | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfigurationTests.cs diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfigurationTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfigurationTests.cs new file mode 100644 index 000000000..c36d0cae8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfigurationTests.cs @@ -0,0 +1,282 @@ +using Altinn.App.Core.Constants; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +namespace Altinn.App.Core.Tests.Internal.Process.Elements.AltinnExtensionProperties; + +public class AltinnEFormidlingConfigurationTests +{ + [Fact] + public void Validate_WithAllRequiredFields_ReturnsValidConfiguration() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Process = [new AltinnEnvironmentConfig { Value = "process-value" }], + Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], + TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], + Type = [new AltinnEnvironmentConfig { Value = "type-value" }], + SecurityLevel = [new AltinnEnvironmentConfig { Value = "3" }], + }; + + // Act + ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); + + // Assert + Assert.True(result.Enabled); + Assert.Null(result.Receiver); + Assert.Equal("process-value", result.Process); + Assert.Equal("standard-value", result.Standard); + Assert.Equal("1.0", result.TypeVersion); + Assert.Equal("type-value", result.Type); + Assert.Equal(3, result.SecurityLevel); + Assert.Null(result.DpfShipmentType); + Assert.Empty(result.DataTypes); + } + + [Fact] + public void Validate_WithOptionalFields_ReturnsValidConfiguration() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Receiver = [new AltinnEnvironmentConfig { Value = "123456789" }], + Process = [new AltinnEnvironmentConfig { Value = "process-value" }], + Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], + TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], + Type = [new AltinnEnvironmentConfig { Value = "type-value" }], + SecurityLevel = [new AltinnEnvironmentConfig { Value = "3" }], + DpfShipmentType = [new AltinnEnvironmentConfig { Value = "shipment-type" }], + DataTypes = [new AltinnEFormidlingDataTypesConfig { DataTypeIds = ["datatype1", "datatype2"] }], + }; + + // Act + ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); + + // Assert + Assert.Equal("123456789", result.Receiver); + Assert.Equal("shipment-type", result.DpfShipmentType); + Assert.Equal(["datatype1", "datatype2"], result.DataTypes); + } + + [Fact] + public void Validate_WithEnabledTrue_ReturnsEnabledTrue() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Enabled = [new AltinnEnvironmentConfig { Value = "true" }], + Process = [new AltinnEnvironmentConfig { Value = "process-value" }], + Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], + TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], + Type = [new AltinnEnvironmentConfig { Value = "type-value" }], + SecurityLevel = [new AltinnEnvironmentConfig { Value = "3" }], + }; + + // Act + ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); + + // Assert + Assert.True(result.Enabled); + } + + [Fact] + public void Validate_WithEnabledFalse_ReturnsEnabledFalse() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Enabled = [new AltinnEnvironmentConfig { Value = "false" }], + Process = [new AltinnEnvironmentConfig { Value = "process-value" }], + Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], + TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], + Type = [new AltinnEnvironmentConfig { Value = "type-value" }], + SecurityLevel = [new AltinnEnvironmentConfig { Value = "3" }], + }; + + // Act + ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); + + // Assert + Assert.False(result.Enabled); + } + + [Fact] + public void Validate_WithoutEnabledField_DefaultsToTrue() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Process = [new AltinnEnvironmentConfig { Value = "process-value" }], + Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], + TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], + Type = [new AltinnEnvironmentConfig { Value = "type-value" }], + SecurityLevel = [new AltinnEnvironmentConfig { Value = "3" }], + }; + + // Act + ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); + + // Assert + Assert.True(result.Enabled); + } + + [Fact] + public void Validate_WithMissingRequiredFields_ThrowsExceptionWithAllErrors() + { + // Arrange - all required fields are missing + var config = new AltinnEFormidlingConfiguration(); + + // Act & Assert + var exception = Assert.Throws(() => config.Validate(HostingEnvironment.Production)); + + // Should contain all error messages + Assert.Contains("No Process configuration found for environment Production", exception.Message); + Assert.Contains("No Standard configuration found for environment Production", exception.Message); + Assert.Contains("No TypeVersion configuration found for environment Production", exception.Message); + Assert.Contains("No Type configuration found for environment Production", exception.Message); + Assert.Contains("No SecurityLevel configuration found for environment Production", exception.Message); + } + + [Fact] + public void Validate_WithInvalidSecurityLevelAndMissingFields_ThrowsExceptionWithAllErrors() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + SecurityLevel = [new AltinnEnvironmentConfig { Value = "invalid" }], + }; + + // Act & Assert + var exception = Assert.Throws(() => config.Validate(HostingEnvironment.Production)); + + // Should contain all error messages + Assert.Contains("No Process configuration found", exception.Message); + Assert.Contains("No Standard configuration found", exception.Message); + Assert.Contains("No TypeVersion configuration found", exception.Message); + Assert.Contains("No Type configuration found", exception.Message); + Assert.Contains("SecurityLevel must be a valid integer", exception.Message); + } + + [Fact] + public void Validate_WithEnvironmentSpecificConfig_UsesCorrectEnvironment() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Process = + [ + new AltinnEnvironmentConfig { Environment = "prod", Value = "prod-process" }, + new AltinnEnvironmentConfig { Environment = "staging", Value = "staging-process" }, + ], + Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], + TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], + Type = [new AltinnEnvironmentConfig { Value = "type-value" }], + SecurityLevel = [new AltinnEnvironmentConfig { Value = "3" }], + }; + + // Act + ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); + + // Assert + Assert.Equal("prod-process", result.Process); + } + + [Fact] + public void Validate_WithGlobalAndEnvironmentSpecificConfig_PrefersEnvironmentSpecific() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Process = + [ + new AltinnEnvironmentConfig { Value = "global-process" }, // No environment = global + new AltinnEnvironmentConfig { Environment = "prod", Value = "prod-process" }, + ], + Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], + TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], + Type = [new AltinnEnvironmentConfig { Value = "type-value" }], + SecurityLevel = [new AltinnEnvironmentConfig { Value = "3" }], + }; + + // Act + ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); + + // Assert + Assert.Equal("prod-process", result.Process); + } + + [Fact] + public void Validate_WithOnlyGlobalConfig_UsesGlobalConfig() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Process = [new AltinnEnvironmentConfig { Value = "global-process" }], + Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], + TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], + Type = [new AltinnEnvironmentConfig { Value = "type-value" }], + SecurityLevel = [new AltinnEnvironmentConfig { Value = "3" }], + }; + + // Act + ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); + + // Assert + Assert.Equal("global-process", result.Process); + } + + [Fact] + public void Validate_WithEnvironmentSpecificDataTypes_ReturnsCorrectDataTypes() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Process = [new AltinnEnvironmentConfig { Value = "process-value" }], + Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], + TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], + Type = [new AltinnEnvironmentConfig { Value = "type-value" }], + SecurityLevel = [new AltinnEnvironmentConfig { Value = "3" }], + DataTypes = + [ + new AltinnEFormidlingDataTypesConfig + { + Environment = "prod", + DataTypeIds = ["prod-datatype1", "prod-datatype2"], + }, + new AltinnEFormidlingDataTypesConfig { Environment = "staging", DataTypeIds = ["staging-datatype1"] }, + ], + }; + + // Act + ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); + + // Assert + Assert.Equal(["prod-datatype1", "prod-datatype2"], result.DataTypes); + } + + [Fact] + public void Validate_WithGlobalAndEnvironmentSpecificDataTypes_PrefersEnvironmentSpecific() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Process = [new AltinnEnvironmentConfig { Value = "process-value" }], + Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], + TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], + Type = [new AltinnEnvironmentConfig { Value = "type-value" }], + SecurityLevel = [new AltinnEnvironmentConfig { Value = "3" }], + DataTypes = + [ + new AltinnEFormidlingDataTypesConfig { DataTypeIds = ["global-datatype"] }, + new AltinnEFormidlingDataTypesConfig { Environment = "prod", DataTypeIds = ["prod-datatype"] }, + ], + }; + + // Act + ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); + + // Assert + Assert.Equal(["prod-datatype"], result.DataTypes); + } +} From 7b0a284be655932c79e979c499a8699b6ec22008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 1 Oct 2025 15:31:08 +0200 Subject: [PATCH 46/60] Change "Enabled" to "Disabled" for eFormidling. Active choice to disable the functionality for specific environments. --- .../AltinnEFormidlingConfiguration.cs | 18 +++++++++--------- .../ServiceTasks/EFormidlingServiceTask.cs | 2 +- .../AltinnEFormidlingConfigurationTests.cs | 18 +++++++++--------- .../EFormidlingServiceTaskTests.cs | 16 ++++++++-------- ...houldNotChange_Unintentionally.verified.txt | 8 ++++---- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs index 3ad3aa611..fefda3af4 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs @@ -10,10 +10,10 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; public sealed class AltinnEFormidlingConfiguration { /// - /// Can be used to disable eFormidling in specific environments. If omitted, defaults to true. + /// Can be used to disable eFormidling in specific environments. If omitted, defaults to false (eFormidling is enabled by default). /// - [XmlElement(ElementName = "enabled", Namespace = "http://altinn.no/process")] - public List Enabled { get; set; } = []; + [XmlElement(ElementName = "disabled", Namespace = "http://altinn.no/process")] + public List Disabled { get; set; } = []; /// /// The organization number of the receiver of the eFormidling message. Can be omitted. @@ -67,9 +67,9 @@ internal ValidAltinnEFormidlingConfiguration Validate(HostingEnvironment env) { var validator = new ConfigValidator(env); - // Default 'enabled' to true if not specified. - string? enabledValue = GetOptionalConfig(Enabled, env); - bool enabled = string.IsNullOrWhiteSpace(enabledValue) || bool.Parse(enabledValue); + // Default 'disabled' to false if not specified (eFormidling is enabled by default). + string? disabledValue = GetOptionalConfig(Disabled, env); + bool disabled = !string.IsNullOrWhiteSpace(disabledValue) && bool.Parse(disabledValue); string? receiver = GetOptionalConfig(Receiver, env); string process = GetRequiredConfig(Process, validator, nameof(Process)); @@ -83,7 +83,7 @@ internal ValidAltinnEFormidlingConfiguration Validate(HostingEnvironment env) validator.ThrowIfErrors(); return new ValidAltinnEFormidlingConfiguration( - enabled, + disabled, receiver, process, standard, @@ -219,7 +219,7 @@ public class AltinnEFormidlingDataTypesConfig /// /// Validated eFormidling configuration with all required fields guaranteed to be non-null /// -/// Whether eFormidling should be sent for the current environment. Only used in service task context, ignored by legacy code. +/// Whether eFormidling should be disabled for the current environment. Only used in service task context, ignored by legacy code. /// The organization number of the receiver. Only Norwegian organizations supported. (Can be omitted) /// The process identifier for the eFormidling message /// The standard identifier for the document @@ -229,7 +229,7 @@ public class AltinnEFormidlingDataTypesConfig /// Optional DPF shipment type for the eFormidling message /// List of data type IDs to include in the eFormidling shipment public readonly record struct ValidAltinnEFormidlingConfiguration( - bool Enabled, + bool Disabled, string? Receiver, string Process, string Standard, diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index 784876ab1..7add16e74 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -53,7 +53,7 @@ public async Task Execute(ServiceTaskContext context) Instance instance = context.InstanceDataMutator.Instance; ValidAltinnEFormidlingConfiguration configuration = await GetValidAltinnEFormidlingConfiguration(taskId); - if (!configuration.Enabled) + if (configuration.Disabled) { _logger.LogInformation( "EFormidling is disabled for task {TaskId}. No eFormidling shipment will be sent, but the service task will be completed.", diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfigurationTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfigurationTests.cs index c36d0cae8..908b756d5 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfigurationTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfigurationTests.cs @@ -23,7 +23,7 @@ public void Validate_WithAllRequiredFields_ReturnsValidConfiguration() ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); // Assert - Assert.True(result.Enabled); + Assert.False(result.Disabled); Assert.Null(result.Receiver); Assert.Equal("process-value", result.Process); Assert.Equal("standard-value", result.Standard); @@ -60,12 +60,12 @@ public void Validate_WithOptionalFields_ReturnsValidConfiguration() } [Fact] - public void Validate_WithEnabledTrue_ReturnsEnabledTrue() + public void Validate_WithDisabledTrue_ReturnsDisabledTrue() { // Arrange var config = new AltinnEFormidlingConfiguration { - Enabled = [new AltinnEnvironmentConfig { Value = "true" }], + Disabled = [new AltinnEnvironmentConfig { Value = "true" }], Process = [new AltinnEnvironmentConfig { Value = "process-value" }], Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], @@ -77,16 +77,16 @@ public void Validate_WithEnabledTrue_ReturnsEnabledTrue() ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); // Assert - Assert.True(result.Enabled); + Assert.True(result.Disabled); } [Fact] - public void Validate_WithEnabledFalse_ReturnsEnabledFalse() + public void Validate_WithDisabledFalse_ReturnsDisabledFalse() { // Arrange var config = new AltinnEFormidlingConfiguration { - Enabled = [new AltinnEnvironmentConfig { Value = "false" }], + Disabled = [new AltinnEnvironmentConfig { Value = "false" }], Process = [new AltinnEnvironmentConfig { Value = "process-value" }], Standard = [new AltinnEnvironmentConfig { Value = "standard-value" }], TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], @@ -98,11 +98,11 @@ public void Validate_WithEnabledFalse_ReturnsEnabledFalse() ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); // Assert - Assert.False(result.Enabled); + Assert.False(result.Disabled); } [Fact] - public void Validate_WithoutEnabledField_DefaultsToTrue() + public void Validate_WithoutDisabledField_DefaultsToFalse() { // Arrange var config = new AltinnEFormidlingConfiguration @@ -118,7 +118,7 @@ public void Validate_WithoutEnabledField_DefaultsToTrue() ValidAltinnEFormidlingConfiguration result = config.Validate(HostingEnvironment.Production); // Assert - Assert.True(result.Enabled); + Assert.False(result.Disabled); } [Fact] 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 7b3afc1bc..c0acd7649 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs @@ -90,11 +90,11 @@ public async Task Execute_Should_Call_SendEFormidlingShipment_When_EFormidlingEn ); } - private static AltinnEFormidlingConfiguration GetConfig(bool enabled = true) + private static AltinnEFormidlingConfiguration GetConfig(bool disabled = false) { return new AltinnEFormidlingConfiguration { - Enabled = [new AltinnEnvironmentConfig { Value = enabled.ToString() }], + Disabled = [new AltinnEnvironmentConfig { Value = disabled.ToString() }], Process = [new AltinnEnvironmentConfig { Value = "process" }], Standard = [new AltinnEnvironmentConfig { Value = "standard" }], TypeVersion = [new AltinnEnvironmentConfig { Value = "1.0" }], @@ -111,10 +111,10 @@ public async Task Execute_Should_UseEnvironmentSpecificBpmnConfig_When_Configure Instance instance = GetInstance(); AltinnEFormidlingConfiguration eFormidlingConfig = GetConfig(); - eFormidlingConfig.Enabled = + eFormidlingConfig.Disabled = [ - new AltinnEnvironmentConfig { Environment = "prod", Value = "true" }, - new AltinnEnvironmentConfig { Environment = "staging", Value = "false" }, + new AltinnEnvironmentConfig { Environment = "prod", Value = "false" }, + new AltinnEnvironmentConfig { Environment = "staging", Value = "true" }, ]; var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = eFormidlingConfig }; @@ -141,7 +141,7 @@ public async Task Execute_Should_SkipExecution_When_BpmnConfigDisabled() // Arrange Instance instance = GetInstance(); - var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = GetConfig(enabled: false) }; + var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = GetConfig(disabled: true) }; _processReaderMock.Setup(x => x.GetAltinnTaskExtension("taskId")).Returns(taskExtension); var instanceMutatorMock = new Mock(); @@ -174,9 +174,9 @@ public async Task Execute_Should_UseGlobalBpmnConfig_When_NoEnvironmentSpecific( Instance instance = GetInstance(); AltinnEFormidlingConfiguration eFormidlingConfig = GetConfig(); - eFormidlingConfig.Enabled = + eFormidlingConfig.Disabled = [ - new AltinnEnvironmentConfig { Value = "true" }, // Global config (no env specified) + new AltinnEnvironmentConfig { Value = "false" }, // Global config (no env specified) ]; var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = eFormidlingConfig }; diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index ec6a23ccf..74435b8ae 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3214,10 +3214,10 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties public AltinnEFormidlingConfiguration() { } [System.Xml.Serialization.XmlElement(ElementName="dataTypes", Namespace="http://altinn.no/process")] public System.Collections.Generic.List DataTypes { get; set; } + [System.Xml.Serialization.XmlElement(ElementName="disabled", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List Disabled { get; set; } [System.Xml.Serialization.XmlElement(ElementName="dpfShipmentType", Namespace="http://altinn.no/process")] public System.Collections.Generic.List DpfShipmentType { get; set; } - [System.Xml.Serialization.XmlElement(ElementName="enabled", Namespace="http://altinn.no/process")] - public System.Collections.Generic.List Enabled { get; set; } [System.Xml.Serialization.XmlElement(ElementName="process", Namespace="http://altinn.no/process")] public System.Collections.Generic.List Process { get; set; } [System.Xml.Serialization.XmlElement(ElementName="receiver", Namespace="http://altinn.no/process")] @@ -3310,10 +3310,10 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties } public readonly struct ValidAltinnEFormidlingConfiguration : System.IEquatable { - public ValidAltinnEFormidlingConfiguration(bool Enabled, string? Receiver, string Process, string Standard, string TypeVersion, string Type, int SecurityLevel, string? DpfShipmentType, System.Collections.Generic.List DataTypes) { } + public ValidAltinnEFormidlingConfiguration(bool Disabled, string? Receiver, string Process, string Standard, string TypeVersion, string Type, int SecurityLevel, string? DpfShipmentType, System.Collections.Generic.List DataTypes) { } public System.Collections.Generic.List DataTypes { get; init; } + public bool Disabled { get; init; } public string? DpfShipmentType { get; init; } - public bool Enabled { get; init; } public string Process { get; init; } public string? Receiver { get; init; } public int SecurityLevel { get; init; } From 8102c43238b279dddc39003f88adbfd3e75cf338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 1 Oct 2025 16:20:13 +0200 Subject: [PATCH 47/60] nitpick --- .../EFormidling/Interface/IEFormidlingService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingService.cs index 1556a05f7..4ce62aa56 100644 --- a/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingService.cs +++ b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingService.cs @@ -9,7 +9,7 @@ namespace Altinn.App.Core.EFormidling.Interface; public interface IEFormidlingService { /// - /// Send the eFormidling shipment using ApplicationMetadata configuration (legacy) + /// Send the eFormidling shipment /// /// Instance data /// @@ -17,8 +17,6 @@ public interface IEFormidlingService /// /// Send the eFormidling shipment with explicit configuration context. - /// Default implementation calls the legacy method for backward compatibility. - /// Override this method to support BPMN-based configuration. /// /// Instance data /// A valid config for eFormidling. From 60c709dbb5d0b2b14b71aafed6ba5555718061f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Tue, 7 Oct 2025 12:25:44 +0200 Subject: [PATCH 48/60] Tweak token names in DefaultEformidlingService. --- .../Implementation/DefaultEFormidlingService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs index 82cafb8c3..508f7a5ee 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs @@ -98,16 +98,16 @@ private async Task SendEFormidlingShipmentInternal(Instance instance, ValidAltin ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - string authToken = _userTokenProvider.GetUserToken(); - string eFormidlingAccessToken = _tokenGenerator.GenerateAccessToken( + string userToken = _userTokenProvider.GetUserToken(); + string platformAccessToken = _tokenGenerator.GenerateAccessToken( applicationMetadata.Org, applicationMetadata.AppIdentifier.App ); var requestHeaders = new Dictionary { - { "Authorization", $"{AuthorizationSchemes.Bearer} {authToken}" }, - { General.EFormidlingAccessTokenHeaderName, eFormidlingAccessToken }, + { "Authorization", $"{AuthorizationSchemes.Bearer} {userToken}" }, + { General.EFormidlingAccessTokenHeaderName, platformAccessToken }, { General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey }, }; From e3a2dc7329ca346c9ba3060d652785e5b718330c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Tue, 7 Oct 2025 13:04:49 +0200 Subject: [PATCH 49/60] Add support for passing a list of task ids to the pdf component for auto pdf. --- .../Internal/Pdf/IPdfService.cs | 2 + .../Internal/Pdf/PdfService.cs | 63 +++++++++++++++++-- .../AltinnPdfConfiguration.cs | 15 ++++- .../ServiceTasks/PdfServiceTask.cs | 1 + .../Internal/Pdf/PdfServiceTests.cs | 53 ++++++++++++++++ .../ServiceTasks/PdfServiceTaskTests.cs | 39 ++++++++++++ 6 files changed, 165 insertions(+), 8 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs index f3b7a351e..11e9226aa 100644 --- a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs @@ -24,12 +24,14 @@ public interface IPdfService /// The task id for which the PDF is generated. /// The data type to use when storing the PDF. /// A text resource element id for the file name of the PDF. If no text resource is found, the literal value will be used. If null, a default file name will be used. + /// Enable auto-pdf for a list of tasks. Will not respect pdfLayoutName on those tasks, but use the main layout-set of the given tasks and render the components in summary mode. This setting will be ignored if the PDF task has a pdf layout set defined. /// Cancellation token for when a request should be stopped before it's completed. Task GenerateAndStorePdf( Instance instance, string taskId, string? dataTypeId, string? fileNameTextResourceElementId, + List? autoGeneratePdfForTaskIds = null, CancellationToken ct = default ) => GenerateAndStorePdf(instance, taskId, ct); diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index a606ca2f5..169a99f65 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -72,7 +72,7 @@ public async Task GenerateAndStorePdf(Instance instance, string taskId, Cancella { using var activity = _telemetry?.StartGenerateAndStorePdfActivity(instance, taskId); - await GenerateAndStorePdfInternal(instance, taskId, null, null, ct); + await GenerateAndStorePdfInternal(instance, taskId, null, null, null, ct); } /// @@ -81,12 +81,20 @@ public async Task GenerateAndStorePdf( string taskId, string? dataTypeId, string? fileNameTextResourceElementId, + List? autoGeneratePdfForTaskIds = null, CancellationToken ct = default ) { using var activity = _telemetry?.StartGenerateAndStorePdfActivity(instance, taskId); - await GenerateAndStorePdfInternal(instance, taskId, dataTypeId, fileNameTextResourceElementId, ct); + await GenerateAndStorePdfInternal( + instance, + taskId, + dataTypeId, + fileNameTextResourceElementId, + autoGeneratePdfForTaskIds, + ct + ); } /// @@ -102,7 +110,7 @@ public async Task GeneratePdf(Instance instance, string taskId, bool isP TextResource? textResource = await GetTextResource(instance, language); - return await GeneratePdfContent(instance, language, isPreview, textResource, ct); + return await GeneratePdfContent(instance, language, isPreview, textResource, null, ct); } /// @@ -116,6 +124,7 @@ private async Task GenerateAndStorePdfInternal( string taskId, string? dataTypeId, string? fileNameTextResourceElementId, + List? autoGeneratePdfForTaskIds = null, CancellationToken ct = default ) { @@ -127,7 +136,14 @@ private async Task GenerateAndStorePdfInternal( TextResource? textResource = await GetTextResource(instance, language); - await using Stream pdfContent = await GeneratePdfContent(instance, language, false, textResource, ct); + await using Stream pdfContent = await GeneratePdfContent( + instance, + language, + false, + textResource, + autoGeneratePdfForTaskIds, + ct + ); string fileName = GetFileName(instance, textResource, fileNameTextResourceElementId); await _dataClient.InsertBinaryData( @@ -146,6 +162,7 @@ private async Task GeneratePdfContent( string language, bool isPreview, TextResource? textResource, + List? autoGeneratePdfForTaskIds, CancellationToken ct ) { @@ -154,7 +171,11 @@ CancellationToken ct .AppPdfPagePathTemplate.ToLowerInvariant() .Replace("{instanceid}", instance.Id); - Uri uri = BuildUri(baseUrl, pagePath, language); + List> autoPdfTaskIdsQueryParams = CreateAutoPdfTaskIdsQueryParams( + autoGeneratePdfForTaskIds + ); + + Uri uri = BuildUri(baseUrl, pagePath, language, autoPdfTaskIdsQueryParams); bool displayFooter = _pdfGeneratorSettings.DisplayFooter; @@ -174,7 +195,12 @@ CancellationToken ct return pdfContent; } - private static Uri BuildUri(string baseUrl, string pagePath, string language) + private static Uri BuildUri( + string baseUrl, + string pagePath, + string language, + List>? additionalQueryParams = null + ) { // Uses string manipulation instead of UriBuilder, since UriBuilder messes up // query parameters in combination with hash fragments in the url. @@ -188,6 +214,14 @@ private static Uri BuildUri(string baseUrl, string pagePath, string language) url += $"?lang={language}"; } + if (additionalQueryParams != null) + { + foreach (KeyValuePair param in additionalQueryParams) + { + url += $"&{param.Key}={param.Value}"; + } + } + return new Uri(url); } @@ -365,4 +399,21 @@ private string GetFooterContent(Instance instance, TextResource? textResource) "; return footerTemplate; } + + private static List> CreateAutoPdfTaskIdsQueryParams( + List? autoGeneratePdfForTaskIds + ) + { + List> additionalQueryParams = []; + // Create query param array for autoGeneratePdfForTaskIds if provided, task=1&task=2 etc. + if (autoGeneratePdfForTaskIds != null && autoGeneratePdfForTaskIds.Count != 0) + { + foreach (string taskId in autoGeneratePdfForTaskIds) + { + additionalQueryParams.Add(new KeyValuePair("task", taskId)); + } + } + + return additionalQueryParams; + } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs index 0a103d6d0..fae411de9 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs @@ -19,13 +19,24 @@ public sealed class AltinnPdfConfiguration [XmlElement("filename", Namespace = "http://altinn.no/process")] public string? Filename { get; set; } + /// + /// Enable auto-pdf for a list of tasks. Will not respect pdfLayoutName on those tasks, but use the main layout-set of the given tasks and render the components in summary mode. This setting will be ignored if the PDF task has a pdf layout set defined. + /// + [XmlArray(ElementName = "autoPdfTaskIds", Namespace = "http://altinn.no/process", IsNullable = true)] + [XmlArrayItem(ElementName = "taskId", Namespace = "http://altinn.no/process")] + public List? AutoPdfTaskIds { get; set; } = []; + internal ValidAltinnPdfConfiguration Validate() { string? normalizedDataTypeId = string.IsNullOrWhiteSpace(DataTypeId) ? null : DataTypeId.Trim(); string? normalizedFilename = string.IsNullOrWhiteSpace(Filename) ? null : Filename.Trim(); - return new ValidAltinnPdfConfiguration(normalizedDataTypeId, normalizedFilename); + return new ValidAltinnPdfConfiguration(normalizedDataTypeId, normalizedFilename, AutoPdfTaskIds); } } -internal readonly record struct ValidAltinnPdfConfiguration(string? DataTypeId, string? Filename); +internal readonly record struct ValidAltinnPdfConfiguration( + string? DataTypeId, + string? Filename, + List? AutoPdfTaskIds +); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs index 4615726c0..7d96dcde5 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -43,6 +43,7 @@ await _pdfService.GenerateAndStorePdf( taskId, config.DataTypeId, config.Filename, + config.AutoPdfTaskIds, context.CancellationToken ); diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index f167f9b60..cc51c1b67 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -299,6 +299,59 @@ public void GetOverridenLanguage_NoLanguageInQuery_ShouldReturnNull() language.Should().BeNull(); } + [Fact] + public async Task GenerateAndStorePdf_WithAutoGeneratePdfForTaskIds_ShouldIncludeTaskIdsInUri() + { + // Arrange + var autoGeneratePdfForTaskIds = new List { "Task_1", "Task_2", "Task_3" }; + + _pdfGeneratorClient.Setup(s => + s.GeneratePdf(It.IsAny(), It.IsAny(), It.IsAny()) + ); + _generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}"; + + var target = SetupPdfService( + pdfGeneratorClient: _pdfGeneratorClient, + generalSettingsOptions: _generalSettingsOptions + ); + + Instance instance = new() + { + Id = $"509378/{Guid.NewGuid()}", + AppId = "digdir/not-really-an-app", + Org = "digdir", + }; + + // Act + await target.GenerateAndStorePdf( + instance, + "Task_PDF", + null, + null, + autoGeneratePdfForTaskIds, + CancellationToken.None + ); + + // Assert + _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) + && u.AbsoluteUri.Contains("task=Task_1") + && u.AbsoluteUri.Contains("task=Task_2") + && u.AbsoluteUri.Contains("task=Task_3") + ), + It.Is(s => s == null), + It.IsAny() + ), + Times.Once + ); + } + private PdfService SetupPdfService( Mock? appResources = null, Mock? dataClient = null, 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 2e8c8af7d..5a08732d4 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs @@ -58,9 +58,48 @@ public async Task Execute_Should_Call_GenerateAndStorePdf() instance.Process.CurrentTask.ElementId, null, FileName, + It.IsAny?>(), It.IsAny() ), Times.Once ); } + + [Fact] + public async Task Execute_Should_Pass_AutoPdfTaskIds_To_PdfService() + { + // Arrange + var taskIds = new List { "Task_1", "Task_2", "Task_3" }; + + _processReaderMock + .Setup(x => x.GetAltinnTaskExtension("pdfTask")) + .Returns( + new AltinnTaskExtension + { + TaskType = "pdf", + PdfConfiguration = new AltinnPdfConfiguration { Filename = "test.pdf", AutoPdfTaskIds = taskIds }, + } + ); + + var instance = new Instance + { + Process = new ProcessState { CurrentTask = new ProcessElementInfo { ElementId = "pdfTask" } }, + }; + + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); + + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; + + var serviceTask = new PdfServiceTask(_pdfServiceMock.Object, _processReaderMock.Object, _loggerMock.Object); + + // Act + await serviceTask.Execute(parameters); + + // Assert + _pdfServiceMock.Verify( + x => x.GenerateAndStorePdf(instance, "pdfTask", null, "test.pdf", taskIds, It.IsAny()), + Times.Once + ); + } } From e6cc72c3dad5514c26f914e1e636e001119369c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 8 Oct 2025 09:23:10 +0200 Subject: [PATCH 50/60] Add public api verified changes after adding auto pdf. --- ....PublicApi_ShouldNotChange_Unintentionally.verified.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 1de7c298f..a4f9efd1a 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3170,7 +3170,7 @@ namespace Altinn.App.Core.Internal.Pdf public interface IPdfService { System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct); - System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? dataTypeId, string? fileNameTextResourceElementId, System.Threading.CancellationToken ct = default); + System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? dataTypeId, string? fileNameTextResourceElementId, System.Collections.Generic.List? autoGeneratePdfForTaskIds = null, System.Threading.CancellationToken ct = default); System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct); System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, bool isPreview, System.Threading.CancellationToken ct); } @@ -3192,7 +3192,7 @@ namespace Altinn.App.Core.Internal.Pdf { public PdfService(Altinn.App.Core.Internal.App.IAppResources appResources, Altinn.App.Core.Internal.Data.IDataClient dataClient, Microsoft.AspNetCore.Http.IHttpContextAccessor httpContextAccessor, Altinn.App.Core.Internal.Pdf.IPdfGeneratorClient pdfGeneratorClient, Microsoft.Extensions.Options.IOptions pdfGeneratorSettings, Microsoft.Extensions.Options.IOptions generalSettings, Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Features.Auth.IAuthenticationContext authenticationContext, Altinn.App.Core.Features.Telemetry? telemetry = null) { } public System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct) { } - public System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? dataTypeId, string? fileNameTextResourceElementId, System.Threading.CancellationToken ct = default) { } + public System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? dataTypeId, string? fileNameTextResourceElementId, System.Collections.Generic.List? autoGeneratePdfForTaskIds = null, System.Threading.CancellationToken ct = default) { } public System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct) { } public System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, bool isPreview, System.Threading.CancellationToken ct) { } } @@ -3306,6 +3306,9 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties public sealed class AltinnPdfConfiguration { public AltinnPdfConfiguration() { } + [System.Xml.Serialization.XmlArray(ElementName="autoPdfTaskIds", IsNullable=true, Namespace="http://altinn.no/process")] + [System.Xml.Serialization.XmlArrayItem(ElementName="taskId", Namespace="http://altinn.no/process")] + public System.Collections.Generic.List? AutoPdfTaskIds { get; set; } [System.Xml.Serialization.XmlElement("dataTypeId", Namespace="http://altinn.no/process")] public string? DataTypeId { get; set; } [System.Xml.Serialization.XmlElement("filename", Namespace="http://altinn.no/process")] From 7a13da860db32aa3ed9fa09268c40d9f23875325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Mon, 13 Oct 2025 11:54:42 +0200 Subject: [PATCH 51/60] Remove data type config for pdf. --- src/Altinn.App.Core/Internal/Pdf/IPdfService.cs | 2 -- src/Altinn.App.Core/Internal/Pdf/PdfService.cs | 7 ++----- .../AltinnPdfConfiguration.cs | 15 ++------------- .../ProcessTasks/ServiceTasks/PdfServiceTask.cs | 1 - .../Internal/Pdf/PdfServiceTests.cs | 9 +-------- .../Process/ServiceTasks/PdfServiceTaskTests.cs | 3 +-- ...i_ShouldNotChange_Unintentionally.verified.txt | 6 ++---- 7 files changed, 8 insertions(+), 35 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs index 11e9226aa..1b036a6b1 100644 --- a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs @@ -22,14 +22,12 @@ public interface IPdfService /// /// The instance details. /// The task id for which the PDF is generated. - /// The data type to use when storing the PDF. /// A text resource element id for the file name of the PDF. If no text resource is found, the literal value will be used. If null, a default file name will be used. /// Enable auto-pdf for a list of tasks. Will not respect pdfLayoutName on those tasks, but use the main layout-set of the given tasks and render the components in summary mode. This setting will be ignored if the PDF task has a pdf layout set defined. /// Cancellation token for when a request should be stopped before it's completed. Task GenerateAndStorePdf( Instance instance, string taskId, - string? dataTypeId, string? fileNameTextResourceElementId, List? autoGeneratePdfForTaskIds = null, CancellationToken ct = default diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index 169a99f65..6bd0bb0a1 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -72,14 +72,13 @@ public async Task GenerateAndStorePdf(Instance instance, string taskId, Cancella { using var activity = _telemetry?.StartGenerateAndStorePdfActivity(instance, taskId); - await GenerateAndStorePdfInternal(instance, taskId, null, null, null, ct); + await GenerateAndStorePdfInternal(instance, taskId, null, null, ct); } /// public async Task GenerateAndStorePdf( Instance instance, string taskId, - string? dataTypeId, string? fileNameTextResourceElementId, List? autoGeneratePdfForTaskIds = null, CancellationToken ct = default @@ -90,7 +89,6 @@ public async Task GenerateAndStorePdf( await GenerateAndStorePdfInternal( instance, taskId, - dataTypeId, fileNameTextResourceElementId, autoGeneratePdfForTaskIds, ct @@ -122,7 +120,6 @@ public async Task GeneratePdf(Instance instance, string taskId, Cancella private async Task GenerateAndStorePdfInternal( Instance instance, string taskId, - string? dataTypeId, string? fileNameTextResourceElementId, List? autoGeneratePdfForTaskIds = null, CancellationToken ct = default @@ -148,7 +145,7 @@ private async Task GenerateAndStorePdfInternal( string fileName = GetFileName(instance, textResource, fileNameTextResourceElementId); await _dataClient.InsertBinaryData( instance.Id, - dataTypeId ?? PdfElementType, + PdfElementType, PdfContentType, fileName, pdfContent, diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs index fae411de9..10f63629b 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs @@ -7,12 +7,6 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; /// public sealed class AltinnPdfConfiguration { - /// - /// Set the data type to use when storing the PDF. If not set, ref-data-as-pdf will be used. - /// - [XmlElement("dataTypeId", Namespace = "http://altinn.no/process")] - public string? DataTypeId { get; set; } - /// /// Set the filename of the PDF. Supports text resource keys for language support. /// @@ -28,15 +22,10 @@ public sealed class AltinnPdfConfiguration internal ValidAltinnPdfConfiguration Validate() { - string? normalizedDataTypeId = string.IsNullOrWhiteSpace(DataTypeId) ? null : DataTypeId.Trim(); string? normalizedFilename = string.IsNullOrWhiteSpace(Filename) ? null : Filename.Trim(); - return new ValidAltinnPdfConfiguration(normalizedDataTypeId, normalizedFilename, AutoPdfTaskIds); + return new ValidAltinnPdfConfiguration(normalizedFilename, AutoPdfTaskIds); } } -internal readonly record struct ValidAltinnPdfConfiguration( - string? DataTypeId, - string? Filename, - List? AutoPdfTaskIds -); +internal readonly record struct ValidAltinnPdfConfiguration(string? Filename, List? AutoPdfTaskIds); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs index 7d96dcde5..11e3f874b 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -41,7 +41,6 @@ public async Task Execute(ServiceTaskContext context) await _pdfService.GenerateAndStorePdf( instance, taskId, - config.DataTypeId, config.Filename, config.AutoPdfTaskIds, context.CancellationToken diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index cc51c1b67..97a9ceffb 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -323,14 +323,7 @@ public async Task GenerateAndStorePdf_WithAutoGeneratePdfForTaskIds_ShouldInclud }; // Act - await target.GenerateAndStorePdf( - instance, - "Task_PDF", - null, - null, - autoGeneratePdfForTaskIds, - CancellationToken.None - ); + await target.GenerateAndStorePdf(instance, "Task_PDF", null, autoGeneratePdfForTaskIds, CancellationToken.None); // Assert _pdfGeneratorClient.Verify( 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 5a08732d4..7ebc87eab 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs @@ -56,7 +56,6 @@ public async Task Execute_Should_Call_GenerateAndStorePdf() x.GenerateAndStorePdf( instance, instance.Process.CurrentTask.ElementId, - null, FileName, It.IsAny?>(), It.IsAny() @@ -98,7 +97,7 @@ public async Task Execute_Should_Pass_AutoPdfTaskIds_To_PdfService() // Assert _pdfServiceMock.Verify( - x => x.GenerateAndStorePdf(instance, "pdfTask", null, "test.pdf", taskIds, It.IsAny()), + x => x.GenerateAndStorePdf(instance, "pdfTask", "test.pdf", taskIds, It.IsAny()), Times.Once ); } diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 5e5040479..2308f49ac 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3170,7 +3170,7 @@ namespace Altinn.App.Core.Internal.Pdf public interface IPdfService { System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct); - System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? dataTypeId, string? fileNameTextResourceElementId, System.Collections.Generic.List? autoGeneratePdfForTaskIds = null, System.Threading.CancellationToken ct = default); + System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? fileNameTextResourceElementId, System.Collections.Generic.List? autoGeneratePdfForTaskIds = null, System.Threading.CancellationToken ct = default); System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct); System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, bool isPreview, System.Threading.CancellationToken ct); } @@ -3192,7 +3192,7 @@ namespace Altinn.App.Core.Internal.Pdf { public PdfService(Altinn.App.Core.Internal.App.IAppResources appResources, Altinn.App.Core.Internal.Data.IDataClient dataClient, Microsoft.AspNetCore.Http.IHttpContextAccessor httpContextAccessor, Altinn.App.Core.Internal.Pdf.IPdfGeneratorClient pdfGeneratorClient, Microsoft.Extensions.Options.IOptions pdfGeneratorSettings, Microsoft.Extensions.Options.IOptions generalSettings, Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Features.Auth.IAuthenticationContext authenticationContext, Altinn.App.Core.Features.Telemetry? telemetry = null) { } public System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct) { } - public System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? dataTypeId, string? fileNameTextResourceElementId, System.Collections.Generic.List? autoGeneratePdfForTaskIds = null, System.Threading.CancellationToken ct = default) { } + public System.Threading.Tasks.Task GenerateAndStorePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, string? fileNameTextResourceElementId, System.Collections.Generic.List? autoGeneratePdfForTaskIds = null, System.Threading.CancellationToken ct = default) { } public System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, System.Threading.CancellationToken ct) { } public System.Threading.Tasks.Task GeneratePdf(Altinn.Platform.Storage.Interface.Models.Instance instance, string taskId, bool isPreview, System.Threading.CancellationToken ct) { } } @@ -3309,8 +3309,6 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties [System.Xml.Serialization.XmlArray(ElementName="autoPdfTaskIds", IsNullable=true, Namespace="http://altinn.no/process")] [System.Xml.Serialization.XmlArrayItem(ElementName="taskId", Namespace="http://altinn.no/process")] public System.Collections.Generic.List? AutoPdfTaskIds { get; set; } - [System.Xml.Serialization.XmlElement("dataTypeId", Namespace="http://altinn.no/process")] - public string? DataTypeId { get; set; } [System.Xml.Serialization.XmlElement("filename", Namespace="http://altinn.no/process")] public string? Filename { get; set; } } From 73b8d83faf430b395c37ad6cb12290c9cf30e4cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Mon, 13 Oct 2025 15:18:42 +0200 Subject: [PATCH 52/60] Sanitize more logs to satisfy sonar. --- .../Internal/Process/ProcessEngine.cs | 6 +++--- .../ServiceTasks/EFormidlingServiceTask.cs | 17 +++++++++++++---- .../ProcessTasks/ServiceTasks/PdfServiceTask.cs | 8 ++++++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 388ccc46d..6492b4040 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -170,7 +170,7 @@ out ProcessChangeResult? invalidProcessStateError Success = false, ErrorType = ProcessErrorType.Unauthorized, ErrorMessage = - $"User is not authorized to perform process next. Task ID: {currentTaskId}. Task type: {altinnTaskType}. Action: {request.Action ?? "none"}.", + $"User is not authorized to perform process next. Task ID: {LogSanitizer.Sanitize(currentTaskId)}. Task type: {LogSanitizer.Sanitize(altinnTaskType)}. Action: {LogSanitizer.Sanitize(request.Action ?? "none")}.", }; activity?.SetProcessChangeResult(result); return result; @@ -178,8 +178,8 @@ out ProcessChangeResult? invalidProcessStateError _logger.LogDebug( "User successfully authorized to perform process next. Task ID: {CurrentTaskId}. Task type: {AltinnTaskType}. Action: {ProcessNextAction}.", - currentTaskId, - altinnTaskType, + LogSanitizer.Sanitize(currentTaskId), + LogSanitizer.Sanitize(altinnTaskType), LogSanitizer.Sanitize(request.Action ?? "none") ); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs index 7add16e74..21ad41ff4 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -1,5 +1,6 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.EFormidling.Interface; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.Platform.Storage.Interface.Models; @@ -57,14 +58,20 @@ public async Task Execute(ServiceTaskContext context) { _logger.LogInformation( "EFormidling is disabled for task {TaskId}. No eFormidling shipment will be sent, but the service task will be completed.", - taskId + LogSanitizer.Sanitize(taskId) ); return ServiceTaskResult.Success(); } - _logger.LogDebug("Calling eFormidlingService for eFormidling Service Task {TaskId}.", taskId); + _logger.LogDebug( + "Calling eFormidlingService for eFormidling Service Task {TaskId}.", + LogSanitizer.Sanitize(taskId) + ); await _eFormidlingService.SendEFormidlingShipment(instance, configuration); - _logger.LogDebug("Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", taskId); + _logger.LogDebug( + "Successfully called eFormidlingService for eFormidling Service Task {TaskId}.", + LogSanitizer.Sanitize(taskId) + ); return ServiceTaskResult.Success(); } @@ -77,7 +84,9 @@ private Task GetValidAltinnEFormidlingConfi AltinnEFormidlingConfiguration? eFormidlingConfig = taskExtension?.EFormidlingConfiguration; if (eFormidlingConfig is null) - throw new ApplicationConfigException($"No eFormidling configuration found in BPMN for task {taskId}"); + throw new ApplicationConfigException( + $"No eFormidling configuration found in BPMN for task {LogSanitizer.Sanitize(taskId)}" + ); HostingEnvironment env = AltinnEnvironments.GetHostingEnvironment(_hostEnvironment); ValidAltinnEFormidlingConfiguration validConfig = eFormidlingConfig.Validate(env); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs index 11e3f874b..71f922a6a 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -1,3 +1,4 @@ +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Pdf; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.Platform.Storage.Interface.Models; @@ -35,7 +36,7 @@ public async Task Execute(ServiceTaskContext context) string taskId = context.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; Instance instance = context.InstanceDataMutator.Instance; - _logger.LogDebug("Calling PdfService for PDF Service Task {TaskId}.", taskId); + _logger.LogDebug("Calling PdfService for PDF Service Task {TaskId}.", LogSanitizer.Sanitize(taskId)); ValidAltinnPdfConfiguration config = GetValidAltinnPdfConfiguration(taskId); await _pdfService.GenerateAndStorePdf( @@ -46,7 +47,10 @@ await _pdfService.GenerateAndStorePdf( context.CancellationToken ); - _logger.LogDebug("Successfully called PdfService for PDF Service Task {TaskId}.", taskId); + _logger.LogDebug( + "Successfully called PdfService for PDF Service Task {TaskId}.", + LogSanitizer.Sanitize(taskId) + ); return ServiceTaskResult.Success(); } From 88032fbe34b1224d36ebbf587f5d2ebea7f46120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 15 Oct 2025 11:57:40 +0200 Subject: [PATCH 53/60] Remove trailing semicolon. --- .../ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs index 34632cdf6..1430312b3 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs @@ -16,7 +16,7 @@ internal interface IPdfServiceTaskLegacy /// Executes the service task. /// Task Execute(string taskId, Instance instance); -}; +} /// internal class PdfServiceTaskLegacy : IPdfServiceTaskLegacy From 2315a72b0d9aaa86dab6c8385e868d77f7063ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 15 Oct 2025 11:59:09 +0200 Subject: [PATCH 54/60] Fix typo. --- .../ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs index 1430312b3..fe19f88bb 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/PdfServiceTaskLegacy.cs @@ -9,7 +9,7 @@ namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; /// /// Service task that generates PDFs for all connected data types that have the EnablePdfCreation flag set to true. /// -/// Planned to be replaced by , but kept for now for backwards compatability. Called inline in , instead of through the service task system. +/// Planned to be replaced by , but kept for now for backwards compatibility. Called inline in , instead of through the service task system. internal interface IPdfServiceTaskLegacy { /// From f5e7499cea51a0a3e68f5577e1ab097dc117b52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 15 Oct 2025 11:59:35 +0200 Subject: [PATCH 55/60] Remove trailing semicolon. --- .../ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs index 307d29772..019b17620 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs @@ -20,7 +20,7 @@ internal interface IEFormidlingServiceTaskLegacy /// Executes the service task. /// Task Execute(string taskId, Instance instance); -}; +} /// internal class EformidlingServiceTaskLegacy : IEFormidlingServiceTaskLegacy From dd2d8ea59d1eb101770374395186529571aad3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 15 Oct 2025 12:00:05 +0200 Subject: [PATCH 56/60] Fix typo. --- .../ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs index 019b17620..a21ab5fb4 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/Legacy/EformidlingServiceTaskLegacy.cs @@ -13,7 +13,7 @@ namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.Legacy; /// /// Service task that sends eFormidling shipment, if EFormidling is enabled in config and EFormidling.SendAfterTaskId matches the current task. /// -/// Planned to be replaced by , but kept for now for backwards compatability. Called inline in instead of through the service task system. +/// Planned to be replaced by , but kept for now for backwards compatibility. Called inline in instead of through the service task system. internal interface IEFormidlingServiceTaskLegacy { /// From 4b41c5005f0bbee9cb2342e3630c6a4a883bb9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 15 Oct 2025 12:02:06 +0200 Subject: [PATCH 57/60] Null guard. --- src/Altinn.App.Core/Internal/Process/ProcessReader.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs index 762156bf5..3476bdc1f 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs @@ -135,25 +135,25 @@ public List GetSequenceFlowIds() ArgumentNullException.ThrowIfNull(elementId); ProcessTask? task = _definitions.Process.Tasks.Find(t => t.Id == elementId); - if (task != null) + if (task is not null) { return task; } - ServiceTask? serviceTask = _definitions.Process.ServiceTasks.Find(t => t.Id == elementId); - if (serviceTask != null) + ServiceTask? serviceTask = _definitions.Process.ServiceTasks?.Find(t => t.Id == elementId); + if (serviceTask is not null) { return serviceTask; } EndEvent? endEvent = _definitions.Process.EndEvents.Find(e => e.Id == elementId); - if (endEvent != null) + if (endEvent is not null) { return endEvent; } StartEvent? startEvent = _definitions.Process.StartEvents.Find(e => e.Id == elementId); - if (startEvent != null) + if (startEvent is not null) { return startEvent; } From 1782006f05237404c67040f66a8b16dd0b93276e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Thu, 16 Oct 2025 13:04:21 +0200 Subject: [PATCH 58/60] Move process next do while inside process engine. --- .../Controllers/ProcessController.cs | 50 +++------ .../Internal/Process/ProcessEngine.cs | 105 +++++++++++++++--- ...ouldNotChange_Unintentionally.verified.txt | 2 +- 3 files changed, 102 insertions(+), 55 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 99f2139d4..c6c37ca4c 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -9,7 +9,6 @@ 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.ProcessTasks.ServiceTasks; using Altinn.App.Core.Internal.Validation; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.Validation; @@ -267,35 +266,23 @@ public async Task> NextElement( { try { - Instance instance; - ProcessChangeResult result; - - bool moveToNextTaskAutomatically; - bool firstIteration = true; + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - do + var processNextRequest = new ProcessNextRequest { - instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - - var processNextRequest = new ProcessNextRequest - { - User = User, - Instance = instance, - Action = firstIteration ? processNext?.Action : null, - ActionOnBehalfOf = firstIteration ? processNext?.ActionOnBehalfOf : null, - Language = language, - }; + User = User, + Instance = instance, + Action = processNext?.Action, + ActionOnBehalfOf = processNext?.ActionOnBehalfOf, + Language = language, + }; - result = await _processEngine.Next(processNextRequest, ct); + ProcessChangeResult result = await _processEngine.Next(processNextRequest, ct); - if (!result.Success) - { - return GetResultForError(result); - } - - moveToNextTaskAutomatically = IsServiceTask(instance); - firstIteration = false; - } while (moveToNextTaskAutomatically); + if (!result.Success) + { + return GetResultForError(result); + } AppProcessState appProcessState = await ConvertAndAuthorizeActions( instance, @@ -518,17 +505,6 @@ private async Task ConvertAndAuthorizeActions(Instance instance return appProcessState; } - private bool IsServiceTask(Instance instance) - { - if (instance.Process.CurrentTask is null) - { - return false; - } - - IServiceTask? serviceTask = _processEngine.CheckIfServiceTask(instance.Process.CurrentTask.AltinnTaskType); - return serviceTask is not null; - } - private ActionResult GetResultForError(ProcessChangeResult result) { switch (result.ErrorType) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 6492b4040..9189db991 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -6,6 +6,7 @@ using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.Base; using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; @@ -26,6 +27,8 @@ namespace Altinn.App.Core.Internal.Process; /// public class ProcessEngine : IProcessEngine { + private const int MaxNextIterationsAllowed = 100; + private readonly IProcessReader _processReader; private readonly IProcessNavigator _processNavigator; private readonly IProcessEventHandlerDelegator _processEventHandlerDelegator; @@ -38,6 +41,7 @@ public class ProcessEngine : IProcessEngine private readonly IProcessEngineAuthorizer _processEngineAuthorizer; private readonly ILogger _logger; private readonly IValidationService _validationService; + private readonly IInstanceClient _instanceClient; /// /// Initializes a new instance of the class. @@ -52,6 +56,7 @@ public ProcessEngine( IServiceProvider serviceProvider, IProcessEngineAuthorizer processEngineAuthorizer, IValidationService validationService, + IInstanceClient instanceClient, ILogger logger, Telemetry? telemetry = null ) @@ -65,6 +70,7 @@ public ProcessEngine( _authenticationContext = authenticationContext; _processEngineAuthorizer = processEngineAuthorizer; _validationService = validationService; + _instanceClient = instanceClient; _logger = logger; _appImplementationFactory = serviceProvider.GetRequiredService(); _instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService(); @@ -147,6 +153,72 @@ public async Task Next(ProcessNextRequest request, Cancella using Activity? activity = _telemetry?.StartProcessNextActivity(instance, request.Action); + ProcessChangeResult result; + bool moveToNextTaskAutomatically; + bool firstIteration = true; + int iterationCount = 0; + + do + { + if (iterationCount >= MaxNextIterationsAllowed) + { + _logger.LogError( + "More than {MaxIterations} iterations detected in process for instance {InstanceId}. Possible loop in process definition.", + MaxNextIterationsAllowed, + instance.Id + ); + var loopError = new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Internal, + ErrorTitle = "Process loop detected", + ErrorMessage = + $"More than {MaxNextIterationsAllowed} iterations detected in process. Possible loop in process definition.", + }; + activity?.SetProcessChangeResult(loopError); + return loopError; + } + + // Fetch fresh instance on subsequent iterations + if (!firstIteration) + { + instance = await _instanceClient.GetInstance(instance); + } + + // Only use action and actionOnBehalfOf on first iteration + var processNextRequest = new ProcessNextRequest + { + User = request.User, + Instance = instance, + Action = firstIteration ? request.Action : null, + ActionOnBehalfOf = firstIteration ? request.ActionOnBehalfOf : null, + Language = request.Language, + }; + + result = await ProcessNext(processNextRequest, ct); + + if (!result.Success) + { + activity?.SetProcessChangeResult(result); + return result; + } + + moveToNextTaskAutomatically = IsServiceTask(instance); + firstIteration = false; + iterationCount++; + } while (moveToNextTaskAutomatically); + + activity?.SetProcessChangeResult(result); + return result; + } + + /// + /// Internal method that performs a single process next operation without automatic service task handling. + /// + private async Task ProcessNext(ProcessNextRequest request, CancellationToken ct = default) + { + Instance instance = request.Instance; + if ( !TryGetCurrentTaskIdAndAltinnTaskType( instance, @@ -155,7 +227,6 @@ out ProcessChangeResult? invalidProcessStateError ) ) { - activity?.SetProcessChangeResult(invalidProcessStateError); return invalidProcessStateError; } @@ -165,15 +236,13 @@ out ProcessChangeResult? invalidProcessStateError if (!authorized) { - var result = new ProcessChangeResult + return new ProcessChangeResult { Success = false, ErrorType = ProcessErrorType.Unauthorized, ErrorMessage = $"User is not authorized to perform process next. Task ID: {LogSanitizer.Sanitize(currentTaskId)}. Task type: {LogSanitizer.Sanitize(altinnTaskType)}. Action: {LogSanitizer.Sanitize(request.Action ?? "none")}.", }; - activity?.SetProcessChangeResult(result); - return result; } _logger.LogDebug( @@ -204,7 +273,6 @@ request with if (!serviceTaskProcessChangeResult.Success) { - activity?.SetProcessChangeResult(serviceTaskProcessChangeResult); return serviceTaskProcessChangeResult; } } @@ -216,14 +284,12 @@ request with if (userActionResult.ResultType is ResultType.Failure) { - var result = new ProcessChangeResult() + return new ProcessChangeResult() { Success = false, ErrorMessage = $"Action handler for action {LogSanitizer.Sanitize(request.Action)} failed!", ErrorType = userActionResult.ErrorType, }; - activity?.SetProcessChangeResult(result); - return result; } } } @@ -260,7 +326,7 @@ request with if (errorCount > 0) { - var result = new ProcessChangeResult + return new ProcessChangeResult { Success = false, ErrorType = ProcessErrorType.Conflict, @@ -268,8 +334,6 @@ request with ErrorMessage = $"{errorCount} validation errors found for task {currentTaskId}", ValidationIssues = validationIssues, }; - activity?.SetProcessChangeResult(result); - return result; } } @@ -281,14 +345,21 @@ request with await RunAppDefinedProcessEndHandlers(instance, moveToNextResult.ProcessStateChange?.Events); } - var changeResult = new ProcessChangeResult() + return new ProcessChangeResult() { Success = true, ProcessStateChange = moveToNextResult.ProcessStateChange }; + } + + /// + /// Check if the current task is a service task that should be automatically processed. + /// + private bool IsServiceTask(Instance instance) + { + if (instance.Process?.CurrentTask is null) { - Success = true, - ProcessStateChange = moveToNextResult.ProcessStateChange, - }; + return false; + } - activity?.SetProcessChangeResult(changeResult); - return changeResult; + IServiceTask? serviceTask = CheckIfServiceTask(instance.Process.CurrentTask.AltinnTaskType); + return serviceTask is not null; } /// diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 2308f49ac..47181bc79 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3620,7 +3620,7 @@ namespace Altinn.App.Core.Internal.Process } public class ProcessEngine : Altinn.App.Core.Internal.Process.IProcessEngine { - public ProcessEngine(Altinn.App.Core.Internal.Process.IProcessReader processReader, Altinn.App.Core.Internal.Process.IProcessNavigator processNavigator, Altinn.App.Core.Internal.Process.IProcessEventHandlerDelegator processEventsDelegator, Altinn.App.Core.Internal.Process.IProcessEventDispatcher processEventDispatcher, Altinn.App.Core.Features.Action.UserActionService userActionService, Altinn.App.Core.Features.Auth.IAuthenticationContext authenticationContext, System.IServiceProvider serviceProvider, Altinn.App.Core.Internal.Process.IProcessEngineAuthorizer processEngineAuthorizer, Altinn.App.Core.Internal.Validation.IValidationService validationService, Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Features.Telemetry? telemetry = null) { } + public ProcessEngine(Altinn.App.Core.Internal.Process.IProcessReader processReader, Altinn.App.Core.Internal.Process.IProcessNavigator processNavigator, Altinn.App.Core.Internal.Process.IProcessEventHandlerDelegator processEventsDelegator, Altinn.App.Core.Internal.Process.IProcessEventDispatcher processEventDispatcher, Altinn.App.Core.Features.Action.UserActionService userActionService, Altinn.App.Core.Features.Auth.IAuthenticationContext authenticationContext, System.IServiceProvider serviceProvider, Altinn.App.Core.Internal.Process.IProcessEngineAuthorizer processEngineAuthorizer, Altinn.App.Core.Internal.Validation.IValidationService validationService, Altinn.App.Core.Internal.Instances.IInstanceClient instanceClient, Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Features.Telemetry? telemetry = null) { } public Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.IServiceTask? CheckIfServiceTask(string? altinnTaskType) { } public System.Threading.Tasks.Task GenerateProcessStartEvents(Altinn.App.Core.Models.Process.ProcessStartRequest processStartRequest) { } public System.Threading.Tasks.Task HandleEventsAndUpdateStorage(Altinn.Platform.Storage.Interface.Models.Instance instance, System.Collections.Generic.Dictionary? prefill, System.Collections.Generic.List? events) { } From fa043be6484d55f4b13aed80f50201bf9f2aa559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Tore=20Gjerde?= Date: Wed, 29 Oct 2025 15:18:54 +0100 Subject: [PATCH 59/60] Unit test custom pdf file name. --- .../Internal/Pdf/PdfServiceTests.cs | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index cbba98a5b..a65dff011 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -348,6 +348,120 @@ public async Task GenerateAndStorePdf_WithAutoGeneratePdfForTaskIds_ShouldInclud ); } + [Fact] + public async Task GenerateAndStorePdf_WithCustomFileNameTextResourceKey_ShouldUseCustomFileName() + { + // Arrange + const string customTextResourceKey = "custom.pdf.filename"; + const string customFileName = "My Custom Receipt"; + + var mockAppResources = new Mock(); + var resource = new TextResource() + { + Id = "digdir-not-really-an-app-nb", + Language = LanguageConst.Nb, + Org = "digdir", + Resources = [new() { Id = customTextResourceKey, Value = customFileName }], + }; + mockAppResources + .Setup(s => s.GetTexts(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(resource); + + _pdfGeneratorClient.Setup(s => + s.GeneratePdf(It.IsAny(), It.IsAny(), It.IsAny()) + ); + _generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}"; + + var target = SetupPdfService( + appResources: mockAppResources, + pdfGeneratorClient: _pdfGeneratorClient, + generalSettingsOptions: _generalSettingsOptions + ); + + Instance instance = new() + { + Id = $"509378/{Guid.NewGuid()}", + AppId = "digdir/not-really-an-app", + Org = "digdir", + }; + + // Act + await target.GenerateAndStorePdf(instance, "Task_1", customTextResourceKey, null, CancellationToken.None); + + // Assert + _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 == "My%20Custom%20Receipt.pdf"), + It.IsAny(), + It.Is(s => s == "Task_1"), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + } + + [Fact] + public async Task GenerateAndStorePdf_WithCustomFileNameIncludingPdfExtension_ShouldNotDuplicateExtension() + { + // Arrange + const string customTextResourceKey = "custom.pdf.filename.with.extension"; + const string customFileName = "My Custom Receipt.pdf"; + + var mockAppResources = new Mock(); + var resource = new TextResource() + { + Id = "digdir-not-really-an-app-nb", + Language = LanguageConst.Nb, + Org = "digdir", + Resources = [new() { Id = customTextResourceKey, Value = customFileName }], + }; + mockAppResources + .Setup(s => s.GetTexts(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(resource); + + _pdfGeneratorClient.Setup(s => + s.GeneratePdf(It.IsAny(), It.IsAny(), It.IsAny()) + ); + _generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}"; + + var target = SetupPdfService( + appResources: mockAppResources, + pdfGeneratorClient: _pdfGeneratorClient, + generalSettingsOptions: _generalSettingsOptions + ); + + Instance instance = new() + { + Id = $"509378/{Guid.NewGuid()}", + AppId = "digdir/not-really-an-app", + Org = "digdir", + }; + + // Act + await target.GenerateAndStorePdf(instance, "Task_1", customTextResourceKey, null, CancellationToken.None); + + // Assert + _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 == "My%20Custom%20Receipt.pdf"), + It.IsAny(), + It.Is(s => s == "Task_1"), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + } + private PdfService SetupPdfService( Mock? appResources = null, Mock? dataClient = null, From 431ac68f356bd317bc1ecc4f1aa072ad77818127 Mon Sep 17 00:00:00 2001 From: Daniel Skovli Date: Thu, 30 Oct 2025 10:45:33 +0100 Subject: [PATCH 60/60] ProcessEngine and related tweaks from feature/fiks-io-client branch (#1548) --- .../Controllers/ProcessController.cs | 21 +----- .../Internal/Process/ProcessEngine.cs | 73 +++++++++++-------- .../ProcessTasks/ServiceTasks/IServiceTask.cs | 48 ++++++++++-- ...ouldNotChange_Unintentionally.verified.txt | 10 ++- 4 files changed, 92 insertions(+), 60 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 685ab25c3..af08623f2 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -392,7 +392,7 @@ instance.Process.EndEvent is null { Instance = instance, User = User, - Action = ConvertTaskTypeToAction(instance.Process.CurrentTask.AltinnTaskType), + Action = ProcessEngine.ConvertTaskTypeToAction(instance.Process.CurrentTask.AltinnTaskType), Language = language, }; ProcessChangeResult result = await _processEngine.Next(request); @@ -646,25 +646,6 @@ private async Task> AuthorizeActions(List actions return await _authorization.AuthorizeActions(instance, HttpContext.User, actions); } - private static string ConvertTaskTypeToAction(string actionOrTaskType) - { - switch (actionOrTaskType) - { - case "data": - case "feedback": - case "pdf": - case "eFormidling": - return "write"; - case "confirmation": - return "confirm"; - case "signing": - return "sign"; - default: - // Not any known task type, so assume it is an action type - return actionOrTaskType; - } - } - private ActionResult HandlePlatformHttpException(PlatformHttpException e, string defaultMessage) { if (e.Response.StatusCode == HttpStatusCode.Forbidden) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 9189db991..425c9e8a6 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -203,6 +203,7 @@ public async Task Next(ProcessNextRequest request, Cancella return result; } + // NOTE: `instance` has now been mutated by ProcessNext moveToNextTaskAutomatically = IsServiceTask(instance); firstIteration = false; iterationCount++; @@ -254,6 +255,9 @@ out ProcessChangeResult? invalidProcessStateError string checkedAction = request.Action ?? ConvertTaskTypeToAction(altinnTaskType); var isServiceTask = false; + ProcessChangeResult? serviceTaskProcessChangeResult = null; + ServiceTaskResult? serviceTaskResult = null; + // If the action is 'reject', we should not run any service task and there is no need to check for a user action handler, since 'reject' doesn't have one. if (request.Action is not "reject") { @@ -261,7 +265,7 @@ out ProcessChangeResult? invalidProcessStateError if (serviceTask is not null) { isServiceTask = true; - ProcessChangeResult serviceTaskProcessChangeResult = await HandleServiceTask( + (serviceTaskProcessChangeResult, serviceTaskResult) = await HandleServiceTask( instance, serviceTask, request with @@ -271,7 +275,7 @@ request with ct ); - if (!serviceTaskProcessChangeResult.Success) + if (serviceTaskResult?.AutoMoveNext is not true) { return serviceTaskProcessChangeResult; } @@ -337,7 +341,9 @@ request with } } - MoveToNextResult moveToNextResult = await HandleMoveToNext(instance, request.Action); + MoveToNextResult moveToNextResult = serviceTaskResult is not null + ? await HandleMoveToNext(instance, serviceTaskResult.AutoMoveNextAction) + : await HandleMoveToNext(instance, request.Action); if (moveToNextResult.IsEndEvent) { @@ -345,7 +351,7 @@ request with await RunAppDefinedProcessEndHandlers(instance, moveToNextResult.ProcessStateChange?.Events); } - return new ProcessChangeResult() { Success = true, ProcessStateChange = moveToNextResult.ProcessStateChange }; + return new ProcessChangeResult { Success = true, ProcessStateChange = moveToNextResult.ProcessStateChange }; } /// @@ -448,7 +454,7 @@ CancellationToken ct return actionResult; } - private async Task HandleServiceTask( + private async Task<(ProcessChangeResult, ServiceTaskResult?)> HandleServiceTask( Instance instance, IServiceTask serviceTask, ProcessNextRequest request, @@ -459,15 +465,16 @@ private async Task HandleServiceTask( if (request.Action is not "write" && request.Action != serviceTask.Type) // serviceTask.Type is accepted to support custom service task types { - var result = new ProcessChangeResult() - { - ErrorTitle = "User action not supported!", - ErrorMessage = - $"Service tasks do not support running user actions! Received action param {LogSanitizer.Sanitize(request.Action)}.", - ErrorType = ProcessErrorType.Conflict, - }; - - return result; + return ( + new ProcessChangeResult + { + ErrorTitle = "User action not supported!", + ErrorMessage = + $"Service tasks do not support running user actions! Received action param {LogSanitizer.Sanitize(request.Action)}.", + ErrorType = ProcessErrorType.Conflict, + }, + null + ); } try @@ -486,13 +493,16 @@ private async Task HandleServiceTask( { _logger.LogError("Service task {ServiceTaskType} returned a failed result.", serviceTask.Type); - return new ProcessChangeResult() - { - Success = false, - ErrorTitle = "Service task failed!", - ErrorMessage = $"Service task {serviceTask.Type} returned a failed result!", - ErrorType = ProcessErrorType.Internal, - }; + return ( + new ProcessChangeResult + { + Success = false, + ErrorTitle = "Service task failed!", + ErrorMessage = $"Service task {serviceTask.Type} returned a failed result!", + ErrorType = ProcessErrorType.Internal, + }, + result + ); } if (cachedDataMutator.HasAbandonIssues) @@ -506,20 +516,23 @@ private async Task HandleServiceTask( await cachedDataMutator.UpdateInstanceData(changes); await cachedDataMutator.SaveChanges(changes); - return new ProcessChangeResult { Success = true }; + return (new ProcessChangeResult { Success = true }, result); } catch (Exception ex) { activity?.Errored(ex); _logger.LogError(ex, "Service task {ServiceTaskType} returned a failed result.", serviceTask.Type); - return new ProcessChangeResult() - { - Success = false, - ErrorTitle = "Service task failed!", - ErrorMessage = $"Service task {serviceTask.Type} failed with an exception!", - ErrorType = ProcessErrorType.Internal, - }; + return ( + new ProcessChangeResult + { + Success = false, + ErrorTitle = "Service task failed!", + ErrorMessage = $"Service task {serviceTask.Type} failed with an exception!", + ErrorType = ProcessErrorType.Internal, + }, + null + ); } } @@ -752,7 +765,7 @@ private sealed record MoveToNextResult(Instance Instance, ProcessStateChange? Pr public bool IsEndEvent => ProcessStateChange?.NewProcessState?.Ended is not null; }; - private static string ConvertTaskTypeToAction(string actionOrTaskType) + internal static string ConvertTaskTypeToAction(string actionOrTaskType) { switch (actionOrTaskType) { diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs index e8883a3bb..b96253177 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs @@ -36,19 +36,55 @@ public sealed record ServiceTaskContext /// public abstract record ServiceTaskResult { - /// Creates a successful result. - public static ServiceTaskSuccessResult Success() => new(); + /// + /// Indicates whether the process should automatically move to the next task after this service task. + /// + public bool? AutoMoveNext { get; init; } + + /// + /// The action to use when automatically moving to the next task. + /// + public string? AutoMoveNextAction { get; init; } + + /// + /// Creates a successful result. + /// + /// Should the process automatically move to the next task? Defaults to true. + /// The action to use when automatically moving to the next task. Defaults to null. + public static ServiceTaskSuccessResult Success(bool? autoMoveNext = true, string? autoMoveNextAction = null) => + new(autoMoveNext, autoMoveNextAction); - /// Creates a failed result. - public static ServiceTaskFailedResult Failed() => new(); + /// + /// Creates a failed result. + /// + /// Should the process automatically move to the next task? Defaults to false. + /// The action to use when automatically moving to the next task. Defaults to reject. + public static ServiceTaskFailedResult Failed(bool? autoMoveNext = false, string? autoMoveNextAction = "reject") => + new(autoMoveNext, autoMoveNextAction); } /// /// Represents a successful result of executing a service task. /// -public sealed record ServiceTaskSuccessResult : ServiceTaskResult; +public sealed record ServiceTaskSuccessResult : ServiceTaskResult +{ + /// + public ServiceTaskSuccessResult(bool? autoMoveNext = null, string? autoMoveNextAction = null) + { + AutoMoveNext = autoMoveNext; + AutoMoveNextAction = autoMoveNextAction; + } +} /// /// Represents a failed result of executing a service task. /// -public sealed record ServiceTaskFailedResult : ServiceTaskResult; +public sealed record ServiceTaskFailedResult : ServiceTaskResult +{ + /// + public ServiceTaskFailedResult(bool? autoMoveNext = null, string? autoMoveNextAction = null) + { + AutoMoveNext = autoMoveNext; + AutoMoveNextAction = autoMoveNextAction; + } +} diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 196c82983..6ede9a19b 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3774,17 +3774,19 @@ namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks } public sealed class ServiceTaskFailedResult : Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskResult, System.IEquatable { - public ServiceTaskFailedResult() { } + public ServiceTaskFailedResult(bool? autoMoveNext = default, string? autoMoveNextAction = null) { } } public abstract class ServiceTaskResult : System.IEquatable { protected ServiceTaskResult() { } - public static Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskFailedResult Failed() { } - public static Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskSuccessResult Success() { } + public bool? AutoMoveNext { get; init; } + public string? AutoMoveNextAction { get; init; } + public static Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskFailedResult Failed(bool? autoMoveNext = false, string? autoMoveNextAction = "reject") { } + public static Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskSuccessResult Success(bool? autoMoveNext = true, string? autoMoveNextAction = null) { } } public sealed class ServiceTaskSuccessResult : Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskResult, System.IEquatable { - public ServiceTaskSuccessResult() { } + public ServiceTaskSuccessResult(bool? autoMoveNext = default, string? autoMoveNextAction = null) { } } } namespace Altinn.App.Core.Internal.Profile