diff --git a/src/Altinn.App.Api/Controllers/ActionsController.cs b/src/Altinn.App.Api/Controllers/ActionsController.cs index daf8d0c6b..3fc0dc242 100644 --- a/src/Altinn.App.Api/Controllers/ActionsController.cs +++ b/src/Altinn.App.Api/Controllers/ActionsController.cs @@ -1,11 +1,13 @@ using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Models; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Validation; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Process; @@ -72,6 +74,7 @@ public ActionsController( [Authorize] [ProducesResponseType(typeof(UserActionResponse), 200)] [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(RedirectResult), 302)] [ProducesResponseType(409)] [ProducesResponseType(500)] [ProducesResponseType(401)] @@ -83,7 +86,7 @@ public async Task> Perform( [FromBody] UserActionRequest actionRequest, [FromQuery] string? language = null) { - var action = actionRequest.Action; + string? action = actionRequest.Action; if (action == null) { return new BadRequestObjectResult(new ProblemDetails() @@ -95,8 +98,7 @@ public async Task> Perform( }); } - var instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); if (instance?.Process == null) { return Conflict($"Process is not started."); @@ -107,20 +109,20 @@ public async Task> Perform( return Conflict($"Process is ended."); } - var userId = HttpContext.User.GetUserIdAsInt(); + int? userId = HttpContext.User.GetUserIdAsInt(); if (userId == null) { return Unauthorized(); } - var authorized = await _authorization.AuthorizeAction(new AppIdentifier(org, app), new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), HttpContext.User, action, instance.Process?.CurrentTask?.ElementId); + bool authorized = await _authorization.AuthorizeAction(new AppIdentifier(org, app), new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), HttpContext.User, action, instance.Process?.CurrentTask?.ElementId); if (!authorized) { return Forbid(); } - UserActionContext userActionContext = new UserActionContext(instance, userId.Value, actionRequest.ButtonId, actionRequest.Metadata); - var actionHandler = _userActionService.GetActionHandler(action); + UserActionContext userActionContext = new(instance, userId.Value, actionRequest.ButtonId, actionRequest.Metadata); + IUserAction? actionHandler = _userActionService.GetActionHandler(action); if (actionHandler == null) { return new NotFoundObjectResult(new UserActionResponse() @@ -133,9 +135,13 @@ public async Task> Perform( }); } - var result = await actionHandler.HandleAction(userActionContext); + UserActionResult result = await actionHandler.HandleAction(userActionContext); + if (result.ResultType == ResultType.Redirect) + { + return new RedirectResult(result.RedirectUrl ?? throw new ProcessException("Redirect URL missing")); + } - if (!result.Success) + if (result.ResultType != ResultType.Success) { return StatusCode( statusCode: result.ErrorType switch diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index 20e5ecdf9..d29f9cd2e 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -221,8 +221,7 @@ public async Task> Post( } ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - - RequestPartValidator requestValidator = new RequestPartValidator(application); + RequestPartValidator requestValidator = new(application); string? multipartError = requestValidator.ValidateParts(parsedRequest.Parts); if (!string.IsNullOrEmpty(multipartError)) @@ -250,7 +249,6 @@ public async Task> Post( } EnforcementResult enforcementResult = await AuthorizeAction(org, app, party.PartyId, null, "instantiate"); - if (!enforcementResult.Authorized) { return Forbidden(enforcementResult); @@ -278,13 +276,13 @@ public async Task> Post( try { // start process and goto next task - ProcessStartRequest processStartRequest = new ProcessStartRequest + ProcessStartRequest processStartRequest = new() { Instance = instanceTemplate, - User = User, - Dryrun = true + User = User }; - var result = await _processEngine.StartProcess(processStartRequest); + + ProcessChangeResult result = await _processEngine.GenerateProcessStartEvents(processStartRequest); if (!result.Success) { return Conflict(result.ErrorMessage); @@ -308,14 +306,8 @@ public async Task> Post( instance = await _instanceClient.GetInstance(app, org, int.Parse(instance.InstanceOwner.PartyId), Guid.Parse(instance.Id.Split("/")[1])); // notify app and store events - var request = new ProcessStartRequest() - { - Instance = instance, - User = User, - Dryrun = false, - }; _logger.LogInformation("Events sent to process engine: {Events}", change?.Events); - await _processEngine.UpdateInstanceAndRerunEvents(request, change?.Events); + await _processEngine.HandleEventsAndUpdateStorage(instance, null, change?.Events); } catch (Exception exception) { @@ -409,14 +401,14 @@ public async Task> PostSimplified( return StatusCode((int)HttpStatusCode.Forbidden, $"Party {party.PartyId} is not allowed to instantiate this application {org}/{app}"); } - Instance instanceTemplate = new Instance() + Instance instanceTemplate = new() { InstanceOwner = instansiationInstance.InstanceOwner, VisibleAfter = instansiationInstance.VisibleAfter, - DueBefore = instansiationInstance.DueBefore + DueBefore = instansiationInstance.DueBefore, + Org = application.Org }; - instanceTemplate.Org = application.Org; ConditionallySetReadStatus(instanceTemplate); // Run custom app logic to validate instantiation @@ -437,11 +429,10 @@ public async Task> PostSimplified( { Instance = instanceTemplate, User = User, - Dryrun = true, Prefill = instansiationInstance.Prefill }; - processResult = await _processEngine.StartProcess(request); + processResult = await _processEngine.GenerateProcessStartEvents(request); Instance? source = null; @@ -473,15 +464,7 @@ public async Task> PostSimplified( } instance = await _instanceClient.GetInstance(instance); - - var updateRequest = new ProcessStartRequest() - { - Instance = instance, - User = User, - Dryrun = false, - Prefill = instansiationInstance.Prefill - }; - await _processEngine.UpdateInstanceAndRerunEvents(updateRequest, processResult.ProcessStateChange?.Events); + await _processEngine.HandleEventsAndUpdateStorage(instance, instansiationInstance.Prefill, processResult.ProcessStateChange?.Events); } catch (Exception exception) { @@ -573,10 +556,10 @@ public async Task CopyInstance( ProcessStartRequest processStartRequest = new() { Instance = targetInstance, - User = User, - Dryrun = true + User = User }; - var startResult = await _processEngine.StartProcess(processStartRequest); + + ProcessChangeResult startResult = await _processEngine.GenerateProcessStartEvents(processStartRequest); targetInstance = await _instanceClient.CreateInstance(org, app, targetInstance); @@ -584,13 +567,7 @@ public async Task CopyInstance( targetInstance = await _instanceClient.GetInstance(targetInstance); - ProcessStartRequest rerunRequest = new() - { - Instance = targetInstance, - Dryrun = false, - User = User - }; - await _processEngine.UpdateInstanceAndRerunEvents(rerunRequest, startResult.ProcessStateChange?.Events); + await _processEngine.HandleEventsAndUpdateStorage(targetInstance, null, startResult.ProcessStateChange?.Events); await RegisterEvent("app.instance.created", targetInstance); diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 944bad525..415f17ddb 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -126,15 +126,16 @@ public async Task> StartProcess( { Instance = instance, StartEventId = startEvent, - User = User, - Dryrun = false + User = User }; - var result = await _processEngine.StartProcess(request); + ProcessChangeResult result = await _processEngine.GenerateProcessStartEvents(request); if (!result.Success) { return Conflict(result.ErrorMessage); } + await _processEngine.HandleEventsAndUpdateStorage(instance, null, result.ProcessStateChange?.Events); + AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, result.ProcessStateChange?.NewProcessState); return Ok(appProcessState); } diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index f40a442c6..5a5168974 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -30,11 +30,13 @@ using Altinn.App.Core.Internal.Pdf; using Altinn.App.Core.Internal.Prefill; using Altinn.App.Core.Internal.Process; -using Altinn.App.Core.Internal.Process.Action; +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.Profile; using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Internal.Secrets; -using Altinn.App.Core.Internal.Sign; using Altinn.App.Core.Internal.Texts; using Altinn.App.Core.Internal.Validation; using Altinn.App.Core.Models; @@ -53,6 +55,8 @@ using IProcessReader = Altinn.App.Core.Internal.Process.IProcessReader; using ProcessEngine = Altinn.App.Core.Internal.Process.ProcessEngine; using ProcessReader = Altinn.App.Core.Internal.Process.ProcessReader; +using Altinn.App.Core.Internal.Sign; +using Altinn.App.Core.Internal.Process.Authorization; namespace Altinn.App.Core.Extensions { @@ -140,7 +144,6 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddTransient(); - services.TryAddTransient(); #pragma warning disable CS0618, CS0612 // Type or member is obsolete services.TryAddTransient(); #pragma warning restore CS0618, CS0612 // Type or member is obsolete @@ -152,14 +155,17 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); + services.AddTransient(); services.Configure(configuration.GetSection("PEPSettings")); services.Configure(configuration.GetSection("PlatformSettings")); services.Configure(configuration.GetSection("AccessTokenSettings")); services.Configure(configuration.GetSection(nameof(FrontEndSettings))); services.Configure(configuration.GetSection(nameof(PdfGeneratorSettings))); + AddAppOptions(services); AddActionServices(services); AddPdfServices(services); + AddSignatureServices(services); AddEventServices(services); AddProcessServices(services); AddFileAnalyserServices(services); @@ -237,6 +243,11 @@ private static void AddPdfServices(IServiceCollection services) #pragma warning restore CS0618 // Type or member is obsolete } + private static void AddSignatureServices(IServiceCollection services) + { + services.AddHttpClient(); + } + private static void AddAppOptions(IServiceCollection services) { // Main service for interacting with options @@ -256,16 +267,35 @@ private static void AddProcessServices(IServiceCollection services) services.TryAddTransient(); services.TryAddTransient(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddTransient(); services.AddTransient(); services.TryAddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + //PROCESS TASKS + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + //SERVICE TASKS + services.AddKeyedTransient("pdfService"); + services.AddKeyedTransient("eFormidlingService"); } private static void AddActionServices(IServiceCollection services) { services.TryAddTransient(); services.AddTransient(); - services.AddHttpClient(); services.AddTransientUserActionAuthorizerForActionInAllTasks("sign"); } diff --git a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs deleted file mode 100644 index 6012f7f5f..000000000 --- a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs +++ /dev/null @@ -1,356 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; -using Altinn.App.Core.Configuration; -using Altinn.App.Core.EFormidling.Interface; -using Altinn.App.Core.Features; -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Internal.AppModel; -using Altinn.App.Core.Internal.Data; -using Altinn.App.Core.Internal.Expressions; -using Altinn.App.Core.Internal.Instances; -using Altinn.App.Core.Internal.Pdf; -using Altinn.App.Core.Internal.Prefill; -using Altinn.App.Core.Internal.Process; -using Altinn.App.Core.Models; -using Altinn.Platform.Storage.Interface.Enums; -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.FeatureManagement; - -namespace Altinn.App.Core.Implementation; - -/// -/// Default handling of task process events. -/// -public class DefaultTaskEvents : ITaskEvents -{ - private readonly ILogger _logger; - private readonly IAppResources _appResources; - private readonly IAppMetadata _appMetadata; - private readonly IDataClient _dataClient; - private readonly IPrefill _prefillService; - private readonly IAppModel _appModel; - private readonly IInstantiationProcessor _instantiationProcessor; - private readonly IInstanceClient _instanceClient; - private readonly IEnumerable _taskStarts; - private readonly IEnumerable _taskEnds; - private readonly IEnumerable _taskAbandons; - private readonly IPdfService _pdfService; - private readonly IEFormidlingService? _eFormidlingService; - private readonly AppSettings? _appSettings; - private readonly LayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer; - - /// - /// Constructor with services from DI - /// - public DefaultTaskEvents( - ILogger logger, - IAppResources appResources, - IAppMetadata appMetadata, - IDataClient dataClient, - IPrefill prefillService, - IAppModel appModel, - IInstantiationProcessor instantiationProcessor, - IInstanceClient instanceClient, - IEnumerable taskStarts, - IEnumerable taskEnds, - IEnumerable taskAbandons, - IPdfService pdfService, - LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, - IOptions? appSettings = null, - IEFormidlingService? eFormidlingService = null - ) - { - _logger = logger; - _appResources = appResources; - _appMetadata = appMetadata; - _dataClient = dataClient; - _prefillService = prefillService; - _appModel = appModel; - _instantiationProcessor = instantiationProcessor; - _instanceClient = instanceClient; - _taskStarts = taskStarts; - _taskEnds = taskEnds; - _taskAbandons = taskAbandons; - _pdfService = pdfService; - _layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer; - _eFormidlingService = eFormidlingService; - _appSettings = appSettings?.Value; - } - - /// - public async Task OnStartProcessTask(string taskId, Instance instance, Dictionary prefill) - { - _logger.LogDebug("OnStartProcessTask for {InstanceId}", instance.Id); - - await RunAppDefinedOnTaskStart(taskId, instance, prefill); - ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); - - // If this is a revisit to a previous task we need to unlock data - foreach (DataType dataType in appMetadata.DataTypes.Where(dt => dt.TaskId == taskId)) - { - DataElement? dataElement = instance.Data.Find(d => d.DataType == dataType.Id); - - if (dataElement != null && dataElement.Locked) - { - _logger.LogDebug("Unlocking data element {DataElementId} of dataType {DataTypeId}", dataElement.Id, dataType.Id); - await _dataClient.UnlockDataElement(new InstanceIdentifier(instance), Guid.Parse(dataElement.Id)); - } - } - - foreach (DataType dataType in appMetadata.DataTypes.Where(dt => - dt.TaskId == taskId && dt.AppLogic?.AutoCreate == true)) - { - _logger.LogDebug("Auto create data element: {DataTypeId}", dataType.Id); - - DataElement? dataElement = instance.Data.Find(d => d.DataType == dataType.Id); - - if (dataElement == null) - { - dynamic data = _appModel.Create(dataType.AppLogic.ClassRef); - - // runs prefill from repo configuration if config exists - await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, dataType.Id, data, prefill); - await _instantiationProcessor.DataCreation(instance, data, prefill); - - Type type = _appModel.GetModelType(dataType.AppLogic.ClassRef); - - ObjectUtils.InitializeAltinnRowId(data); - - DataElement createdDataElement = - await _dataClient.InsertFormData(instance, dataType.Id, data, type); - instance.Data.Add(createdDataElement); - - await UpdatePresentationTextsOnInstance(instance, dataType.Id, data); - await UpdateDataValuesOnInstance(instance, dataType.Id, data); - - _logger.LogDebug("Created data element: {CreatedDataElementId}", createdDataElement.Id); - } - } - } - - private async Task RunAppDefinedOnTaskStart(string taskId, Instance instance, Dictionary prefill) - { - foreach (var taskStart in _taskStarts) - { - await taskStart.Start(taskId, instance, prefill); - } - } - - /// - public async Task OnEndProcessTask(string endEvent, Instance instance) - { - Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); - List dataTypesToLock = appMetadata.DataTypes.FindAll(dt => dt.TaskId == endEvent); - - await RunRemoveDataElementsGeneratedFromTask(instance, endEvent); - - await RunRemoveFieldsInModelOnTaskComplete(instance, dataTypesToLock); - - await RunAppDefinedOnTaskEnd(endEvent, instance); - - await RunLockDataAndGeneratePdf(endEvent, instance, dataTypesToLock); - - await RunEformidling(endEvent, instance); - - await RunAutoDeleteOnProcessEnd(instance, instanceGuid); - } - - private async Task RunRemoveFieldsInModelOnTaskComplete(Instance instance, List dataTypesToLock) - { - ArgumentNullException.ThrowIfNull(instance.Data); - - dataTypesToLock = dataTypesToLock.Where(d => !string.IsNullOrEmpty(d.AppLogic?.ClassRef)).ToList(); - await Task.WhenAll( - instance.Data - .Join(dataTypesToLock, de => de.DataType, dt => dt.Id, (de, dt) => (dataElement: de, dataType: dt)) - .Select(async (d) => - { - await RemoveFieldsOnTaskComplete(instance, dataTypesToLock, d.dataElement, d.dataType); - })); - } - - private async Task RemoveFieldsOnTaskComplete(Instance instance, List dataTypesToLock, DataElement dataElement, DataType dataType) - { - // Download the data - Type modelType = _appModel.GetModelType(dataType.AppLogic.ClassRef); - var instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - var dataGuid = Guid.Parse(dataElement.Id); - string app = instance.AppId.Split("/")[1]; - int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - object data = await _dataClient.GetFormData( - instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, dataGuid); - - // Remove hidden data before validation, ignore hidden rows. - if (_appSettings?.RemoveHiddenData == true) - { - var layoutSet = _appResources.GetLayoutSetForTask(dataType.TaskId); - var evaluationState = await _layoutEvaluatorStateInitializer.Init(instance, data, layoutSet?.Id); - LayoutEvaluator.RemoveHiddenData(evaluationState, RowRemovalOption.Ignore); - } - - // Remove shadow fields - if (dataType.AppLogic?.ShadowFields?.Prefix != null) - { - var modifier = new IgnorePropertiesWithPrefix(dataType.AppLogic.ShadowFields.Prefix); - JsonSerializerOptions options = new() - { - TypeInfoResolver = new DefaultJsonTypeInfoResolver - { - Modifiers = { modifier.ModifyPrefixInfo } - }, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }; - - string serializedData = JsonSerializer.Serialize(data, options); - if (dataType.AppLogic.ShadowFields.SaveToDataType != null) - { - // Save the shadow fields to another data type - var saveToDataType = dataTypesToLock.Find(dt => dt.Id == dataType.AppLogic.ShadowFields.SaveToDataType); - if (saveToDataType == null) - { - throw new Exception($"SaveToDataType {dataType.AppLogic.ShadowFields.SaveToDataType} not found"); - } - - Type saveToModelType = _appModel.GetModelType(saveToDataType.AppLogic.ClassRef); - var updatedData = JsonSerializer.Deserialize(serializedData, saveToModelType); - await _dataClient.InsertFormData(updatedData, instanceGuid, saveToModelType ?? modelType, instance.Org, app, instanceOwnerPartyId, saveToDataType.Id); - } - else - { - // Remove the shadow fields from the data - data = JsonSerializer.Deserialize(serializedData, modelType)!; - } - } - // remove AltinnRowIds - ObjectUtils.RemoveAltinnRowId(data); - - // Save the updated data - await _dataClient.UpdateData(data, instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, dataGuid); - } - - private async Task RunRemoveDataElementsGeneratedFromTask(Instance instance, string endEvent) - { - AppIdentifier appIdentifier = new AppIdentifier(instance.AppId); - InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instance); - foreach (var dataElement in instance.Data?.Where(de => de.References != null && de.References.Exists(r => r.ValueType == ReferenceType.Task && r.Value == endEvent)) ?? Enumerable.Empty()) - { - await _dataClient.DeleteData(appIdentifier.Org, appIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, Guid.Parse(dataElement.Id), false); - } - } - - private async Task RunAppDefinedOnTaskEnd(string endEvent, Instance instance) - { - foreach (var taskEnd in _taskEnds) - { - await taskEnd.End(endEvent, instance); - } - } - - private async Task RunLockDataAndGeneratePdf(string endEvent, Instance instance, List? dataTypesToLock) - { - _logger.LogDebug("OnEndProcessTask for {instanceId}. Locking data elements connected to {endEvent}", instance.Id, endEvent); - - foreach (DataType dataType in dataTypesToLock ?? Enumerable.Empty()) - { - bool generatePdf = dataType.AppLogic?.ClassRef != null && dataType.EnablePdfCreation; - - foreach (DataElement dataElement in instance.Data.FindAll(de => de.DataType == dataType.Id)) - { - _logger.LogDebug("Locking data element {dataElementId} of dataType {dataTypeId}.", dataElement.Id, dataType.Id); - Task updateData = _dataClient.LockDataElement(new InstanceIdentifier(instance), Guid.Parse(dataElement.Id)); - - if (generatePdf) - { - Task createPdf = _pdfService.GenerateAndStorePdf(instance, endEvent, CancellationToken.None); - - await Task.WhenAll(updateData, createPdf); - } - else - { - await updateData; - } - } - } - } - - private async Task RunAutoDeleteOnProcessEnd(Instance instance, Guid instanceGuid) - { - ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); - if (appMetadata.AutoDeleteOnProcessEnd && instance.Process?.Ended != null) - { - int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - await _instanceClient.DeleteInstance(instanceOwnerPartyId, instanceGuid, true); - } - } - - private async Task RunEformidling(string endEvent, Instance instance) - { - ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); - if (_appSettings?.EnableEFormidling == true && appMetadata.EFormidling?.SendAfterTaskId == endEvent && _eFormidlingService != null) - { - // The code above updates data elements on the instance. To ensure - // we have the latest instance with all the data elements including pdf, - // we reload the instance before we pass it on to eFormidling. - var updatedInstance = await _instanceClient.GetInstance(instance); - await _eFormidlingService.SendEFormidlingShipment(updatedInstance); - } - } - - - /// - public async Task OnAbandonProcessTask(string taskId, Instance instance) - { - foreach (var taskAbandon in _taskAbandons) - { - await taskAbandon.Abandon(taskId, instance); - } - - _logger.LogDebug("OnAbandonProcessTask for {instanceId}. Locking data elements connected to {taskId}", instance.Id, taskId); - await Task.CompletedTask; - } - - private async Task UpdatePresentationTextsOnInstance(Instance instance, string dataType, dynamic data) - { - ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); - var updatedValues = DataHelper.GetUpdatedDataValues( - appMetadata?.PresentationFields, - instance.PresentationTexts, - dataType, - data); - - if (updatedValues.Count > 0) - { - var updatedInstance = await _instanceClient.UpdatePresentationTexts( - int.Parse(instance.Id.Split("/")[0]), - Guid.Parse(instance.Id.Split("/")[1]), - new PresentationTexts { Texts = updatedValues }); - - instance.PresentationTexts = updatedInstance.PresentationTexts; - } - } - - private async Task UpdateDataValuesOnInstance(Instance instance, string dataType, object data) - { - ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); - var updatedValues = DataHelper.GetUpdatedDataValues( - appMetadata?.DataFields, - instance.DataValues, - dataType, - data); - - if (updatedValues.Count > 0) - { - var updatedInstance = await _instanceClient.UpdateDataValues( - int.Parse(instance.Id.Split("/")[0]), - Guid.Parse(instance.Id.Split("/")[1]), - new DataValues { Values = updatedValues }); - - instance.DataValues = updatedInstance.DataValues; - } - } -} diff --git a/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs index 508393797..95b921154 100644 --- a/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs +++ b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs @@ -1,6 +1,6 @@ using System.Security.Claims; using Altinn.App.Core.Features.Action; -using Altinn.App.Core.Internal.Process.Action; +using Altinn.App.Core.Internal.Process.Authorization; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; diff --git a/src/Altinn.App.Core/Internal/Data/DataService.cs b/src/Altinn.App.Core/Internal/Data/DataService.cs new file mode 100644 index 000000000..711561ff4 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Data/DataService.cs @@ -0,0 +1,89 @@ +using System.Text.Json; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Data +{ + /// + internal class DataService : IDataService + { + private readonly IDataClient _dataClient; + private readonly IAppMetadata _appMetadata; + + private readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); + + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public DataService(IDataClient dataClient, IAppMetadata appMetadata) + { + _dataClient = dataClient; + _appMetadata = appMetadata; + } + + /// + public async Task<(Guid dataElementId, T? model)> GetByType(Instance instance, string dataTypeId) + { + DataElement? dataElement = instance.Data.SingleOrDefault(d => d.DataType.Equals(dataTypeId)); + + if (dataElement == null) + { + return (Guid.Empty, default); + } + + var data = await GetDataForDataElement(new InstanceIdentifier(instance), dataElement); + + return (Guid.Parse(dataElement.Id), data); + } + + /// + public async Task GetById(Instance instance, Guid dataElementId) + { + DataElement dataElement = instance.Data.SingleOrDefault(d => d.Id == dataElementId.ToString()) ?? throw new ArgumentNullException($"Failed to locate data element with id {dataElementId} in instance {instance.Id}"); + return await GetDataForDataElement(new InstanceIdentifier(instance), dataElement); + } + + /// + public async Task InsertJsonObject(InstanceIdentifier instanceIdentifier, string dataTypeId, object data) + { + using var referenceStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(referenceStream, data, _jsonSerializerOptions); + referenceStream.Position = 0; + return await _dataClient.InsertBinaryData(instanceIdentifier.ToString(), dataTypeId, "application/json", dataTypeId + ".json", referenceStream); + } + + /// + public async Task UpdateJsonObject(InstanceIdentifier instanceIdentifier, string dataTypeId, Guid dataElementId, object data) + { + using var referenceStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(referenceStream, data, _jsonSerializerOptions); + referenceStream.Position = 0; + return await _dataClient.UpdateBinaryData(instanceIdentifier, "application/json", dataTypeId + ".json", dataElementId, referenceStream); + } + + /// + public async Task DeleteById(InstanceIdentifier instanceIdentifier, Guid dataElementId) + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + + return await _dataClient.DeleteData(applicationMetadata.AppIdentifier.Org, applicationMetadata.AppIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataElementId, false); + } + + private async Task GetDataForDataElement(InstanceIdentifier instanceIdentifier, DataElement dataElement) + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + + Stream dataStream = await _dataClient.GetBinaryData(applicationMetadata.AppIdentifier.Org, applicationMetadata.AppIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, new Guid(dataElement.Id)); + if (dataStream == null) + { + throw new ArgumentNullException($"Failed to retrieve binary dataStream from dataClient using dataElement.Id {dataElement.Id}."); + } + + return await JsonSerializer.DeserializeAsync(dataStream, _jsonSerializerOptions) ?? throw new InvalidOperationException($"Unable to deserialize data from dataStream to type {nameof(T)}."); + } + } +} diff --git a/src/Altinn.App.Core/Internal/Data/IDataService.cs b/src/Altinn.App.Core/Internal/Data/IDataService.cs new file mode 100644 index 000000000..67b2f547b --- /dev/null +++ b/src/Altinn.App.Core/Internal/Data/IDataService.cs @@ -0,0 +1,57 @@ +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Data +{ + /// + /// DRAFT. Don't make public yet. + /// Service that simplifies access to data elements through the IDataClient. + /// + public interface IDataService + { + /// + /// Retrieves a single data element by dataTypeId and deserializes it to an object of type T. + /// + /// The type of the data element. + /// The instance associated with the object. + /// The ID of the data type. + /// A tuple containing the ID of the data element and the retrieved model. + Task<(Guid dataElementId, T? model)> GetByType(Instance instance, string dataTypeId); + + /// + /// Retrieves a single data element by its ID and deserializes it to an object of type T. + /// + /// The type of the data element. + /// The instance associated with the object. + /// The ID of the data element. + /// The object of type T. + Task GetById(Instance instance, Guid dataElementId); + + /// + /// Inserts a data element for the instance. + /// + /// + /// + /// + /// + Task InsertJsonObject(InstanceIdentifier instanceIdentifier, string dataTypeId, object data); + + /// + /// Updates a data element for the instance. + /// + /// + /// + /// + /// + /// + Task UpdateJsonObject(InstanceIdentifier instanceIdentifier, string dataTypeId, Guid dataElementId, object data); + + /// + /// Deletes a data element by its ID. + /// + /// The instance associated with the object. + /// The ID of the data element to delete. + /// A boolean indicating success/failure. + Task DeleteById(InstanceIdentifier instanceIdentifier, Guid dataElementId); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Action/IUserActionAuthorizerProvider.cs b/src/Altinn.App.Core/Internal/Process/Authorization/IUserActionAuthorizerProvider.cs similarity index 90% rename from src/Altinn.App.Core/Internal/Process/Action/IUserActionAuthorizerProvider.cs rename to src/Altinn.App.Core/Internal/Process/Authorization/IUserActionAuthorizerProvider.cs index 861d6261e..666d96bcb 100644 --- a/src/Altinn.App.Core/Internal/Process/Action/IUserActionAuthorizerProvider.cs +++ b/src/Altinn.App.Core/Internal/Process/Authorization/IUserActionAuthorizerProvider.cs @@ -1,6 +1,6 @@ using Altinn.App.Core.Features; -namespace Altinn.App.Core.Internal.Process.Action; +namespace Altinn.App.Core.Internal.Process.Authorization; /// /// Register a user action authorizer for a given action and/or task diff --git a/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerProvider.cs b/src/Altinn.App.Core/Internal/Process/Authorization/UserActionAuthorizerProvider.cs similarity index 93% rename from src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerProvider.cs rename to src/Altinn.App.Core/Internal/Process/Authorization/UserActionAuthorizerProvider.cs index c21a359b7..341220430 100644 --- a/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerProvider.cs +++ b/src/Altinn.App.Core/Internal/Process/Authorization/UserActionAuthorizerProvider.cs @@ -1,6 +1,6 @@ using Altinn.App.Core.Features; -namespace Altinn.App.Core.Internal.Process.Action; +namespace Altinn.App.Core.Internal.Process.Authorization; /// /// Register a user action authorizer for a given action and/or task diff --git a/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtension.cs b/src/Altinn.App.Core/Internal/Process/Authorization/UserActionAuthorizerServiceCollectionExtension.cs similarity index 98% rename from src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtension.cs rename to src/Altinn.App.Core/Internal/Process/Authorization/UserActionAuthorizerServiceCollectionExtension.cs index 08af22f80..46278cf8f 100644 --- a/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtension.cs +++ b/src/Altinn.App.Core/Internal/Process/Authorization/UserActionAuthorizerServiceCollectionExtension.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Altinn.App.Core.Internal.Process.Action; +namespace Altinn.App.Core.Internal.Process.Authorization; /// /// Extension methods for adding user action authorizers to the service collection connected to a action and/or task diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs deleted file mode 100644 index 8e823501f..000000000 --- a/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Internal.Process.Elements -{ - /// - /// Represents the process task responsible for collecting user confirmation. - /// - public class ConfirmationTask : TaskBase - { - private readonly ITaskEvents _taskEvents; - - /// - /// Initializes a new instance of the class. - /// - public ConfirmationTask(ITaskEvents taskEvents) - { - _taskEvents = taskEvents; - } - - /// - public override async Task HandleTaskAbandon(string elementId, Instance instance) - { - await _taskEvents.OnAbandonProcessTask(elementId, instance); - } - - /// - public override async Task HandleTaskComplete(string elementId, Instance instance) - { - await _taskEvents.OnEndProcessTask(elementId, instance); - } - - /// - public override async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) - { - await _taskEvents.OnStartProcessTask(elementId, instance, prefill); - } - } -} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs deleted file mode 100644 index 4821f61c5..000000000 --- a/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Internal.Process.Elements -{ - /// - /// Represents the process task responsible for form filling steps. - /// - public class DataTask : TaskBase - { - private readonly ITaskEvents _taskEvents; - - /// - /// Initializes a new instance of the class. - /// - public DataTask(ITaskEvents taskEvents) - { - _taskEvents = taskEvents; - } - - /// - public override async Task HandleTaskAbandon(string elementId, Instance instance) - { - await _taskEvents.OnAbandonProcessTask(elementId, instance); - } - - /// - public override async Task HandleTaskComplete(string elementId, Instance instance) - { - await _taskEvents.OnEndProcessTask(elementId, instance); - } - - /// - public override async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) - { - await _taskEvents.OnStartProcessTask(elementId, - instance, prefill); - } - } -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs deleted file mode 100644 index c5ccdf41a..000000000 --- a/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Internal.Process.Elements -{ - /// - /// Represents the process task responsible for waiting for feedback from application owner. - /// - public class FeedbackTask : TaskBase - { - private readonly ITaskEvents _taskEvents; - - /// - /// Initializes a new instance of the class. - /// - public FeedbackTask(ITaskEvents taskEvents) - { - _taskEvents = taskEvents; - } - - /// - public override async Task HandleTaskAbandon(string elementId, Instance instance) - { - await _taskEvents.OnAbandonProcessTask(elementId, instance); - } - - /// - public override async Task HandleTaskComplete(string elementId, Instance instance) - { - await _taskEvents.OnEndProcessTask(elementId, instance); - } - - /// - public override async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) - { - await _taskEvents.OnStartProcessTask(elementId, instance, prefill); - } - } -} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ITask.cs b/src/Altinn.App.Core/Internal/Process/Elements/ITask.cs deleted file mode 100644 index ab07f273e..000000000 --- a/src/Altinn.App.Core/Internal/Process/Elements/ITask.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Altinn.App.Core.Models; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Internal.Process.Elements -{ - /// - /// Interface desbring the basic task - /// - public interface ITask - { - /// - /// This operations triggers process logic needed to start the current task. The logic depend on the different types of task - /// - Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill); - - /// - /// This operatin triggers process logic need to complete a given task. The Logic depend on the different types of task. - /// - Task HandleTaskComplete(string elementId, Instance instance); - - /// - /// This operatin triggers process logic need to abandon a Task without completing it - /// - Task HandleTaskAbandon(string elementId, Instance instance); - } -} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/NullTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/NullTask.cs deleted file mode 100644 index c3f4a2fbb..000000000 --- a/src/Altinn.App.Core/Internal/Process/Elements/NullTask.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Altinn.App.Core.Models; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Internal.Process.Elements; - -/// -/// Null implementation. Used when no other can be found -/// -public class NullTask : ITask -{ - /// - public async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) - { - await Task.CompletedTask; - } - - /// - public async Task HandleTaskComplete(string elementId, Instance instance) - { - await Task.CompletedTask; - } - - /// - public async Task HandleTaskAbandon(string elementId, Instance instance) - { - await Task.CompletedTask; - } -} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/TaskBase.cs b/src/Altinn.App.Core/Internal/Process/Elements/TaskBase.cs deleted file mode 100644 index 7eb8e9af6..000000000 --- a/src/Altinn.App.Core/Internal/Process/Elements/TaskBase.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Altinn.App.Core.Models; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Internal.Process.Elements -{ - /// - /// TaskBase with default implentation - /// - public abstract class TaskBase : ITask - { - /// - /// hallooo asdf - /// - public abstract Task HandleTaskComplete(string elementId, Instance instance); - - /// - /// Handle task start - /// - public abstract Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill); - - /// - /// Handle task abandon - /// - public abstract Task HandleTaskAbandon(string elementId, Instance instance); - } -} diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/EndEventEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/EndEventEventHandler.cs new file mode 100644 index 000000000..9a2561773 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/EndEventEventHandler.cs @@ -0,0 +1,58 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.EventHandlers +{ + /// + /// This event handler is responsible for handling the end event for a process. + /// + public class EndEventEventHandler : IEndEventEventHandler + { + private readonly IAppEvents _appEvents; + private readonly IInstanceClient _instanceClient; + private readonly IAppMetadata _appMetadata; + + /// + /// This event handler is responsible for handling the end event for a process. + /// + public EndEventEventHandler(IAppEvents appEvents, IInstanceClient instanceClient, IAppMetadata appMetadata) + { + _appEvents = appEvents; + _instanceClient = instanceClient; + _appMetadata = appMetadata; + } + + /// + /// Execute the event handler logic. + /// + /// + /// + /// + /// + public async Task Execute(InstanceEvent instanceEvent, Instance instance) + { + string? endEvent = instanceEvent.ProcessInfo?.EndEvent; + + if (string.IsNullOrEmpty(endEvent)) + { + throw new ArgumentException($"End event is not set for instance event {instanceEvent.EventType} {instanceEvent.Id} on instance {instance.Id}."); + } + + await _appEvents.OnEndAppEvent(endEvent, instance); + await AutoDeleteOnProcessEndIfEnabled(instance); + } + + private async Task AutoDeleteOnProcessEndIfEnabled(Instance instance) + { + InstanceIdentifier instanceIdentifier = new(instance); + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + if (applicationMetadata.AutoDeleteOnProcessEnd && instance.Process?.Ended != null) + { + int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); + await _instanceClient.DeleteInstance(instanceOwnerPartyId, instanceIdentifier.InstanceGuid, true); + } + } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs new file mode 100644 index 000000000..0a34769be --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/Interfaces/IEndEventEventHandler.cs @@ -0,0 +1,18 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.EventHandlers +{ + /// + /// Interface for end event handlers, which are executed when a process end event is triggered. + /// + public interface IEndEventEventHandler + { + /// + /// Execute the end event handler + /// + /// + /// + /// + Task Execute(InstanceEvent instanceEvent, Instance instance); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandler.cs new file mode 100644 index 000000000..2447a06bc --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandler.cs @@ -0,0 +1,50 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +{ + /// + /// This event handler is responsible for handling the abandon event for a process task. + /// + public class AbandonTaskEventHandler : IAbandonTaskEventHandler + { + private readonly IEnumerable _processTaskAbondons; + + /// + /// This event handler is responsible for handling the abandon event for a process task. + /// + /// + public AbandonTaskEventHandler(IEnumerable processTaskAbondons) + { + _processTaskAbondons = processTaskAbondons; + } + + /// + /// Handles the abandon event for a process task. + /// + /// + /// + /// + /// + public async Task Execute(IProcessTask processTask, string taskId, Instance instance) + { + await processTask.Abandon(taskId, instance); + await RunAppDefinedProcessTaskAbandonHandlers(taskId, instance); + } + + /// + /// Runs IProcessTaskAbandons defined in the app. + /// + /// + /// + /// + private async Task RunAppDefinedProcessTaskAbandonHandlers(string taskId, Instance instance) + { + foreach (IProcessTaskAbandon taskAbandon in _processTaskAbondons) + { + await taskAbandon.Abandon(taskId, instance); + } + } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs new file mode 100644 index 000000000..566174444 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandler.cs @@ -0,0 +1,71 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Internal.Process.ServiceTasks; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +{ + /// + /// This event handler is responsible for handling the end event for a process task. + /// + public class EndTaskEventHandler : IEndTaskEventHandler + { + private readonly IProcessTaskDataLocker _processTaskDataLocker; + private readonly IProcessTaskFinalizer _processTaskFinisher; + private readonly IServiceTask _pdfServiceTask; + private readonly IServiceTask _eformidlingServiceTask; + private readonly IEnumerable _processTaskEnds; + + /// + /// This event handler is responsible for handling the end event for a process task. + /// + public EndTaskEventHandler( + IProcessTaskDataLocker processTaskDataLocker, + IProcessTaskFinalizer processTaskFinisher, + [FromKeyedServices("pdfService")] IServiceTask pdfServiceTask, + [FromKeyedServices("eFormidlingService")] IServiceTask eformidlingServiceTask, + IEnumerable processTaskEnds + ) + { + _processTaskDataLocker = processTaskDataLocker; + _processTaskFinisher = processTaskFinisher; + _pdfServiceTask = pdfServiceTask; + _eformidlingServiceTask = eformidlingServiceTask; + _processTaskEnds = processTaskEnds; + } + + /// + /// Execute the event handler logic. + /// + /// + /// + /// + /// + public async Task Execute(IProcessTask processTask, string taskId, Instance instance) + { + 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. + await _pdfServiceTask.Execute(taskId, instance); + await _eformidlingServiceTask.Execute(taskId, instance); + } + + /// + /// Runs IProcessTaskEnds defined in the app. + /// + /// + /// + /// + private async Task RunAppDefinedProcessTaskEndHandlers(string endEvent, Instance instance) + { + foreach (IProcessTaskEnd taskEnd in _processTaskEnds) + { + await taskEnd.End(endEvent, instance); + } + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IAbandonTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IAbandonTaskEventHandler.cs new file mode 100644 index 000000000..a64fb9d66 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IAbandonTaskEventHandler.cs @@ -0,0 +1,20 @@ +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +{ + /// + /// Interface for abandon task event handlers, which are executed when a process abandon task event is triggered. + /// + public interface IAbandonTaskEventHandler + { + /// + /// Execute the abandon task event handler + /// + /// + /// + /// + /// + Task Execute(IProcessTask processTask, string taskId, Instance instance); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IEndTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IEndTaskEventHandler.cs new file mode 100644 index 000000000..9348308b3 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IEndTaskEventHandler.cs @@ -0,0 +1,20 @@ +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +{ + /// + /// Interface for end task event handlers, which are executed when a process end task event is triggered. + /// + public interface IEndTaskEventHandler + { + /// + /// Execute the end task event handler + /// + /// + /// + /// + /// + Task Execute(IProcessTask processTask, string taskId, Instance instance); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IStartTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IStartTaskEventHandler.cs new file mode 100644 index 000000000..c76fdb838 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/Interfaces/IStartTaskEventHandler.cs @@ -0,0 +1,16 @@ +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +{ + /// + /// Interface for start task event handlers, which are executed when a process start task event is triggered. + /// + public interface IStartTaskEventHandler + { + /// + /// Execute the start task event handler + /// + Task Execute(IProcessTask processTask, string taskId, Instance instance, Dictionary? prefill); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs new file mode 100644 index 000000000..2f08897f6 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandler.cs @@ -0,0 +1,63 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask +{ + /// + /// This event handler is responsible for handling the start event for a process task. + /// + public class StartTaskEventHandler : IStartTaskEventHandler + { + private readonly IProcessTaskDataLocker _processTaskDataLocker; + private readonly IProcessTaskInitializer _processTaskInitializer; + private readonly IEnumerable _processTaskStarts; + + /// + /// This event handler is responsible for handling the start event for a process task. + /// + public StartTaskEventHandler( + IProcessTaskDataLocker processTaskDataLocker, + IProcessTaskInitializer processTaskInitializer, + IEnumerable processTaskStarts + ) + { + _processTaskDataLocker = processTaskDataLocker; + _processTaskInitializer = processTaskInitializer; + _processTaskStarts = processTaskStarts; + } + + /// + /// Execute the event handler logic. + /// + /// + /// + /// + /// + /// + public async Task Execute(IProcessTask processTask, string taskId, Instance instance, + Dictionary? prefill) + { + await _processTaskDataLocker.Unlock(taskId, instance); + await RunAppDefinedProcessTaskStartHandlers(taskId, instance, prefill); + await _processTaskInitializer.Initialize(taskId, instance, prefill); + await processTask.Start(taskId, instance); + } + + /// + /// Runs IProcessTaskStarts defined in the app. + /// + /// + /// + /// + /// + private async Task RunAppDefinedProcessTaskStartHandlers(string taskId, Instance instance, + Dictionary? prefill) + { + foreach (IProcessTaskStart processTaskStarts in _processTaskStarts) + { + await processTaskStarts.Start(taskId, instance, prefill ?? []); + } + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ITaskEvents.cs b/src/Altinn.App.Core/Internal/Process/ITaskEvents.cs deleted file mode 100644 index 0026ae19d..000000000 --- a/src/Altinn.App.Core/Internal/Process/ITaskEvents.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Internal.Process; - -/// -/// Interface for implementing a receiver handling task process events. -/// -public interface ITaskEvents -{ - /// - /// Callback to app after task has been started. - /// - /// task id of task started - /// Instance data - /// Prefill data - /// - public Task OnStartProcessTask(string taskId, Instance instance, Dictionary prefill); - - /// - /// Is called after the process task is ended. Method can update instance and data element metadata. - /// - /// task id of task ended - /// Instance data - /// - public Task OnEndProcessTask(string endEvent, Instance instance); - - /// - /// Is called after the process task is abonded. Method can update instance and data element metadata. - /// - /// task id of task to abandon - /// Instance data - public Task OnAbandonProcessTask(string taskId, Instance instance); - -} diff --git a/src/Altinn.App.Core/Internal/Process/IProcessClient.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessClient.cs similarity index 100% rename from src/Altinn.App.Core/Internal/Process/IProcessClient.cs rename to src/Altinn.App.Core/Internal/Process/Interfaces/IProcessClient.cs diff --git a/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs similarity index 62% rename from src/Altinn.App.Core/Internal/Process/IProcessEngine.cs rename to src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs index bb62df68a..f6676252f 100644 --- a/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs @@ -11,7 +11,7 @@ public interface IProcessEngine /// /// Method to start a new process /// - Task StartProcess(ProcessStartRequest processStartRequest); + Task GenerateProcessStartEvents(ProcessStartRequest processStartRequest); /// /// Method to move process to next task/event @@ -19,11 +19,11 @@ public interface IProcessEngine Task Next(ProcessNextRequest request); /// - /// Update Instance and rerun instance events + /// Handle process events and update storage /// - /// + /// + /// /// - /// - Task UpdateInstanceAndRerunEvents(ProcessStartRequest startRequest, List? events); + Task HandleEventsAndUpdateStorage(Instance instance, Dictionary? prefill, List? events); } } diff --git a/src/Altinn.App.Core/Internal/Process/IProcessEventDispatcher.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventDispatcher.cs similarity index 81% rename from src/Altinn.App.Core/Internal/Process/IProcessEventDispatcher.cs rename to src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventDispatcher.cs index 119fedf29..49152b56b 100644 --- a/src/Altinn.App.Core/Internal/Process/IProcessEventDispatcher.cs +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventDispatcher.cs @@ -11,10 +11,9 @@ public interface IProcessEventDispatcher /// Updates the instance process in storage and dispatches instance events /// /// The instance with updated process - /// Prefill data /// Events that should be dispatched /// Instance from storage after update - Task UpdateProcessAndDispatchEvents(Instance instance, Dictionary? prefill, List? events); + Task DispatchToStorage(Instance instance, List? events); /// /// Dispatch events for instance to the events system if AppSettings.RegisterEventsWithEventsComponent is true /// diff --git a/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventHandlerDelegator.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventHandlerDelegator.cs new file mode 100644 index 000000000..4041aeb77 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEventHandlerDelegator.cs @@ -0,0 +1,19 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process +{ + /// + /// This interface is responsible for delegating process events to the correct event handler. + /// + public interface IProcessEventHandlerDelegator + { + /// + /// Handle process events. + /// + /// + /// + /// + /// + Task HandleEvents(Instance instance, Dictionary? prefill, List? events); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/IProcessNavigator.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessNavigator.cs similarity index 100% rename from src/Altinn.App.Core/Internal/Process/IProcessNavigator.cs rename to src/Altinn.App.Core/Internal/Process/Interfaces/IProcessNavigator.cs diff --git a/src/Altinn.App.Core/Internal/Process/IProcessReader.cs b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessReader.cs similarity index 91% rename from src/Altinn.App.Core/Internal/Process/IProcessReader.cs rename to src/Altinn.App.Core/Internal/Process/Interfaces/IProcessReader.cs index fe6b69649..d5af41496 100644 --- a/src/Altinn.App.Core/Internal/Process/IProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/Interfaces/IProcessReader.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Internal.Process.Elements.Base; namespace Altinn.App.Core.Internal.Process; @@ -115,4 +116,9 @@ public interface IProcessReader /// /// public List GetAllFlowElements(); + + /// + /// If the element with the given elementId is a ProcessTask, this method will return the AltinnTaskExtension for that task. If it is not, null is returned. + /// + public AltinnTaskExtension? GetAltinnTaskExtension(string elementId); } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 42238dff6..fa19beac3 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Process.Elements; @@ -21,6 +22,7 @@ public class ProcessEngine : IProcessEngine private readonly IProcessReader _processReader; private readonly IProfileClient _profileClient; private readonly IProcessNavigator _processNavigator; + private readonly IProcessEventHandlerDelegator _processEventHandlerDelegator; private readonly IProcessEventDispatcher _processEventDispatcher; private readonly UserActionService _userActionService; @@ -30,24 +32,27 @@ public class ProcessEngine : IProcessEngine /// Process reader service /// The profile service /// The process navigator + /// /// The process event dispatcher /// The action handler factory public ProcessEngine( IProcessReader processReader, IProfileClient profileClient, IProcessNavigator processNavigator, + IProcessEventHandlerDelegator processEventsDelegator, IProcessEventDispatcher processEventDispatcher, UserActionService userActionService) { _processReader = processReader; _profileClient = profileClient; _processNavigator = processNavigator; + _processEventHandlerDelegator = processEventsDelegator; _processEventDispatcher = processEventDispatcher; _userActionService = userActionService; } /// - public async Task StartProcess(ProcessStartRequest processStartRequest) + public async Task GenerateProcessStartEvents(ProcessStartRequest processStartRequest) { if (processStartRequest.Instance.Process != null) { @@ -75,7 +80,7 @@ public async Task StartProcess(ProcessStartRequest processS InstanceEvent? startEvent = startChange?.Events?[0].CopyValues(); ProcessStateChange? nextChange = await ProcessNext(processStartRequest.Instance, processStartRequest.User); InstanceEvent? goToNextEvent = nextChange?.Events?[0].CopyValues(); - List events = new List(); + List events = []; if (startEvent is not null) { events.Add(startEvent); @@ -86,18 +91,13 @@ public async Task StartProcess(ProcessStartRequest processS events.Add(goToNextEvent); } - ProcessStateChange processStateChange = new ProcessStateChange + ProcessStateChange processStateChange = new() { OldProcessState = startChange?.OldProcessState, NewProcessState = nextChange?.NewProcessState, Events = events }; - if (!processStartRequest.Dryrun) - { - await _processEventDispatcher.UpdateProcessAndDispatchEvents(processStartRequest.Instance, processStartRequest.Prefill, events); - } - return new ProcessChangeResult() { Success = true, @@ -108,7 +108,7 @@ public async Task StartProcess(ProcessStartRequest processS /// public async Task Next(ProcessNextRequest request) { - var instance = request.Instance; + Instance instance = request.Instance; string? currentElementId = instance.Process?.CurrentTask?.ElementId; if (currentElementId == null) @@ -123,11 +123,11 @@ public async Task Next(ProcessNextRequest request) int? userId = request.User.GetUserIdAsInt(); - var actionHandler = _userActionService.GetActionHandler(request.Action); + IUserAction? actionHandler = _userActionService.GetActionHandler(request.Action); - var actionResult = actionHandler is null ? UserActionResult.SuccessResult() : await actionHandler.HandleAction(new UserActionContext(request.Instance, userId)); + UserActionResult actionResult = actionHandler is null ? UserActionResult.SuccessResult() : await actionHandler.HandleAction(new UserActionContext(request.Instance, userId)); - if (!actionResult.Success) + if (actionResult.ResultType != ResultType.Success) { return new ProcessChangeResult() { @@ -137,7 +137,7 @@ public async Task Next(ProcessNextRequest request) }; } - var nextResult = await HandleMoveToNext(instance, request.User, request.Action); + ProcessStateChange? nextResult = await HandleMoveToNext(instance, request.User, request.Action); return new ProcessChangeResult() { @@ -147,9 +147,10 @@ public async Task Next(ProcessNextRequest request) } /// - public async Task UpdateInstanceAndRerunEvents(ProcessStartRequest startRequest, List? events) + public async Task HandleEventsAndUpdateStorage(Instance instance, Dictionary? prefill, List? events) { - return await _processEventDispatcher.UpdateProcessAndDispatchEvents(startRequest.Instance, startRequest.Prefill, events); + await _processEventHandlerDelegator.HandleEvents(instance, prefill, events); + return await _processEventDispatcher.DispatchToStorage(instance, events); } /// @@ -157,33 +158,33 @@ public async Task UpdateInstanceAndRerunEvents(ProcessStartRequest sta /// private async Task ProcessStart(Instance instance, string startEvent, ClaimsPrincipal user) { - if (instance.Process == null) + if (instance.Process != null) { - DateTime now = DateTime.UtcNow; + return null; + } - ProcessState startState = new ProcessState - { - Started = now, - StartEvent = startEvent, - CurrentTask = new ProcessElementInfo { Flow = 1, ElementId = startEvent } - }; + DateTime now = DateTime.UtcNow; + ProcessState startState = new() + { + Started = now, + StartEvent = startEvent, + CurrentTask = new ProcessElementInfo { Flow = 1, ElementId = startEvent } + }; - instance.Process = startState; + instance.Process = startState; - List events = new List - { - await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString(), instance, now, user), - }; + List events = + [ + await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString(), instance, now, user) + ]; - return new ProcessStateChange - { - OldProcessState = null!, - NewProcessState = startState, - Events = events, - }; - } + return new ProcessStateChange + { + OldProcessState = null!, + NewProcessState = startState, + Events = events, + }; - return null; } /// @@ -191,24 +192,24 @@ await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString() /// private async Task ProcessNext(Instance instance, ClaimsPrincipal userContext, string? action = null) { - if (instance.Process != null) + if (instance.Process == null) { - ProcessStateChange result = new ProcessStateChange - { - OldProcessState = new ProcessState() - { - Started = instance.Process.Started, - CurrentTask = instance.Process.CurrentTask, - StartEvent = instance.Process.StartEvent - } - }; - - result.Events = await MoveProcessToNext(instance, userContext, action); - result.NewProcessState = instance.Process; - return result; + return null; } - return null; + ProcessStateChange result = new() + { + OldProcessState = new ProcessState() + { + Started = instance.Process.Started, + CurrentTask = instance.Process.CurrentTask, + StartEvent = instance.Process.StartEvent + }, + Events = await MoveProcessToNext(instance, userContext, action), + NewProcessState = instance.Process + }; + return result; + } private async Task> MoveProcessToNext( @@ -216,7 +217,7 @@ private async Task> MoveProcessToNext( ClaimsPrincipal user, string? action = null) { - List events = new List(); + List events = []; ProcessState previousState = instance.Process.Copy(); ProcessState currentState = instance.Process; @@ -228,7 +229,7 @@ private async Task> MoveProcessToNext( if (_processReader.IsProcessTask(previousElementId)) { instance.Process = previousState; - string eventType = InstanceEventType.process_EndTask.ToString(); + var eventType = InstanceEventType.process_EndTask.ToString(); if (action is "reject") { eventType = InstanceEventType.process_AbandonTask.ToString(); @@ -276,7 +277,7 @@ private async Task> MoveProcessToNext( private async Task GenerateProcessChangeEvent(string eventType, Instance instance, DateTime now, ClaimsPrincipal user) { int? userId = user.GetUserIdAsInt(); - InstanceEvent instanceEvent = new InstanceEvent + InstanceEvent instanceEvent = new() { InstanceId = instance.Id, InstanceOwnerPartyId = instance.InstanceOwner.PartyId, @@ -293,8 +294,8 @@ private async Task GenerateProcessChangeEvent(string eventType, I if (string.IsNullOrEmpty(instanceEvent.User.OrgId) && userId != null) { - UserProfile up = await _profileClient.GetUserProfile((int)userId); - instanceEvent.User.NationalIdentityNumber = up.Party.SSN; + UserProfile? up = await _profileClient.GetUserProfile((int)userId); + instanceEvent.User.NationalIdentityNumber = up?.Party.SSN; //TODO: Should we throw error if both OrgId and userProfile is null? } return instanceEvent; @@ -302,14 +303,16 @@ private async Task GenerateProcessChangeEvent(string eventType, I private async Task HandleMoveToNext(Instance instance, ClaimsPrincipal user, string? action) { - var processStateChange = await ProcessNext(instance, user, action); - if (processStateChange != null) - { - instance = await _processEventDispatcher.UpdateProcessAndDispatchEvents(instance, new Dictionary(), processStateChange.Events); + ProcessStateChange? processStateChange = await ProcessNext(instance, user, action); - await _processEventDispatcher.RegisterEventWithEventsComponent(instance); + if (processStateChange == null) + { + return processStateChange; } + instance = await HandleEventsAndUpdateStorage(instance, null, processStateChange.Events); + await _processEventDispatcher.RegisterEventWithEventsComponent(instance); + return processStateChange; } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs index f01150401..c9c12fee6 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs @@ -25,9 +25,9 @@ public ProcessEngineMetricsDecorator(IProcessEngine processEngine) } /// - public async Task StartProcess(ProcessStartRequest processStartRequest) + public async Task GenerateProcessStartEvents(ProcessStartRequest processStartRequest) { - var result = await _processEngine.StartProcess(processStartRequest); + var result = await _processEngine.GenerateProcessStartEvents(processStartRequest); ProcessTaskStartCounter.WithLabels(result.Success ? "success" : "failure").Inc(); return result; } @@ -49,8 +49,8 @@ public async Task Next(ProcessNextRequest request) } /// - public async Task UpdateInstanceAndRerunEvents(ProcessStartRequest startRequest, List? events) + public async Task HandleEventsAndUpdateStorage(Instance instance, Dictionary? prefill, List? events) { - return await _processEngine.UpdateInstanceAndRerunEvents(startRequest, events); + return await _processEngine.HandleEventsAndUpdateStorage(instance, prefill, events); } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs b/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs index 63313ee9a..2df78e4de 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs @@ -1,9 +1,6 @@ using Altinn.App.Core.Configuration; -using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Internal.Instances; -using Altinn.App.Core.Internal.Process.Elements; -using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -13,39 +10,33 @@ namespace Altinn.App.Core.Internal.Process; /// /// Default implementation of the process event dispatcher /// -class ProcessEventDispatcher : IProcessEventDispatcher +public class ProcessEventDispatcher : IProcessEventDispatcher { private readonly IInstanceClient _instanceClient; private readonly IInstanceEventClient _instanceEventClient; - private readonly ITaskEvents _taskEvents; - private readonly IAppEvents _appEvents; private readonly IEventsClient _eventsClient; - private readonly bool _registerWithEventSystem; + private readonly IOptions _appSettings; private readonly ILogger _logger; - public ProcessEventDispatcher( - IInstanceClient instanceClient, + /// + /// Default implementation of the process event dispatcher + /// + public ProcessEventDispatcher(IInstanceClient instanceClient, IInstanceEventClient instanceEventClient, - ITaskEvents taskEvents, - IAppEvents appEvents, IEventsClient eventsClient, IOptions appSettings, ILogger logger) { _instanceClient = instanceClient; _instanceEventClient = instanceEventClient; - _taskEvents = taskEvents; - _appEvents = appEvents; _eventsClient = eventsClient; - _registerWithEventSystem = appSettings.Value.RegisterEventsWithEventsComponent; + _appSettings = appSettings; _logger = logger; } /// - public async Task UpdateProcessAndDispatchEvents(Instance instance, Dictionary? prefill, List? events) + public async Task DispatchToStorage(Instance instance, List? events) { - await HandleProcessChanges(instance, events, prefill); - // need to update the instance process and then the instance in case appbase has changed it, e.g. endEvent sets status.archived Instance updatedInstance = await _instanceClient.UpdateProcess(instance); await DispatchProcessEventsToStorage(updatedInstance, events); @@ -59,13 +50,14 @@ public async Task UpdateProcessAndDispatchEvents(Instance instance, Di /// public async Task RegisterEventWithEventsComponent(Instance instance) { - if (_registerWithEventSystem) + if (_appSettings.Value.RegisterEventsWithEventsComponent) { try { if (!string.IsNullOrWhiteSpace(instance.Process.CurrentTask?.ElementId)) { - await _eventsClient.AddEvent($"app.instance.process.movedTo.{instance.Process.CurrentTask.ElementId}", instance); + await _eventsClient.AddEvent( + $"app.instance.process.movedTo.{instance.Process.CurrentTask.ElementId}", instance); } else if (instance.Process.EndEvent != null) { @@ -78,8 +70,6 @@ public async Task RegisterEventWithEventsComponent(Instance instance) } } } - - private async Task DispatchProcessEventsToStorage(Instance instance, List? events) { string org = instance.Org; @@ -94,65 +84,4 @@ private async Task DispatchProcessEventsToStorage(Instance instance, List - /// Will for each process change trigger relevant Process Elements to perform the relevant change actions. - /// - /// Each implementation - /// - private async Task HandleProcessChanges(Instance instance, List? events, Dictionary? prefill) - { - if (events != null) - { - foreach (InstanceEvent instanceEvent in events) - { - if (Enum.TryParse(instanceEvent.EventType, true, out InstanceEventType eventType)) - { - string? elementId = instanceEvent.ProcessInfo?.CurrentTask?.ElementId; - ITask task = GetProcessTask(instanceEvent.ProcessInfo?.CurrentTask?.AltinnTaskType); - switch (eventType) - { - case InstanceEventType.process_StartEvent: - break; - case InstanceEventType.process_StartTask: - await task.HandleTaskStart(elementId, instance, prefill); - break; - case InstanceEventType.process_EndTask: - await task.HandleTaskComplete(elementId, instance); - break; - case InstanceEventType.process_AbandonTask: - await task.HandleTaskAbandon(elementId, instance); - break; - case InstanceEventType.process_EndEvent: - await _appEvents.OnEndAppEvent(instanceEvent.ProcessInfo?.EndEvent, instance); - break; - } - } - } - } - } - - /// - /// Identify the correct task implementation - /// - /// - private ITask GetProcessTask(string? altinnTaskType) - { - if (string.IsNullOrEmpty(altinnTaskType)) - { - return new NullTask(); - } - - ITask task = new DataTask(_taskEvents); - if (altinnTaskType.Equals("confirmation")) - { - task = new ConfirmationTask(_taskEvents); - } - else if (altinnTaskType.Equals("feedback")) - { - task = new FeedbackTask(_taskEvents); - } - - return task; - } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs b/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs new file mode 100644 index 000000000..00f5ff275 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessEventHandlingDelegator.cs @@ -0,0 +1,115 @@ +using Altinn.App.Core.Internal.Process.EventHandlers; +using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Internal.Process +{ + /// + /// This class is responsible for delegating process events to the correct event handler. + /// + public class ProcessEventHandlingDelegator : IProcessEventHandlerDelegator + { + private readonly ILogger _logger; + private readonly IStartTaskEventHandler _startTaskEventHandler; + private readonly IEndTaskEventHandler _endTaskEventHandler; + private readonly IAbandonTaskEventHandler _abandonTaskEventHandler; + private readonly IEndEventEventHandler _endEventHandler; + private readonly IEnumerable _processTasks; + + /// + /// This class is responsible for delegating process events to the correct event handler. + /// + public ProcessEventHandlingDelegator(ILogger logger, + IStartTaskEventHandler startTaskEventHandler, + IEndTaskEventHandler endTaskEventHandler, + IAbandonTaskEventHandler abandonTaskEventHandler, + IEndEventEventHandler endEventHandler, + IEnumerable processTasks) + { + _logger = logger; + _startTaskEventHandler = startTaskEventHandler; + _endTaskEventHandler = endTaskEventHandler; + _abandonTaskEventHandler = abandonTaskEventHandler; + _endEventHandler = endEventHandler; + _processTasks = processTasks; + } + + /// + /// Loops through all events and delegates the event to the correct event handler. + /// + /// + /// + /// + /// + public async Task HandleEvents(Instance instance, Dictionary? prefill, List? events) + { + if (events == null) + { + return; + } + + foreach (InstanceEvent instanceEvent in events) + { + if (Enum.TryParse(instanceEvent.EventType, true, out InstanceEventType eventType)) + { + string? taskId = instanceEvent.ProcessInfo?.CurrentTask?.ElementId; + if (instanceEvent.ProcessInfo?.CurrentTask != null && string.IsNullOrEmpty(taskId)) + { + throw new ProcessException( + $"Unable to parse taskId from CurrentTask on instance event {eventType} ({instanceEvent.Id})"); + } + + string? altinnTaskType = instanceEvent.ProcessInfo?.CurrentTask?.AltinnTaskType; + + switch (eventType) + { + case InstanceEventType.process_StartEvent: + break; + case InstanceEventType.process_StartTask: + await _startTaskEventHandler.Execute(GetProcessTaskInstance(altinnTaskType), taskId!, + instance, prefill); + break; + case InstanceEventType.process_EndTask: + await _endTaskEventHandler.Execute(GetProcessTaskInstance(altinnTaskType), taskId!, instance); + break; + case InstanceEventType.process_AbandonTask: + // InstanceEventType is set to Abandon when action performed is `Reject`. This is to keep backwards compatability with existing code that only should be run when a task is abandoned/rejected. + await _abandonTaskEventHandler.Execute(GetProcessTaskInstance(altinnTaskType), taskId!, instance); + break; + case InstanceEventType.process_EndEvent: + await _endEventHandler.Execute(instanceEvent, instance); + break; + } + } + else + { + _logger.LogError("Unable to parse instanceEvent eventType {EventType}", instanceEvent.EventType); + } + } + } + + /// + /// Identify the correct task implementation + /// + /// + private IProcessTask GetProcessTaskInstance(string? altinnTaskType) + { + if (string.IsNullOrEmpty(altinnTaskType)) + { + altinnTaskType = "NullType"; + } + + IProcessTask? processTask = _processTasks.FirstOrDefault(pt => pt.Type == altinnTaskType); + + if (processTask == null) + { + throw new ProcessException($"No process task instance found for altinnTaskType {altinnTaskType}"); + } + + return processTask; + } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs index 02d4884e0..95a88b997 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs @@ -1,5 +1,6 @@ using System.Xml.Serialization; using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Internal.Process.Elements.Base; namespace Altinn.App.Core.Internal.Process; @@ -168,4 +169,17 @@ public List GetAllFlowElements() flowElements.AddRange(GetEndEvents()); return flowElements; } + + /// + public AltinnTaskExtension? GetAltinnTaskExtension(string elementId) + { + ProcessElement? flowElement = GetFlowElement(elementId); + + if (flowElement is ProcessTask processTask) + { + return processTask.ExtensionElements?.TaskExtension ?? throw new ProcessException("No AltinnTaskExtension found on task"); + } + + return null; + } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskDataLocker.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskDataLocker.cs new file mode 100644 index 000000000..d495a8ed3 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskDataLocker.cs @@ -0,0 +1,26 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks +{ + /// + /// Can be used to lock data elements connected to a process task + /// + public interface IProcessTaskDataLocker + { + /// + /// Unlock data elements connected to a specific task + /// + /// + /// + /// + Task Unlock(string taskId, Instance instance); + + /// + /// Lock data elements connected to a specific task + /// + /// + /// + /// + Task Lock(string taskId, Instance instance); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskFinalizer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskFinalizer.cs new file mode 100644 index 000000000..68fe1a5d1 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskFinalizer.cs @@ -0,0 +1,18 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks +{ + /// + /// Contains common logic for ending a process task. + /// + public interface IProcessTaskFinalizer + { + /// + /// Runs common finalization logic for process tasks for a given task ID and instance. This method removes data elements generated from the task, removes hidden data and shadow fields. + /// + /// + /// + /// + Task Finalize(string taskId, Instance instance); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskInitializer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskInitializer.cs new file mode 100644 index 000000000..d6102443f --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/Interfaces/IProcessTaskInitializer.cs @@ -0,0 +1,18 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks +{ + /// + /// Contains common logic for initializing a process task. + /// + public interface IProcessTaskInitializer + { + /// + /// Runs common initialization logic for process tasks for a given task ID and instance. This method initializes the data elements for the instance based on application metadata and prefill configurations. Also updates presentation texts and data values on the instance. + /// + /// + /// + /// + Task Initialize(string taskId, Instance instance, Dictionary? prefill); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskDataLocker.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskDataLocker.cs new file mode 100644 index 000000000..7c44c7d6c --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskDataLocker.cs @@ -0,0 +1,56 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +public class ProcessTaskDataLocker : IProcessTaskDataLocker +{ + private readonly IAppMetadata _appMetadata; + private readonly IDataClient _dataClient; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public ProcessTaskDataLocker(IAppMetadata appMetadata, IDataClient dataClient) + { + _appMetadata = appMetadata; + _dataClient = dataClient; + } + + /// + public async Task Unlock(string taskId, Instance instance) + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + List connectedDataTypes = applicationMetadata.DataTypes.FindAll(dt => dt.TaskId == taskId); + InstanceIdentifier instanceIdentifier = new(instance); + foreach (DataType dataType in connectedDataTypes) + { + List dataElements = instance.Data.FindAll(de => de.DataType == dataType.Id); + foreach (DataElement dataElement in dataElements) + { + await _dataClient.UnlockDataElement(instanceIdentifier, Guid.Parse(dataElement.Id)); + } + } + } + + /// + public async Task Lock(string taskId, Instance instance) + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + List connectedDataTypes = applicationMetadata.DataTypes.FindAll(dt => dt.TaskId == taskId); + InstanceIdentifier instanceIdentifier = new(instance); + foreach (DataType dataType in connectedDataTypes) + { + List dataElements = instance.Data.FindAll(de => de.DataType == dataType.Id); + foreach (DataElement dataElement in dataElements) + { + await _dataClient.LockDataElement(instanceIdentifier, Guid.Parse(dataElement.Id)); + } + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs new file mode 100644 index 000000000..ca787b250 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizer.cs @@ -0,0 +1,154 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +public class ProcessTaskFinalizer : IProcessTaskFinalizer +{ + private readonly IAppMetadata _appMetadata; + private readonly IDataClient _dataClient; + private readonly IAppModel _appModel; + private readonly IAppResources _appResources; + private readonly LayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer; + private readonly IOptions _appSettings; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + public ProcessTaskFinalizer(IAppMetadata appMetadata, + IDataClient dataClient, + IAppModel appModel, + IAppResources appResources, + LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, + IOptions appSettings) + { + _appMetadata = appMetadata; + _dataClient = dataClient; + _appModel = appModel; + _appResources = appResources; + _layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer; + _appSettings = appSettings; + } + + /// + public async Task Finalize(string taskId, Instance instance) + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + List connectedDataTypes = applicationMetadata.DataTypes.FindAll(dt => dt.TaskId == taskId); + + await RemoveDataElementsGeneratedFromTask(instance, taskId); + + await RunRemoveFieldsInModelOnTaskComplete(instance, connectedDataTypes); + } + + private async Task RemoveDataElementsGeneratedFromTask(Instance instance, string taskId) + { + AppIdentifier appIdentifier = new(instance.AppId); + InstanceIdentifier instanceIdentifier = new(instance); + foreach (DataElement dataElement in instance.Data?.Where(de => + de.References != null && + de.References.Exists(r => + r.ValueType == ReferenceType.Task && r.Value == taskId)) ?? + Enumerable.Empty()) + { + await _dataClient.DeleteData(appIdentifier.Org, appIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, Guid.Parse(dataElement.Id), false); + } + } + + private async Task RunRemoveFieldsInModelOnTaskComplete(Instance instance, List dataTypesToLock) + { + ArgumentNullException.ThrowIfNull(instance.Data); + + dataTypesToLock = dataTypesToLock.Where(d => !string.IsNullOrEmpty(d.AppLogic?.ClassRef)).ToList(); + await Task.WhenAll( + instance.Data + .Join(dataTypesToLock, de => de.DataType, dt => dt.Id, (de, dt) => (dataElement: de, dataType: dt)) + .Select(async (d) => + { + await RemoveFieldsOnTaskComplete(instance, dataTypesToLock, d.dataElement, d.dataType); + })); + } + + private async Task RemoveFieldsOnTaskComplete(Instance instance, List dataTypesToLock, DataElement dataElement, DataType dataType) + { + // Download the data + Type modelType = _appModel.GetModelType(dataType.AppLogic.ClassRef); + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + Guid dataGuid = Guid.Parse(dataElement.Id); + string app = instance.AppId.Split("/")[1]; + int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); + object data = await _dataClient.GetFormData( + instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, dataGuid); + + // Remove hidden data before validation, ignore hidden rows. + if (_appSettings.Value?.RemoveHiddenData == true) + { + LayoutSet? layoutSet = _appResources.GetLayoutSetForTask(dataType.TaskId); + LayoutEvaluatorState evaluationState = + await _layoutEvaluatorStateInitializer.Init(instance, data, layoutSet?.Id); + LayoutEvaluator.RemoveHiddenData(evaluationState, RowRemovalOption.Ignore); + } + + // Remove shadow fields + if (dataType.AppLogic?.ShadowFields?.Prefix != null) + { + var modifier = new IgnorePropertiesWithPrefix(dataType.AppLogic.ShadowFields.Prefix); + +#pragma warning disable CA1869 //Not caching options since dynamic param is being used. Consider dict cache. + JsonSerializerOptions options = new() +#pragma warning restore CA1869 + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = { modifier.ModifyPrefixInfo } + }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + string serializedData = JsonSerializer.Serialize(data, options); + if (dataType.AppLogic.ShadowFields.SaveToDataType != null) + { + // Save the shadow fields to another data type + DataType? saveToDataType = + dataTypesToLock.Find(dt => dt.Id == dataType.AppLogic.ShadowFields.SaveToDataType); + if (saveToDataType == null) + { + throw new ProcessException($"SaveToDataType {dataType.AppLogic.ShadowFields.SaveToDataType} not found"); + } + + Type saveToModelType = _appModel.GetModelType(saveToDataType.AppLogic.ClassRef); + object? updatedData = JsonSerializer.Deserialize(serializedData, saveToModelType); + await _dataClient.InsertFormData(updatedData, instanceGuid, saveToModelType ?? modelType, instance.Org, app, instanceOwnerPartyId, saveToDataType.Id); + } + else + { + // Remove the shadow fields from the data + data = JsonSerializer.Deserialize(serializedData, modelType)!; + } + } + // remove AltinnRowIds + ObjectUtils.RemoveAltinnRowId(data); + + // Save the updated data + await _dataClient.UpdateData(data, instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, dataGuid); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs new file mode 100644 index 000000000..9f98f02b5 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs @@ -0,0 +1,130 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +public class ProcessTaskInitializer : IProcessTaskInitializer +{ + private readonly ILogger _logger; + private readonly IAppMetadata _appMetadata; + private readonly IDataClient _dataClient; + private readonly IPrefill _prefillService; + private readonly IAppModel _appModel; + private readonly IInstantiationProcessor _instantiationProcessor; + private readonly IInstanceClient _instanceClient; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + public ProcessTaskInitializer(ILogger logger, + IAppMetadata appMetadata, + IDataClient dataClient, + IPrefill prefillService, + IAppModel appModel, + IInstantiationProcessor instantiationProcessor, + IInstanceClient instanceClient) + { + _logger = logger; + _appMetadata = appMetadata; + _dataClient = dataClient; + _prefillService = prefillService; + _appModel = appModel; + _instantiationProcessor = instantiationProcessor; + _instanceClient = instanceClient; + } + + /// + public async Task Initialize(string taskId, Instance instance, Dictionary? prefill) + { + _logger.LogDebug("OnStartProcessTask for {InstanceId}", instance.Id); + + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + + foreach (DataType dataType in applicationMetadata.DataTypes.Where(dt => + dt.TaskId == taskId && dt.AppLogic?.AutoCreate == true)) + { + _logger.LogDebug("Auto create data element: {DataTypeId}", dataType.Id); + + DataElement? dataElement = instance.Data?.Find(d => d.DataType == dataType.Id); + if (dataElement != null) + { + continue; + } + + dynamic data = _appModel.Create(dataType.AppLogic.ClassRef); + + // runs prefill from repo configuration if config exists + await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, dataType.Id, data, prefill); + await _instantiationProcessor.DataCreation(instance, data, prefill); + + Type type = _appModel.GetModelType(dataType.AppLogic.ClassRef); + + ObjectUtils.InitializeAltinnRowId(data); + + DataElement createdDataElement = await _dataClient.InsertFormData(instance, dataType.Id, data, type); + instance.Data ??= []; + instance.Data.Add(createdDataElement); + + await UpdatePresentationTextsOnInstance(instance, dataType.Id, data); + await UpdateDataValuesOnInstance(instance, dataType.Id, data); + + _logger.LogDebug("Created data element: {CreatedDataElementId}", createdDataElement.Id); + } + } + + private async Task UpdatePresentationTextsOnInstance(Instance instance, string dataType, dynamic data) + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + dynamic? updatedValues = DataHelper.GetUpdatedDataValues( + applicationMetadata?.PresentationFields, + instance.PresentationTexts, + dataType, + data); + + if (updatedValues.Count > 0) + { + Instance updatedInstance = await _instanceClient.UpdatePresentationTexts( + int.Parse(instance.Id.Split("/")[0]), + Guid.Parse(instance.Id.Split("/")[1]), + new PresentationTexts { Texts = updatedValues }); + + instance.PresentationTexts = updatedInstance.PresentationTexts; + } + } + + private async Task UpdateDataValuesOnInstance(Instance instance, string dataType, object data) + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + Dictionary updatedValues = DataHelper.GetUpdatedDataValues( + applicationMetadata?.DataFields, + instance.DataValues, + dataType, + data); + + if (updatedValues.Count > 0) + { + Instance updatedInstance = await _instanceClient.UpdateDataValues( + int.Parse(instance.Id.Split("/")[0]), + Guid.Parse(instance.Id.Split("/")[1]), + new DataValues { Values = updatedValues }); + + instance.DataValues = updatedInstance.DataValues; + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/ConfirmationProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ConfirmationProcessTask.cs new file mode 100644 index 000000000..7c625d43b --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/ConfirmationProcessTask.cs @@ -0,0 +1,31 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks +{ + /// + /// Represents the process task responsible for collecting user confirmation. + /// + public class ConfirmationProcessTask : IProcessTask + { + /// + public string Type => "confirmation"; + + /// + public async Task Abandon(string taskId, Instance instance) + { + await Task.CompletedTask; + } + + /// + public async Task End(string taskId, Instance instance) + { + await Task.CompletedTask; + } + + /// + public async Task Start(string taskId, Instance instance) + { + await Task.CompletedTask; + } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/DataProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/DataProcessTask.cs new file mode 100644 index 000000000..d528bb62d --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/DataProcessTask.cs @@ -0,0 +1,31 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks +{ + /// + /// Represents the process task responsible for form filling steps. + /// + public class DataProcessTask : IProcessTask + { + /// + public string Type => "data"; + + /// + public async Task Abandon(string taskId, Instance instance) + { + await Task.CompletedTask; + } + + /// + public async Task End(string taskId, Instance instance) + { + await Task.CompletedTask; + } + + /// + public async Task Start(string taskId, Instance instance) + { + await Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/FeedbackProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/FeedbackProcessTask.cs new file mode 100644 index 000000000..e81c58a06 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/FeedbackProcessTask.cs @@ -0,0 +1,31 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks +{ + /// + /// Represents the process task responsible for waiting for feedback from application owner. + /// + public class FeedbackProcessTask : IProcessTask + { + /// + public string Type => "feedback"; + + /// + public async Task Abandon(string taskId, Instance instance) + { + await Task.CompletedTask; + } + + /// + public async Task End(string taskId, Instance instance) + { + await Task.CompletedTask; + } + + /// + public async Task Start(string taskId, Instance instance) + { + await Task.CompletedTask; + } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Interfaces/IProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Interfaces/IProcessTask.cs new file mode 100644 index 000000000..85e935559 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Interfaces/IProcessTask.cs @@ -0,0 +1,29 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks +{ + /// + /// Implement this interface to create a new type of task for the process engine. + /// + public interface IProcessTask + { + /// + /// The type is used to identify the correct task implementation for a given task type in the process config file. + /// + string Type { get; } + /// + /// Any logic to be executed when a task is started should be put in this method. + /// + Task Start(string taskId, Instance instance); + + /// + /// Any logic to be executed when a task is ended should be put in this method. + /// + Task End(string taskId, Instance instance); + + /// + /// Any logic to be executed when a task is abandoned should be put in this method. + /// + Task Abandon(string taskId, Instance instance); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/NullTypeProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/NullTypeProcessTask.cs new file mode 100644 index 000000000..5df54ef3a --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/NullTypeProcessTask.cs @@ -0,0 +1,31 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks; + +/// +/// Null implementation. Used when no other can be found +/// +public class NullTypeProcessTask : IProcessTask +{ + + /// + public string Type => "NullType"; + + /// + public async Task Start(string taskId, Instance instance) + { + await Task.CompletedTask; + } + + /// + public async Task End(string taskId, Instance instance) + { + await Task.CompletedTask; + } + + /// + public async Task Abandon(string taskId, Instance instance) + { + await Task.CompletedTask; + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs new file mode 100644 index 000000000..99164775b --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/SigningProcessTask.cs @@ -0,0 +1,30 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ProcessTasks +{ + /// + /// Represents the process task responsible for signing. + /// + internal class SigningProcessTask : IProcessTask + { + public string Type => "signing"; + + /// + public async Task Start(string taskId, Instance instance) + { + await Task.CompletedTask; + } + + /// + public async Task End(string taskId, Instance instance) + { + await Task.CompletedTask; + } + + /// + public async Task Abandon(string taskId, Instance instance) + { + await Task.CompletedTask; + } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ServiceTasks/EformidlingServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ServiceTasks/EformidlingServiceTask.cs new file mode 100644 index 000000000..acc304e0c --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ServiceTasks/EformidlingServiceTask.cs @@ -0,0 +1,52 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.EFormidling.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Instances; +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; + +/// +/// Service task that sends eFormidling shipment, if EFormidling is enabled in config and EFormidling.SendAfterTaskId matches the current task. +/// +public class EformidlingServiceTask : IServiceTask +{ + 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. + /// + public EformidlingServiceTask(ILogger logger, IAppMetadata appMetadata, IInstanceClient instanceClient, IEFormidlingService? eFormidlingService = null, IOptions? appSettings = null) + { + _logger = logger; + _appMetadata = appMetadata; + _instanceClient = instanceClient; + _eFormidlingService = eFormidlingService; + _appSettings = appSettings; + } + + /// + public async Task Execute(string taskId, Instance instance) + { + ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); + if (_appSettings?.Value?.EnableEFormidling == true && applicationMetadata.EFormidling?.SendAfterTaskId == taskId) + { + if (_eFormidlingService != null) + { + Instance updatedInstance = await _instanceClient.GetInstance(instance); + await _eFormidlingService.SendEFormidlingShipment(updatedInstance); + } + else + { + _logger.LogError("EformidlingService is not configured. No eFormidling shipment will be sent."); + } + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ServiceTasks/Interfaces/IServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ServiceTasks/Interfaces/IServiceTask.cs new file mode 100644 index 000000000..f9a16608e --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ServiceTasks/Interfaces/IServiceTask.cs @@ -0,0 +1,17 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ServiceTasks; + +/// +/// Interface for service tasks that can be executed during a process. +/// +public interface IServiceTask +{ + /// + /// Executes the service task. + /// + /// + /// + /// + public Task Execute(string taskId, Instance instance); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/ServiceTasks/PdfServiceTask.cs b/src/Altinn.App.Core/Internal/Process/ServiceTasks/PdfServiceTask.cs new file mode 100644 index 000000000..6065aa5cd --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ServiceTasks/PdfServiceTask.cs @@ -0,0 +1,42 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.ServiceTasks; + +/// +/// Service task that generates PDFs for all connected datatypes that have the EnablePdfCreation flag set to true. +/// +public class PdfServiceTask : IServiceTask +{ + private readonly IAppMetadata _appMetadata; + private readonly IPdfService _pdfService; + + /// + /// Initializes a new instance of the class. + /// + public PdfServiceTask(IAppMetadata appMetadata, IPdfService pdfService) + { + _pdfService = pdfService; + _appMetadata = appMetadata; + } + + /// + public async Task Execute(string taskId, Instance instance) + { + ArgumentNullException.ThrowIfNull(taskId); + ArgumentNullException.ThrowIfNull(instance); + + ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); + List dataTypesWithPdf = appMetadata.DataTypes.FindAll(dt => + dt.TaskId == taskId && + dt.AppLogic?.ClassRef != null && + dt.EnablePdfCreation); + + if (instance.Data.Exists(dataElement => dataTypesWithPdf.Exists(dataType => dataType.Id == dataElement.DataType))) + { + await _pdfService.GenerateAndStorePdf(instance, taskId, CancellationToken.None); + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs b/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs index bfa6c1642..dd9d9cb50 100644 --- a/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs @@ -24,8 +24,4 @@ public class ProcessStartRequest /// The start event id, only needed if multiple start events in process /// public string? StartEventId { get; set; } - /// - /// If set to true the instance is not updated and the events are not dispatched - /// - public bool Dryrun { get; set; } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs b/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs index f333e3c0f..e90ad14ad 100644 --- a/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs +++ b/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs @@ -1,8 +1,25 @@ - using Altinn.App.Core.Models.Process; - namespace Altinn.App.Core.Models.UserAction; +/// +/// Represents the result of a user action +/// +public enum ResultType +{ + /// + /// The user action succeeded + /// + Success, + /// + /// The user action failed + /// + Failure, + /// + /// The client should redirect the user to a new url + /// + Redirect +} + /// /// Represents the result of a user action /// @@ -11,7 +28,7 @@ public class UserActionResult /// /// Gets or sets a value indicating whether the user action was a success /// - public bool Success { get; init; } + public ResultType ResultType { get; init; } /// /// Gets or sets a dictionary of updated data models. Key should be elementId and value should be the updated data model @@ -33,6 +50,10 @@ public class UserActionResult /// public ProcessErrorType ErrorType { get; set; } + /// + /// If this is set, the client should redirect to this url + /// + public string? RedirectUrl { get; set; } /// /// Creates a success result /// @@ -42,7 +63,7 @@ public static UserActionResult SuccessResult(List? clientActions = { var userActionResult = new UserActionResult { - Success = true, + ResultType = ResultType.Success, ClientActions = clientActions }; return userActionResult; @@ -56,13 +77,27 @@ public static UserActionResult FailureResult(ActionError error, List + /// Creates a redirect result + /// + /// + /// + public static UserActionResult RedirectResult(string redirectUrl) + { + return new UserActionResult + { + ResultType = ResultType.Redirect, + RedirectUrl = redirectUrl + }; + } + /// /// Adds an updated data model to the result /// diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs index 9d97db270..27ca14e47 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs @@ -360,9 +360,9 @@ public async Task CopyInstance_EverythingIsFine_ReturnsRedirect() _instanceClient.Setup(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(instance); _instanceClient.Setup(i => i.GetInstance(It.IsAny())).ReturnsAsync(instance); _instantiationValidator.Setup(v => v.Validate(It.IsAny())).ReturnsAsync(instantiationValidationResult); - _processEngine.Setup(p => p.StartProcess(It.IsAny())) + _processEngine.Setup(p => p.GenerateProcessStartEvents(It.IsAny())) .ReturnsAsync(() => { return new ProcessChangeResult() { Success = true }; }); - _processEngine.Setup(p => p.UpdateInstanceAndRerunEvents(It.IsAny(), It.IsAny>())); + _processEngine.Setup(p => p.HandleEventsAndUpdateStorage(It.IsAny(), It.IsAny>(), It.IsAny>())); _data.Setup(p => p.GetFormData(instanceGuid, It.IsAny()!, Org, AppName, InstanceOwnerPartyId, dataGuid)) .ReturnsAsync(new { test = "test" }); _data.Setup(p => p.InsertFormData(It.IsAny(), instanceGuid, It.IsAny()!, Org, AppName, InstanceOwnerPartyId, dataTypeId)) diff --git a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs index cc357e464..86ed42861 100644 --- a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs @@ -7,6 +7,7 @@ using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; +using DataElement = Altinn.Platform.Storage.Interface.Models.DataElement; namespace App.IntegrationTests.Mocks.Services @@ -340,9 +341,9 @@ public Task LockDataElement(InstanceIdentifier instanceIdentifier, { // 🤬The signature does not take org/app, // but our test data is organized by org/app. - var (org, app) = TestData.GetInstanceOrgApp(instanceIdentifier); - var dataElement = GetDataElements(org, app, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid); - var element = dataElement.FirstOrDefault(d => d.Id == dataGuid.ToString()); + (string org, string app) = TestData.GetInstanceOrgApp(instanceIdentifier); + List dataElement = GetDataElements(org, app, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid); + DataElement? element = dataElement.FirstOrDefault(d => d.Id == dataGuid.ToString()); if (element is null) { throw new Exception("Data element not found."); @@ -354,7 +355,18 @@ public Task LockDataElement(InstanceIdentifier instanceIdentifier, public Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) { - throw new NotImplementedException(); + // 🤬The signature does not take org/app, + // but our test data is organized by org/app. + (string org, string app) = TestData.GetInstanceOrgApp(instanceIdentifier); + List dataElement = GetDataElements(org, app, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid); + DataElement? element = dataElement.FirstOrDefault(d => d.Id == dataGuid.ToString()); + if (element is null) + { + throw new Exception("Data element not found."); + } + element.Locked = false; + WriteDataElementToFile(element, org, app, instanceIdentifier.InstanceOwnerPartyId); + return Task.FromResult(element); } private static void WriteDataElementToFile(DataElement dataElement, string org, string app, int instanceOwnerPartyId) diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs index c856c54b6..25fe95ceb 100644 --- a/test/Altinn.App.Api.Tests/Program.cs +++ b/test/Altinn.App.Api.Tests/Program.cs @@ -19,7 +19,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; diff --git a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs deleted file mode 100644 index 9590d9254..000000000 --- a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs +++ /dev/null @@ -1,692 +0,0 @@ -#nullable disable -using Altinn.App.Core.Features; -using Altinn.App.Core.Implementation; -using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Internal.AppModel; -using Altinn.App.Core.Internal.Data; -using Altinn.App.Core.Internal.Expressions; -using Altinn.App.Core.Internal.Instances; -using Altinn.App.Core.Internal.Pdf; -using Altinn.App.Core.Internal.Prefill; -using Altinn.App.Core.Models; -using Altinn.App.Core.Tests.Implementation.TestData.AppDataModel; -using Altinn.Platform.Storage.Interface.Enums; -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.FeatureManagement; -using Moq; -using Xunit; - -namespace Altinn.App.PlatformServices.Tests.Implementation; - -public class DefaultTaskEventsTests : IDisposable -{ - private readonly ILogger _logger = NullLogger.Instance; - private readonly Mock _resMock; - private readonly Mock _metaMock; - private readonly ApplicationMetadata _application; - private readonly Mock _dataMock; - private readonly Mock _prefillMock; - private readonly IAppModel _appModel; - private readonly Mock _appModelMock; - private readonly Mock _instantiationMock; - private readonly Mock _instanceMock; - private IEnumerable _taskStarts; - private IEnumerable _taskEnds; - private IEnumerable _taskAbandons; - private readonly Mock _pdfMock; - private readonly LayoutEvaluatorStateInitializer _layoutStateInitializer; - - public DefaultTaskEventsTests() - { - _application = new ApplicationMetadata("ttd/test"); - _resMock = new Mock(); - _metaMock = new Mock(); - _dataMock = new Mock(MockBehavior.Strict); - _prefillMock = new Mock(); - _appModel = new DefaultAppModel(NullLogger.Instance); - _appModelMock = new Mock(); - _instantiationMock = new Mock(); - _instanceMock = new Mock(); - _taskStarts = new List(); - _taskEnds = new List(); - _taskAbandons = new List(); - _pdfMock = new Mock(); - _layoutStateInitializer = new LayoutEvaluatorStateInitializer(_resMock.Object, Microsoft.Extensions.Options.Options.Create(new Core.Configuration.FrontEndSettings())); - } - - [Fact] - public async void OnAbandonProcessTask_handles_no_IProcessTaskAbandon_injected() - { - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(_application); - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModel, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - await te.OnAbandonProcessTask("Task_1", new Instance()); - } - - [Fact] - public async void OnAbandonProcessTask_calls_all_added_implementations() - { - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(_application); - Mock abandonOne = new Mock(); - Mock abandonTwo = new Mock(); - _taskAbandons = new List() { abandonOne.Object, abandonTwo.Object }; - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModel, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - var instance = new Instance(); - await te.OnAbandonProcessTask("Task_1", instance); - abandonOne.Verify(a => a.Abandon("Task_1", instance)); - abandonTwo.Verify(a => a.Abandon("Task_1", instance)); - abandonOne.VerifyNoOtherCalls(); - abandonTwo.VerifyNoOtherCalls(); - } - - [Fact] - public async void OnEndProcessTask_calls_all_added_implementations_of_IProcessTaskEnd() - { - _application.DataTypes = new List(); - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(_application); - Mock endOne = new Mock(); - Mock endTwo = new Mock(); - _taskEnds = new List() { endOne.Object, endTwo.Object }; - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModel, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - var instance = new Instance() - { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", - AppId = "ttd/test", - Data = new List(), - }; - await te.OnEndProcessTask("Task_1", instance); - _metaMock.Verify(r => r.GetApplicationMetadata()); - endOne.Verify(a => a.End("Task_1", instance)); - endTwo.Verify(a => a.End("Task_1", instance)); - endOne.VerifyNoOtherCalls(); - endTwo.VerifyNoOtherCalls(); - } - - [Fact] - public async void OnEndProcessTask_removes_all_shadow_fields_and_saves_to_specified_datatype() - { - var org = "ttd"; - var appId = "shadow-fields-test"; - var application = GetApplicationMetadataForShadowFields(); - var instanceOwnerPartyId = 1337; - var instanceGuid = Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"); - var dataGuid = Guid.Parse("03ea848c-64f0-40f4-b5b4-30e1642d09b5"); - var instance = new Instance() - { - Id = $"{instanceOwnerPartyId}/{instanceGuid}", - AppId = $"{org}/{appId}", - Data = new List() - { - { - new() - { - DataType = "model", - Id = dataGuid.ToString(), - } - } - }, - InstanceOwner = new InstanceOwner() - { - PartyId = instanceOwnerPartyId.ToString() - }, - Org = org - }; - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(application); - _appModelMock.Setup(r => r.GetModelType("Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields")).Returns(typeof(Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields)); - Type modelType = typeof(ModelWithShadowFields); - _dataMock.Setup(r => r.GetFormData(instanceGuid, modelType, org, appId, instanceOwnerPartyId, dataGuid)) - .ReturnsAsync(GetDataElementForShadowFields()) - .Verifiable(Times.Once); - _dataMock.Setup(d => d.InsertFormData(It.IsAny(), instanceGuid, modelType, org, appId, instanceOwnerPartyId, "model-clean")) - .ReturnsAsync(new DataElement()) - .Verifiable(Times.Once); - _dataMock.Setup(r => r.UpdateData(It.IsAny(), instanceGuid, modelType, "ttd", "shadow-fields-test", instanceOwnerPartyId, dataGuid)) - .ReturnsAsync(new DataElement()) - .Verifiable(Times.Once); - _dataMock.Setup(d => d.LockDataElement(It.IsAny(), dataGuid)) - .ReturnsAsync(new DataElement()) - .Verifiable(Times.Once); - - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModelMock.Object, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - - await te.OnEndProcessTask("Task_1", instance); - _metaMock.Verify(r => r.GetApplicationMetadata()); - _dataMock.Verify(); - } - - [Fact] - public async void OnEndProcessTask_removes_all_shadow_fields_and_saves_to_current_datatype_when_saveToDataType_not_specified() - { - var instanceOwnerPartyId = 1337; - var instanceGuid = Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"); - var dataGuid = Guid.Parse("03ea848c-64f0-40f4-b5b4-30e1642d09b5"); - var application = GetApplicationMetadataForShadowFields(false); - var instance = new Instance() - { - Id = $"{instanceOwnerPartyId}/{instanceGuid}", - AppId = "ttd/shadow-fields-test", - Data = new List() - { - { - new() - { - DataType = "model", - Id = dataGuid.ToString(), - } - } - }, - InstanceOwner = new InstanceOwner() - { - PartyId = instanceOwnerPartyId.ToString() - }, - Org = "ttd" - }; - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(application); - _appModelMock.Setup(r => r.GetModelType("Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields")).Returns(typeof(Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields)); - Type modelType = typeof(Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields); - _dataMock.Setup(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", instanceOwnerPartyId, dataGuid)) - .ReturnsAsync(GetDataElementForShadowFields()) - .Verifiable(Times.Once); - _dataMock.Setup(r => r.UpdateData(It.IsAny(), instanceGuid, modelType, "ttd", "shadow-fields-test", instanceOwnerPartyId, dataGuid)) - .ReturnsAsync(new DataElement()) - .Verifiable(Times.Once); - _dataMock.Setup(d => d.LockDataElement(It.IsAny(), dataGuid)) - .ReturnsAsync(new DataElement()) - .Verifiable(Times.Once); - - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModelMock.Object, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - - await te.OnEndProcessTask("Task_1", instance); - _metaMock.Verify(r => r.GetApplicationMetadata()); - - _dataMock.Verify(); - } - - [Fact] - public async void OnEndProcessTask_throws_exception_when_saveToDataType_is_specified_but_does_not_exist() - { - var application = GetApplicationMetadataForShadowFields(true, saveToDataType: "does-not-exist"); - var instance = new Instance() - { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", - AppId = "ttd/shadow-fields-test", - Data = new List() - { - { - new() - { - DataType = "model", - Id = "03ea848c-64f0-40f4-b5b4-30e1642d09b5", - } - } - }, - InstanceOwner = new InstanceOwner() - { - PartyId = "1000" - }, - Org = "ttd" - }; - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(application); - _appModelMock.Setup(r => r.GetModelType("Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields")).Returns(typeof(Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields)); - var instanceGuid = Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"); - var dataElementId = Guid.Parse("03ea848c-64f0-40f4-b5b4-30e1642d09b5"); - Type modelType = typeof(Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields); - _dataMock.Setup(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)) - .ReturnsAsync(GetDataElementForShadowFields()); - - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModelMock.Object, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - - await Assert.ThrowsAsync(async () => await te.OnEndProcessTask("Task_1", instance)); - _metaMock.Verify(r => r.GetApplicationMetadata()); - _dataMock.Verify(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)); - } - - [Fact] - public async void OnEndProcessTask_calls_all_added_implementations_of_IProcessTaskStart() - { - _application.DataTypes = new List(); - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(_application); - Mock startOne = new Mock(); - Mock startTwo = new Mock(); - _taskStarts = new List() { startOne.Object, startTwo.Object }; - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModel, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - var instance = new Instance() - { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d" - }; - var prefill = new Dictionary() - { - { - "aa", - "bb" - } - }; - await te.OnStartProcessTask("Task_1", instance, prefill); - _metaMock.Verify(r => r.GetApplicationMetadata()); - startOne.Verify(a => a.Start("Task_1", instance, prefill)); - startTwo.Verify(a => a.Start("Task_1", instance, prefill)); - startOne.VerifyNoOtherCalls(); - startTwo.VerifyNoOtherCalls(); - } - - [Fact] - public async void OnEndProcessTask_does_not_sets_hard_soft_delete_if_process_ended_and_autoDeleteOnProcessEnd_false() - { - _application.DataTypes = new List(); - _application.AutoDeleteOnProcessEnd = false; - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(_application); - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModel, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - var instance = new Instance() - { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", - AppId = "ttd/test", - InstanceOwner = new() - { - PartyId = "1000" - }, - Process = new() - { - Ended = DateTime.Now - }, - Data = new List(), - }; - await te.OnEndProcessTask("EndEvent_1", instance); - _metaMock.Verify(r => r.GetApplicationMetadata()); - _instanceMock.Verify(i => i.DeleteInstance(1000, Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"), true), Times.Never); - } - - [Fact] - public async void OnEndProcessTask_deletes_old_datatypes_generated_from_task_beeing_ended() - { - var org = "ttd"; - var app = "test"; - var instanceOwenerPartyId = 1337; - var instanceGuid = Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"); - var dataGuid = Guid.Parse("ba0678ad-960d-4307-aba2-ba29c9804c9d"); - _application.DataTypes = new List(); - _application.AutoDeleteOnProcessEnd = false; - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(_application); - _dataMock.Setup(d => d.DeleteData(org, app, instanceOwenerPartyId, instanceGuid, dataGuid, false)) - .ReturnsAsync(true); - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModel, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - var instance = new Instance() - { - Id = $"{instanceOwenerPartyId}/{instanceGuid}", - AppId = "ttd/test", - InstanceOwner = new() - { - PartyId = instanceOwenerPartyId.ToString() - }, - Process = new() - { - Ended = DateTime.Now - }, - Data = new() - { - new() - { - Id = dataGuid.ToString(), - References = new() - { - new() - { - Relation = RelationType.GeneratedFrom, - Value = "Task_1", - ValueType = ReferenceType.Task - }, - new() - { - Relation = RelationType.GeneratedFrom, - Value = "EndEvent_1", - ValueType = ReferenceType.Task - } - } - } - } - }; - await te.OnEndProcessTask("EndEvent_1", instance); - _metaMock.Verify(r => r.GetApplicationMetadata()); - _instanceMock.Verify(i => i.DeleteInstance(1000, Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"), true), Times.Never); - _dataMock.Verify(d => d.DeleteData("ttd", "test", 1337, Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"), Guid.Parse("ba0678ad-960d-4307-aba2-ba29c9804c9d"), false), Times.Once); - } - - [Fact] - public async void OnEndProcessTask_sets_hard_soft_delete_if_process_ended_and_autoDeleteOnProcessEnd_true() - { - _application.DataTypes = new List(); - _application.AutoDeleteOnProcessEnd = true; - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(_application); - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModel, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - var instance = new Instance() - { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", - AppId = "ttd/test", - InstanceOwner = new() - { - PartyId = "1000" - }, - Process = new() - { - Ended = DateTime.Now - }, - Data = new List() - }; - await te.OnEndProcessTask("EndEvent_1", instance); - _metaMock.Verify(r => r.GetApplicationMetadata()); - _instanceMock.Verify(i => i.DeleteInstance(1000, Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"), true), Times.Once); - } - - [Fact] - public async void OnEndProcessTask_does_not_sets_hard_soft_delete_if_process_not_ended_and_autoDeleteOnProcessEnd_true() - { - _application.DataTypes = new List(); - _application.AutoDeleteOnProcessEnd = true; - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(_application); - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModel, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - var instance = new Instance() - { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", - AppId = "ttd/test", - InstanceOwner = new() - { - PartyId = "1000" - }, - Process = new(), - Data = new List(), - }; - await te.OnEndProcessTask("EndEvent_1", instance); - _metaMock.Verify(r => r.GetApplicationMetadata()); - _instanceMock.Verify(i => i.DeleteInstance(1000, Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"), true), Times.Never); - } - - [Fact] - public async void OnEndProcessTask_does_not_sets_hard_soft_delete_if_process_null_and_autoDeleteOnProcessEnd_true() - { - _application.DataTypes = new List(); - _application.AutoDeleteOnProcessEnd = true; - _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(_application); - DefaultTaskEvents te = new DefaultTaskEvents( - _logger, - _resMock.Object, - _metaMock.Object, - _dataMock.Object, - _prefillMock.Object, - _appModel, - _instantiationMock.Object, - _instanceMock.Object, - _taskStarts, - _taskEnds, - _taskAbandons, - _pdfMock.Object, - _layoutStateInitializer); - var instance = new Instance() - { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", - AppId = "ttd/test", - InstanceOwner = new() - { - PartyId = "1000" - }, - Data = new List(), - }; - await te.OnEndProcessTask("EndEvent_1", instance); - _metaMock.Verify(r => r.GetApplicationMetadata()); - _instanceMock.Verify(i => i.DeleteInstance(1000, Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"), true), Times.Never); - } - - public void Dispose() - { - _metaMock.VerifyNoOtherCalls(); - _resMock.VerifyNoOtherCalls(); - _dataMock.VerifyNoOtherCalls(); - _prefillMock.VerifyNoOtherCalls(); - _instantiationMock.VerifyNoOtherCalls(); - _instanceMock.VerifyNoOtherCalls(); - _pdfMock.VerifyNoOtherCalls(); - } - - private ApplicationMetadata GetApplicationMetadataForShadowFields(bool useSaveToDataType = true, string saveToDataType = "model-clean") - { - return new ApplicationMetadata("tdd/bestilling") - { - Id = "tdd/bestilling", - Org = "tdd", - Created = DateTime.Parse("2019-09-16T22:22:22"), - CreatedBy = "username", - Title = new Dictionary() - { - { "nb", "Bestillingseksempelapp" } - }, - DataTypes = new List() - { - new() - { - Id = "ref-data-as-pdf", - AllowedContentTypes = new List() { "application/pdf" }, - MinCount = 1, - TaskId = "Task_1" - }, - new() - { - Id = "model", - AllowedContentTypes = new List() { "application/xml" }, - MinCount = 1, - MaxCount = 1, - TaskId = "Task_1", - EnablePdfCreation = false, - AppLogic = new ApplicationLogic() - { - AllowAnonymousOnStateless = false, - AutoCreate = true, - ClassRef = "Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields", - AutoDeleteOnProcessEnd = false, - ShadowFields = new ShadowFields() - { - Prefix = "AltinnSF_", - SaveToDataType = useSaveToDataType ? saveToDataType : null, - } - } - }, - new() - { - Id = "model-clean", - AllowedContentTypes = new List() { "application/xml" }, - MinCount = 0, - MaxCount = 1, - TaskId = "Task_1", - AppLogic = new ApplicationLogic() - { - AllowAnonymousOnStateless = false, - AutoCreate = false, - ClassRef = "Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields", - AutoDeleteOnProcessEnd = false, - } - } - }, - PartyTypesAllowed = new PartyTypesAllowed() - { - BankruptcyEstate = true, - Organisation = true, - Person = true, - SubUnit = true - }, - OnEntry = new OnEntry() - { - Show = "select-instance" - } - }; - } - - private ModelWithShadowFields GetDataElementForShadowFields() - { - return new ModelWithShadowFields() - { - AltinnSF_hello = "hello", - AltinnSF_test = "test", - Property1 = 1, - Property2 = 2, - AltinnSF_gruppeish = new AltinnSF_gruppeish() - { - F1 = "f1", - F2 = "f2", - }, - Gruppe = new List() - { - new() - { - AltinnSF_gfhjelpefelt = "gfhjelpefelt", - Gf1 = "gf1", - }, - new() - { - AltinnSF_gfhjelpefelt = "gfhjelpefelt2", - Gf1 = "gf1-v2", - } - } - }; - } -} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs index 071d09860..57bbef7bd 100644 --- a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs @@ -2,7 +2,7 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; using Altinn.App.Core.Internal.Auth; -using Altinn.App.Core.Internal.Process.Action; +using Altinn.App.Core.Internal.Process.Authorization; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; diff --git a/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs new file mode 100644 index 000000000..1edd90a2b --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs @@ -0,0 +1,204 @@ +using System.Text.Json; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Data +{ + public class DataServiceTests + { + private readonly Mock _mockDataClient; + private readonly Mock _mockAppMetadata; + private readonly DataService _dataService; + + private readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); + + public DataServiceTests() + { + _mockDataClient = new Mock(); + _mockAppMetadata = new Mock(); + _dataService = new DataService(_mockDataClient.Object, _mockAppMetadata.Object); + } + + [Fact] + public async Task GetByType_ReturnsCorrectDataElementAndModel_WhenDataElementExists() + { + // Arrange + Instance instance = CreateInstance(); + InstanceIdentifier instanceIdentifier = new(instance); + const string dataType = "dataType"; + instance.Data = [new DataElement { DataType = dataType, Id = Guid.NewGuid().ToString() }]; + + ApplicationMetadata applicationMetadata = CreateAppMetadata(instance); + + TestModel expectedModel = new(); + using var referenceStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(referenceStream, expectedModel, _jsonSerializerOptions); + referenceStream.Position = 0; + + _mockDataClient.Setup(dc => dc.GetBinaryData( + applicationMetadata.AppIdentifier.Org, + applicationMetadata.AppIdentifier.App, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + new Guid(instance.Data.First().Id))) + .ReturnsAsync(referenceStream); + + _mockAppMetadata.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + (Guid dataElementId, TestModel? model) = await _dataService.GetByType(instance, dataType); + + // Assert + Assert.Equal(instance.Data.First().Id, dataElementId.ToString()); + Assert.Equivalent(expectedModel, model); + } + + [Fact] + public async Task GetByType_ReturnsEmptyGuidAndNullModel_WhenDataElementDoesNotExist() + { + // Arrange + Instance instance = CreateInstance(); + const string dataType = "dataType"; + const string otherType = "otherType"; + instance.Data = [new DataElement() { DataType = otherType, Id = Guid.NewGuid().ToString() }]; + + // Act + (Guid dataElementId, TestModel? model) = await _dataService.GetByType(instance, dataType); + + // Assert + Assert.Equal(Guid.Empty, dataElementId); + Assert.Null(model); + } + + [Fact] + public async Task GetById_ReturnsCorrectModel_WhenDataElementExists() + { + // Arrange + var expectedDataId = Guid.NewGuid(); + Instance instance = CreateInstance(); + InstanceIdentifier instanceIdentifier = new(instance); + instance.Data = [new DataElement { Id = expectedDataId.ToString() }]; + + ApplicationMetadata applicationMetadata = CreateAppMetadata(instance); + + var expectedModel = new TestModel(); + using var referenceStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(referenceStream, expectedModel, _jsonSerializerOptions); + referenceStream.Position = 0; + + _mockDataClient.Setup(dc => dc.GetBinaryData( + applicationMetadata.AppIdentifier.Org, + applicationMetadata.AppIdentifier.App, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + expectedDataId)) + .ReturnsAsync(referenceStream); + + _mockAppMetadata.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + var model = await _dataService.GetById(instance, expectedDataId); + + // Assert + Assert.Equivalent(expectedModel, model); + } + + [Fact] + public async Task GetById_ThrowsArgumentException_WhenDataElementDoesNotExist() + { + // Arrange + Instance instance = CreateInstance(); + instance.Data = [new DataElement { Id = Guid.NewGuid().ToString() }]; + + // Act & Assert + await Assert.ThrowsAsync(() => _dataService.GetById(instance, Guid.NewGuid())); + } + + [Fact] + public async Task InsertJsonObject_ReturnsExpectedResult() + { + // Arrange + Instance instance = CreateInstance(); + InstanceIdentifier instanceIdentifier = new(instance); + const string dataTypeId = "dataTypeId"; + object data = new(); + DataElement expectedDataElement = new(); + + _mockDataClient.Setup(x => + x.InsertBinaryData( + instanceIdentifier.ToString(), + dataTypeId, + "application/json", + dataTypeId + ".json", + It.IsAny(), + null)) + .ReturnsAsync(expectedDataElement); + + // Act + DataElement result = await _dataService.InsertJsonObject(instanceIdentifier, dataTypeId, data); + + // Assert + Assert.Equivalent(expectedDataElement, result); + } + + [Fact] + public async Task UpdateJsonObject_ReturnsExpectedResult() + { + // Arrange + Instance instance = CreateInstance(); + InstanceIdentifier instanceIdentifier = new(instance); + const string dataTypeId = "dataTypeId"; + var dataElementId = Guid.NewGuid(); + object data = new(); + DataElement expectedDataElement = new(); + + _mockDataClient.Setup(x => x.UpdateBinaryData( + instanceIdentifier, + "application/json", + dataTypeId + ".json", + dataElementId, + It.IsAny())) + .ReturnsAsync(expectedDataElement); + + // Act + DataElement result = await _dataService.UpdateJsonObject(instanceIdentifier, dataTypeId, dataElementId, data); + + // Assert + Assert.Equivalent(expectedDataElement, result); + } + + private static Instance CreateInstance() + { + return new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + InstanceOwner = new InstanceOwner { PartyId = "123" }, + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo + { + AltinnTaskType = "dataTask", + ElementId = "Task_1", + }, + }, + }; + } + + private static ApplicationMetadata CreateAppMetadata(Instance instance) + { + return new ApplicationMetadata(instance.AppId) + { + CopyInstanceSettings = new CopyInstanceSettings { Enabled = true }, + }; + } + } + + public class TestModel + { + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Action/TestData/UserActionAuthorizerStub.cs b/test/Altinn.App.Core.Tests/Internal/Process/Authorization/TestData/UserActionAuthorizerStub.cs similarity index 100% rename from test/Altinn.App.Core.Tests/Internal/Process/Action/TestData/UserActionAuthorizerStub.cs rename to test/Altinn.App.Core.Tests/Internal/Process/Authorization/TestData/UserActionAuthorizerStub.cs diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtensionTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/Authorization/UserActionAuthorizerServiceCollectionExtensionTests.cs similarity index 99% rename from test/Altinn.App.Core.Tests/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtensionTests.cs rename to test/Altinn.App.Core.Tests/Internal/Process/Authorization/UserActionAuthorizerServiceCollectionExtensionTests.cs index 33bb34d72..8b7bd88d5 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtensionTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/Authorization/UserActionAuthorizerServiceCollectionExtensionTests.cs @@ -1,6 +1,6 @@ #nullable disable using Altinn.App.Core.Extensions; -using Altinn.App.Core.Internal.Process.Action; +using Altinn.App.Core.Internal.Process.Authorization; using Altinn.App.Core.Tests.Internal.Process.Action.TestData; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandlerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandlerTests.cs new file mode 100644 index 000000000..257e6a90d --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/AbandonTaskEventHandlerTests.cs @@ -0,0 +1,47 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.Platform.Storage.Interface.Models; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.EventHandlers.ProcessTask; + +public class AbandonTaskEventHandlerTests +{ + private IEnumerable _taskAbandons; + + public AbandonTaskEventHandlerTests() + { + _taskAbandons = new List(); + } + + [Fact] + public async Task Execute_handles_no_IProcessTaskAbandon_injected() + { + AbandonTaskEventHandler abandonTaskEventHandler = new AbandonTaskEventHandler(_taskAbandons); + Mock mockProcessTask = new Mock(); + var instance = new Instance(); + await abandonTaskEventHandler.Execute(mockProcessTask.Object, "Task_1", instance); + mockProcessTask.Verify(p => p.Abandon("Task_1", instance)); + mockProcessTask.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Execute_calls_all_added_implementations() + { + Mock mockProcessTask = new Mock(); + Mock abandonOne = new Mock(); + Mock abandonTwo = new Mock(); + _taskAbandons = new List() { abandonOne.Object, abandonTwo.Object }; + AbandonTaskEventHandler abandonTaskEventHandler = new AbandonTaskEventHandler(_taskAbandons); + var instance = new Instance(); + await abandonTaskEventHandler.Execute(mockProcessTask.Object, "Task_1", instance); + mockProcessTask.Verify(p => p.Abandon("Task_1", instance)); + mockProcessTask.VerifyNoOtherCalls(); + abandonOne.Verify(a => a.Abandon("Task_1", instance)); + abandonTwo.Verify(a => a.Abandon("Task_1", instance)); + abandonOne.VerifyNoOtherCalls(); + abandonTwo.VerifyNoOtherCalls(); + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..af3a24f36 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/EndTaskEventHandlerTests.cs @@ -0,0 +1,83 @@ +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.Platform.Storage.Interface.Models; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.EventHandlers.ProcessTask; + +public class EndTaskEventHandlerTests +{ + private readonly Mock _processTaskDataLocker = new(); + private readonly Mock _processTaskFinisher = new(); + private readonly Mock _pdfServiceTask = new(); + private readonly Mock _eformidlingServiceTask = new(); + private IEnumerable _processTaskEnds = new List(); + + [Fact] + public async Task Execute_handles_no_IProcessTaskAbandon_injected() + { + EndTaskEventHandler eteh = new EndTaskEventHandler( + _processTaskDataLocker.Object, + _processTaskFinisher.Object, + _pdfServiceTask.Object, + _eformidlingServiceTask.Object, + _processTaskEnds); + var instance = new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + }; + Mock mockProcessTask = new(); + await eteh.Execute(mockProcessTask.Object, "Task_1", instance); + _processTaskDataLocker.Verify(p => p.Lock("Task_1", instance)); + _processTaskFinisher.Verify(p => p.Finalize("Task_1", instance)); + _pdfServiceTask.Verify(p => p.Execute("Task_1", instance)); + _eformidlingServiceTask.Verify(p => p.Execute("Task_1", instance)); + mockProcessTask.Verify(p => p.End("Task_1", instance)); + + _processTaskDataLocker.VerifyNoOtherCalls(); + _processTaskFinisher.VerifyNoOtherCalls(); + _pdfServiceTask.VerifyNoOtherCalls(); + _eformidlingServiceTask.VerifyNoOtherCalls(); + mockProcessTask.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Execute_calls_all_added_implementations_of_IProcessTaskEnd() + { + Mock endOne = new(); + Mock endTwo = new(); + _processTaskEnds = new List() { endOne.Object, endTwo.Object }; + EndTaskEventHandler eteh = new( + _processTaskDataLocker.Object, + _processTaskFinisher.Object, + _pdfServiceTask.Object, + _eformidlingServiceTask.Object, + _processTaskEnds); + var instance = new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + }; + Mock mockProcessTask = new(); + await eteh.Execute(mockProcessTask.Object, "Task_1", instance); + endOne.Verify(a => a.End("Task_1", instance)); + endTwo.Verify(a => a.End("Task_1", instance)); + _processTaskDataLocker.Verify(p => p.Lock("Task_1", instance)); + _processTaskFinisher.Verify(p => p.Finalize("Task_1", instance)); + _pdfServiceTask.Verify(p => p.Execute("Task_1", instance)); + _eformidlingServiceTask.Verify(p => p.Execute("Task_1", instance)); + mockProcessTask.Verify(p => p.End("Task_1", instance)); + + _processTaskDataLocker.VerifyNoOtherCalls(); + _processTaskFinisher.VerifyNoOtherCalls(); + _pdfServiceTask.VerifyNoOtherCalls(); + _eformidlingServiceTask.VerifyNoOtherCalls(); + mockProcessTask.VerifyNoOtherCalls(); + endOne.VerifyNoOtherCalls(); + endTwo.VerifyNoOtherCalls(); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandlerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandlerTests.cs new file mode 100644 index 000000000..3875f9840 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/EventHandlers/ProcessTask/StartTaskEventHandlerTests.cs @@ -0,0 +1,79 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.Process.EventHandlers.ProcessTask; +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.Platform.Storage.Interface.Models; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.EventHandlers.ProcessTask; + +public class StartTaskEventHandlerTests +{ + private readonly Mock _processTaskDataLocker; + private readonly Mock _processTaskInitializer; + private IEnumerable _processTaskStarts; + + public StartTaskEventHandlerTests() + { + _processTaskDataLocker = new Mock(); + _processTaskInitializer = new Mock(); + _processTaskStarts = new List(); + } + + [Fact] + public async Task Execute_handles_no_IProcessTaskStart_injected() + { + StartTaskEventHandler steh = new StartTaskEventHandler( + _processTaskDataLocker.Object, + _processTaskInitializer.Object, + _processTaskStarts); + var instance = new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + }; + Mock mockProcessTask = new Mock(); + await steh.Execute(mockProcessTask.Object, "Task_1", instance, []); + _processTaskDataLocker.Verify(p => p.Unlock("Task_1", instance)); + _processTaskInitializer.Verify(p => p.Initialize("Task_1", instance, new Dictionary())); + mockProcessTask.Verify(p => p.Start("Task_1", instance)); + + _processTaskDataLocker.VerifyNoOtherCalls(); + _processTaskInitializer.VerifyNoOtherCalls(); + mockProcessTask.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Execute_calls_all_added_implementations_of_IProcessTaskStart() + { + Mock startOne = new Mock(); + Mock startTwo = new Mock(); + _processTaskStarts = new List() + { + startOne.Object, + startTwo.Object, + }; + StartTaskEventHandler steh = new StartTaskEventHandler( + _processTaskDataLocker.Object, + _processTaskInitializer.Object, + _processTaskStarts); + var instance = new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + }; + Mock mockProcessTask = new Mock(); + await steh.Execute(mockProcessTask.Object, "Task_1", instance, []); + _processTaskDataLocker.Verify(p => p.Unlock("Task_1", instance)); + _processTaskInitializer.Verify(p => p.Initialize("Task_1", instance, new Dictionary())); + mockProcessTask.Verify(p => p.Start("Task_1", instance)); + startOne.Verify(p => p.Start("Task_1", instance, new Dictionary())); + startTwo.Verify(p => p.Start("Task_1", instance, new Dictionary())); + + _processTaskDataLocker.VerifyNoOtherCalls(); + _processTaskInitializer.VerifyNoOtherCalls(); + mockProcessTask.VerifyNoOtherCalls(); + startOne.VerifyNoOtherCalls(); + startTwo.VerifyNoOtherCalls(); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs index 00af473d0..26b0602d0 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs @@ -22,18 +22,18 @@ public async Task StartProcess_calls_decorated_service_and_increments_success_co { // Arrange var processEngine = new Mock(); - processEngine.Setup(p => p.StartProcess(It.IsAny())).ReturnsAsync(new ProcessChangeResult { Success = true }); + processEngine.Setup(p => p.GenerateProcessStartEvents(It.IsAny())).ReturnsAsync(new ProcessChangeResult { Success = true }); var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_start_count{result=\"success\"}"); - var result = await decorator.StartProcess(new ProcessStartRequest()); + var result = await decorator.GenerateProcessStartEvents(new ProcessStartRequest()); (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"success\"} 1"); result.Success.Should().BeTrue(); - result = await decorator.StartProcess(new ProcessStartRequest()); + result = await decorator.GenerateProcessStartEvents(new ProcessStartRequest()); (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"success\"} 2"); result.Success.Should().BeTrue(); - processEngine.Verify(p => p.StartProcess(It.IsAny()), Times.Exactly(2)); + processEngine.Verify(p => p.GenerateProcessStartEvents(It.IsAny()), Times.Exactly(2)); processEngine.VerifyNoOtherCalls(); } @@ -42,18 +42,18 @@ public async Task StartProcess_calls_decorated_service_and_increments_failure_co { // Arrange var processEngine = new Mock(); - processEngine.Setup(p => p.StartProcess(It.IsAny())).ReturnsAsync(new ProcessChangeResult { Success = false }); + processEngine.Setup(p => p.GenerateProcessStartEvents(It.IsAny())).ReturnsAsync(new ProcessChangeResult { Success = false }); var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_start_count{result=\"failure\"}"); - var result = await decorator.StartProcess(new ProcessStartRequest()); + var result = await decorator.GenerateProcessStartEvents(new ProcessStartRequest()); (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"failure\"} 1"); result.Success.Should().BeFalse(); - result = await decorator.StartProcess(new ProcessStartRequest()); + result = await decorator.GenerateProcessStartEvents(new ProcessStartRequest()); (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"failure\"} 2"); result.Success.Should().BeFalse(); - processEngine.Verify(p => p.StartProcess(It.IsAny()), Times.Exactly(2)); + processEngine.Verify(p => p.GenerateProcessStartEvents(It.IsAny()), Times.Exactly(2)); processEngine.VerifyNoOtherCalls(); } @@ -288,13 +288,13 @@ public async Task UpdateInstanceAndRerunEvents_calls_decorated_service() { // Arrange var processEngine = new Mock(); - processEngine.Setup(p => p.UpdateInstanceAndRerunEvents(It.IsAny(), It.IsAny>())).ReturnsAsync(new Instance { }); + processEngine.Setup(p => p.HandleEventsAndUpdateStorage(It.IsAny(), It.IsAny>(), It.IsAny>())).ReturnsAsync(new Instance { }); var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_start_count{result=\"success\"}"); - await decorator.UpdateInstanceAndRerunEvents(new ProcessStartRequest(), new List()); + await decorator.HandleEventsAndUpdateStorage(It.IsAny(), It.IsAny>(), new List()); - processEngine.Verify(p => p.UpdateInstanceAndRerunEvents(It.IsAny(), It.IsAny>()), Times.Once); + processEngine.Verify(p => p.HandleEventsAndUpdateStorage(It.IsAny(), It.IsAny>(), It.IsAny>()), Times.Once); processEngine.VerifyNoOtherCalls(); } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index d12a3b506..d2b461285 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -24,6 +24,7 @@ public class ProcessEngineTest : IDisposable private Mock _processReaderMock; private readonly Mock _profileMock; private readonly Mock _processNavigatorMock; + private readonly Mock _processEventHandlingDelegatorMock; private readonly Mock _processEventDispatcherMock; public ProcessEngineTest() @@ -31,6 +32,7 @@ public ProcessEngineTest() _processReaderMock = new(); _profileMock = new(); _processNavigatorMock = new(); + _processEventHandlingDelegatorMock = new(); _processEventDispatcherMock = new(); } @@ -40,7 +42,7 @@ public async Task StartProcess_returns_unsuccessful_when_process_already_started ProcessEngine processEngine = GetProcessEngine(); Instance instance = new Instance() { Process = new ProcessState() { CurrentTask = new ProcessElementInfo() { ElementId = "Task_1" } } }; ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance }; - ProcessChangeResult result = await processEngine.StartProcess(processStartRequest); + ProcessChangeResult result = await processEngine.GenerateProcessStartEvents(processStartRequest); result.Success.Should().BeFalse(); result.ErrorMessage.Should().Be("Process is already started. Use next."); result.ErrorType.Should().Be(ProcessErrorType.Conflict); @@ -54,7 +56,7 @@ public async Task StartProcess_returns_unsuccessful_when_no_matching_startevent_ ProcessEngine processEngine = GetProcessEngine(processReaderMock); Instance instance = new Instance(); ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, StartEventId = "NotTheStartEventYouAreLookingFor" }; - ProcessChangeResult result = await processEngine.StartProcess(processStartRequest); + ProcessChangeResult result = await processEngine.GenerateProcessStartEvents(processStartRequest); _processReaderMock.Verify(r => r.GetStartEventIds(), Times.Once); result.Success.Should().BeFalse(); result.ErrorMessage.Should().Be("No matching startevent"); @@ -78,8 +80,8 @@ public async Task StartProcess_starts_process_and_moves_to_first_task_without_ev new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), new(AltinnCoreClaimTypes.Org, "tdd"), })); - ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = user, Dryrun = true }; - ProcessChangeResult result = await processEngine.StartProcess(processStartRequest); + ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = user }; + ProcessChangeResult result = await processEngine.GenerateProcessStartEvents(processStartRequest); _processReaderMock.Verify(r => r.GetStartEventIds(), Times.Once); _processReaderMock.Verify(r => r.IsProcessTask("StartEvent_1"), Times.Once); _processReaderMock.Verify(r => r.IsEndEvent("Task_1"), Times.Once); @@ -106,7 +108,8 @@ public async Task StartProcess_starts_process_and_moves_to_first_task() new(AltinnCoreClaimTypes.Org, "tdd"), })); ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = user }; - ProcessChangeResult result = await processEngine.StartProcess(processStartRequest); + ProcessChangeResult result = await processEngine.GenerateProcessStartEvents(processStartRequest); + await processEngine.HandleEventsAndUpdateStorage(instance, null, result.ProcessStateChange?.Events); _processReaderMock.Verify(r => r.GetStartEventIds(), Times.Once); _processReaderMock.Verify(r => r.IsProcessTask("StartEvent_1"), Times.Once); _processReaderMock.Verify(r => r.IsEndEvent("Task_1"), Times.Once); @@ -185,10 +188,16 @@ public async Task StartProcess_starts_process_and_moves_to_first_task() } } }; - _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + + _processEventHandlingDelegatorMock.Verify(d => d.HandleEvents( + It.Is(i => CompareInstance(expectedInstance, i)), + It.IsAny>(), + It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + + _processEventDispatcherMock.Verify(d => d.DispatchToStorage( It.Is(i => CompareInstance(expectedInstance, i)), - null, It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + result.Success.Should().BeTrue(); } @@ -211,7 +220,8 @@ public async Task StartProcess_starts_process_and_moves_to_first_task_with_prefi })); var prefill = new Dictionary() { { "test", "test" } }; ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = user, Prefill = prefill }; - ProcessChangeResult result = await processEngine.StartProcess(processStartRequest); + ProcessChangeResult result = await processEngine.GenerateProcessStartEvents(processStartRequest); + await processEngine.HandleEventsAndUpdateStorage(instance, prefill, result.ProcessStateChange?.Events); _processReaderMock.Verify(r => r.GetStartEventIds(), Times.Once); _processReaderMock.Verify(r => r.IsProcessTask("StartEvent_1"), Times.Once); _processReaderMock.Verify(r => r.IsEndEvent("Task_1"), Times.Once); @@ -290,10 +300,16 @@ public async Task StartProcess_starts_process_and_moves_to_first_task_with_prefi } } }; - _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + + _processEventHandlingDelegatorMock.Verify(d => d.HandleEvents( + It.Is(i => CompareInstance(expectedInstance, i)), + It.IsAny>(), + It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + + _processEventDispatcherMock.Verify(d => d.DispatchToStorage( It.Is(i => CompareInstance(expectedInstance, i)), - prefill, It.Is>(l => CompareInstanceEvents(l, expectedInstanceEvents)))); + result.Success.Should().BeTrue(); } @@ -489,11 +505,18 @@ public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() } } }; - _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + + _processEventHandlingDelegatorMock.Verify(d => d.HandleEvents( + It.Is(i => CompareInstance(expectedInstance, i)), + It.IsAny>(), + It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + + _processEventDispatcherMock.Verify(d => d.DispatchToStorage( It.Is(i => CompareInstance(expectedInstance, i)), - It.IsAny?>(), It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + _processEventDispatcherMock.Verify(d => d.RegisterEventWithEventsComponent(It.Is(i => CompareInstance(expectedInstance, i)))); + result.Success.Should().BeTrue(); result.ProcessStateChange.Should().BeEquivalentTo( new ProcessStateChange() @@ -614,11 +637,18 @@ public async Task Next_moves_instance_to_next_task_and_produces_abandon_instance } } }; - _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + + _processEventHandlingDelegatorMock.Verify(d => d.HandleEvents( It.Is(i => CompareInstance(expectedInstance, i)), - It.IsAny?>(), + It.IsAny>(), It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + + _processEventDispatcherMock.Verify(d => d.DispatchToStorage( + It.Is(i => CompareInstance(expectedInstance, i)), + It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + _processEventDispatcherMock.Verify(d => d.RegisterEventWithEventsComponent(It.Is(i => CompareInstance(expectedInstance, i)))); + result.Success.Should().BeTrue(); result.ProcessStateChange.Should().BeEquivalentTo( new ProcessStateChange() @@ -743,11 +773,18 @@ public async Task Next_moves_instance_to_end_event_and_ends_proces() } } }; - _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + + _processEventHandlingDelegatorMock.Verify(d => d.HandleEvents( It.Is(i => CompareInstance(expectedInstance, i)), - It.IsAny?>(), + It.IsAny>(), It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + + _processEventDispatcherMock.Verify(d => d.DispatchToStorage( + It.Is(i => CompareInstance(expectedInstance, i)), + It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); + _processEventDispatcherMock.Verify(d => d.RegisterEventWithEventsComponent(It.Is(i => CompareInstance(expectedInstance, i)))); + result.Success.Should().BeTrue(); result.ProcessStateChange.Should().BeEquivalentTo( new ProcessStateChange() @@ -842,11 +879,17 @@ public async Task UpdateInstanceAndRerunEvents_sends_instance_and_events_to_even Instance = instance, Prefill = prefill, }; - Instance result = await processEngine.UpdateInstanceAndRerunEvents(processStartRequest, events); - _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + Instance result = await processEngine.HandleEventsAndUpdateStorage(processStartRequest.Instance, processStartRequest.Prefill, events); + + _processEventHandlingDelegatorMock.Verify(d => d.HandleEvents( It.Is(i => CompareInstance(instance, i)), - prefill, + It.IsAny>(), It.Is>(l => CompareInstanceEvents(events, l)))); + + _processEventDispatcherMock.Verify(d => d.DispatchToStorage( + It.Is(i => CompareInstance(instance, i)), + It.Is>(l => CompareInstanceEvents(events, l)))); + result.Should().Be(updatedInstance); } @@ -919,7 +962,7 @@ private ProcessEngine GetProcessEngine(Mock? processReaderMock = }); if (updatedInstance is not null) { - _processEventDispatcherMock.Setup(d => d.UpdateProcessAndDispatchEvents(It.IsAny(), It.IsAny?>(), It.IsAny>())) + _processEventDispatcherMock.Setup(d => d.DispatchToStorage(It.IsAny(), It.IsAny>())) .ReturnsAsync(() => updatedInstance); } @@ -927,6 +970,7 @@ private ProcessEngine GetProcessEngine(Mock? processReaderMock = _processReaderMock.Object, _profileMock.Object, _processNavigatorMock.Object, + _processEventHandlingDelegatorMock.Object, _processEventDispatcherMock.Object, new UserActionService(userActions ?? [])); } @@ -936,6 +980,7 @@ public void Dispose() _processReaderMock.VerifyNoOtherCalls(); _profileMock.VerifyNoOtherCalls(); _processNavigatorMock.VerifyNoOtherCalls(); + _processEventHandlingDelegatorMock.VerifyNoOtherCalls(); _processEventDispatcherMock.VerifyNoOtherCalls(); } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventHandlingTests.cs similarity index 78% rename from test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs rename to test/Altinn.App.Core.Tests/Internal/Process/ProcessEventHandlingTests.cs index 066fda8dd..bf9d19ee6 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventHandlingTests.cs @@ -4,6 +4,10 @@ using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Process; +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.Models; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; @@ -14,27 +18,46 @@ namespace Altinn.App.Core.Tests.Internal.Process; -public class ProcessEventDispatcherTests +public class ProcessEventHandlingTests { + private readonly List processTasks = + [ + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + ]; + [Fact] public async Task UpdateProcessAndDispatchEvents_StartEvent_instance_updated_and_events_sent_to_storage_nothing_sent_to_ITask() { // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + var startTaskEventHandler = new Mock(); + var endTaskEventHandler = new Mock(); + var abandonTaskEventHandler = new Mock(); + var endEventHandler = new Mock(); + + ProcessEventHandlingDelegator delegator = new ProcessEventHandlingDelegator( + new NullLogger(), + startTaskEventHandler.Object, + endTaskEventHandler.Object, + abandonTaskEventHandler.Object, + endEventHandler.Object, + processTasks); + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { Id = Guid.NewGuid().ToString(), @@ -89,7 +112,8 @@ public async Task UpdateProcessAndDispatchEvents_StartEvent_instance_updated_and Dictionary prefill = new Dictionary(); // Act - var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + await delegator.HandleEvents(instance, prefill, events); + var result = await dispatcher.DispatchToStorage(instance, events); // Assert result.Should().Be(getInstanceResponse); @@ -98,7 +122,6 @@ public async Task UpdateProcessAndDispatchEvents_StartEvent_instance_updated_and instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } @@ -109,19 +132,30 @@ public async Task UpdateProcessAndDispatchEvents_StartTask_instance_updated_and_ // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + var startTaskEventHandler = new Mock(); + var endTaskEventHandler = new Mock(); + var abandonTaskEventHandler = new Mock(); + var endEventHandler = new Mock(); + + ProcessEventHandlingDelegator delegator = new ProcessEventHandlingDelegator( + new NullLogger(), + startTaskEventHandler.Object, + endTaskEventHandler.Object, + abandonTaskEventHandler.Object, + endEventHandler.Object, + processTasks); + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { Id = Guid.NewGuid().ToString(), @@ -173,7 +207,8 @@ public async Task UpdateProcessAndDispatchEvents_StartTask_instance_updated_and_ Dictionary prefill = new Dictionary(); // Act - var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + await delegator.HandleEvents(instance, prefill, events); + var result = await dispatcher.DispatchToStorage(instance, events); // Assert result.Should().Be(getInstanceResponse); @@ -182,7 +217,6 @@ public async Task UpdateProcessAndDispatchEvents_StartTask_instance_updated_and_ instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } @@ -193,19 +227,30 @@ public async Task UpdateProcessAndDispatchEvents_StartTask_data_instance_updated // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + var startTaskEventHandler = new Mock(); + var endTaskEventHandler = new Mock(); + var abandonTaskEventHandler = new Mock(); + var endEventHandler = new Mock(); + + ProcessEventHandlingDelegator delegator = new ProcessEventHandlingDelegator( + new NullLogger(), + startTaskEventHandler.Object, + endTaskEventHandler.Object, + abandonTaskEventHandler.Object, + endEventHandler.Object, + processTasks); + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { Id = Guid.NewGuid().ToString(), @@ -261,17 +306,16 @@ public async Task UpdateProcessAndDispatchEvents_StartTask_data_instance_updated Dictionary prefill = new Dictionary(); // Act - var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + await delegator.HandleEvents(instance, prefill, events); + var result = await dispatcher.DispatchToStorage(instance, events); // Assert result.Should().Be(getInstanceResponse); - taskEvents.Verify(t => t.OnStartProcessTask("Task_1", instance, prefill), Times.Once); instanceService.Verify(i => i.UpdateProcess(instance), Times.Once); instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } @@ -282,19 +326,30 @@ public async Task UpdateProcessAndDispatchEvents_EndTask_confirmation_instance_u // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + var startTaskEventHandler = new Mock(); + var endTaskEventHandler = new Mock(); + var abandonTaskEventHandler = new Mock(); + var endEventHandler = new Mock(); + + ProcessEventHandlingDelegator delegator = new ProcessEventHandlingDelegator( + new NullLogger(), + startTaskEventHandler.Object, + endTaskEventHandler.Object, + abandonTaskEventHandler.Object, + endEventHandler.Object, + processTasks); + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { Id = Guid.NewGuid().ToString(), @@ -350,17 +405,16 @@ public async Task UpdateProcessAndDispatchEvents_EndTask_confirmation_instance_u Dictionary prefill = new Dictionary(); // Act - var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + await delegator.HandleEvents(instance, prefill, events); + var result = await dispatcher.DispatchToStorage(instance, events); // Assert result.Should().Be(getInstanceResponse); - taskEvents.Verify(t => t.OnEndProcessTask("Task_2", instance), Times.Once); instanceService.Verify(i => i.UpdateProcess(instance), Times.Once); instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } @@ -371,19 +425,30 @@ public async Task UpdateProcessAndDispatchEvents_AbandonTask_feedback_instance_u // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + var startTaskEventHandler = new Mock(); + var endTaskEventHandler = new Mock(); + var abandonTaskEventHandler = new Mock(); + var endEventHandler = new Mock(); + + ProcessEventHandlingDelegator delegator = new ProcessEventHandlingDelegator( + new NullLogger(), + startTaskEventHandler.Object, + endTaskEventHandler.Object, + abandonTaskEventHandler.Object, + endEventHandler.Object, + processTasks); + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { Id = Guid.NewGuid().ToString(), @@ -439,17 +504,16 @@ public async Task UpdateProcessAndDispatchEvents_AbandonTask_feedback_instance_u Dictionary prefill = new Dictionary(); // Act - var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + await delegator.HandleEvents(instance, prefill, events); + var result = await dispatcher.DispatchToStorage(instance, events); // Assert result.Should().Be(getInstanceResponse); - taskEvents.Verify(t => t.OnAbandonProcessTask("Task_2", instance), Times.Once); instanceService.Verify(i => i.UpdateProcess(instance), Times.Once); instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } @@ -460,22 +524,34 @@ public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_ // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); + var appMetadata = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + var startTaskEventHandler = new Mock(); + var endTaskEventHandler = new Mock(); + var abandonTaskEventHandler = new Mock(); + var endEventHandler = new EndEventEventHandler(appEvents.Object, instanceService.Object, appMetadata.Object); + + ProcessEventHandlingDelegator delegator = new ProcessEventHandlingDelegator( + new NullLogger(), + startTaskEventHandler.Object, + endTaskEventHandler.Object, + abandonTaskEventHandler.Object, + endEventHandler, + processTasks); + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { - Id = Guid.NewGuid().ToString(), + Id = $"{1234}/{Guid.NewGuid()}", Org = "ttd", AppId = "ttd/test-app", }; @@ -522,12 +598,33 @@ public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_ } } }; + + var applicationMetadata = new ApplicationMetadata(instance.AppId) + { + CopyInstanceSettings = new CopyInstanceSettings { Enabled = true }, + DataTypes = new List + { + new DataType + { + Id = "data_type_1", + AppLogic = new ApplicationLogic + { + ClassRef = "App.Models.Skjema", + }, + TaskId = "First" + } + } + }; + + appMetadata.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + instanceService.Setup(i => i.UpdateProcess(instance)).ReturnsAsync(updateInstanceResponse); instanceService.Setup(i => i.GetInstance(updateInstanceResponse)).ReturnsAsync(getInstanceResponse); Dictionary prefill = new Dictionary(); // Act - var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + await delegator.HandleEvents(instance, prefill, events); + var result = await dispatcher.DispatchToStorage(instance, events); // Assert result.Should().Be(getInstanceResponse); @@ -537,7 +634,6 @@ public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_ instanceEvent.Verify(p => p.SaveInstanceEvent(events[0], instance.Org, "test-app"), Times.Once); instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } @@ -548,19 +644,30 @@ public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_ // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + var startTaskEventHandler = new Mock(); + var endTaskEventHandler = new Mock(); + var abandonTaskEventHandler = new Mock(); + var endEventHandler = new Mock(); + + ProcessEventHandlingDelegator delegator = new ProcessEventHandlingDelegator( + new NullLogger(), + startTaskEventHandler.Object, + endTaskEventHandler.Object, + abandonTaskEventHandler.Object, + endEventHandler.Object, + processTasks); + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { Id = Guid.NewGuid().ToString(), @@ -598,7 +705,8 @@ public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_ Dictionary prefill = new Dictionary(); // Act - var result = await dispatcher.UpdateProcessAndDispatchEvents(instance, prefill, events); + await delegator.HandleEvents(instance, prefill, events); + var result = await dispatcher.DispatchToStorage(instance, events); // Assert result.Should().Be(getInstanceResponse); @@ -606,7 +714,6 @@ public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_ instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } @@ -617,7 +724,6 @@ public async Task RegisterEventWithEventsComponent_sends_movedTo_event_to_events // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings() @@ -625,14 +731,14 @@ public async Task RegisterEventWithEventsComponent_sends_movedTo_event_to_events RegisterEventsWithEventsComponent = true }); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { Id = Guid.NewGuid().ToString(), @@ -652,7 +758,6 @@ public async Task RegisterEventWithEventsComponent_sends_movedTo_event_to_events eventsService.Verify(e => e.AddEvent("app.instance.process.movedTo.Task_1", instance), Times.Once); instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } @@ -663,7 +768,6 @@ public async Task RegisterEventWithEventsComponent_sends_complete_event_to_event // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings() @@ -671,14 +775,14 @@ public async Task RegisterEventWithEventsComponent_sends_complete_event_to_event RegisterEventsWithEventsComponent = true }); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { Id = Guid.NewGuid().ToString(), @@ -696,7 +800,6 @@ public async Task RegisterEventWithEventsComponent_sends_complete_event_to_event eventsService.Verify(e => e.AddEvent("app.instance.process.completed", instance), Times.Once); instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } @@ -707,7 +810,6 @@ public async Task RegisterEventWithEventsComponent_sends_no_events_when_process_ // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings() @@ -715,14 +817,14 @@ public async Task RegisterEventWithEventsComponent_sends_no_events_when_process_ RegisterEventsWithEventsComponent = true }); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { Id = Guid.NewGuid().ToString(), @@ -735,7 +837,6 @@ public async Task RegisterEventWithEventsComponent_sends_no_events_when_process_ // Assert instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } @@ -746,7 +847,6 @@ public async Task RegisterEventWithEventsComponent_sends_no_events_when_current_ // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings() @@ -754,14 +854,14 @@ public async Task RegisterEventWithEventsComponent_sends_no_events_when_current_ RegisterEventsWithEventsComponent = true }); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { Id = Guid.NewGuid().ToString(), @@ -774,7 +874,6 @@ public async Task RegisterEventWithEventsComponent_sends_no_events_when_current_ // Assert instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } @@ -785,7 +884,6 @@ public async Task RegisterEventWithEventsComponent_sends_no_events_when_register // Arrange var instanceService = new Mock(); var instanceEvent = new Mock(); - var taskEvents = new Mock(); var appEvents = new Mock(); var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings() @@ -793,14 +891,14 @@ public async Task RegisterEventWithEventsComponent_sends_no_events_when_register RegisterEventsWithEventsComponent = false }); var logger = new NullLogger(); - IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + + ProcessEventDispatcher dispatcher = new ProcessEventDispatcher( instanceService.Object, instanceEvent.Object, - taskEvents.Object, - appEvents.Object, eventsService.Object, appSettings, logger); + Instance instance = new Instance() { Id = Guid.NewGuid().ToString(), @@ -819,7 +917,6 @@ public async Task RegisterEventWithEventsComponent_sends_no_events_when_register // Assert instanceService.VerifyNoOtherCalls(); instanceEvent.VerifyNoOtherCalls(); - taskEvents.VerifyNoOtherCalls(); appEvents.VerifyNoOtherCalls(); eventsService.VerifyNoOtherCalls(); } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs new file mode 100644 index 000000000..87a250529 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskDataLockerTests.cs @@ -0,0 +1,165 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.ProcessTasks +{ + public class ProcessTaskDataLockerTests + { + private readonly Mock _appMetadataMock; + private readonly Mock _dataClientMock; + private readonly ProcessTaskDataLocker _processTaskDataLocker; + + public ProcessTaskDataLockerTests() + { + _appMetadataMock = new Mock(); + _dataClientMock = new Mock(); + _processTaskDataLocker = new ProcessTaskDataLocker(_appMetadataMock.Object, _dataClientMock.Object); + } + + [Fact] + public async Task Unlock_ShouldUnlockAllDataElementsConnectedToTask() + { + // Arrange + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; + + instance.Data = + [ + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } + ]; + + var applicationMetadata = new ApplicationMetadata(instance.AppId) + { + DataTypes = + [ + new DataType { Id = "dataType1", TaskId = taskId }, + new DataType { Id = "dataType2", TaskId = taskId } + ] + }; + + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + await _processTaskDataLocker.Unlock(taskId, instance); + + // Assert + _dataClientMock.Verify(x => x.UnlockDataElement(It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task Lock_ShouldLockAllDataElementsConnectedToTask() + { + // Arrange + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; + + instance.Data = + [ + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } + ]; + + var applicationMetadata = new ApplicationMetadata(instance.AppId) + { + DataTypes = + [ + new DataType { Id = "dataType1", TaskId = taskId }, + new DataType { Id = "dataType2", TaskId = taskId } + ] + }; + + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + await _processTaskDataLocker.Lock(taskId, instance); + + // Assert + _dataClientMock.Verify(x => x.LockDataElement(It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task Unlock_ShouldNotUnlockDataElementsNotConnectedToTask() + { + // Arrange + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; + + instance.Data = + [ + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } + ]; + + var applicationMetadata = new ApplicationMetadata(instance.AppId) + { + DataTypes = + [ + new DataType { Id = "dataType1", TaskId = taskId }, + new DataType { Id = "dataType3", TaskId = "task2" } + ] + }; + + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + await _processTaskDataLocker.Unlock(taskId, instance); + + // Assert + _dataClientMock.Verify(x => x.UnlockDataElement(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Lock_ShouldNotLockDataElementsNotConnectedToTask() + { + // Arrange + Instance instance = CreateInstance(); + string taskId = instance.Process.CurrentTask.ElementId; + + instance.Data = + [ + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType1" }, + new DataElement { Id = Guid.NewGuid().ToString(), DataType = "dataType2" } + ]; + + var applicationMetadata = new ApplicationMetadata(instance.AppId) + { + DataTypes = + [ + new DataType { Id = "dataType1", TaskId = taskId }, + new DataType { Id = "dataType3", TaskId = "task2" } + ] + }; + + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + await _processTaskDataLocker.Lock(taskId, instance); + + // Assert + _dataClientMock.Verify(x => x.LockDataElement(It.IsAny(), It.IsAny()), Times.Once); + } + + private static Instance CreateInstance() + { + return new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo + { + AltinnTaskType = "datatask", + ElementId = "datatask", + }, + }, + }; + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs new file mode 100644 index 000000000..a28ef40b4 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessTasks/Common/ProcessTaskFinalizerTests.cs @@ -0,0 +1,85 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Internal.Process.ProcessTasks; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.ProcessTasks.Common +{ + public class ProcessTaskFinalizerTests + { + private readonly Mock _appMetadataMock; + private readonly Mock _dataClientMock; + private readonly Mock _appModelMock; + private readonly Mock _appResourcesMock; + private readonly Mock _layoutEvaluatorStateInitializerMock; + private readonly IOptions _appSettingsMock; + private readonly ProcessTaskFinalizer _processTaskFinalizer; + + public ProcessTaskFinalizerTests() + { + _appMetadataMock = new Mock(); + _dataClientMock = new Mock(); + _appModelMock = new Mock(); + _appResourcesMock = new Mock(); + var frontendSettingsMock = new Mock>(); + _layoutEvaluatorStateInitializerMock = new Mock(MockBehavior.Strict, [_appResourcesMock.Object, frontendSettingsMock.Object]); + _appSettingsMock = Options.Create(new AppSettings()); + + _processTaskFinalizer = new ProcessTaskFinalizer( + _appMetadataMock.Object, + _dataClientMock.Object, + _appModelMock.Object, + _appResourcesMock.Object, + _layoutEvaluatorStateInitializerMock.Object, + _appSettingsMock); + } + + [Fact] + public async Task Finalize_WithValidInputs_ShouldCallCorrectMethods() + { + // Arrange + Instance instance = CreateInstance(); + + instance.Data = [new DataElement { Id = Guid.NewGuid().ToString(), References = [new Reference { ValueType = ReferenceType.Task, Value = instance.Process.CurrentTask.ElementId }] }]; + + var applicationMetadata = new ApplicationMetadata(instance.AppId) + { + DataTypes = [new DataType { TaskId = instance.Process.CurrentTask.ElementId }] + }; + + _appMetadataMock.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + + // Act + await _processTaskFinalizer.Finalize(instance.Process.CurrentTask.ElementId, instance); + + // Assert + _appMetadataMock.Verify(x => x.GetApplicationMetadata(), Times.Once); + _dataClientMock.Verify(x => x.DeleteData(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.AtLeastOnce); + } + + private static Instance CreateInstance() + { + return new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo + { + AltinnTaskType = "signing", + ElementId = "EndEvent", + }, + }, + }; + } + } +} \ No newline at end of file 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..bcd2a19a3 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/EformidlingServiceTaskTests.cs @@ -0,0 +1,161 @@ +#nullable enable +using Altinn.App.Core.Configuration; +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.Models; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks; + +public class EformidlingServiceTaskTests +{ + 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(); + } + + [Fact] + public async Task Execute_EFormidlingIsEnabledAndSendAfterTaskIdMatchesCurrentTask_EFormidlingShipment_is_sent() + { + // Arrange + var appSettings = new AppSettings + { + EnableEFormidling = true + }; + var applicationMetadata = new ApplicationMetadata("ttd/test") + { + EFormidling = new EFormidlingContract() + { + SendAfterTaskId = "Task_1" + } + }; + var instance = new Instance(); + _appMetadata.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + _instanceClient.Setup(x => x.GetInstance(instance)).ReturnsAsync(instance); + _eFormidlingService.Setup(x => x.SendEFormidlingShipment(instance)).Returns(Task.CompletedTask); + var eformidlingServiceTask = GetEformidlingServiceTask(appSettings, _eFormidlingService.Object); + + // Act + await eformidlingServiceTask.Execute("Task_1", instance); + + // Assert + _appMetadata.Verify(x => x.GetApplicationMetadata(), Times.Once); + _instanceClient.Verify(x => x.GetInstance(instance), Times.Once); + _eFormidlingService.Verify(x => x.SendEFormidlingShipment(instance), Times.Once); + _appMetadata.VerifyNoOtherCalls(); + _instanceClient.VerifyNoOtherCalls(); + _eFormidlingService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Execute_EFormidlingIsEnabledAndSendAfterTaskIdMatchesCurrentTask_but_EformidlingService_is_null_EFormidlingShipment_is_not_sent() + { + // Arrange + var appSettings = new AppSettings + { + EnableEFormidling = true + }; + var applicationMetadata = new ApplicationMetadata("ttd/test") + { + EFormidling = new EFormidlingContract() + { + SendAfterTaskId = "Task_1" + } + }; + var instance = new Instance(); + _appMetadata.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + var eformidlingServiceTask = GetEformidlingServiceTask(appSettings); + + // Act + await eformidlingServiceTask.Execute("Task_1", instance); + + // Assert + _appMetadata.Verify(x => x.GetApplicationMetadata(), Times.Once); + _appMetadata.VerifyNoOtherCalls(); + _instanceClient.VerifyNoOtherCalls(); + _eFormidlingService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Execute_EFormidlingIsNotEnabledAndSendAfterTaskIdMatchesCurrentTask_EFormidlingShipment_is_not_sent() + { + // Arrange + var appSettings = new AppSettings + { + EnableEFormidling = false + }; + var applicationMetadata = new ApplicationMetadata("ttd/test") + { + EFormidling = new EFormidlingContract() + { + SendAfterTaskId = "Task_1" + } + }; + var instance = new Instance(); + _appMetadata.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + var eformidlingServiceTask = GetEformidlingServiceTask(appSettings, _eFormidlingService.Object); + + // Act + await eformidlingServiceTask.Execute("Task_1", instance); + + // Assert + _appMetadata.Verify(x => x.GetApplicationMetadata(), Times.Once); + _appMetadata.VerifyNoOtherCalls(); + _instanceClient.VerifyNoOtherCalls(); + _eFormidlingService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Execute_EFormidlingIsEnabledAndSendAfterTaskIdNotMatchingCurrentTask_EFormidlingShipment_is_not_sent() + { + // Arrange + var appSettings = new AppSettings + { + EnableEFormidling = true + }; + var applicationMetadata = new ApplicationMetadata("ttd/test") + { + EFormidling = new EFormidlingContract() + { + SendAfterTaskId = "Task_2" + } + }; + var instance = new Instance(); + _appMetadata.Setup(x => x.GetApplicationMetadata()).ReturnsAsync(applicationMetadata); + var eformidlingServiceTask = GetEformidlingServiceTask(appSettings, _eFormidlingService.Object); + + // Act + await eformidlingServiceTask.Execute("Task_1", instance); + + // Assert + _appMetadata.Verify(x => x.GetApplicationMetadata(), Times.Once); + _appMetadata.VerifyNoOtherCalls(); + _instanceClient.VerifyNoOtherCalls(); + _eFormidlingService.VerifyNoOtherCalls(); + } + + public EformidlingServiceTask GetEformidlingServiceTask(AppSettings? appSettings, IEFormidlingService? eFormidlingService = null) + { + return new EformidlingServiceTask( + _logger, + _appMetadata.Object, + _instanceClient.Object, + eFormidlingService, + appSettings == null ? null : Options.Create(appSettings)); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs new file mode 100644 index 000000000..8b9f3d177 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/PdfServiceTaskTests.cs @@ -0,0 +1,229 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Process.ServiceTasks; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks; + +public class PdfServiceTaskTests +{ + private readonly Mock _appMetadata; + private readonly Mock _pdfService; + private readonly Mock _appModel; + + public PdfServiceTaskTests() + { + _appMetadata = new Mock(); + _pdfService = new Mock(); + _appModel = new Mock(); + } + + [Fact] + public async Task Execute_calls_pdf_service() + { + Instance i = new Instance() + { + Data = + [ + new DataElement() + { + DataType = "DataType_1" + }, + ] + }; + SetupAppMetadataWithDataTypes([ + new DataType + { + 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(); + } + + [Fact] + public async Task Execute_pdf_service_is_called_only_once() + { + Instance i = new Instance() + { + Data = + [ + new DataElement() + { + DataType = "DataType_1" + }, + new DataElement() + { + DataType = "DataType_1" + }, + new DataElement() + { + DataType = "DataType_2" + }, + new DataElement() + { + DataType = "DataType_2" + }, + ] + }; + SetupAppMetadataWithDataTypes([ + new DataType + { + Id = "DataType_1", + TaskId = "Task_1", + AppLogic = new ApplicationLogic() + { + ClassRef = "DataType_1" + }, + EnablePdfCreation = true + }, + new DataType + { + Id = "DataType_2", + TaskId = "Task_1", + AppLogic = new ApplicationLogic() + { + ClassRef = "DataType_2" + }, + EnablePdfCreation = true + }, + ]); + PdfServiceTask pst = new PdfServiceTask(_appMetadata.Object, _pdfService.Object); + await pst.Execute("Task_1", i); + _appMetadata.Verify(am => am.GetApplicationMetadata()); + _pdfService.Verify(ps => ps.GenerateAndStorePdf(i, "Task_1", CancellationToken.None), Times.Once); + _appMetadata.VerifyNoOtherCalls(); + _pdfService.VerifyNoOtherCalls(); + _appModel.VerifyNoOtherCalls(); + } + + [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(); + } + + [Fact] + public async Task Execute_does_not_call_pdfservice_if_generate_pdf_are_false_for_all_datatypes() + { + DataElement d = new DataElement() + { + Id = "DataElement_1", + DataType = "DataType_1" + }; + Instance i = new Instance() + { + Data = + [ + d, + ] + }; + SetupAppMetadataWithDataTypes([ + new DataType + { + Id = "DataType_1", + TaskId = "Task_1", + AppLogic = new ApplicationLogic() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.ServiceTasks.TestData.DummyDataType" + }, + EnablePdfCreation = false + }, + ]); + PdfServiceTask pst = new PdfServiceTask(_appMetadata.Object, _pdfService.Object); + await pst.Execute("Task_1", i); + _appMetadata.Verify(am => am.GetApplicationMetadata(), Times.Once); + _appMetadata.VerifyNoOtherCalls(); + _pdfService.VerifyNoOtherCalls(); + _appModel.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Execute_does_not_call_pdfservice_if_generate_pdf_are_false_for_all_datatypes_nde_pdf_flag_true() + { + DataElement d = new DataElement() + { + Id = "DataElement_1", + DataType = "DataType_1" + }; + Instance i = new Instance() + { + Data = + [ + d, + ] + }; + SetupAppMetadataWithDataTypes([ + new DataType + { + Id = "DataType_1", + TaskId = "Task_1", + AppLogic = new ApplicationLogic() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.ServiceTasks.TestData.DummyDataType" + }, + EnablePdfCreation = false + }, + ]); + PdfServiceTask pst = new PdfServiceTask(_appMetadata.Object, _pdfService.Object); + await pst.Execute("Task_1", i); + _appMetadata.Verify(am => am.GetApplicationMetadata(), Times.Once); + _appMetadata.VerifyNoOtherCalls(); + _pdfService.VerifyNoOtherCalls(); + _appModel.VerifyNoOtherCalls(); + } + + private void SetupAppMetadataWithDataTypes(List? dataTypes = null) + { + _appMetadata.Setup(am => am.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/test") + { + DataTypes = dataTypes ?? new List { } + }); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/TestData/DummyDataType.cs b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/TestData/DummyDataType.cs new file mode 100644 index 000000000..30a3462dd --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ServiceTasks/TestData/DummyDataType.cs @@ -0,0 +1,6 @@ +namespace Altinn.App.Core.Tests.Internal.Process.ServiceTasks.TestData +{ + public class DummyDataType + { + } +} \ No newline at end of file