diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 28b9156f0..af08623f2 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -11,7 +11,6 @@ using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; 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 +34,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 +59,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 +237,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. /// @@ -301,110 +268,16 @@ public async Task> NextElement( { Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - string? currentTaskId = instance.Process.CurrentTask?.ElementId; - - if (currentTaskId is null) + var processNextRequest = new ProcessNextRequest { - 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; - - 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, + Instance = instance, + Action = processNext?.Action, 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 failedUserActionResult = new ProcessChangeResult() - { - Success = false, - ErrorMessage = $"Action handler for action {request.Action} failed!", - ErrorType = userActionResult.ErrorType, - }; - - return GetResultForError(failedUserActionResult); - } - } - - // 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) - { - return Conflict(validationProblem); - } - } - - ProcessChangeResult result = await _processEngine.Next(request); + ProcessChangeResult result = await _processEngine.Next(processNextRequest, ct); if (!result.Success) { @@ -420,7 +293,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) @@ -429,52 +302,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 +388,14 @@ instance.Process.EndEvent is null try { - ProcessNextRequest request = new ProcessNextRequest() + ProcessNextRequest request = new() { Instance = instance, User = User, - Action = ConvertTaskTypeToAction(instance.Process.CurrentTask.AltinnTaskType), + Action = ProcessEngine.ConvertTaskTypeToAction(instance.Process.CurrentTask.AltinnTaskType), Language = language, }; - var result = await _processEngine.Next(request); + ProcessChangeResult result = await _processEngine.Next(request); if (!result.Success) { @@ -656,16 +483,18 @@ 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(); } } 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, + ElementType = processElement.ElementType(), AltinnTaskType = processElement.ExtensionElements?.TaskExtension?.TaskType, } ); @@ -676,6 +505,56 @@ private async Task ConvertAndAuthorizeActions(Instance instance return appProcessState; } + 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); @@ -730,26 +609,41 @@ private ObjectResult ExceptionResponse(Exception exception, string message) ); } - private async Task> AuthorizeActions(List actions, Instance instance) + private async Task GetValidationProblemDetails( + Instance instance, + string currentTaskId, + string? language + ) { - return await _authorization.AuthorizeActions(instance, HttpContext.User, actions); + 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 static string ConvertTaskTypeToAction(string actionOrTaskType) + private async Task> AuthorizeActions(List actions, Instance instance) { - switch (actionOrTaskType) - { - case "data": - case "feedback": - 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; - } + return await _authorization.AuthorizeActions(instance, HttpContext.User, actions); } private ActionResult HandlePlatformHttpException(PlatformHttpException e, string defaultMessage) diff --git a/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/EFormidling/Extensions/ServiceCollectionExtensions.cs index c83830c1e..4f747e809 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..6869c6c36 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingReceivers.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingReceivers.cs @@ -1,5 +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; @@ -24,17 +25,44 @@ public DefaultEFormidlingReceivers(IAppMetadata appMetadata) /// public async Task> GetEFormidlingReceivers(Instance instance) { - await Task.CompletedTask; + ArgumentNullException.ThrowIfNull(instance); - Identifier identifier = new Identifier + 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) + { + ArgumentNullException.ThrowIfNull(instance); + + if (string.IsNullOrWhiteSpace(receiverFromConfig)) + { + 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:{(await _appMetadata.GetApplicationMetadata()).EFormidling.Receiver.Trim()}", + Value = $"0192:{receiver}", Authority = "iso6523-actorid-upis", }; - Receiver receiver = new Receiver { Identifier = identifier }; - - return new List { receiver }; + return [new Receiver { Identifier = identifier }]; } } diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs index 2ca8a5fe5..508f7a5ee 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 IEFormidlingLegacyConfigurationProvider _configurationProvider; /// /// Initializes a new instance of the class. @@ -45,6 +47,7 @@ public DefaultEFormidlingService( IDataClient dataClient, IEventsClient eventClient, IServiceProvider sp, + IEFormidlingLegacyConfigurationProvider 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 userToken = _userTokenProvider.GetUserToken(); + string platformAccessToken = _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} {userToken}" }, + { General.EFormidlingAccessTokenHeaderName, platformAccessToken }, { 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/EFormidlingLegacyConfigurationProvider.cs b/src/Altinn.App.Core/EFormidling/Implementation/EFormidlingLegacyConfigurationProvider.cs new file mode 100644 index 000000000..ff8568c55 --- /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 EFormidlingLegacyConfigurationProvider : IEFormidlingLegacyConfigurationProvider +{ + private readonly IAppMetadata _appMetadata; + + public EFormidlingLegacyConfigurationProvider(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/EFormidling/Interface/IEFormidlingReceivers.cs b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs index 3c07c7695..7b5854db1 100644 --- a/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs +++ b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingReceivers.cs @@ -11,12 +11,30 @@ 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 /// List of eFormidling receivers + /// Thrown when is 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/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Interface/IEFormidlingService.cs index 52b8312b6..4ce62aa56 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; @@ -13,4 +14,16 @@ public interface IEFormidlingService /// Instance data /// public Task SendEFormidlingShipment(Instance instance); + + /// + /// Send the eFormidling shipment with explicit configuration context. + /// + /// 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/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 693c1a62a..e463ded3a 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -47,7 +47,8 @@ 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.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; @@ -371,8 +372,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 557004217..b4b1907ee 100644 --- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs +++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs @@ -236,6 +236,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 c69a2caf3..9047c7787 100644 --- a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs @@ -12,15 +12,32 @@ 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); + /// + /// 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 which the PDF is generated. + /// A text resource element id for the file name of the PDF. 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? customFileNameTextResourceKey, + List? autoGeneratePdfForTaskIds = null, + 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); diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index e80e5a7ce..fd60b4b9b 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -62,23 +62,26 @@ 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(); + await GenerateAndStorePdfInternal(instance, taskId, null, null, ct); + } - var pdfContent = await GeneratePdfContent(instance, language, isPreview: false, ct); + /// + public async Task GenerateAndStorePdf( + Instance instance, + string taskId, + string? customFileNameTextResourceKey, + List? autoGeneratePdfForTaskIds = null, + CancellationToken ct = default + ) + { + using var activity = _telemetry?.StartGenerateAndStorePdfActivity(instance, taskId); - string fileName = await GetFileName(language); - await _dataClient.InsertBinaryData( - instance.Id, - PdfElementType, - PdfContentType, - fileName, - pdfContent, + await GenerateAndStorePdfInternal( + instance, taskId, - cancellationToken: ct + customFileNameTextResourceKey, + autoGeneratePdfForTaskIds, + ct ); } @@ -93,7 +96,7 @@ public async Task GeneratePdf(Instance instance, string taskId, bool isP var language = GetOverriddenLanguage(queries) ?? await auth.GetLanguage(); - return await GeneratePdfContent(instance, language, isPreview, ct); + return await GeneratePdfContent(instance, language, isPreview, null, ct); } /// @@ -102,10 +105,45 @@ 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? customFileNameTextResourceKey, + List? autoGeneratePdfForTaskIds = null, + CancellationToken ct = default + ) + { + HttpContext? httpContext = _httpContextAccessor.HttpContext; + var queries = httpContext?.Request.Query; + var auth = _authenticationContext.Current; + + var language = GetOverriddenLanguage(queries) ?? await auth.GetLanguage(); + + await using Stream pdfContent = await GeneratePdfContent( + instance, + language, + false, + autoGeneratePdfForTaskIds, + ct + ); + + string fileName = await GetFileName(language, customFileNameTextResourceKey); + await _dataClient.InsertBinaryData( + instance.Id, + PdfElementType, + PdfContentType, + fileName, + pdfContent, + taskId, + cancellationToken: ct + ); + } + private async Task GeneratePdfContent( Instance instance, string language, bool isPreview, + List? autoGeneratePdfForTaskIds, CancellationToken ct ) { @@ -114,7 +152,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; @@ -134,7 +176,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. @@ -149,6 +196,14 @@ private static Uri BuildUri(string baseUrl, string pagePath, string language) url += $"?lang={lang}"; } + if (additionalQueryParams != null) + { + foreach (KeyValuePair param in additionalQueryParams) + { + url += $"&{param.Key}={param.Value}"; + } + } + return new Uri(url); } @@ -170,9 +225,12 @@ private static Uri BuildUri(string baseUrl, string pagePath, string language) return null; } - private async Task GetFileName(string? language) + private async Task GetFileName(string? language, string? customFileNameTextResourceKey) { - string? titleText = await _translationService.TranslateTextKey("backend.pdf_default_file_name", language); + string? titleText = await _translationService.TranslateTextKey( + customFileNameTextResourceKey ?? "backend.pdf_default_file_name", + language + ); if (string.IsNullOrEmpty(titleText)) { @@ -241,4 +299,21 @@ private async Task GetFooterContent(Instance instance, string? language) "; 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/AltinnEFormidlingConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs new file mode 100644 index 000000000..fefda3af4 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnEFormidlingConfiguration.cs @@ -0,0 +1,241 @@ +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. All properties support environment-specific values using 'env' attributes. +/// +public sealed class AltinnEFormidlingConfiguration +{ + /// + /// Can be used to disable eFormidling in specific environments. If omitted, defaults to false (eFormidling is enabled by default). + /// + [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. + /// + [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) + { + var validator = new ConfigValidator(env); + + // 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)); + 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( + disabled, + receiver, + process, + standard, + typeVersion, + type, + securityLevel, + dpfShipmentType, + dataTypes + ); + } + + private static string? GetOptionalConfig(List configs, HostingEnvironment env) + { + return AltinnTaskExtension.GetConfigForEnvironment(env, configs)?.Value; + } + + private static string GetRequiredConfig( + 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 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) + ); + } + } + } + + /// + /// 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 +/// +/// 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 +/// 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( + bool Disabled, + 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/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..10f63629b --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnPdfConfiguration.cs @@ -0,0 +1,31 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +/// +/// Configuration properties for PDF in a process task +/// +public sealed 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; } + + /// + /// 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? normalizedFilename = string.IsNullOrWhiteSpace(Filename) ? null : Filename.Trim(); + + return new ValidAltinnPdfConfiguration(normalizedFilename, AutoPdfTaskIds); + } +} + +internal readonly record struct ValidAltinnPdfConfiguration(string? Filename, List? AutoPdfTaskIds); 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..649783d4f 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,18 @@ 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; } + + /// + /// 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/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/src/Altinn.App.Core/Internal/Process/Elements/Process.cs b/src/Altinn.App.Core/Internal/Process/Elements/Process.cs index 70b4e3179..e52f74a90 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..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,6 @@ 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.ServiceTasks.Legacy; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -15,6 +15,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 +32,8 @@ ILogger logger _processTaskDataLocker = processTaskDataLocker; _processTaskFinisher = processTaskFinisher; _appImplementationFactory = serviceProvider.GetRequiredService(); + _pdfServiceTaskLegacy = serviceProvider.GetRequiredService(); + _eformidlingServiceTaskLegacy = serviceProvider.GetRequiredService(); _logger = logger; } @@ -38,23 +42,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 +61,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/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..425c9e8a6 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -6,13 +6,19 @@ 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; +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; @@ -21,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; @@ -30,9 +38,13 @@ 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; + private readonly IInstanceClient _instanceClient; /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class. /// public ProcessEngine( IProcessReader processReader, @@ -42,6 +54,10 @@ public ProcessEngine( UserActionService userActionService, IAuthenticationContext authenticationContext, IServiceProvider serviceProvider, + IProcessEngineAuthorizer processEngineAuthorizer, + IValidationService validationService, + IInstanceClient instanceClient, + ILogger logger, Telemetry? telemetry = null ) { @@ -52,6 +68,10 @@ public ProcessEngine( _userActionService = userActionService; _telemetry = telemetry; _authenticationContext = authenticationContext; + _processEngineAuthorizer = processEngineAuthorizer; + _validationService = validationService; + _instanceClient = instanceClient; + _logger = logger; _appImplementationFactory = serviceProvider.GetRequiredService(); _instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService(); } @@ -99,7 +119,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) @@ -127,17 +147,276 @@ 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); + + 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; + } + + // NOTE: `instance` has now been mutated by ProcessNext + 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, + out CurrentTaskIdAndAltinnTaskType? currentTaskIdAndAltinnTaskType, + out ProcessChangeResult? invalidProcessStateError + ) + ) + { + return invalidProcessStateError; + } + + (string currentTaskId, string altinnTaskType) = currentTaskIdAndAltinnTaskType; + + bool authorized = await _processEngineAuthorizer.AuthorizeProcessNext(instance, request.Action); + + if (!authorized) + { + 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")}.", + }; + } + + _logger.LogDebug( + "User successfully authorized to perform process next. Task ID: {CurrentTaskId}. Task type: {AltinnTaskType}. Action: {ProcessNextAction}.", + LogSanitizer.Sanitize(currentTaskId), + LogSanitizer.Sanitize(altinnTaskType), + LogSanitizer.Sanitize(request.Action ?? "none") + ); + + 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") + { + IServiceTask? serviceTask = CheckIfServiceTask(altinnTaskType); + if (serviceTask is not null) + { + isServiceTask = true; + (serviceTaskProcessChangeResult, serviceTaskResult) = await HandleServiceTask( + instance, + serviceTask, + request with + { + Action = checkedAction, + }, + ct + ); + + if (serviceTaskResult?.AutoMoveNext is not true) + { + return serviceTaskProcessChangeResult; + } + } + else + { + if (request.Action is not null) + { + UserActionResult userActionResult = await HandleUserAction(instance, request, ct); + + if (userActionResult.ResultType is ResultType.Failure) + { + return new ProcessChangeResult() + { + Success = false, + ErrorMessage = $"Action handler for action {LogSanitizer.Sanitize(request.Action)} failed!", + ErrorType = userActionResult.ErrorType, + }; + } + } + } + } + + // 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 if (isServiceTask) + { + _logger.LogInformation("Skipping validation during process next because the task is a service task."); + } + 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) + { + return new ProcessChangeResult + { + Success = false, + ErrorType = ProcessErrorType.Conflict, + ErrorTitle = "Validation failed for task", + ErrorMessage = $"{errorCount} validation errors found for task {currentTaskId}", + ValidationIssues = validationIssues, + }; + } + } + + MoveToNextResult moveToNextResult = serviceTaskResult is not null + ? await HandleMoveToNext(instance, serviceTaskResult.AutoMoveNextAction) + : await HandleMoveToNext(instance, request.Action); + + if (moveToNextResult.IsEndEvent) + { + _telemetry?.ProcessEnded(moveToNextResult.ProcessStateChange); + await RunAppDefinedProcessEndHandlers(instance, moveToNextResult.ProcessStateChange?.Events); + } + + 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) + { + return false; + } + + IServiceTask? serviceTask = CheckIfServiceTask(instance.Process.CurrentTask.AltinnTaskType); + return serviceTask is not null; + } + + /// + public async Task HandleEventsAndUpdateStorage( + Instance instance, + Dictionary? prefill, + List? events + ) + { + using (_telemetry?.StartProcessHandleEventsActivity(instance)) + { + await _processEventHandlerDelegator.HandleEvents(instance, prefill, events); + } + + using (_telemetry?.StartProcessStoreEventsActivity(instance)) + { + return await _processEventDispatcher.DispatchToStorage(instance, events); + } + } + + /// + 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,64 +447,92 @@ 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); return actionResult; } - /// - public async Task Next(ProcessNextRequest request) + private async Task<(ProcessChangeResult, ServiceTaskResult?)> HandleServiceTask( + Instance instance, + IServiceTask serviceTask, + ProcessNextRequest request, + CancellationToken ct = default + ) { - using var activity = _telemetry?.StartProcessNextActivity(request.Instance, request.Action); - - Instance instance = request.Instance; - string? currentElementId = instance.Process?.CurrentTask?.ElementId; + using Activity? activity = _telemetry?.StartProcessExecuteServiceTaskActivity(instance, serviceTask.Type); - 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() - { - Success = false, - ErrorMessage = $"Instance does not have current task information!", - ErrorType = ProcessErrorType.Conflict, - }; - activity?.SetProcessChangeResult(result); - 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 + ); } - MoveToNextResult moveToNextResult = await HandleMoveToNext(instance, request.Action); - - if (moveToNextResult.IsEndEvent) + try { - _telemetry?.ProcessEnded(moveToNextResult.ProcessStateChange); - await RunAppDefinedProcessEndHandlers(instance, moveToNextResult.ProcessStateChange?.Events); - } + InstanceDataUnitOfWork cachedDataMutator = await _instanceDataUnitOfWorkInitializer.Init( + instance, + instance.Process?.CurrentTask?.ElementId, + request.Language + ); - var changeResult = new ProcessChangeResult() - { - Success = true, - ProcessStateChange = moveToNextResult.ProcessStateChange, - }; - activity?.SetProcessChangeResult(changeResult); - return changeResult; - } + ServiceTaskContext context = new() { InstanceDataMutator = cachedDataMutator, CancellationToken = ct }; - /// - public async Task HandleEventsAndUpdateStorage( - Instance instance, - Dictionary? prefill, - List? events - ) - { - using (var activity = _telemetry?.StartProcessHandleEventsActivity(instance)) - { - await _processEventHandlerDelegator.HandleEvents(instance, prefill, events); + ServiceTaskResult result = await serviceTask.Execute(context); + + if (result is ServiceTaskFailedResult) + { + _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, + }, + result + ); + } + + 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 }, result); } - using (var activity = _telemetry?.StartProcessStoreEventsActivity(instance)) + catch (Exception ex) { - return await _processEventDispatcher.DispatchToStorage(instance, events); + 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, + }, + null + ); } } @@ -266,7 +573,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) { @@ -281,13 +591,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 = []; @@ -415,7 +728,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) { @@ -451,4 +764,84 @@ private sealed record MoveToNextResult(Instance Instance, ProcessStateChange? Pr [MemberNotNullWhen(true, nameof(ProcessStateChange))] public bool IsEndEvent => ProcessStateChange?.NewProcessState?.Ended is not null; }; + + internal 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 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); } 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..3476bdc1f 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]; } /// @@ -135,19 +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 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; } 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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/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(); 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..21ad41ff4 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/EFormidlingServiceTask.cs @@ -0,0 +1,96 @@ +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; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks; + +internal interface IEFormidlingServiceTask : IServiceTask { } + +/// +/// Service task that sends eFormidling shipment, if EFormidling is enabled in config. +/// +internal sealed class EFormidlingServiceTask : IEFormidlingServiceTask +{ + private readonly ILogger _logger; + private readonly IProcessReader _processReader; + private readonly IHostEnvironment _hostEnvironment; + private readonly IEFormidlingService? _eFormidlingService; + + /// + /// Initializes a new instance of the class. + /// + public EFormidlingServiceTask( + ILogger logger, + IProcessReader processReader, + IHostEnvironment hostEnvironment, + IEFormidlingService? eFormidlingService = null + ) + { + _logger = logger; + _processReader = processReader; + _hostEnvironment = hostEnvironment; + _eFormidlingService = eFormidlingService; + } + + /// + public string Type => "eFormidling"; + + /// + 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." + ); + } + + string taskId = context.InstanceDataMutator.Instance.Process.CurrentTask.ElementId; + Instance instance = context.InstanceDataMutator.Instance; + ValidAltinnEFormidlingConfiguration configuration = await GetValidAltinnEFormidlingConfiguration(taskId); + + if (configuration.Disabled) + { + _logger.LogInformation( + "EFormidling is disabled for task {TaskId}. No eFormidling shipment will be sent, but the service task will be completed.", + LogSanitizer.Sanitize(taskId) + ); + return ServiceTaskResult.Success(); + } + + _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}.", + LogSanitizer.Sanitize(taskId) + ); + + return ServiceTaskResult.Success(); + } + + private Task GetValidAltinnEFormidlingConfiguration(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 {LogSanitizer.Sanitize(taskId)}" + ); + + HostingEnvironment env = AltinnEnvironments.GetHostingEnvironment(_hostEnvironment); + ValidAltinnEFormidlingConfiguration validConfig = eFormidlingConfig.Validate(env); + + return Task.FromResult(validConfig); + } +} 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..b96253177 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/IServiceTask.cs @@ -0,0 +1,90 @@ +using Altinn.App.Core.Features; + +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. + /// + public Task Execute(ServiceTaskContext context); +} + +/// +/// This class represents the parameters for executing a service task. +/// +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; } + + /// + /// Cancellation token for the operation. + /// + public CancellationToken CancellationToken { get; init; } = CancellationToken.None; +} + +/// +/// Base type for the result of executing a service task. +/// +public abstract record ServiceTaskResult +{ + /// + /// 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. + /// + /// 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 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 ServiceTaskFailedResult(bool? autoMoveNext = null, string? autoMoveNextAction = null) + { + AutoMoveNext = autoMoveNext; + AutoMoveNextAction = autoMoveNextAction; + } +} 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..a21ab5fb4 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 compatibility. 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..fe19f88bb 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 compatibility. 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..71f922a6a --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ServiceTasks/PdfServiceTask.cs @@ -0,0 +1,71 @@ +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; +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. +/// +internal sealed 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) + { + _pdfService = pdfService; + _processReader = processReader; + _logger = logger; + } + + /// + public string Type => "pdf"; + + /// + 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}.", LogSanitizer.Sanitize(taskId)); + + ValidAltinnPdfConfiguration config = GetValidAltinnPdfConfiguration(taskId); + await _pdfService.GenerateAndStorePdf( + instance, + taskId, + config.Filename, + config.AutoPdfTaskIds, + context.CancellationToken + ); + + _logger.LogDebug( + "Successfully called PdfService for PDF Service Task {TaskId}.", + LogSanitizer.Sanitize(taskId) + ); + + return ServiceTaskResult.Success(); + } + + 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 e6c8609d8..ad10b6bf5 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 e40ce4214..af13990e5 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..d9737c0ae --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/ttd/service-tasks/config/process/process.bpmn @@ -0,0 +1,77 @@ + + + + + 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 + + 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 + + + + + + + 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/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json index fc9018585..d7d5a36dd 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json +++ b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json @@ -7069,6 +7069,10 @@ }, "write": { "type": "boolean" + }, + "elementType": { + "type": "string", + "nullable": true } }, "additionalProperties": false @@ -7117,6 +7121,10 @@ "elementId": { "type": "string", "nullable": true + }, + "elementType": { + "type": "string", + "nullable": true } }, "additionalProperties": false 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..476b11e10 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Process/ServiceTasks/EFormidling/EFormidlingServiceTaskTests.cs @@ -0,0 +1,151 @@ +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; +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(); + + 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(), 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..e544180da --- /dev/null +++ b/test/Altinn.App.Api.Tests/Process/ServiceTasks/Pdf/PdfServiceTaskTests.cs @@ -0,0 +1,169 @@ +using System.Net; +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; +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(); + var eFormidlingConfigurationProviderMock = new Mock(); + OverrideServicesForAllTests = (services) => + { + services.AddSingleton(eFormidlingServiceMock.Object); + services.AddSingleton(eFormidlingConfigurationProviderMock.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\":\"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); + 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 ed6e48f9e..079af9037 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 Altinn.App.Tests.Common.Mocks; using AltinnCore.Authentication.JwtCookie; using App.IntegrationTests.Mocks.Services; @@ -112,6 +113,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/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs index ac19829f5..72a33e394 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 eFormidlingLegacyConfigProvider = new Mock(); var instanceGuid = Guid.Parse("41C1099C-7EDD-47F5-AD1F-6267B497796F"); var instance = new Instance @@ -151,13 +157,30 @@ 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(() => { 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( @@ -178,13 +201,15 @@ private Fixture CreateFixture( 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(); var serviceProvider = services.BuildStrictServiceProvider(); @@ -213,7 +238,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 +373,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..7dad52815 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/EFormidlingConfigurationProviderTests.cs @@ -0,0 +1,92 @@ +using Altinn.App.Core.EFormidling.Implementation; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Moq; + +namespace Altinn.App.Core.Tests.Eformidling.Implementation; + +public class EFormidlingLegacyConfigurationProviderTests +{ + private readonly Mock _appMetadataMock = new(); + private readonly EFormidlingLegacyConfigurationProvider _provider; + + public EFormidlingLegacyConfigurationProviderTests() + { + _provider = new EFormidlingLegacyConfigurationProvider(_appMetadataMock.Object); + } + + [Fact] + public async Task GetLegacyConfiguration_ReturnsConfigFromApplicationMetadata() + { + // Arrange + var applicationMetadata = new ApplicationMetadata("tdd/test") + { + EFormidling = new EFormidlingContract + { + Receiver = "123456789", + 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 + 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); + Assert.Equal("arkivmelding", result.Type); + Assert.Equal(3, result.SecurityLevel); + Assert.Equal("altinn3.skjema", result.DpfShipmentType); + Assert.Equal(new[] { "datatype1", "datatype2" }, result.DataTypes); + } + + [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 + ValidAltinnEFormidlingConfiguration result = await _provider.GetLegacyConfiguration(); + + // 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()); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index 55b7f9af0..a65dff011 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -302,6 +302,166 @@ 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, 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 + ); + } + + [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, 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..908b756d5 --- /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.False(result.Disabled); + 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_WithDisabledTrue_ReturnsDisabledTrue() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Disabled = [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.Disabled); + } + + [Fact] + public void Validate_WithDisabledFalse_ReturnsDisabledFalse() + { + // Arrange + var config = new AltinnEFormidlingConfiguration + { + Disabled = [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.Disabled); + } + + [Fact] + public void Validate_WithoutDisabledField_DefaultsToFalse() + { + // 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.False(result.Disabled); + } + + [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); + } +} 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..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,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.ServiceTasks; +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 +37,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 +68,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 +94,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 +121,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 +132,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 +155,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 +166,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/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 781a26144..697825613 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; @@ -12,9 +13,11 @@ using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Texts; +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; @@ -378,51 +381,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; - Instance instance = new Instance() - { - Id = _instanceId, - AppId = "org/app", - Process = null, - }; - ProcessNextRequest processNextRequest = new ProcessNextRequest() + public static TheoryData InvalidProcessStatesData => + new() { - 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("Instance does not have current task 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() + + var instance = new Instance() { Id = _instanceId, AppId = "org/app", - Process = new ProcessState() { CurrentTask = 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 task information!"); + result.ErrorMessage.Should().Be(expectedErrorMessage); result.ErrorType.Should().Be(ProcessErrorType.Conflict); } @@ -442,18 +449,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", @@ -471,9 +482,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, @@ -481,7 +494,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); } @@ -502,7 +516,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())) @@ -512,13 +527,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", @@ -536,17 +554,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); } @@ -573,13 +594,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", @@ -597,7 +621,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() @@ -608,13 +634,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); @@ -909,11 +937,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() { @@ -937,13 +966,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); @@ -1256,6 +1287,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 @@ -1265,6 +1316,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); @@ -1274,6 +1326,7 @@ public static Fixture Create( services.TryAddTransient(_ => appResourcesMock.Object); services.TryAddTransient(_ => translationServiceMock.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/ServiceTasks/EFormidlingServiceTaskTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs new file mode 100644 index 000000000..c0acd7649 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EFormidlingServiceTaskTests.cs @@ -0,0 +1,207 @@ +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 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 _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 + ); + } + + [Fact] + public async Task Execute_Should_BeEnabled_When_NoBpmnConfig() + { + Instance instance = GetInstance(); + + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); + + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; + + var exception = await Assert.ThrowsAsync(() => _serviceTask.Execute(parameters)); + Assert.Contains("No eFormidling configuration found in BPMN for task", exception.Message); + } + + [Fact] + public async Task Execute_Should_ThrowException_When_EFormidlingServiceIsNull() + { + // Arrange + Instance instance = GetInstance(); + + var serviceTask = new EFormidlingServiceTask( + _loggerMock.Object, + _processReaderMock.Object, + _hostEnvironmentMock.Object, + null + ); + + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); + + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; + + // Act & Assert + await Assert.ThrowsAsync(() => serviceTask.Execute(parameters)); + } + + [Fact] + public async Task Execute_Should_Call_SendEFormidlingShipment_When_EFormidlingEnabled() + { + // Arrange + Instance instance = GetInstance(); + + 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); + + // Assert + _eFormidlingServiceMock.Verify( + x => x.SendEFormidlingShipment(instance, It.IsAny()), + Times.Once + ); + } + + private static AltinnEFormidlingConfiguration GetConfig(bool disabled = false) + { + return new AltinnEFormidlingConfiguration + { + Disabled = [new AltinnEnvironmentConfig { Value = disabled.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(); + + AltinnEFormidlingConfiguration eFormidlingConfig = GetConfig(); + eFormidlingConfig.Disabled = + [ + new AltinnEnvironmentConfig { Environment = "prod", Value = "false" }, + new AltinnEnvironmentConfig { Environment = "staging", Value = "true" }, + ]; + + 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, It.IsAny()), + Times.Once + ); + } + + [Fact] + public async Task Execute_Should_SkipExecution_When_BpmnConfigDisabled() + { + // Arrange + Instance instance = GetInstance(); + + var taskExtension = new AltinnTaskExtension { EFormidlingConfiguration = GetConfig(disabled: true) }; + _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_UseGlobalBpmnConfig_When_NoEnvironmentSpecific() + { + // Arrange + Instance instance = GetInstance(); + + AltinnEFormidlingConfiguration eFormidlingConfig = GetConfig(); + eFormidlingConfig.Disabled = + [ + new AltinnEnvironmentConfig { Value = "false" }, // 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, It.IsAny()), + 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/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..7ebc87eab 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,104 @@ -using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Pdf; -using Altinn.App.Core.Internal.Process.ServiceTasks; -using Altinn.App.Core.Models; +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 }, + } + ); + + _serviceTask = new PdfServiceTask(_pdfServiceMock.Object, _processReaderMock.Object, _loggerMock.Object); } [Fact] - public async Task Execute_pdf_service_is_called_only_once() + public async Task Execute_Should_Call_GenerateAndStorePdf() { - Instance i = new Instance() + // Arrange + var instance = new Instance { - Data = - [ - new DataElement() { DataType = "DataType_1" }, - new DataElement() { DataType = "DataType_1" }, - new DataElement() { DataType = "DataType_2" }, - new DataElement() { DataType = "DataType_2" }, - ], + Process = new ProcessState { CurrentTask = new ProcessElementInfo { ElementId = "taskId" } }, }; - 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(); - } - [Fact] - public async Task Execute_pdf_generation_is_never_called_if_no_dataelements_for_datatype() - { - 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(); - } + var instanceMutatorMock = new Mock(); + instanceMutatorMock.Setup(x => x.Instance).Returns(instance); - [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, - }, - ] + var parameters = new ServiceTaskContext { InstanceDataMutator = instanceMutatorMock.Object }; + + // Act + await _serviceTask.Execute(parameters); + + // Assert + _pdfServiceMock.Verify( + x => + x.GenerateAndStorePdf( + instance, + instance.Process.CurrentTask.ElementId, + FileName, + It.IsAny?>(), + 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(); } [Fact] - public async Task Execute_does_not_call_pdfservice_if_generate_pdf_are_false_for_all_datatypes_nde_pdf_flag_true() + public async Task Execute_Should_Pass_AutoPdfTaskIds_To_PdfService() { - DataElement d = new DataElement() { Id = "DataElement_1", DataType = "DataType_1" }; - Instance i = new Instance() { Data = [d] }; - SetupAppMetadataWithDataTypes( - [ - new DataType + // Arrange + var taskIds = new List { "Task_1", "Task_2", "Task_3" }; + + _processReaderMock + .Setup(x => x.GetAltinnTaskExtension("pdfTask")) + .Returns( + new AltinnTaskExtension { - 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(); - } + TaskType = "pdf", + PdfConfiguration = new AltinnPdfConfiguration { Filename = "test.pdf", AutoPdfTaskIds = taskIds }, + } + ); - private void SetupAppMetadataWithDataTypes(List? dataTypes = null) - { - _appMetadata - .Setup(am => am.GetApplicationMetadata()) - .ReturnsAsync(new ApplicationMetadata("ttd/test") { DataTypes = dataTypes ?? new List { } }); + 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", "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 48dcb9951..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 @@ -146,11 +146,13 @@ namespace Altinn.App.Core.EFormidling.Implementation { 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 { - 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.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) { } } public class EformidlingDeliveryException : Altinn.App.Core.Exceptions.AltinnException { @@ -171,6 +173,10 @@ namespace Altinn.App.Core.EFormidling.Implementation public string EventType { get; } public System.Threading.Tasks.Task ProcessEvent(Altinn.App.Core.Models.CloudEvent cloudEvent) { } } + public interface IEFormidlingLegacyConfigurationProvider + { + System.Threading.Tasks.Task GetLegacyConfiguration(); + } } namespace Altinn.App.Core.EFormidling.Interface { @@ -184,10 +190,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); } 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 @@ -3171,6 +3179,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? customFileNameTextResourceKey, 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,6 +3201,7 @@ namespace Altinn.App.Core.Internal.Pdf { public PdfService(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.Internal.Texts.ITranslationService translationService, 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? customFileNameTextResourceKey, 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) { } } @@ -3250,6 +3260,36 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties [System.Xml.Serialization.XmlText] public string Value { get; set; } } + public sealed 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="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="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() { } @@ -3272,6 +3312,15 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties [System.Xml.Serialization.XmlElement("paymentReceiptPdfDataType", Namespace="http://altinn.no/process")] public string? PaymentReceiptPdfDataType { get; set; } } + 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("filename", Namespace="http://altinn.no/process")] + public string? Filename { get; set; } + } public class AltinnSignatureConfiguration { public AltinnSignatureConfiguration() { } @@ -3300,13 +3349,30 @@ 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")] + 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")] public string? TaskType { get; set; } } + public readonly struct ValidAltinnEFormidlingConfiguration : System.IEquatable + { + 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 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 { @@ -3316,6 +3382,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")] @@ -3337,6 +3405,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")] @@ -3385,6 +3455,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")] @@ -3411,6 +3483,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() { } @@ -3507,10 +3584,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 Next(Altinn.App.Core.Models.Process.ProcessNextRequest request, System.Threading.CancellationToken ct = default); } public interface IProcessEngineAuthorizer { @@ -3552,11 +3629,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, 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) { } - 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 Next(Altinn.App.Core.Models.Process.ProcessNextRequest request, System.Threading.CancellationToken ct = default) { } } public class ProcessEventDispatcher : Altinn.App.Core.Internal.Process.IProcessEventDispatcher { @@ -3683,21 +3760,33 @@ namespace Altinn.App.Core.Internal.Process.ProcessTasks 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.ServiceTasks +namespace Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks { - public class EformidlingServiceTask : Altinn.App.Core.Internal.Process.ServiceTasks.IServiceTask + 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 sealed class ServiceTaskContext : System.IEquatable + { + 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, 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 ServiceTaskFailedResult(bool? autoMoveNext = default, string? autoMoveNextAction = null) { } } - public interface IServiceTask + public abstract class ServiceTaskResult : System.IEquatable { - System.Threading.Tasks.Task Execute(string taskId, Altinn.Platform.Storage.Interface.Models.Instance instance); + protected ServiceTaskResult() { } + 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 class PdfServiceTask : Altinn.App.Core.Internal.Process.ServiceTasks.IServiceTask + public sealed class ServiceTaskSuccessResult : Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks.ServiceTaskResult, System.IEquatable { - 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(bool? autoMoveNext = default, string? autoMoveNextAction = null) { } } } namespace Altinn.App.Core.Internal.Profile @@ -4622,6 +4711,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?[]?[] { @@ -4637,6 +4727,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 { @@ -4651,7 +4742,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; }